On macOS only track nots that request close events

Send an untracked event when giving up tracking of the notification
This commit is contained in:
Kovid Goyal 2024-07-28 13:19:47 +05:30
parent 2bffea2bdc
commit 02bc104091
No known key found for this signature in database
GPG key ID: 06BC317B515ACE7C
6 changed files with 59 additions and 27 deletions

View file

@ -131,13 +131,15 @@ then both the activation report and close notification are sent. If the notifica
is updated then the close event is not sent unless the updated notification
also requests a close notification.
.. 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.
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::
<OSC> 99 ; i=mynotification : p=close ; untracked <terminator>
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
for a short period to see if they are closed, after which it gives up
and replies with an ``untracked`` response.
Updating or closing an existing notification
----------------------------------------------

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, closed_notifications;
cocoa_list open_urls, closed_notifications, untracked_notifications;
} CocoaPendingActionsData;
static CocoaPendingActionsData cocoa_pending_actions_data = {0};
@ -1166,6 +1166,7 @@ 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.closed_notifications);
cocoa_free_pending_list(&cocoa_pending_actions_data.untracked_notifications);
}
void
@ -1176,6 +1177,8 @@ set_cocoa_pending_action(CocoaPendingAction action, const char *data) {
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:
cocoa_append_to_pending_list(&cocoa_pending_actions_data.untracked_notifications, data); break;
default:
if (cocoa_pending_actions_data.wd) free(cocoa_pending_actions_data.wd);
cocoa_pending_actions_data.wd = strdup(data);
@ -1226,14 +1229,26 @@ 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]);
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++) {
if (cocoa_pending_actions_data.untracked_notifications.items[cpa]) {
cocoa_report_closed_notification(cocoa_pending_actions_data.untracked_notifications.items[cpa], true);
free(cocoa_pending_actions_data.untracked_notifications.items[cpa]);
cocoa_pending_actions_data.untracked_notifications.items[cpa] = NULL;
}
}
cocoa_pending_actions_data.untracked_notifications.count = 0;
memset(cocoa_pending_actions, 0, sizeof(cocoa_pending_actions));
has_cocoa_pending_actions = false;

View file

@ -35,6 +35,7 @@ typedef enum {
QUIT,
USER_MENU_ACTION,
COCOA_NOTIFICATION_CLOSED,
COCOA_NOTIFICATION_UNTRACKED,
NUM_COCOA_PENDING_ACTIONS
} CocoaPendingAction;
@ -59,4 +60,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);
void cocoa_report_closed_notification(const char* ident, bool untracked);

View file

