From 5905ea0d84530c85899292e38be4cd98156f7cbf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Tue, 23 Jul 2024 06:59:46 +0200 Subject: [PATCH] osc: kitty notifications: implement focus|report This patch adds support for window focusing, and sending events back to the client application when a notification is closed. * Refactor notification related configuration options: - add desktop-notifications sub-section - deprecate 'notify' in favor of 'desktop-notifications.command' - deprecate 'notify-focus-inhibit' in favor of 'desktop-notifications.inhibit-when-focused' * Refactor: rename 'struct kitty_notification' to 'struct notification' * Pass a 'struct notification' to notify_notify(), instead of many arguments. * notify_notify() now registers a reaper callback. When the notifier process has terminated, the notification is considered closed, and we either try to focus (activate) the window, or send an event to the client application, depending on the notification setting. * For the window activation, we need an XDG activation token. For now, assume *everything* written on stdout is part of the token. * Refactor: remove much of the warnings from OSC-99; we don't typically log anything when an OSC/CSI has invalid values. * Add icon support to OSC-99. This isn't part of the upstream spec. Foot's implementation: - uses the 'I' parameter - the value is expected to be a symbolic icon name - a quick check for absolute paths is done, and such icon requests are ignored. * Added ${icon} to the 'desktop-notifications.command' template. Uses the icon specified in the notification, or ${app-id} if not set. --- config.c | 63 +++++++++++--- config.h | 6 +- doc/foot-ctlseqs.7.scd | 7 +- doc/foot.ini.5.scd | 109 +++++++++++++++++------- foot.ini | 7 +- notify.c | 185 +++++++++++++++++++++++++++++++++++++---- notify.h | 21 +++-- osc.c | 103 +++++++++++------------ terminal.c | 23 +++-- terminal.h | 4 +- tests/test-config.c | 19 ++++- 11 files changed, 410 insertions(+), 137 deletions(-) diff --git a/config.c b/config.c index d34df132..a624f95c 100644 --- a/config.c +++ b/config.c @@ -1024,11 +1024,29 @@ parse_section_main(struct context *ctx) else if (streq(key, "word-delimiters")) return value_to_wchars(ctx, &conf->word_delimiters); - else if (streq(key, "notify")) - return value_to_spawn_template(ctx, &conf->notify); + else if (streq(key, "notify")) { + user_notification_add( + &conf->notifications, USER_NOTIFICATION_DEPRECATED, + xstrdup("notify: use desktop-notifications.command instead")); + log_msg( + LOG_CLASS_WARNING, LOG_MODULE, __FILE__, __LINE__, + "deprecated: notify: use desktop-notifications.command instead"); + return value_to_spawn_template( + ctx, &conf->desktop_notifications.command); + } - else if (streq(key, "notify-focus-inhibit")) - return value_to_bool(ctx, &conf->notify_focus_inhibit); + else if (streq(key, "notify-focus-inhibit")) { + user_notification_add( + &conf->notifications, USER_NOTIFICATION_DEPRECATED, + xstrdup("notify-focus-inhibit: " + "use desktop-notifications.inhibit-when-focused instead")); + log_msg( + LOG_CLASS_WARNING, LOG_MODULE, __FILE__, __LINE__, + "deprecrated: notify-focus-inhibit: " + "use desktop-notifications.inhibit-when-focused instead"); + return value_to_bool( + ctx, &conf->desktop_notifications.inhibit_when_focused); + } else if (streq(key, "selection-target")) { _Static_assert(sizeof(conf->selection_target) == sizeof(int), @@ -1083,6 +1101,24 @@ parse_section_bell(struct context *ctx) } } +static bool +parse_section_desktop_notifications(struct context *ctx) +{ + struct config *conf = ctx->conf; + const char *key = ctx->key; + + if (streq(key, "command")) + return value_to_spawn_template( + ctx, &conf->desktop_notifications.command); + else if (streq(key, "inhibit-when-focused")) + return value_to_bool( + ctx, &conf->desktop_notifications.inhibit_when_focused); + else { + LOG_CONTEXTUAL_ERR("not a valid option: %s", key); + return false; + } +} + static bool parse_section_scrollback(struct context *ctx) { @@ -2662,6 +2698,7 @@ parse_key_value(char *kv, const char **section, const char **key, const char **v enum section { SECTION_MAIN, SECTION_BELL, + SECTION_DESKTOP_NOTIFICATIONS, SECTION_SCROLLBACK, SECTION_URL, SECTION_COLORS, @@ -2688,6 +2725,7 @@ static const struct { } section_info[] = { [SECTION_MAIN] = {&parse_section_main, "main"}, [SECTION_BELL] = {&parse_section_bell, "bell"}, + [SECTION_DESKTOP_NOTIFICATIONS] = {&parse_section_desktop_notifications, "desktop-notifications"}, [SECTION_SCROLLBACK] = {&parse_section_scrollback, "scrollback"}, [SECTION_URL] = {&parse_section_url, "url"}, [SECTION_COLORS] = {&parse_section_colors, "colors"}, @@ -3144,10 +3182,12 @@ config_load(struct config *conf, const char *conf_path, .presentation_timings = false, .selection_target = SELECTION_TARGET_PRIMARY, .hold_at_exit = false, - .notify = { - .argv = {.args = NULL}, + .desktop_notifications = { + .command = { + .argv = {.args = NULL}, + }, + .inhibit_when_focused = true, }, - .notify_focus_inhibit = true, .tweak = { .fcft_filter = FCFT_SCALING_FILTER_LANCZOS3, @@ -3185,8 +3225,8 @@ config_load(struct config *conf, const char *conf_path, parse_modifiers(XKB_MOD_NAME_SHIFT, 5, &conf->mouse.selection_override_modifiers); tokenize_cmdline( - "notify-send -a ${app-id} -i ${app-id} -u ${urgency} ${title} ${body}", - &conf->notify.argv.args); + "notify-send --wait --app-name ${app-id} --icon ${icon} --urgency ${urgency} -- ${title} ${body}", + &conf->desktop_notifications.command.argv.args); tokenize_cmdline("xdg-open ${url}", &conf->url.launch.argv.args); static const char32_t *url_protocols[] = { @@ -3439,7 +3479,8 @@ config_clone(const struct config *old) conf->scrollback.indicator.text = xc32dup(old->scrollback.indicator.text); conf->server_socket_path = xstrdup(old->server_socket_path); spawn_template_clone(&conf->bell.command, &old->bell.command); - spawn_template_clone(&conf->notify, &old->notify); + spawn_template_clone(&conf->desktop_notifications.command, + &old->desktop_notifications.command); for (size_t i = 0; i < ALEN(conf->fonts); i++) config_font_list_clone(&conf->fonts[i], &old->fonts[i]); @@ -3521,7 +3562,7 @@ config_free(struct config *conf) free(conf->word_delimiters); spawn_template_free(&conf->bell.command); free(conf->scrollback.indicator.text); - spawn_template_free(&conf->notify); + spawn_template_free(&conf->desktop_notifications.command); for (size_t i = 0; i < ALEN(conf->fonts); i++) config_font_list_destroy(&conf->fonts[i]); free(conf->server_socket_path); diff --git a/config.h b/config.h index 4ce36486..b3688f28 100644 --- a/config.h +++ b/config.h @@ -338,8 +338,10 @@ struct config { SELECTION_TARGET_BOTH } selection_target; - struct config_spawn_template notify; - bool notify_focus_inhibit; + struct { + struct config_spawn_template command; + bool inhibit_when_focused; + } desktop_notifications; env_var_list_t env_vars; diff --git a/doc/foot-ctlseqs.7.scd b/doc/foot-ctlseqs.7.scd index 5c611c92..998b6843 100644 --- a/doc/foot-ctlseqs.7.scd +++ b/doc/foot-ctlseqs.7.scd @@ -718,6 +718,10 @@ All _OSC_ sequences begin with *\\E]*, sometimes abbreviated _OSC_. : Copy _Pd_ (base64 encoded text) to the clipboard. _Pc_ denotes the target: *c* targets the clipboard and *s* and *p* the primary selection. +| \\E] 99 ; _params_ ; _payload_ \\E\\ +: kitty +: Desktop notification; uses *desktop-notifications.command* in + *foot.ini*(5). | \\E] 104 ; _c_ \\E\\ : xterm : Reset color number _c_ (multiple semicolon separated _c_ values may @@ -757,7 +761,8 @@ All _OSC_ sequences begin with *\\E]*, sometimes abbreviated _OSC_. : Flash the entire terminal (foot extension) | \\E] 777;notify;_title_;_msg_ \\E\\ : urxvt -: Desktop notification, uses *notify* in *foot.ini*(5). +: Desktop notification, uses *desktop-notifications.command* in + *foot.ini*(5). # DCS diff --git a/doc/foot.ini.5.scd b/doc/foot.ini.5.scd index fe6ff57b..e6b554bb 100644 --- a/doc/foot.ini.5.scd +++ b/doc/foot.ini.5.scd @@ -342,32 +342,6 @@ empty string to be set, but it must be quoted: *KEY=""*) text. Note that whitespace characters are _always_ word delimiters, regardless of this setting. Default: _,│`|:"'()[]{}<>_ -*notify* - Command to execute to display a notification. _${title}_ and - _${body}_ will be replaced with the notification's actual _title_ - and _body_ (message content). - - _${app-id}_ is replaced with the value of the command line option - _--app-id_, and defaults to *foot* (normal mode), or - *footclient* (server mode). - - _${window-title}_ is replaced with the current window title. - - Applications can trigger notifications in the following ways: - - - OSC 777: *\\e]777;notify;;<body>\\e\\\\* - - By default, notifications are *inhibited* if the foot window - has keyboard focus. See _notify-focus-inhibit_. - - Default: _notify-send -a ${app-id} -i ${app-id} -u ${urgency} ${title} ${body}_. - -*notify-focus-inhibit* - Boolean. If enabled, foot will not display notifications if the - terminal window has keyboard focus. - - Default: _yes_ - *selection-target* Clipboard target to automatically copy selected text to. One of *none*, *primary*, *clipboard* or *both*. Default: _primary_. @@ -426,10 +400,11 @@ Note: do not set *TERM* here; use the *term* option in the main Default: _no_ *notify* - When set to _yes_, foot will emit a desktop notification using - the command specified in the *notify* option whenever *BEL* is + When set to _yes_, foot will emit a desktop notification using the + command specified in the *notify* option whenever *BEL* is received. By default, bell notifications are shown only when the - window does *not* have keyboard focus. See _notify-focus-inhibit_. + window does *not* have keyboard focus. See + _desktop-notifications.inhibit-when-focused_. Default: _no_ @@ -445,6 +420,82 @@ Note: do not set *TERM* here; use the *term* option in the main Whether to run the command on *BEL* even while focused. Default: _no_ +# SECTION: desktop-notifications + +*command* + Command to execute to display a notification. + + Template arguments + _${title}_ and _${body}_ will be replaced with the + notification's actual _title_ and _body_ (message content). + + _${app-id}_ is replaced with the value of the command line + option _--app-id_, and defaults to *foot* (normal mode), or + *footclient* (server mode). + + _${window-title}_ is replaced with the current window title. + + _${icon}_ is replaced by the icon specified in the + notification request, or _${app_id}_ if the notification did + not set an icon. Note that only symbolic icon names are + supported, not filenames. + + _${urgency}_ is replaced with the notifications urgency; + *low*, *normal* or *critical*. + + Ways to trigger notifications + Applications can trigger notifications in the following ways: + + - OSC 777: *\\e]777;notify;<title>;<body>\\e\\\\* + - OSC 99: *\\e]99;;<title>\\e\\\\* (this is just a bare bones + example; this protocol has lots of features, see + https://sw.kovidgoyal.net/kitty/desktop-notifications) + + By default, notifications are *inhibited* if the foot window + has keyboard focus. See + _desktop-notifications.inhibit-when-focused_. + + Window activation (focusing) + Foot can focus the window when the notification is + "activated". This typically happens when the default action is + invoked, and/or when the notification is clicked, but exact + behavior depends on the notification daemon in use, and how it + has been configured. + + For this to work, foot needs an XDG activation token. To this + end, foot will read the command's stdout; everything printed + there, not including trailing newlines, are assumed to be part + of the activation token. There is no harm in printing + something else on stdout - it will simply result in the + activation failing (i.e. the window will not be focused). + + *Note*: notify-send does not, out of the box, support + reporting the XDG activation token in any way. This means + window activation will not work by default. + + Notification dismissal + The kitty desktop notifications protocol (OSC-99) allows the + terminal application to request an event be sent to it when + the notification has been dismissed (by setting *a=report* in + the notification request). + + To be able to send this event, foot needs to know when the + notification is dismissed. This is handled in a very simple + manner; the command signals notification dismissal by + exiting. That is, as soon as the command returns, foot + considers the notification dismissed. + + For *notify-send*, this can be achieved with the *--wait* + option. + + Default: _notify-send --wait --app-name ${app-id} --icon ${icon} --urgency ${urgency} -- ${title} ${body}_. + +*inhibit-when-focused* + Boolean. If enabled, foot will not display notifications if the + terminal window has keyboard focus. + + Default: _yes_ + # SECTION: scrollback *lines* diff --git a/foot.ini b/foot.ini index 7a1db9ba..33727dd3 100644 --- a/foot.ini +++ b/foot.ini @@ -29,8 +29,6 @@ # resize-by-cells=yes # resize-delay-ms=100 -# notify=notify-send -a ${app-id} -i ${app-id} -u ${urgency} ${title} ${body} - # bold-text-in-bright=no # word-delimiters=,│`|:"'()[]{}<> # selection-target=primary @@ -48,6 +46,11 @@ # command= # command-focused=no +[desktop-notifications] +# command=notify-send --wait --app-name ${app-id} --icon ${icon} --urgency ${urgency} -- ${title} ${body} +# inhibit-when-focused=yes + + [scrollback] # lines=1000 # multiplier=3.0 diff --git a/notify.c b/notify.c index 7cd22ccb..043f41a5 100644 --- a/notify.c +++ b/notify.c @@ -1,9 +1,11 @@ #include "notify.h" +#include <errno.h> #include <stdlib.h> #include <string.h> #include <unistd.h> +#include <sys/epoll.h> #include <sys/stat.h> #include <fcntl.h> @@ -13,50 +15,189 @@ #include "config.h" #include "spawn.h" #include "terminal.h" +#include "wayland.h" #include "xmalloc.h" +#include "xsnprintf.h" void -notify_notify(const struct terminal *term, const char *title, const char *body, - enum notify_when when, enum notify_urgency urgency) +notify_free(struct terminal *term, struct notification *notif) { - LOG_DBG("notify: title=\"%s\", msg=\"%s\"", title, body); + fdm_del(term->fdm, notif->stdout_fd); + free(notif->id); + free(notif->title); + free(notif->body); + free(notif->icon); + free(notif->xdg_token); +} - if ((term->conf->notify_focus_inhibit || when != NOTIFY_ALWAYS) +static bool +fdm_notify_stdout(struct fdm *fdm, int fd, int events, void *data) +{ + const struct terminal *term = data; + struct notification *notif = NULL; + + /* Find notification */ + tll_foreach(term->notifications, it) { + if (it->item.stdout_fd == fd) { + notif = &it->item; + break; + } + } + + if (events & EPOLLIN) { + char buf[512]; + ssize_t count = read(fd, buf, sizeof(buf) - 1); + + if (count < 0) { + if (errno == EINTR) + return true; + + LOG_ERRNO("failed to read notification activation token"); + return false; + } + + if (count > 0) { + buf[count - 1] = '\0'; + + if (notif != NULL) { + if (notif->xdg_token == NULL) { + notif->xdg_token = xstrdup(buf); + } else { + char *new_token = xstrjoin(notif->xdg_token, buf); + free(notif->xdg_token); + notif->xdg_token = new_token; + } + } + } + } + + if (events & EPOLLHUP) { + fdm_del(fdm, fd); + if (notif != NULL) + notif->stdout_fd = -1; + + /* Strip trailing newlines */ + if (notif != NULL && notif->xdg_token != NULL) { + size_t len = strlen(notif->xdg_token); + + while (len > 0 && notif->xdg_token[len - 1] == '\n') + len--; + + notif->xdg_token[len] = '\0'; + } + } + + return true; +} + +static void +notif_done(struct reaper *reaper, pid_t pid, int status, void *data) +{ + struct terminal *term = data; + + tll_foreach(term->notifications, it) { + struct notification *notif = &it->item; + if (notif->pid != pid) + continue; + + LOG_DBG("notification %s dismissed", notif->id); + + if (notif->focus) { + LOG_DBG("focus window on notification activation: \"%s\"", notif->xdg_token); + wayl_activate(term->wl, term->window, notif->xdg_token); + } + + if (notif->report) { + xassert(notif->id != NULL); + + LOG_DBG("sending notification report to client"); + + char reply[5 + strlen(notif->id) + 1 + 2 + 1]; + int n = xsnprintf( + reply, sizeof(reply), "\033]99;%s;\033\\", notif->id); + term_to_slave(term, reply, n); + } + + notify_free(term, notif); + tll_remove(term->notifications, it); + return; + } +} + +bool +notify_notify(const struct terminal *term, struct notification *notif) +{ + xassert(notif->xdg_token == NULL); + xassert(notif->pid == 0); + xassert(notif->stdout_fd == 0); + + notif->pid = -1; + notif->stdout_fd = -1; + + /* Use body as title, if title is unset */ + const char *title = notif->title != NULL ? notif->title : notif->body; + const char *body = notif->title != NULL && notif->body != NULL ? notif->body : ""; + + /* Icon: use symbolic name from notification, if present, + otherwise fallback to the application ID */ + const char *icon = notif->icon != NULL + ? notif->icon + : term->app_id != NULL + ? term->app_id + : term->conf->app_id; + + LOG_DBG("notify: title=\"%s\", body=\"%s\"", title, body); + + xassert(title != NULL); + if (title == NULL) + return false; + + if ((term->conf->desktop_notifications.inhibit_when_focused || + notif->when != NOTIFY_ALWAYS) && term->kbd_focus) { /* No notifications while we're focused */ - return; + return false; } - if (title == NULL || body == NULL) - return; - - if (term->conf->notify.argv.args == NULL) - return; + if (term->conf->desktop_notifications.command.argv.args == NULL) + return false; char **argv = NULL; size_t argc = 0; const char *urgency_str = - urgency == NOTIFY_URGENCY_LOW + notif->urgency == NOTIFY_URGENCY_LOW ? "low" - : urgency == NOTIFY_URGENCY_NORMAL + : notif->urgency == NOTIFY_URGENCY_NORMAL ? "normal" : "critical"; if (!spawn_expand_template( - &term->conf->notify, 5, - (const char *[]){"app-id", "window-title", "title", "body", "urgency"}, + &term->conf->desktop_notifications.command, 6, + (const char *[]){"app-id", "window-title", "icon", "title", "body", "urgency"}, (const char *[]){term->app_id ? term->app_id : term->conf->app_id, - term->window_title, title, body, urgency_str}, + term->window_title, icon, title, body, urgency_str}, &argc, &argv)) { - return; + return false; } LOG_DBG("notify command:"); for (size_t i = 0; i < argc; i++) LOG_DBG(" argv[%zu] = \"%s\"", i, argv[i]); + int stdout_fds[2] = {-1, -1}; + if (notif->focus && pipe2(stdout_fds, O_CLOEXEC | O_NONBLOCK) < 0) { + LOG_WARN("failed to create stdout pipe"); + /* Non-fatal */ + } + + if (stdout_fds[0] >= 0) { + xassert(notif->xdg_token == NULL); + fdm_add(term->fdm, stdout_fds[0], EPOLLIN, + &fdm_notify_stdout, (void *)term); + } + /* Redirect stdin to /dev/null, but ignore failure to open */ int devnull = open("/dev/null", O_RDONLY); pid_t pid = spawn( @@ -64,6 +205,14 @@ notify_notify(const struct terminal *term, const char *title, const char *body, ¬if_done, (void *)term, NULL); if (stdout_fds[1] >= 0) { + /* Close write-end of stdout pipe */ + close(stdout_fds[1]); + } + + if (pid < 0 && stdout_fds[0] >= 0) { + /* Remove FDM callback if we failed to spawn */ + fdm_del(term->fdm, stdout_fds[0]); + } if (devnull >= 0) close(devnull); @@ -71,4 +220,8 @@ notify_notify(const struct terminal *term, const char *title, const char *body, for (size_t i = 0; i < argc; i++) free(argv[i]); free(argv); + + notif->pid = pid; + notif->stdout_fd = stdout_fds[0]; + return true; } diff --git a/notify.h b/notify.h index be79b41a..6c43e294 100644 --- a/notify.h +++ b/notify.h @@ -1,30 +1,41 @@ #pragma once #include <stdbool.h> +#include <unistd.h> struct terminal; enum notify_when { + /* First, so that it can be left out of initializer and still be + the default */ NOTIFY_ALWAYS, + NOTIFY_UNFOCUSED, NOTIFY_INVISIBLE }; enum notify_urgency { - NOTIFY_URGENCY_LOW, + /* First, so that it can be left out of initializer and still be + the default */ NOTIFY_URGENCY_NORMAL, + + NOTIFY_URGENCY_LOW, NOTIFY_URGENCY_CRITICAL, }; -struct kitty_notification { +struct notification { char *id; char *title; char *body; + char *icon; + char *xdg_token; enum notify_when when; enum notify_urgency urgency; bool focus; bool report; + + pid_t pid; + int stdout_fd; }; -void notify_notify( - const struct terminal *term, const char *title, const char *body, - enum notify_when when, enum notify_urgency urgency); +bool notify_notify(const struct terminal *term, struct notification *notif); +void notify_free(struct terminal *term, struct notification *notif); diff --git a/osc.c b/osc.c index 7485cada..6926c3ca 100644 --- a/osc.c +++ b/osc.c @@ -560,9 +560,9 @@ osc_notify(struct terminal *term, char *string) return; } - notify_notify( - term, title, msg != NULL ? msg : "", - NOTIFY_ALWAYS, NOTIFY_URGENCY_NORMAL); + notify_notify(term, &(struct notification){ + .title = (char *)title, + .body = (char *)msg}); } static void @@ -571,16 +571,15 @@ kitty_notification(struct terminal *term, char *string) /* https://sw.kovidgoyal.net/kitty/desktop-notifications */ char *payload = strchr(string, ';'); - if (payload == NULL) { - LOG_ERR("OSC-99: payload missing"); + if (payload == NULL) return; - } char *parameters = string; *payload = '\0'; payload++; char *id = xstrdup("0"); /* The 'i' parameter */ + char *icon = NULL; /* The 'I' parameter */ bool focus = true; /* The 'a' parameter */ bool report = false; /* The 'a' parameter */ bool done = true; /* The 'd' parameter */ @@ -601,10 +600,8 @@ kitty_notification(struct terminal *term, char *string) { /* All parameters are on the form X=value, where X is always exactly one character */ - if (param[0] == '\0' || param[1] != '=') { - LOG_WARN("OSC-99: invalid parameter: \"%s\"", param); + if (param[0] == '\0' || param[1] != '=') continue; - } char *value = ¶m[2]; @@ -613,25 +610,19 @@ kitty_notification(struct terminal *term, char *string) /* notification activation action: focus|report|-focus|-report */ have_a = true; char *a_ctx = NULL; + for (const char *v = strtok_r(value, ",", &a_ctx); v != NULL; v = strtok_r(NULL, ",", &a_ctx)) { - LOG_WARN(" a: \"%s\"", v); bool reverse = v[0] == '-'; if (reverse) v++; - if (strcmp(v, "focus") == 0) { + if (strcmp(v, "focus") == 0) focus = !reverse; - if (focus) - LOG_WARN("unimplemented: OSC-99: focus on notification activation"); - } else if (strcmp(v, "report") == 0) { + else if (strcmp(v, "report") == 0) report = !reverse; - if (report) - LOG_WARN("unimplemented: OSC-99: report on notification activation"); - } else - LOG_WARN("OSC-99: unrecognized value for 'a': \"%s\", ignoring", v); } break; @@ -643,8 +634,6 @@ kitty_notification(struct terminal *term, char *string) done = false; else if (value[0] == '1' && value[1] == '\0') done = true; - else - LOG_WARN("OSC-99: unrecognized value for 'd': \"%s\", ignoring", value); break; case 'e': @@ -653,8 +642,6 @@ kitty_notification(struct terminal *term, char *string) base64 = false; else if (value[0] == '1' && value[1] == '\0') base64 = true; - else - LOG_WARN("OSC-99: unrecognized value for 'e': \"%s\", ignoring", value); break; case 'i': @@ -669,8 +656,6 @@ kitty_notification(struct terminal *term, char *string) payload_is_title = true; else if (strcmp(value, "body") == 0) payload_is_title = false; - else - LOG_WARN("OSC-99: unrecognized value for 'p': \"%s\", ignoring", value); break; case 'o': @@ -682,8 +667,6 @@ kitty_notification(struct terminal *term, char *string) when = NOTIFY_UNFOCUSED; else if (strcmp(value, "invisible") == 0) when = NOTIFY_INVISIBLE; - else - LOG_WARN("OSC-99: unrecognized value for 'o': \"%s\", ignoring", value); break; case 'u': @@ -695,13 +678,16 @@ kitty_notification(struct terminal *term, char *string) urgency = NOTIFY_URGENCY_NORMAL; else if (value[0] == '2' && value[1] == '\0') urgency = NOTIFY_URGENCY_CRITICAL; - else - LOG_WARN("OSC-99: unrecognized value for 'u': \"%s\", ignoring", value); break; - default: - LOG_WARN("OSC-99: unrecognized parameter: \"%s\", ignoring", param); - break; + /* + * The options below are not (yet) part of the official spec. + */ + case 'I': + /* icon: only symbolic names allowed; absolute paths are ignored */ + if (value[0] != '/') + icon = xstrdup(value); + break; } } @@ -710,9 +696,9 @@ kitty_notification(struct terminal *term, char *string) else payload = xstrdup(payload); - LOG_DBG("id=%s, done=%d, focus=%d, report=%d, base64=%d, payload: %s, " + LOG_DBG("id=%s, done=%d, focus=%d, report=%d, base64=%d, icon=%s, payload: %s, " "honor: %s, urgency: %s, %s: %s", - id, done, focus, report, base64, + id, done, focus, report, base64, icon != NULL ? icon : "<not set>", payload_is_title ? "title" : "body", (when == NOTIFY_ALWAYS ? "always" @@ -726,11 +712,10 @@ kitty_notification(struct terminal *term, char *string) payload_is_title ? "title" : "body", payload); /* Search for an existing (d=0) notification to update */ - struct kitty_notification *notif = NULL; - tll_foreach(term->kitty_notifications, it) { + struct notification *notif = NULL; + tll_foreach(term->notifications, it) { if (strcmp(it->item.id, id) == 0) { /* Found existing notification */ - LOG_WARN("found existing kitty notification"); notif = &it->item; break; } @@ -739,8 +724,9 @@ kitty_notification(struct terminal *term, char *string) if (notif == NULL) { /* Somewhat unoptimized... this will be free:d and removed immediately if d=1 */ - tll_push_front(term->kitty_notifications, ((struct kitty_notification){ + tll_push_front(term->notifications, ((struct notification){ .id = id, + .icon = NULL, .title = NULL, .body = NULL, .when = when, @@ -749,8 +735,13 @@ kitty_notification(struct terminal *term, char *string) .report = report, })); - id = NULL; /* Prevent double free */ - notif = &tll_front(term->kitty_notifications); + id = NULL; /* Prevent double free */ + notif = &tll_front(term->notifications); + } + + if (notif->pid > 0) { + /* Notification has already been completed, ignore new metadata */ + goto out; } /* Update notification metadata */ @@ -764,6 +755,12 @@ kitty_notification(struct terminal *term, char *string) if (have_u) notif->urgency = urgency; + if (icon != NULL) { + free(notif->icon); + notif->icon = icon; + icon = NULL; /* Prevent double free */ + } + if (payload_is_title) { if (notif->title == NULL) { notif->title = payload; @@ -784,26 +781,22 @@ kitty_notification(struct terminal *term, char *string) } } - free(id); - free(payload); - if (done) { - notify_notify( - term, - notif->title != NULL ? notif->title : notif->body, - notif->title != NULL && notif->body != NULL ? notif->body : "", - notif->when, notif->urgency); - - tll_foreach(term->kitty_notifications, it) { - if (&it->item == notif) { - free(it->item.id); - free(it->item.title); - free(it->item.body); - tll_remove(term->kitty_notifications, it); - break; + if (!notify_notify(term, notif)) { + tll_foreach(term->notifications, it) { + if (&it->item == notif) { + notify_free(term, &it->item); + tll_remove(term->notifications, it); + break; + } } } } + +out: + free(id); + free(icon); + free(payload); } void diff --git a/terminal.c b/terminal.c index bf0c78dc..a0be4518 100644 --- a/terminal.c +++ b/terminal.c @@ -1313,7 +1313,7 @@ term_init(const struct config *conf, struct fdm *fdm, struct reaper *reaper, #if defined(FOOT_IME_ENABLED) && FOOT_IME_ENABLED .ime_enabled = true, #endif - .kitty_notifications = tll_init(), + .notifications = tll_init(), }; pixman_region32_init(&term->render.last_overlay_clip); @@ -1818,11 +1818,9 @@ term_destroy(struct terminal *term) tll_remove(term->ptmx_paste_buffers, it); } - tll_foreach(term->kitty_notifications, it) { - free(it->item.id); - free(it->item.body); - free(it->item.title); - tll_remove(term->kitty_notifications, it); + tll_foreach(term->notifications, it) { + notify_free(term, &it->item); + tll_remove(term->notifications, it); } sixel_fini(term); @@ -2030,11 +2028,9 @@ term_reset(struct terminal *term, bool hard) tll_remove(term->alt.sixel_images, it); } - tll_foreach(term->kitty_notifications, it) { - free(it->item.id); - free(it->item.title); - free(it->item.body); - tll_remove(term->kitty_notifications, it); + tll_foreach(term->notifications, it) { + notify_free(term, &it->item); + tll_remove(term->notifications, it); } term->grapheme_shaping = term->conf->tweak.grapheme_shaping; @@ -3582,8 +3578,9 @@ term_bell(struct terminal *term) } if (term->conf->bell.notify) { - notify_notify(term, "Bell", "Bell in terminal", - NOTIFY_ALWAYS, NOTIFY_URGENCY_NORMAL); + notify_notify(term, &(struct notification){ + .title = (char *)"Bell", + .body = (char *)"Bell in terminal"}); } if (term->conf->bell.flash) diff --git a/terminal.h b/terminal.h index 65dde151..0967bf14 100644 --- a/terminal.h +++ b/terminal.h @@ -799,7 +799,9 @@ struct terminal { void *cb_data; } shutdown; - tll(struct kitty_notification) kitty_notifications; + /* Notifications that either haven't been sent yet, or have been + sent but not yet dismissed */ + tll(struct notification) notifications; char *foot_exe; char *cwd; diff --git a/tests/test-config.c b/tests/test-config.c index 4a0fd755..ec718c24 100644 --- a/tests/test-config.c +++ b/tests/test-config.c @@ -511,7 +511,7 @@ test_section_main(void) test_boolean(&ctx, &parse_section_main, "login-shell", &conf.login_shell); test_boolean(&ctx, &parse_section_main, "box-drawings-uses-font-glyphs", &conf.box_drawings_uses_font_glyphs); test_boolean(&ctx, &parse_section_main, "locked-title", &conf.locked_title); - test_boolean(&ctx, &parse_section_main, "notify-focus-inhibit", &conf.notify_focus_inhibit); + test_boolean(&ctx, &parse_section_main, "notify-focus-inhibit", &conf.desktop_notifications.inhibit_when_focused); /* Deprecated */ test_boolean(&ctx, &parse_section_main, "dpi-aware", &conf.dpi_aware); test_pt_or_px(&ctx, &parse_section_main, "font-size-adjustment", &conf.font_size_adjustment.pt_or_px); /* TODO: test ‘N%’ values too */ @@ -524,7 +524,7 @@ test_section_main(void) test_uint16(&ctx, &parse_section_main, "resize-delay-ms", &conf.resize_delay_ms); test_uint16(&ctx, &parse_section_main, "workers", &conf.render_worker_count); - test_spawn_template(&ctx, &parse_section_main, "notify", &conf.notify); + test_spawn_template(&ctx, &parse_section_main, "notify", &conf.desktop_notifications.command); /* Deprecated */ test_enum(&ctx, &parse_section_main, "selection-target", 4, @@ -570,6 +570,20 @@ test_section_bell(void) config_free(&conf); } +static void +test_section_desktop_notifications(void) +{ + struct config conf = {0}; + struct context ctx = {.conf = &conf, .section = "desktop-notifications", .path = "unittest"}; + + test_invalid_key(&ctx, &parse_section_desktop_notifications, "invalid-key"); + + test_boolean(&ctx, &parse_section_desktop_notifications, "inhibit-when-focused", &conf.desktop_notifications.inhibit_when_focused); + test_spawn_template(&ctx, &parse_section_desktop_notifications, "command", &conf.desktop_notifications.command); + + config_free(&conf); +} + static void test_section_scrollback(void) { @@ -1391,6 +1405,7 @@ main(int argc, const char *const *argv) log_init(LOG_COLORIZE_AUTO, false, 0, LOG_CLASS_ERROR); test_section_main(); test_section_bell(); + test_section_desktop_notifications(); test_section_scrollback(); test_section_url(); test_section_cursor();