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