@ -500,8 +500,8 @@ poll_for_closed_notifications(void) {
}
void
cocoa_report_closed_notification(const char* ident) {
do_notification_callback(@(ident), "closed");
cocoa_report_closed_notification(const char* ident, bool untracked) {
do_notification_callback(@(ident), untracked ? "untracked" : "closed");
}
static void
@ -514,7 +514,15 @@ dispatch_closed_notifications(void) {
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 {
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];
@ -549,7 +557,7 @@ track_notification(char *ident) {
}
static void
schedule_notification(const char *identifier, const char *title, const char *body, int urgency) {
schedule_notification(const char *identifier, const char *title, const char *body, bool track_closing, int urgency) {
UNUserNotificationCenter *center = get_notification_center_safely();
if (!center) return;
// Configure the notification's payload.
@ -584,7 +592,7 @@ schedule_notification(const char *identifier, const char *title, const char *bod
bool ok = error == nil;
dispatch_async(dispatch_get_main_queue(), ^{
do_notification_callback(@(duped_ident), ok ? "created" : "closed");
if (ok) track_notification(duped_ident);
if (ok && track_closing) track_notification(duped_ident);
else free(duped_ident);
});
}];
@ -594,7 +602,7 @@ schedule_notification(const char *identifier, const char *title, const char *bod
typedef struct {
char *identifier, *title, *body;
int urgency;
int urgency; bool track_closing;
} QueuedNotification;
typedef struct {
@ -604,13 +612,13 @@ typedef struct {
static NotificationQueue notification_queue = {0};
static void
queue_notification(const char *identifier, const char *title, const char* body, int urgency) {
queue_notification(const char *identifier, const char *title, const char* body, bool track_closing, int urgency) {
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;
n->urgency = urgency;
n->urgency = urgency; n->track_closing = track_closing;
}
static void
@ -618,7 +626,7 @@ drain_pending_notifications(BOOL granted) {
if (granted) {
for (size_t i = 0; i < notification_queue.count; i++) {
QueuedNotification *n = notification_queue.notifications + i;
schedule_notification(n->identifier, n->title, n->body, n->urgency);
schedule_notification(n->identifier, n->title, n->body, n->track_closing, n->urgency);
}
}
while(notification_queue.count) {
@ -639,12 +647,13 @@ cocoa_remove_delivered_notification(PyObject *self UNUSED, PyObject *x) {
static PyObject*
cocoa_send_notification(PyObject *self UNUSED, PyObject *args) {
char *identifier = NULL, *title = NULL, *body = NULL; int urgency = 1;
if (!PyArg_ParseTuple(args, "sss|i", &identifier, &title, &body, &urgency)) return NULL;
int track_closing;
if (!PyArg_ParseTuple(args, "sssp|i", &identifier, &title, &body, &track_closing, &urgency)) return NULL;
UNUserNotificationCenter *center = get_notification_center_safely();
if (!center) Py_RETURN_NONE;
if (!center.delegate) center.delegate = [[NotificationDelegate alloc] init];
queue_notification(identifier, title, body, urgency);
queue_notification(identifier, title, body, track_closing, urgency);
// 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.

View file

@ -568,6 +568,7 @@ def cocoa_send_notification(
identifier: str,
title: str,
body: str,
track_closing: bool,
urgency: int = 1,
) -> None:
pass

View file

@ -203,7 +203,7 @@ class NotificationCommand:
# event callbacks
on_activation: Optional[Callable[['NotificationCommand'], None]] = None
on_close: Optional[Callable[['NotificationCommand'], None]] = None
on_close: Optional[Callable[['NotificationCommand', bool], None]] = None
on_update: Optional[Callable[['NotificationCommand', 'NotificationCommand'], None]] = None
# metadata
@ -378,6 +378,7 @@ class NotificationCommand:
self.title = sanitize_text(self.body)
self.body = ''
self.urgency = Urgency.Normal if self.urgency is None else self.urgency
self.close_response_requested = bool(self.close_response_requested)
def matches_rule_item(self, location:str, query:str) -> bool:
import re
@ -454,7 +455,7 @@ class MacOSIntegration(DesktopIntegration):
# for %% escaping.
body = (nc.body or ' ')
assert nc.urgency is not None
cocoa_send_notification(str(desktop_notification_id), nc.title, body, nc.urgency.value)
cocoa_send_notification(str(desktop_notification_id), nc.title, body, bool(nc.close_response_requested), nc.urgency.value)
return desktop_notification_id
def notification_activated(self, event: str, ident: str) -> None:
@ -471,6 +472,8 @@ class MacOSIntegration(DesktopIntegration):
self.notification_manager.notification_activated(desktop_notification_id)
elif event == "closed":
self.notification_manager.notification_closed(desktop_notification_id)
elif event == "untracked":
self.notification_manager.notification_closed(desktop_notification_id, True)
class FreeDesktopIntegration(DesktopIntegration):
@ -669,14 +672,14 @@ class NotificationManager:
except Exception as e:
self.log('Notification on_update handler failed with error:', e)
def notification_closed(self, desktop_notification_id: int) -> None:
def notification_closed(self, desktop_notification_id: int, untracked: bool = False) -> None:
if n := self.in_progress_notification_commands.get(desktop_notification_id):
self.purge_notification(n)
if n.close_response_requested:
self.send_closed_response(n.channel_id, n.identifier)
self.send_closed_response(n.channel_id, n.identifier, untracked)
if n.on_close is not None:
try:
n.on_close(n)
n.on_close(n, untracked)
except Exception as e:
self.log('Notification on_close handler failed with error:', e)
@ -778,8 +781,9 @@ class NotificationManager:
cmd.set_payload(payload_type, payload_is_encoded, payload, prev_cmd)
return cmd
def send_closed_response(self, channel_id: int, client_id: str) -> None:
self.channel.send(channel_id, f'99;i={client_id}:p=close;')
def send_closed_response(self, channel_id: int, client_id: str, untracked: bool = False) -> None:
payload = 'untracked' if untracked else ''
self.channel.send(channel_id, f'99;i={client_id}:p=close;{payload}')
def purge_notification(self, cmd: NotificationCommand) -> None:
self.in_progress_notification_commands_by_client_id.pop(cmd.identifier, None)