diff --git a/CHANGELOG.md b/CHANGELOG.md index dfa2b9ad..65ddfc3b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -70,6 +70,13 @@ * Support for [in-band window resize notifications](https://gist.github.com/rockorager/e695fb2924d36b2bcf1fff4a3704bd83), private mode `2048`. +* Support for OSC-99 [_"Kitty desktop + notifications"_](https://sw.kovidgoyal.net/kitty/desktop-notifications/). +* `desktop-notifications.command` option, replaces `notify`. +* `desktop-notifications.inhibit-when-focused` option, replaces + `notify-focus-inhibit`. +* `${icon}`, `${urgency}`,`${action-name}` and `${action-label}` added + to the `desktop-notifications.command` template. [1707]: https://codeberg.org/dnkl/foot/issues/1707 [1738]: https://codeberg.org/dnkl/foot/issues/1738 @@ -116,6 +123,12 @@ ### Deprecated + +* `notify` option; replaced by `desktop-notifications.command`. +* `notify-focus-inhibit` option; replaced by + `desktop-notifications.inhibit-when-focused`. + + ### Removed ### Fixed diff --git a/base64.c b/base64.c index 5d01ab07..db697cb0 100644 --- a/base64.c +++ b/base64.c @@ -42,7 +42,7 @@ static const char lookup[64] = { }; char * -base64_decode(const char *s) +base64_decode(const char *s, size_t *size) { const size_t len = strlen(s); if (unlikely(len % 4 != 0)) { @@ -54,6 +54,9 @@ base64_decode(const char *s) if (unlikely(ret == NULL)) return NULL; + if (unlikely(size != NULL)) + *size = len / 4 * 3; + for (size_t i = 0, o = 0; i < len; i += 4, o += 3) { unsigned a = reverse_lookup[(unsigned char)s[i + 0]]; unsigned b = reverse_lookup[(unsigned char)s[i + 1]]; @@ -68,6 +71,13 @@ base64_decode(const char *s) if (unlikely(i + 4 != len || (a | b) & P || (c & P && !(d & P)))) goto invalid; + if (unlikely(size != NULL)) { + if (c & P) + *size = len / 4 * 3 - 2; + else + *size = len / 4 * 3 - 1; + } + c &= 63; d &= 63; } diff --git a/base64.h b/base64.h index d4042512..3fa3d078 100644 --- a/base64.h +++ b/base64.h @@ -3,6 +3,6 @@ #include #include -char *base64_decode(const char *s); +char *base64_decode(const char *s, size_t *out_len); char *base64_encode(const uint8_t *data, size_t size); void base64_encode_final(const uint8_t *data, size_t size, char result[4]); diff --git a/config.c b/config.c index 5d9a0b75..664474ca 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, @@ -3184,8 +3224,9 @@ config_load(struct config *conf, const char *conf_path, memcpy(conf->colors.table, default_color_table, sizeof(default_color_table)); parse_modifiers(XKB_MOD_NAME_SHIFT, 5, &conf->mouse.selection_override_modifiers); - tokenize_cmdline("notify-send -a ${app-id} -i ${app-id} ${title} ${body}", - &conf->notify.argv.args); + tokenize_cmdline( + "notify-send --wait --app-name ${app-id} --icon ${icon} --urgency ${urgency} --action ${action-name}=${action-label} --print-id -- ${title} ${body}", + &conf->desktop_notifications.command.argv.args); tokenize_cmdline("xdg-open ${url}", &conf->url.launch.argv.args); static const char32_t *url_protocols[] = { @@ -3438,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]); @@ -3520,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 680768e0..72e6d052 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} ${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,106 @@ 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'. It can also send an event back to the client + application, notifying it that the notification has been + '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 to know when the notification was + activated (as opposed to just dismissed), and it needs an XDG + activation token. + + There are two parts to handle this. First, the notification + must define an action. For this purpose, foot definse the + template parameters *${action-name}* and + *${action-label}*. They are intended to be used with + e.g. notify-send's *-A,--action* option. + + Second, foot needs to know when the notification activated, + and it needs to get hold of the XDG activation token. + + Both are expected to be printed on stdout. + + Foot expects the action name (not label) to be printed on a + single line. No prefix, no postfix. + + Foot expects the activation token to be printed on a single + line, prefixed with *xdgtoken=*. + + Example: + default + xdgtoken=18179adf579a7a904ce73754964b1ec3 + + The expected format of stdout may change at any time. Please + read the changelog when upgrading foot. + + *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. + + Stdout + Foot recognizes the following things from the notification + helper's stdout: + + - _nnn_: integer in base 10, daemon assigned notification ID + - _id=nnn_: same as plain _nnn_. + - _default_: the 'default' action was triggered + - _action=default_: same as _default_ + - _xdgtoken=xyz_: XDG activation token. + + Example: + 17++ +action=default++ +xdgtoken=95ebdfe56e4f47ddb5bba9d7dc3a2c35 + + Default: _notify-send --wait --app-name ${app-id} --icon ${icon} --urgency ${urgency} --action ${action-name}=${action-label} --print-id -- ${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 7ae9ba1b..46b2d5f0 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} ${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} --action ${action-name}=${action-label} --print-id -- ${title} ${body} +# inhibit-when-focused=yes + + [scrollback] # lines=1000 # multiplier=3.0 diff --git a/input.c b/input.c index dc588f75..98f5c89c 100644 --- a/input.c +++ b/input.c @@ -306,8 +306,8 @@ execute_binding(struct seat *seat, struct terminal *term, } } - if (!spawn(term->reaper, term->cwd, binding->aux->pipe.args, - pipe_fd[0], stdout_fd, stderr_fd, NULL)) + if (spawn(term->reaper, term->cwd, binding->aux->pipe.args, + pipe_fd[0], stdout_fd, stderr_fd, NULL, NULL, NULL) < 0) goto pipe_err; /* Close read end */ diff --git a/notify.c b/notify.c index 7a208479..05ef6f58 100644 --- a/notify.c +++ b/notify.c @@ -1,54 +1,322 @@ #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> #define LOG_MODULE "notify" -#define LOG_ENABLE_DBG 0 +#define LOG_ENABLE_DBG 1 #include "log.h" #include "config.h" #include "spawn.h" +#include "terminal.h" +#include "util.h" +#include "wayland.h" #include "xmalloc.h" +#include "xsnprintf.h" void -notify_notify(const struct terminal *term, const char *title, const char *body) +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_id); + free(notif->icon_symbolic_name); + free(notif->icon_data); + free(notif->xdg_token); + free(notif->stdout_data); +} - if (term->conf->notify_focus_inhibit && term->kbd_focus) { - /* No notifications while we're focused */ - return; +static bool +to_integer(const char *line, size_t len, uint32_t *res) +{ + bool is_id = true; + uint32_t maybe_id = 0; + + for (size_t i = 0; i < len; i++) { + char digit = line[i]; + if (digit < '0' || digit > '9') { + is_id = false; + break; + } + + maybe_id *= 10; + maybe_id += digit - '0'; } - if (title == NULL || body == NULL) - return; + *res = maybe_id; + return is_id; +} - if (term->conf->notify.argv.args == NULL) +static void +consume_stdout(struct notification *notif, bool eof) +{ + char *data = notif->stdout_data; + const char *line = data; + size_t left = notif->stdout_sz; + + /* Process stdout, line-by-line */ + while (left > 0) { + line = data; + size_t len = left; + char *eol = memchr(line, '\n', left); + + if (eol != NULL) { + *eol = '\0'; + len = strlen(line); + data = eol + 1; + } else if (!eof) + break; + + uint32_t maybe_id = 0; + + /* Check for daemon assigned ID, either '123', or 'id=123' */ + if (to_integer(line, len, &maybe_id) || + (len > 3 && memcmp(line, "id=", 3) == 0 && + to_integer(&line[3], len - 3, &maybe_id))) + { + notif->external_id = maybe_id; + LOG_DBG("external ID: %u", notif->external_id); + } + + /* Check for triggered action, either 'default' or 'action=default' */ + else if ((len == 7 && memcmp(line, "default", 7) == 0) || + (len == 7 + 7 && memcmp(line, "action=default", 7 + 7) == 0)) + { + notif->activated = true; + LOG_DBG("notification's default action was triggered"); + } + + /* Check for XDG activation token, 'xdgtoken=xyz' */ + else if (len > 9 && memcmp(line, "xdgtoken=", 9) == 0) { + notif->xdg_token = xstrndup(&line[9], len - 9); + LOG_DBG("XDG token: \"%s\"", notif->xdg_token); + } + + left -= len + (eol != NULL ? 1 : 0); + } + + if (left > 0) + memmove(notif->stdout_data, data, left); + + notif->stdout_sz = left; +} + +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->active_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 && notif != NULL) { + if (notif->stdout_data == NULL) { + xassert(notif->stdout_sz == 0); + notif->stdout_data = xmemdup(buf, count); + } else { + notif->stdout_data = xrealloc(notif->stdout_data, notif->stdout_sz + count); + memcpy(¬if->stdout_data[notif->stdout_sz], buf, count); + } + + notif->stdout_sz += count; + consume_stdout(notif, false); + } + } + + if (events & EPOLLHUP) { + fdm_del(fdm, fd); + if (notif != NULL) { + notif->stdout_fd = -1; + consume_stdout(notif, true); + } + } + + return true; +} + +static void +notif_done(struct reaper *reaper, pid_t pid, int status, void *data) +{ + struct terminal *term = data; + + tll_foreach(term->active_notifications, it) { + struct notification *notif = &it->item; + if (notif->pid != pid) + continue; + + LOG_DBG("notification %s dismissed", notif->id); + + if (notif->activated && notif->focus) { + LOG_DBG("focus window on notification activation: \"%s\"", + notif->xdg_token); + wayl_activate(term->wl, term->window, notif->xdg_token); + } + + if (notif->activated && 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->active_notifications, it); return; + } +} + +bool +notify_notify(struct terminal *term, struct notification *notif) +{ + xassert(notif->xdg_token == NULL); + xassert(notif->pid == 0); + xassert(notif->stdout_fd <= 0); + xassert(notif->stdout_data == NULL); + + 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_name_or_path = term->app_id != NULL + ? term->app_id + : term->conf->app_id; + + if (notif->icon_id != NULL) { + for (size_t i = 0; i < ALEN(term->notification_icons); i++) { + const struct notification_icon *icon = &term->notification_icons[i]; + + if (icon->id != NULL && streq(icon->id, notif->icon_id)) { + icon_name_or_path = icon->symbolic_name != NULL + ? icon->symbolic_name + : icon->tmp_file_name; + break; + } + } + } else if (notif->icon_symbolic_name != NULL) { + icon_name_or_path = notif->icon_symbolic_name; + } + + bool track_notification = notif->focus || notif->report; + + LOG_DBG("notify: title=\"%s\", body=\"%s\", icon=\"%s\" (tracking: %s)", + title, body, icon_name_or_path, track_notification ? "yes" : "no"); + + 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 false; + } + + if (term->conf->desktop_notifications.command.argv.args == NULL) + return false; char **argv = NULL; size_t argc = 0; + const char *urgency_str = + notif->urgency == NOTIFY_URGENCY_LOW + ? "low" + : notif->urgency == NOTIFY_URGENCY_NORMAL + ? "normal" : "critical"; + if (!spawn_expand_template( - &term->conf->notify, 4, - (const char *[]){"app-id", "window-title", "title", "body"}, - (const char *[]){term->app_id ? term->app_id : term->conf->app_id, term->window_title, title, body}, - &argc, &argv)) + &term->conf->desktop_notifications.command, 8, + (const char *[]){ + "app-id", "window-title", "icon", "title", "body", "urgency", "action-name", "action-label"}, + (const char *[]){ + term->app_id ? term->app_id : term->conf->app_id, + term->window_title, icon_name_or_path, title, body, urgency_str, + "default", "Click to activate"}, + &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 (track_notification) { + if (pipe2(stdout_fds, O_CLOEXEC | O_NONBLOCK) < 0) { + LOG_WARN("failed to create stdout pipe"); + track_notification = false; + /* Non-fatal */ + } else { + tll_push_back(term->active_notifications, *notif); + notif->id = NULL; + notif->title = NULL; + notif->body = NULL; + notif->icon_id = NULL; + notif->icon_symbolic_name = NULL; + notif->icon_data = NULL; + notif->icon_data_sz = 0; + notif = &tll_back(term->active_notifications); + } + } + + + if (stdout_fds[0] >= 0) { + 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); - spawn(term->reaper, NULL, argv, devnull, -1, -1, NULL); + pid_t pid = spawn( + term->reaper, NULL, argv, devnull, stdout_fds[1], -1, + track_notification ? ¬if_done : NULL, (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); @@ -56,4 +324,115 @@ 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; +} + +static void +add_icon(struct notification_icon *icon, const char *id, const char *symbolic_name, + const uint8_t *data, size_t data_sz) +{ + icon->id = xstrdup(id); + icon->symbolic_name = symbolic_name != NULL ? xstrdup(symbolic_name) : NULL; + icon->tmp_file_name = NULL; + icon->tmp_file_fd = -1; + + /* + * Dump in-line data to a temporary file. This allows us to pass + * the filename as a parameter to notification helpers + * (i.e. notify-send -i <path>). + * + * Optimization: since we always prefer (i.e. use) the symbolic + * name if present, there's no need to create a file on disk if we + * have a symbolic name. + */ + if (symbolic_name == NULL && data_sz > 0) { + char name[64] = "/tmp/foot-notification-icon-cache-XXXXXX"; + int fd = mkostemp(name, O_CLOEXEC); + + if (fd < 0) { + LOG_ERRNO("failed to create temporary file for icon cache"); + return; + } + + if (write(fd, data, data_sz) != (ssize_t)data_sz) { + LOG_ERRNO("failed to write icon data to temporary file"); + close(fd); + } else { + LOG_DBG("wrote icon data to %s", name); + icon->tmp_file_name = xstrdup(name); + icon->tmp_file_fd = fd; + } + } + + LOG_DBG("added icon to cache: ID=%s: sym=%s, file=%s", + icon->id, icon->symbolic_name, icon->tmp_file_name); +} + +void +notify_icon_add(struct terminal *term, const char *id, + const char *symbolic_name, const uint8_t *data, size_t data_sz) +{ +#if defined(_DEBUG) + for (size_t i = 0; i < ALEN(term->notification_icons); i++) { + struct notification_icon *icon = &term->notification_icons[i]; + if (icon->id != NULL && streq(icon->id, id)) { + BUG("notification icon cache already contains \"%s\"", id); + } + } +#endif + + for (size_t i = 0; i < ALEN(term->notification_icons); i++) { + struct notification_icon *icon = &term->notification_icons[i]; + if (icon->id == NULL) { + add_icon(icon, id, symbolic_name, data, data_sz); + return; + } + } + + /* Cache full - throw out first entry, add new entry last */ + notify_icon_free(&term->notification_icons[0]); + memmove(&term->notification_icons[0], + &term->notification_icons[1], + ((ALEN(term->notification_icons) - 1) * + sizeof(term->notification_icons[0]))); + + add_icon( + &term->notification_icons[ALEN(term->notification_icons) - 1], + id, symbolic_name, data, data_sz); +} + +void +notify_icon_del(struct terminal *term, const char *id) +{ + for (size_t i = 0; i < ALEN(term->notification_icons); i++) { + struct notification_icon *icon = &term->notification_icons[i]; + + if (icon->id == NULL || !streq(icon->id, id)) + continue; + + LOG_DBG("expelled %s from the notification icon cache", icon->id); + notify_icon_free(icon); + return; + } +} + +void +notify_icon_free(struct notification_icon *icon) +{ + if (icon->tmp_file_fd >= 0) + close(icon->tmp_file_fd); + if (icon->tmp_file_name != NULL) + unlink(icon->tmp_file_name); + + free(icon->id); + free(icon->symbolic_name); + free(icon->tmp_file_name); + + icon->id = NULL; + icon->symbolic_name = NULL; + icon->tmp_file_name = NULL; + icon->tmp_file_fd = -1; } diff --git a/notify.h b/notify.h index ce60562f..231495dc 100644 --- a/notify.h +++ b/notify.h @@ -1,6 +1,73 @@ #pragma once +#include <stdbool.h> +#include <stdint.h> +#include <unistd.h> -#include "terminal.h" +struct terminal; -void notify_notify( - const struct terminal *term, const char *title, const char *body); +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 { + /* 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 notification { + /* + * Set by caller of notify_notify() + */ + char *id; + char *title; + char *body; + + char *icon_id; + char *icon_symbolic_name; + uint8_t *icon_data; + size_t icon_data_sz; + + enum notify_when when; + enum notify_urgency urgency; + bool focus; + bool report; + + /* + * Used internally by notify + */ + + uint32_t external_id; /* Daemon assigned notification ID */ + bool activated; /* User 'activated' the notification */ + char *xdg_token; /* XDG activation token, from daemon */ + + pid_t pid; /* Notifier command PID */ + int stdout_fd; /* Notifier command's stdout */ + + char *stdout_data; /* Data we've reado from command's stdout */ + size_t stdout_sz; +}; + +struct notification_icon { + char *id; + char *symbolic_name; + char *tmp_file_name; + int tmp_file_fd; +}; + +bool notify_notify(struct terminal *term, struct notification *notif); +void notify_free(struct terminal *term, struct notification *notif); + +void notify_icon_add(struct terminal *term, const char *id, + const char *symbolic_name, const uint8_t *data, + size_t data_sz); +void notify_icon_del(struct terminal *term, const char *id); +void notify_icon_free(struct notification_icon *icon); diff --git a/osc.c b/osc.c index a3dc1715..19d9f097 100644 --- a/osc.c +++ b/osc.c @@ -12,15 +12,12 @@ #include "log.h" #include "base64.h" #include "config.h" -#include "grid.h" #include "macros.h" #include "notify.h" -#include "render.h" #include "selection.h" #include "terminal.h" #include "uri.h" #include "util.h" -#include "vt.h" #include "xmalloc.h" #include "xsnprintf.h" @@ -67,7 +64,7 @@ osc_to_clipboard(struct terminal *term, const char *target, return; } - char *decoded = base64_decode(base64_data); + char *decoded = base64_decode(base64_data, NULL); if (decoded == NULL) { if (errno == EINVAL) LOG_WARN("OSC: invalid clipboard data: %s", base64_data); @@ -560,7 +557,306 @@ osc_notify(struct terminal *term, char *string) return; } - notify_notify(term, title, msg != NULL ? msg : ""); + notify_notify(term, &(struct notification){ + .title = (char *)title, + .body = (char *)msg}); +} + +static void +kitty_notification(struct terminal *term, char *string) +{ + /* https://sw.kovidgoyal.net/kitty/desktop-notifications */ + + char *payload_raw = strchr(string, ';'); + if (payload_raw == NULL) + return; + + char *parameters = string; + *payload_raw = '\0'; + payload_raw++; + + char *id = xstrdup("0"); /* The 'i' parameter */ + char *icon_id = NULL; /* The 'g' parameter */ + char *symbolic_icon = NULL; /* The 'n' parameter */ + char *payload = NULL; + + bool focus = true; /* The 'a' parameter */ + bool report = false; /* The 'a' parameter */ + bool done = true; /* The 'd' parameter */ + bool base64 = false; /* The 'e' parameter */ + + size_t payload_size; + enum { + PAYLOAD_TITLE, + PAYLOAD_BODY, + PAYLOAD_ICON, + } payload_type = PAYLOAD_TITLE; /* The 'p' parameter */ + + enum notify_when when = NOTIFY_ALWAYS; + enum notify_urgency urgency = NOTIFY_URGENCY_NORMAL; + + bool have_a = false; + bool have_o = false; + bool have_u = false; + + char *ctx = NULL; + for (char *param = strtok_r(parameters, ":", &ctx); + param != NULL; + param = strtok_r(NULL, ":", &ctx)) + { + /* All parameters are on the form X=value, where X is always + exactly one character */ + if (param[0] == '\0' || param[1] != '=') + continue; + + char *value = ¶m[2]; + + switch (param[0]) { + case 'a': { + /* 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)) + { + bool reverse = v[0] == '-'; + if (reverse) + v++; + + if (streq(v, "focus")) + focus = !reverse; + else if (streq(v, "report")) + report = !reverse; + } + + break; + } + + case 'd': + /* done: 0|1 */ + if (value[0] == '0' && value[1] == '\0') + done = false; + else if (value[0] == '1' && value[1] == '\0') + done = true; + break; + + case 'e': + /* base64 (payload encoding): 0=utf8, 1=base64(utf8) */ + if (value[0] == '0' && value[1] == '\0') + base64 = false; + else if (value[0] == '1' && value[1] == '\0') + base64 = true; + break; + + case 'i': + /* id */ + free(id); + id = xstrdup(value); + break; + + case 'p': + /* payload content: title|body */ + if (streq(value, "title")) + payload_type = PAYLOAD_TITLE; + else if (streq(value, "body")) + payload_type = PAYLOAD_BODY; + else if (streq(value, "icon")) + payload_type = PAYLOAD_ICON; + else if (streq(value, "?")) { + /* Query capabilities */ + + char when_str[64]; + strcpy(when_str, "unfocused"); + if (!term->conf->desktop_notifications.inhibit_when_focused) + strcat(when_str, ",always"); + + const char *terminator = term->vt.osc.bel ? "\a" : "\033\\"; + + char reply[128]; + int n = xsnprintf( + reply, sizeof(reply), + "\033]99;i=%s:p=?;p=title,body,icon:a=focus,report:o=%s:u=0,1,2%s", + id, when_str, terminator); + + term_to_slave(term, reply, n); + goto out; + } + break; + + case 'o': + /* honor when: always|unfocused|invisible */ + have_o = true; + if (streq(value, "always")) + when = NOTIFY_ALWAYS; + else if (streq(value, "unfocused")) + when = NOTIFY_UNFOCUSED; + else if (streq(value, "invisible")) + when = NOTIFY_INVISIBLE; + break; + + case 'u': + /* urgency: 0=low, 1=normal, 2=critical */ + have_u = true; + if (value[0] == '0' && value[1] == '\0') + urgency = NOTIFY_URGENCY_LOW; + else if (value[0] == '1' && value[1] == '\0') + urgency = NOTIFY_URGENCY_NORMAL; + else if (value[0] == '2' && value[1] == '\0') + urgency = NOTIFY_URGENCY_CRITICAL; + break; + + case 'g': + /* graphical ID */ + free(icon_id); + icon_id = xstrdup(value); + break; + + case 'n': + /* Symbolic icon name, used with 'g' */ + free(symbolic_icon); + symbolic_icon = xstrdup(value); + break; + } + } + + if (base64) { + payload = base64_decode(payload_raw, &payload_size); + if (payload == NULL) + goto out; + } else { + payload = xstrdup(payload_raw); + payload_size = strlen(payload); + } + + /* Search for an existing (d=0) notification to update */ + struct notification *notif = NULL; + tll_foreach(term->kitty_notifications, it) { + if (streq(it->item.id, id)) { + /* Found existing notification */ + notif = &it->item; + break; + } + } + + if (notif == NULL) { + tll_push_front(term->kitty_notifications, ((struct notification){ + .id = id, + .when = when, + .urgency = urgency, + .focus = focus, + .report = report, + .stdout_fd = -1, + })); + + id = NULL; /* Prevent double free */ + notif = &tll_front(term->kitty_notifications); + } + + if (notif->pid > 0) { + /* Notification has already been completed, ignore new metadata */ + goto out; + } + + /* Update notification metadata */ + if (have_a) { + notif->focus = focus; + notif->report = report; + } + + if (have_o) + notif->when = when; + if (have_u) + notif->urgency = urgency; + + if (icon_id != NULL) { + free(notif->icon_id); + notif->icon_id = icon_id; + icon_id = NULL; /* Prevent double free */ + } + + if (symbolic_icon != NULL) { + free(notif->icon_symbolic_name); + notif->icon_symbolic_name = symbolic_icon; + symbolic_icon = NULL; + } + + /* Handled chunked payload - append to existing metadata */ + switch (payload_type) { + case PAYLOAD_TITLE: + case PAYLOAD_BODY: { + char **ptr = payload_type == PAYLOAD_TITLE + ? ¬if->title + : ¬if->body; + + if (*ptr == NULL) { + *ptr = payload; + payload = NULL; + } else { + char *old = *ptr; + *ptr = xstrjoin(old, payload); + free(old); + } + break; + } + + case PAYLOAD_ICON: + if (notif->icon_data == NULL) { + notif->icon_data = (uint8_t *)payload; + notif->icon_data_sz = payload_size; + payload = NULL; + } else { + notif->icon_data = xrealloc( + notif->icon_data, notif->icon_data_sz + payload_size); + memcpy(¬if->icon_data[notif->icon_data_sz], payload, payload_size); + notif->icon_data_sz += payload_size; + } + break; + } + + if (done) { + /* Update icon cache, if necessary */ + if (notif->icon_id != NULL && + (notif->icon_symbolic_name != NULL || notif->icon_data != NULL)) + { + notify_icon_del(term, notif->icon_id); + notify_icon_add(term, notif->icon_id, + notif->icon_symbolic_name, + notif->icon_data, notif->icon_data_sz); + + /* Don't need this anymore */ + free(notif->icon_symbolic_name); + free(notif->icon_data); + notif->icon_symbolic_name = NULL; + notif->icon_data = NULL; + notif->icon_data_sz = 0; + } + + /* + * Show notification. + * + * The checks for title|body is to handle notifications that + * only load icon data into the icon cache + */ + if (notif->title != NULL || notif->body != NULL) { + notify_notify(term, notif); + } + + tll_foreach(term->kitty_notifications, it) { + if (&it->item == notif) { + notify_free(term, &it->item); + tll_remove(term->kitty_notifications, it); + break; + } + } + } + +out: + free(id); + free(icon_id); + free(symbolic_icon); + free(payload); } void @@ -780,6 +1076,10 @@ osc_dispatch(struct terminal *term) osc_selection(term, string); break; + case 99: /* Kitty notifications */ + kitty_notification(term, string); + break; + case 104: { /* Reset Color Number 'c' (whole table if no parameter) */ diff --git a/pgo/pgo.c b/pgo/pgo.c index 204c024d..6dc0dd10 100644 --- a/pgo/pgo.c +++ b/pgo/pgo.c @@ -100,12 +100,12 @@ void wayl_win_alpha_changed(struct wl_window *win) {} bool wayl_win_set_urgent(struct wl_window *win) { return true; } bool wayl_fractional_scaling(const struct wayland *wayl) { return true; } -bool +pid_t spawn(struct reaper *reaper, const char *cwd, char *const argv[], int stdin_fd, int stdout_fd, int stderr_fd, - const char *xdg_activation_token) + reaper_cb cb, void *cb_data, const char *xdg_activation_token) { - return true; + return 2; } pid_t @@ -151,8 +151,31 @@ void ime_enable(struct seat *seat) {} void ime_disable(struct seat *seat) {} void ime_reset_preedit(struct seat *seat) {} +bool +notify_notify(struct terminal *term, struct notification *notif) +{ + return true; +} + void -notify_notify(const struct terminal *term, const char *title, const char *body) +notify_free(struct terminal *term, struct notification *notif) +{ +} + +void +notify_icon_add(struct terminal *term, const char *id, + const char *symbolic_name, const uint8_t *data, + size_t data_sz) +{ +} + +void +notify_icon_del(struct terminal *term, const char *id) +{ +} + +void +notify_icon_free(struct notification_icon *icon) { } diff --git a/spawn.c b/spawn.c index 6935a29a..17c821b5 100644 --- a/spawn.c +++ b/spawn.c @@ -15,9 +15,9 @@ #include "debug.h" #include "xmalloc.h" -bool +pid_t spawn(struct reaper *reaper, const char *cwd, char *const argv[], - int stdin_fd, int stdout_fd, int stderr_fd, + int stdin_fd, int stdout_fd, int stderr_fd, reaper_cb cb, void *cb_data, const char *xdg_activation_token) { int pipe_fds[2] = {-1, -1}; @@ -104,16 +104,16 @@ spawn(struct reaper *reaper, const char *cwd, char *const argv[], close(pipe_fds[0]); if (ret == 0) { - reaper_add(reaper, pid, NULL, NULL); - return true; + reaper_add(reaper, pid, cb, cb_data); + return pid; } else if (ret < 0) { LOG_ERRNO("failed to read from pipe"); - return false; + return -1; } else { LOG_ERRNO_P(errno_copy, "%s: failed to spawn", argv[0]); errno = errno_copy; waitpid(pid, NULL, 0); - return false; + return -1; } err: @@ -121,7 +121,7 @@ err: close(pipe_fds[0]); if (pipe_fds[1] != -1) close(pipe_fds[1]); - return false; + return -1; } bool diff --git a/spawn.h b/spawn.h index 0fc95041..1693f1a8 100644 --- a/spawn.h +++ b/spawn.h @@ -1,12 +1,14 @@ #pragma once #include <stdbool.h> +#include <unistd.h> + #include "config.h" #include "reaper.h" -bool spawn(struct reaper *reaper, const char *cwd, char *const argv[], - int stdin_fd, int stdout_fd, int stderr_fd, - const char *xdg_activation_token); +pid_t spawn(struct reaper *reaper, const char *cwd, char *const argv[], + int stdin_fd, int stdout_fd, int stderr_fd, + reaper_cb cb, void *cb_data, const char *xdg_activation_token); bool spawn_expand_template( const struct config_spawn_template *template, diff --git a/terminal.c b/terminal.c index 35cfe8aa..dc4f37b6 100644 --- a/terminal.c +++ b/terminal.c @@ -198,7 +198,7 @@ add_utmp_record(const struct config *conf, struct reaper *reaper, int ptmx) return true; char *const argv[] = {conf->utmp_helper_path, UTMP_ADD, getenv("WAYLAND_DISPLAY"), NULL}; - return spawn(reaper, NULL, argv, ptmx, ptmx, -1, NULL); + return spawn(reaper, NULL, argv, ptmx, ptmx, -1, NULL, NULL, NULL) >= 0; #else return true; #endif @@ -222,7 +222,7 @@ del_utmp_record(const struct config *conf, struct reaper *reaper, int ptmx) ; char *const argv[] = {conf->utmp_helper_path, UTMP_DEL, del_argument, NULL}; - return spawn(reaper, NULL, argv, ptmx, ptmx, -1, NULL); + return spawn(reaper, NULL, argv, ptmx, ptmx, -1, NULL, NULL, NULL) >= 0; #else return true; #endif @@ -1313,6 +1313,8 @@ 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(), + .active_notifications = tll_init(), }; pixman_region32_init(&term->render.last_overlay_clip); @@ -1817,6 +1819,19 @@ term_destroy(struct terminal *term) tll_remove(term->ptmx_paste_buffers, it); } + tll_foreach(term->kitty_notifications, it) { + notify_free(term, &it->item); + tll_remove(term->kitty_notifications, it); + } + + tll_foreach(term->active_notifications, it) { + notify_free(term, &it->item); + tll_remove(term->active_notifications, it); + } + + for (size_t i = 0; i < ALEN(term->notification_icons); i++) + notify_icon_free(&term->notification_icons[i]); + sixel_fini(term); term_ime_reset(term); @@ -2022,6 +2037,19 @@ term_reset(struct terminal *term, bool hard) tll_remove(term->alt.sixel_images, it); } + tll_foreach(term->kitty_notifications, it) { + notify_free(term, &it->item); + tll_remove(term->kitty_notifications, it); + } + + tll_foreach(term->active_notifications, it) { + notify_free(term, &it->item); + tll_remove(term->active_notifications, it); + } + + for (size_t i = 0; i < ALEN(term->notification_icons); i++) + notify_icon_free(&term->notification_icons[i]); + term->grapheme_shaping = term->conf->tweak.grapheme_shaping; #if defined(FOOT_IME_ENABLED) && FOOT_IME_ENABLED @@ -3566,8 +3594,11 @@ term_bell(struct terminal *term) } } - if (term->conf->bell.notify) - notify_notify(term, "Bell", "Bell in terminal"); + if (term->conf->bell.notify) { + notify_notify(term, &(struct notification){ + .title = (char *)"Bell", + .body = (char *)"Bell in terminal"}); + } if (term->conf->bell.flash) term_flash(term, 100); @@ -3577,7 +3608,7 @@ term_bell(struct terminal *term) { int devnull = open("/dev/null", O_RDONLY); spawn(term->reaper, NULL, term->conf->bell.command.argv.args, - devnull, -1, -1, NULL); + devnull, -1, -1, NULL, NULL, NULL); if (devnull >= 0) close(devnull); @@ -3589,7 +3620,7 @@ term_spawn_new(const struct terminal *term) { return spawn( term->reaper, term->cwd, (char *const []){term->foot_exe, NULL}, - -1, -1, -1, NULL); + -1, -1, -1, NULL, NULL, NULL) >= 0; } void diff --git a/terminal.h b/terminal.h index 44a101eb..92d1e8f5 100644 --- a/terminal.h +++ b/terminal.h @@ -20,6 +20,7 @@ #include "fdm.h" #include "key-binding.h" #include "macros.h" +#include "notify.h" #include "reaper.h" #include "shm.h" #include "wayland.h" @@ -798,6 +799,12 @@ struct terminal { void *cb_data; } shutdown; + /* Notifications that either haven't been sent yet, or have been + sent but not yet dismissed */ + tll(struct notification) kitty_notifications; + tll(struct notification) active_notifications; + struct notification_icon notification_icons[32]; + 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(); diff --git a/unicode-mode.c b/unicode-mode.c index b902b5f4..1acdc664 100644 --- a/unicode-mode.c +++ b/unicode-mode.c @@ -1,7 +1,7 @@ #include "unicode-mode.h" #define LOG_MODULE "unicode-input" -#define LOG_ENABLE_DBG 1 +#define LOG_ENABLE_DBG 0 #include "log.h" #include "render.h" #include "search.h" diff --git a/url-mode.c b/url-mode.c index 57f47dd0..c6340e94 100644 --- a/url-mode.c +++ b/url-mode.c @@ -74,8 +74,9 @@ spawn_url_launcher_with_token(struct terminal *term, (const char *[]){url}, &argc, &argv)) { - ret = spawn(term->reaper, term->cwd, argv, - dev_null, dev_null, dev_null, xdg_activation_token); + ret = spawn( + term->reaper, term->cwd, argv, + dev_null, dev_null, dev_null, NULL, NULL, xdg_activation_token) >= 0; for (size_t i = 0; i < argc; i++) free(argv[i]); diff --git a/wayland.c b/wayland.c index 04f50bda..3f65901b 100644 --- a/wayland.c +++ b/wayland.c @@ -1824,8 +1824,7 @@ wayl_win_init(struct terminal *term, const char *token) wl_surface_commit(win->surface.surf); /* Complete XDG startup notification */ - if (token && wayl->xdg_activation != NULL) - xdg_activation_v1_activate(wayl->xdg_activation, token, win->surface.surf); + wayl_activate(wayl, win, token); if (!wayl_win_subsurface_new(win, &win->overlay, false)) { LOG_ERR("failed to create overlay surface"); @@ -2377,3 +2376,15 @@ wayl_get_activation_token( xdg_activation_token_v1_commit(token); return true; } + +void +wayl_activate(struct wayland *wayl, struct wl_window *win, const char *token) +{ + if (wayl->xdg_activation == NULL) + return; + + if (token == NULL) + return; + + xdg_activation_v1_activate(wayl->xdg_activation, token, win->surface.surf); +} diff --git a/wayland.h b/wayland.h index 9db02d89..ca9c05fa 100644 --- a/wayland.h +++ b/wayland.h @@ -499,3 +499,5 @@ void wayl_win_subsurface_destroy(struct wayl_sub_surface *surf); bool wayl_get_activation_token( struct wayland *wayl, struct seat *seat, uint32_t serial, struct wl_window *win, activation_token_cb_t cb, void *cb_data); +void wayl_activate(struct wayland *wayl, struct wl_window *win, const char *token); +