From b0bf8ca5f7034f6d95bc3b58082e719a81d99859 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Fri, 19 Jul 2024 15:04:28 +0200 Subject: [PATCH 01/32] osc/notify: add support for OSC-99, kitty desktop notifications This adds limited support for OSC-99, kitty desktop notifications[^1]. We support everything defined by the "protocol", except: * 'a': action to perform on notification activation. Since we don't trigger the notification ourselves (over D-Bus), we don't know a) which ID the notification got, or b) when it is clicked. * ... and that's it. Everything else is supported To be explicit, we *do* support: * Chunked notifications (d=0|1), allowing the application to append data to a notification in chunks, before it's finally displayed. * Plain UTF-8, or base64-encoded UTF-8 payload (e=0|1). * Notification identifier (i=xyz). * Payload type (p=title|body). * When to honor the notification (o=always|unfocused|invisible), with the following quirks: - we don't know when the window is invisible, thus it's treated as 'unfocused'. - the foot option 'notify-focus-inhibit' overrides 'always' * Urgency (u=0|1|2) [^1]: https://sw.kovidgoyal.net/kitty/desktop-notifications/ --- CHANGELOG.md | 2 + config.c | 5 +- doc/foot.ini.5.scd | 2 +- foot.ini | 2 +- notify.c | 21 +++- notify.h | 28 ++++- osc.c | 249 ++++++++++++++++++++++++++++++++++++++++++++- pgo/pgo.c | 4 +- terminal.c | 21 +++- terminal.h | 3 + 10 files changed, 322 insertions(+), 15 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index dfa2b9ad..057924fa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -70,6 +70,8 @@ * 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/). [1707]: https://codeberg.org/dnkl/foot/issues/1707 [1738]: https://codeberg.org/dnkl/foot/issues/1738 diff --git a/config.c b/config.c index 5d9a0b75..d34df132 100644 --- a/config.c +++ b/config.c @@ -3184,8 +3184,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 -a ${app-id} -i ${app-id} -u ${urgency} ${title} ${body}", + &conf->notify.argv.args); tokenize_cmdline("xdg-open ${url}", &conf->url.launch.argv.args); static const char32_t *url_protocols[] = { diff --git a/doc/foot.ini.5.scd b/doc/foot.ini.5.scd index 680768e0..fe6ff57b 100644 --- a/doc/foot.ini.5.scd +++ b/doc/foot.ini.5.scd @@ -360,7 +360,7 @@ empty string to be set, but it must be quoted: *KEY=""*) 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}_. + 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 diff --git a/foot.ini b/foot.ini index 7ae9ba1b..7a1db9ba 100644 --- a/foot.ini +++ b/foot.ini @@ -29,7 +29,7 @@ # resize-by-cells=yes # resize-delay-ms=100 -# notify=notify-send -a ${app-id} -i ${app-id} ${title} ${body} +# notify=notify-send -a ${app-id} -i ${app-id} -u ${urgency} ${title} ${body} # bold-text-in-bright=no # word-delimiters=,│`|:"'()[]{}<> diff --git a/notify.c b/notify.c index 7a208479..09e230a0 100644 --- a/notify.c +++ b/notify.c @@ -12,14 +12,18 @@ #include "log.h" #include "config.h" #include "spawn.h" +#include "terminal.h" #include "xmalloc.h" void -notify_notify(const struct terminal *term, const char *title, const char *body) +notify_notify(const struct terminal *term, const char *title, const char *body, + enum notify_when when, enum notify_urgency urgency) { LOG_DBG("notify: title=\"%s\", msg=\"%s\"", title, body); - if (term->conf->notify_focus_inhibit && term->kbd_focus) { + if ((term->conf->notify_focus_inhibit || when != NOTIFY_ALWAYS) + && term->kbd_focus) + { /* No notifications while we're focused */ return; } @@ -33,10 +37,17 @@ notify_notify(const struct terminal *term, const char *title, const char *body) char **argv = NULL; size_t argc = 0; + const char *urgency_str = + urgency == NOTIFY_URGENCY_LOW + ? "low" + : 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}, + &term->conf->notify, 5, + (const char *[]){"app-id", "window-title", "title", "body", "urgency"}, + (const char *[]){term->app_id ? term->app_id : term->conf->app_id, + term->window_title, title, body, urgency_str}, &argc, &argv)) { return; diff --git a/notify.h b/notify.h index ce60562f..be79b41a 100644 --- a/notify.h +++ b/notify.h @@ -1,6 +1,30 @@ #pragma once +#include -#include "terminal.h" +struct terminal; + +enum notify_when { + NOTIFY_ALWAYS, + NOTIFY_UNFOCUSED, + NOTIFY_INVISIBLE +}; + +enum notify_urgency { + NOTIFY_URGENCY_LOW, + NOTIFY_URGENCY_NORMAL, + NOTIFY_URGENCY_CRITICAL, +}; + +struct kitty_notification { + char *id; + char *title; + char *body; + enum notify_when when; + enum notify_urgency urgency; + bool focus; + bool report; +}; void notify_notify( - const struct terminal *term, const char *title, const char *body); + const struct terminal *term, const char *title, const char *body, + enum notify_when when, enum notify_urgency urgency); diff --git a/osc.c b/osc.c index a3dc1715..a9a83df9 100644 --- a/osc.c +++ b/osc.c @@ -560,7 +560,250 @@ osc_notify(struct terminal *term, char *string) return; } - notify_notify(term, title, msg != NULL ? msg : ""); + notify_notify( + term, title, msg != NULL ? msg : "", + NOTIFY_ALWAYS, NOTIFY_URGENCY_NORMAL); +} + +static void +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"); + return; + } + + char *parameters = string; + *payload = '\0'; + payload++; + + char *id = xstrdup("0"); /* The 'i' parameter */ + bool focus = true; /* The 'a' parameter */ + bool report = false; /* The 'a' parameter */ + bool done = true; /* The 'd' parameter */ + bool base64 = false; /* The 'e' parameter */ + bool payload_is_title = true; /* 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] != '=') { + LOG_WARN("OSC-99: invalid parameter: \"%s\"", param); + 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)) + { + LOG_WARN(" a: \"%s\"", v); + bool reverse = v[0] == '-'; + if (reverse) + v++; + + if (strcmp(v, "focus") == 0) { + focus = !reverse; + if (focus) + LOG_WARN("unimplemented: OSC-99: focus on notification activation"); + } 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; + } + + case 'd': + /* done: 0|1 */ + if (value[0] == '0' && value[1] == '\0') + 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': + /* base64: 0=utf8, 1=base64(utf8) */ + if (value[0] == '0' && value[1] == '\0') + 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': + /* id */ + free(id); + id = xstrdup(value); + break; + + case 'p': + /* payload content: title|body */ + if (strcmp(value, "title") == 0) + 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': + /* honor when: always|unfocused|invisible */ + have_o = true; + if (strcmp(value, "always") == 0) + when = NOTIFY_ALWAYS; + else if (strcmp(value, "unfocused") == 0) + 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': + /* 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; + else + LOG_WARN("OSC-99: unrecognized value for 'u': \"%s\", ignoring", value); + break; + + default: + LOG_WARN("OSC-99: unrecognized parameter: \"%s\", ignoring", param); + break; + } + } + + if (base64) + payload = base64_decode(payload); + else + payload = xstrdup(payload); + + LOG_DBG("id=%s, done=%d, focus=%d, report=%d, base64=%d, payload: %s, " + "honor: %s, urgency: %s, %s: %s", + id, done, focus, report, base64, + payload_is_title ? "title" : "body", + (when == NOTIFY_ALWAYS + ? "always" + : when == NOTIFY_UNFOCUSED + ? "unfocused" + : "invisible"), + (urgency == NOTIFY_URGENCY_LOW + ? "low" : urgency == NOTIFY_URGENCY_NORMAL + ? "normal" + : "critical"), + 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) { + if (strcmp(it->item.id, id) == 0) { + /* Found existing notification */ + LOG_WARN("found existing kitty notification"); + notif = &it->item; + break; + } + } + + 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){ + .id = id, + .title = NULL, + .body = NULL, + .when = when, + .urgency = urgency, + .focus = focus, + .report = report, + })); + + id = NULL; /* Prevent double free */ + notif = &tll_front(term->kitty_notifications); + } + + /* 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 (payload_is_title) { + if (notif->title == NULL) { + notif->title = payload; + payload = NULL; + } else { + char *new_title = xasprintf("%s%s", notif->title, payload); + free(notif->title); + notif->title = new_title; + } + } else { + if (notif->body == NULL) { + notif->body = payload; + payload = NULL; + } else { + char *new_body = xasprintf("%s%s", notif->body, payload); + free(notif->body); + notif->body = new_body; + } + } + + free(id); + free(payload); + + if (done) { + notify_notify( + term, + notif->title != NULL ? notif->title : "", + 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; + } + } + } } void @@ -780,6 +1023,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..e8080521 100644 --- a/pgo/pgo.c +++ b/pgo/pgo.c @@ -152,7 +152,9 @@ void ime_disable(struct seat *seat) {} void ime_reset_preedit(struct seat *seat) {} void -notify_notify(const struct terminal *term, const char *title, const char *body) + notify_notify( + const struct terminal *term, const char *title, const char *body, + enum notify_when when, enum notify_urgency urgency) { } diff --git a/terminal.c b/terminal.c index 35cfe8aa..712b9d4e 100644 --- a/terminal.c +++ b/terminal.c @@ -1313,6 +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(), }; pixman_region32_init(&term->render.last_overlay_clip); @@ -1817,6 +1818,13 @@ 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); + } + sixel_fini(term); term_ime_reset(term); @@ -2022,6 +2030,13 @@ 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); + } + term->grapheme_shaping = term->conf->tweak.grapheme_shaping; #if defined(FOOT_IME_ENABLED) && FOOT_IME_ENABLED @@ -3566,8 +3581,10 @@ 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, "Bell", "Bell in terminal", + NOTIFY_ALWAYS, NOTIFY_URGENCY_NORMAL); + } if (term->conf->bell.flash) term_flash(term, 100); diff --git a/terminal.h b/terminal.h index 44a101eb..65dde151 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,8 @@ struct terminal { void *cb_data; } shutdown; + tll(struct kitty_notification) kitty_notifications; + char *foot_exe; char *cwd; From 57af75f98877220a2e8827bb3cb32c780df25d8d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Fri, 19 Jul 2024 15:26:08 +0200 Subject: [PATCH 02/32] osc: kitty notifications: use body as title, if no title is set This mirrors kitty's behavior; if the user didn't set a title, but did set the body/text, use the body as title instead. --- osc.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osc.c b/osc.c index a9a83df9..7485cada 100644 --- a/osc.c +++ b/osc.c @@ -790,8 +790,8 @@ kitty_notification(struct terminal *term, char *string) if (done) { notify_notify( term, - notif->title != NULL ? notif->title : "", - notif->body != NULL ? notif->body : "", + 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) { From a42f99081876dd02e28b931f7c0e70230355bb58 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Tue, 23 Jul 2024 06:57:30 +0200 Subject: [PATCH 03/32] spawn: add optional reaper callback, return pid_t This will allow spawn() callers to do things when the spawned process has terminated. --- input.c | 4 ++-- notify.c | 6 +++++- spawn.c | 14 +++++++------- spawn.h | 8 +++++--- terminal.c | 8 ++++---- url-mode.c | 5 +++-- 6 files changed, 26 insertions(+), 19 deletions(-) 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 09e230a0..7cd22ccb 100644 --- a/notify.c +++ b/notify.c @@ -59,7 +59,11 @@ notify_notify(const struct terminal *term, const char *title, const char *body, /* 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, + ¬if_done, (void *)term, NULL); + + if (stdout_fds[1] >= 0) { if (devnull >= 0) close(devnull); 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 +#include + #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 712b9d4e..bf0c78dc 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 @@ -3594,7 +3594,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); @@ -3606,7 +3606,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/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]); From 69f56b86b7ad30a028917121783ea51e9fe9c3a3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Tue, 23 Jul 2024 06:58:37 +0200 Subject: [PATCH 04/32] wayland: add wayl_activate() wayl_activate() takes an XDG activation token and does an XDG activation request. --- wayland.c | 15 +++++++++++++-- wayland.h | 2 ++ 2 files changed, 15 insertions(+), 2 deletions(-) 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); + From 12152a8ae42951dc58e654b512a7901e0a857eb8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Tue, 23 Jul 2024 06:59:14 +0200 Subject: [PATCH 05/32] unicode-mode: disable debug logging --- unicode-mode.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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" 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 06/32] 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(); From 0209458cc0b4fc502492253d3a9e9338e590a97e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Tue, 23 Jul 2024 07:09:54 +0200 Subject: [PATCH 07/32] changelog: new desktop-notifications config section --- CHANGELOG.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 057924fa..861245b2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -72,6 +72,11 @@ 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}` and `${urgency}` added to the + `desktop-notifications.command` template. [1707]: https://codeberg.org/dnkl/foot/issues/1707 [1738]: https://codeberg.org/dnkl/foot/issues/1738 @@ -118,6 +123,12 @@ ### Deprecated + +* `notify` option; replaced by `desktop-notifications.command`. +* `notify-focus-inhibit` option; replaced by + `desktop-notifications.inhibit-when-focused`. + + ### Removed ### Fixed From 7268ee9078e8c0ce4b7cdaa479b67ea34aa8e88f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Tue, 23 Jul 2024 07:43:42 +0200 Subject: [PATCH 08/32] pgo: update spawn() prototype --- pgo/pgo.c | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pgo/pgo.c b/pgo/pgo.c index e8080521..7aabebb4 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 From e88ec86c9337b0a2e1b9d3d1d207005a4c084a79 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Tue, 23 Jul 2024 07:43:56 +0200 Subject: [PATCH 09/32] pgo: update notify_notify() prototype, add notify_free() --- pgo/pgo.c | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/pgo/pgo.c b/pgo/pgo.c index 7aabebb4..6c13b72a 100644 --- a/pgo/pgo.c +++ b/pgo/pgo.c @@ -151,13 +151,18 @@ void ime_enable(struct seat *seat) {} void ime_disable(struct seat *seat) {} void ime_reset_preedit(struct seat *seat) {} +bool +notify_notify(const struct terminal *term, struct notification *notif) +{ + return true; +} + 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) { } + void reaper_add(struct reaper *reaper, pid_t pid, reaper_cb cb, void *cb_data) {} void reaper_del(struct reaper *reaper, pid_t pid) {} From e52d6e3fb8648c34e9e859db03ce3c0ad50ca952 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Tue, 23 Jul 2024 08:05:19 +0200 Subject: [PATCH 10/32] osc: kitty notifications: use xstrjoin() instead of xasprintf() --- osc.c | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/osc.c b/osc.c index 6926c3ca..ef54f520 100644 --- a/osc.c +++ b/osc.c @@ -722,8 +722,6 @@ 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->notifications, ((struct notification){ .id = id, .icon = NULL, @@ -766,18 +764,18 @@ kitty_notification(struct terminal *term, char *string) notif->title = payload; payload = NULL; } else { - char *new_title = xasprintf("%s%s", notif->title, payload); - free(notif->title); - notif->title = new_title; + char *old_title = notif->title; + notif->title = xstrjoin(old_title, payload); + free(old_title); } } else { if (notif->body == NULL) { notif->body = payload; payload = NULL; } else { - char *new_body = xasprintf("%s%s", notif->body, payload); - free(notif->body); - notif->body = new_body; + char *old_body = notif->body; + notif->body = xstrjoin(old_body, payload); + free(old_body); } } From b319618af15b5afd9d0f63271bd09b4a4a27ad52 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Tue, 23 Jul 2024 09:33:18 +0200 Subject: [PATCH 11/32] notify: XDG token is now expected to be prefixed with xdgtoken= This patch modifies our stdout reader to consume input as we go, instead of all at once when stdout is closed. This will make it easier to add support for reading e.g. the daemon assigned notification ID in the future, and also ensures we see the XDG activation token "as soon as possible". Furthermore, to be more future proof, require the XDG activation token to be prefixed with 'xdgtoken=', and ignore other lines. Thus, instead of treating *all* of stdout as the XDG activation token, parse stdout line-by-line, and ignore everything that does not begin with 'xdgtoken='. Everything (on that line) following 'xdgtoken=' is treated as the activation token. --- doc/foot.ini.5.scd | 13 +++++---- notify.c | 73 ++++++++++++++++++++++++++++++++-------------- notify.h | 18 ++++++++++-- 3 files changed, 74 insertions(+), 30 deletions(-) diff --git a/doc/foot.ini.5.scd b/doc/foot.ini.5.scd index e6b554bb..5135e080 100644 --- a/doc/foot.ini.5.scd +++ b/doc/foot.ini.5.scd @@ -463,11 +463,14 @@ Note: do not set *TERM* here; use the *term* option in the main 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). + end, foot will read the command's stdout; a line prefixed with + *xdgtoken=* will be recognized as containing the XDG + activation token: + + 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 diff --git a/notify.c b/notify.c index 043f41a5..d2ff9d07 100644 --- a/notify.c +++ b/notify.c @@ -28,6 +28,40 @@ notify_free(struct terminal *term, struct notification *notif) free(notif->body); free(notif->icon); free(notif->xdg_token); + free(notif->stdout); +} + +static void +consume_stdout(struct notification *notif, bool eof) +{ + char *data = notif->stdout; + 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; + + /* Check for 'xdgtoken=xyz' */ + 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); + } + + memmove(notif->stdout, data, left); + notif->stdout_sz = left; } static bool @@ -36,6 +70,7 @@ 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) { @@ -56,34 +91,25 @@ fdm_notify_stdout(struct fdm *fdm, int fd, int events, void *data) 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 (count > 0 && notif != NULL) { + if (notif->stdout == NULL) { + xassert(notif->stdout_sz == 0); + notif->stdout = xmemdup(buf, count); + } else { + notif->stdout = xrealloc(notif->stdout, notif->stdout_sz + count); + memcpy(¬if->stdout[notif->stdout_sz], buf, count); } + + notif->stdout_sz += count; + consume_stdout(notif, false); } } if (events & EPOLLHUP) { fdm_del(fdm, fd); - if (notif != NULL) + 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'; + consume_stdout(notif, true); } } @@ -130,6 +156,7 @@ notify_notify(const struct terminal *term, struct notification *notif) xassert(notif->xdg_token == NULL); xassert(notif->pid == 0); xassert(notif->stdout_fd == 0); + xassert(notif->stdout == NULL); notif->pid = -1; notif->stdout_fd = -1; @@ -187,7 +214,9 @@ notify_notify(const struct terminal *term, struct notification *notif) 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) { + if ((notif->focus || notif->report) && + pipe2(stdout_fds, O_CLOEXEC | O_NONBLOCK) < 0) + { LOG_WARN("failed to create stdout pipe"); /* Non-fatal */ } diff --git a/notify.h b/notify.h index 6c43e294..cc34ff74 100644 --- a/notify.h +++ b/notify.h @@ -23,18 +23,30 @@ enum notify_urgency { }; struct notification { + /* + * Set by caller of notify_notify() + */ 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; + /* + * Used internally by notify + */ + + char *xdg_token; /* XDG activation token, from daemon */ + + pid_t pid; /* Notifier command PID */ + int stdout_fd; /* Notifier command's stdout */ + + char *stdout; /* Data we've reado from command's stdout */ + size_t stdout_sz; }; bool notify_notify(const struct terminal *term, struct notification *notif); From c7cffea9ee65dc38a1e87d1254c4097990d7e96b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Tue, 23 Jul 2024 09:42:14 +0200 Subject: [PATCH 12/32] notify: stdout is a bad name --- notify.c | 16 ++++++++-------- notify.h | 2 +- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/notify.c b/notify.c index d2ff9d07..d6d1d8a0 100644 --- a/notify.c +++ b/notify.c @@ -28,13 +28,13 @@ notify_free(struct terminal *term, struct notification *notif) free(notif->body); free(notif->icon); free(notif->xdg_token); - free(notif->stdout); + free(notif->stdout_data); } static void consume_stdout(struct notification *notif, bool eof) { - char *data = notif->stdout; + char *data = notif->stdout_data; const char *line = data; size_t left = notif->stdout_sz; @@ -60,7 +60,7 @@ consume_stdout(struct notification *notif, bool eof) left -= len + (eol != NULL ? 1 : 0); } - memmove(notif->stdout, data, left); + memmove(notif->stdout_data, data, left); notif->stdout_sz = left; } @@ -92,12 +92,12 @@ fdm_notify_stdout(struct fdm *fdm, int fd, int events, void *data) } if (count > 0 && notif != NULL) { - if (notif->stdout == NULL) { + if (notif->stdout_data == NULL) { xassert(notif->stdout_sz == 0); - notif->stdout = xmemdup(buf, count); + notif->stdout_data = xmemdup(buf, count); } else { - notif->stdout = xrealloc(notif->stdout, notif->stdout_sz + count); - memcpy(¬if->stdout[notif->stdout_sz], buf, count); + notif->stdout_data = xrealloc(notif->stdout_data, notif->stdout_sz + count); + memcpy(¬if->stdout_data[notif->stdout_sz], buf, count); } notif->stdout_sz += count; @@ -156,7 +156,7 @@ notify_notify(const struct terminal *term, struct notification *notif) xassert(notif->xdg_token == NULL); xassert(notif->pid == 0); xassert(notif->stdout_fd == 0); - xassert(notif->stdout == NULL); + xassert(notif->stdout_data == NULL); notif->pid = -1; notif->stdout_fd = -1; diff --git a/notify.h b/notify.h index cc34ff74..ec62e03e 100644 --- a/notify.h +++ b/notify.h @@ -45,7 +45,7 @@ struct notification { pid_t pid; /* Notifier command PID */ int stdout_fd; /* Notifier command's stdout */ - char *stdout; /* Data we've reado from command's stdout */ + char *stdout_data; /* Data we've reado from command's stdout */ size_t stdout_sz; }; From ccb184ae645cd92a6e3aba429ab88a94a09bbf8c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Tue, 23 Jul 2024 11:29:05 +0200 Subject: [PATCH 13/32] osc: kitty notifications: updated support for icons This implements the suggested protocol discussed in https://github.com/kovidgoyal/kitty/issues/7657. Icons are handled by loading a cache. Both in-band PNG data, and symbolic names are allowed. Applications use a graphical ID to reference the icon both when loading the cache, and when showing a notification. * 'g' is the graphical ID * 'n' is optional, and assigns a symbolic name to the icon * 'p=icon' - the payload is icon PNG data. It needs to be base64 encoded, but this is *not* implied. I.e. the application *must* use e=1 explicitly. To load an icon (in-band PNG data): printf '\e]99;g=123:p=icon;<base64-encoded-png-data>\e\\' or (symbolic name) printf '\e]99;g=123:n=firefox:p=icon;\e\\' Of course, we can combine the two, assigning *both* a symbolic name, *and* PNG data: printf '\e]99;g=123:n=firefox:p=icon;<base64-encoded-png>\e\\' Then, to use the icon in a notification: printf '\e]99;g=123;this is a notification\e\\' Foot also allows a *symbolic* icon to be defined and used at the same time: printf '\e]99;g=123:n=firefox;this is a notification\e\\' This obviously won't work with PNG data, since it uses the payload portion of the escape sequence. --- base64.c | 12 ++++- base64.h | 2 +- notify.c | 126 +++++++++++++++++++++++++++++++++++++++++++++++----- notify.h | 19 +++++++- osc.c | 128 ++++++++++++++++++++++++++++++++++++++--------------- terminal.c | 6 +++ terminal.h | 1 + 7 files changed, 245 insertions(+), 49 deletions(-) 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 <stdint.h> #include <stddef.h> -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/notify.c b/notify.c index d6d1d8a0..67cf5bd1 100644 --- a/notify.c +++ b/notify.c @@ -10,11 +10,12 @@ #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" @@ -26,7 +27,9 @@ notify_free(struct terminal *term, struct notification *notif) free(notif->id); free(notif->title); free(notif->body); - free(notif->icon); + free(notif->icon_id); + free(notif->icon_symbolic_name); + free(notif->icon_data); free(notif->xdg_token); free(notif->stdout_data); } @@ -167,11 +170,22 @@ notify_notify(const struct terminal *term, struct notification *notif) /* 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; + 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 && strcmp(icon->id, notif->icon_id) == 0) { + icon_name_or_path = icon->symbolic_name != NULL + ? icon->symbolic_name + : icon->tmp_file_on_disk; + break; + } + } + } LOG_DBG("notify: title=\"%s\", body=\"%s\"", title, body); @@ -201,9 +215,11 @@ notify_notify(const struct terminal *term, struct notification *notif) if (!spawn_expand_template( &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, icon, title, body, urgency_str}, + (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, icon_name_or_path, title, body, urgency_str}, &argc, &argv)) { return false; @@ -254,3 +270,93 @@ notify_notify(const struct terminal *term, struct notification *notif) 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_on_disk = NULL; + + if (data_sz > 0) { + char name[64] = "/tmp/foot-notification-icon-cache-XXXXXX"; + int fd = mkstemp(name); + + if (fd < 0) { + LOG_ERRNO("failed to create temporary file for icon cache"); + return; + } + + write(fd, data, data_sz); + close(fd); + + LOG_DBG("wrote icon data to %s", name); + icon->tmp_file_on_disk = xstrdup(name); + } + + LOG_DBG("added icon to cache: %s: sym=%s, file=%s", + icon->id, icon->symbolic_name, icon->tmp_file_on_disk); +} + +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 && strcmp(icon->id, id) == 0) { + 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 || strcmp(icon->id, id) != 0) + continue; + + notify_icon_free(icon); + return; + } +} + +void +notify_icon_free(struct notification_icon *icon) +{ + if (icon->tmp_file_on_disk != NULL) + unlink(icon->tmp_file_on_disk); + + free(icon->id); + free(icon->symbolic_name); + free(icon->tmp_file_on_disk); + + icon->id = NULL; + icon->symbolic_name = NULL; + icon->tmp_file_on_disk = NULL; +} diff --git a/notify.h b/notify.h index ec62e03e..55ace52d 100644 --- a/notify.h +++ b/notify.h @@ -1,5 +1,6 @@ #pragma once #include <stdbool.h> +#include <stdint.h> #include <unistd.h> struct terminal; @@ -29,7 +30,11 @@ struct notification { char *id; char *title; char *body; - char *icon; + + char *icon_id; + char *icon_symbolic_name; + uint8_t *icon_data; + size_t icon_data_sz; enum notify_when when; enum notify_urgency urgency; @@ -49,5 +54,17 @@ struct notification { size_t stdout_sz; }; +struct notification_icon { + char *id; + char *symbolic_name; + char *tmp_file_on_disk; +}; + bool notify_notify(const 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 ef54f520..5a001b7a 100644 --- a/osc.c +++ b/osc.c @@ -67,7 +67,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); @@ -579,12 +579,19 @@ kitty_notification(struct terminal *term, char *string) payload++; char *id = xstrdup("0"); /* The 'i' parameter */ - char *icon = NULL; /* The 'I' parameter */ + char *icon_id = NULL; /* The 'g' parameter */ + char *symbolic_icon = NULL; /* The 'n' parameter */ bool focus = true; /* The 'a' parameter */ bool report = false; /* The 'a' parameter */ bool done = true; /* The 'd' parameter */ bool base64 = false; /* The 'e' parameter */ - bool payload_is_title = true; /* The 'p' 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; @@ -653,9 +660,11 @@ kitty_notification(struct terminal *term, char *string) case 'p': /* payload content: title|body */ if (strcmp(value, "title") == 0) - payload_is_title = true; + payload_type = PAYLOAD_TITLE; else if (strcmp(value, "body") == 0) - payload_is_title = false; + payload_type = PAYLOAD_BODY; + else if (strcmp(value, "icon") == 0) + payload_type = PAYLOAD_ICON; break; case 'o': @@ -680,21 +689,26 @@ kitty_notification(struct terminal *term, char *string) urgency = NOTIFY_URGENCY_CRITICAL; 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); + case 'g': + free(icon_id); + icon_id = xstrdup(value); + break; + + case 'n': + free(symbolic_icon); + symbolic_icon = xstrdup(value); break; } } - if (base64) - payload = base64_decode(payload); - else + if (base64) { + payload = base64_decode(payload, &payload_size); + if (payload == NULL) + goto out; + } else { payload = xstrdup(payload); + payload_size = strlen(payload); + } LOG_DBG("id=%s, done=%d, focus=%d, report=%d, base64=%d, icon=%s, payload: %s, " "honor: %s, urgency: %s, %s: %s", @@ -724,9 +738,6 @@ kitty_notification(struct terminal *term, char *string) if (notif == NULL) { tll_push_front(term->notifications, ((struct notification){ .id = id, - .icon = NULL, - .title = NULL, - .body = NULL, .when = when, .urgency = urgency, .focus = focus, @@ -753,34 +764,78 @@ 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 (icon_id != NULL) { + free(notif->icon_id); + notif->icon_id = icon_id; + icon_id = NULL; /* Prevent double free */ } - if (payload_is_title) { - if (notif->title == NULL) { - notif->title = payload; + 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_title = notif->title; - notif->title = xstrjoin(old_title, payload); - free(old_title); + char *old = *ptr; + *ptr = xstrjoin(old, payload); + free(old); } - } else { - if (notif->body == NULL) { - notif->body = payload; + break; + } + + case PAYLOAD_ICON: + if (notif->icon_data == NULL) { + notif->icon_data = (uint8_t *)payload; + notif->icon_data_sz = payload_size; payload = NULL; } else { - char *old_body = notif->body; - notif->body = xstrjoin(old_body, payload); - free(old_body); + 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) { - if (!notify_notify(term, notif)) { + /* 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->notifications, it) { if (&it->item == notif) { notify_free(term, &it->item); @@ -793,7 +848,8 @@ kitty_notification(struct terminal *term, char *string) out: free(id); - free(icon); + free(icon_id); + free(symbolic_icon); free(payload); } diff --git a/terminal.c b/terminal.c index a0be4518..eadb9dbc 100644 --- a/terminal.c +++ b/terminal.c @@ -1823,6 +1823,9 @@ term_destroy(struct terminal *term) tll_remove(term->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); @@ -2033,6 +2036,9 @@ term_reset(struct terminal *term, bool hard) tll_remove(term->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 diff --git a/terminal.h b/terminal.h index 0967bf14..7b726c62 100644 --- a/terminal.h +++ b/terminal.h @@ -802,6 +802,7 @@ struct terminal { /* Notifications that either haven't been sent yet, or have been sent but not yet dismissed */ tll(struct notification) notifications; + struct notification_icon notification_icons[32]; char *foot_exe; char *cwd; From b3108e1ad23387327d6cc7ebfdb20307878dd5c3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Tue, 23 Jul 2024 11:53:30 +0200 Subject: [PATCH 14/32] notify: separate active notifications from unfinished kitty notifications This fixes an issue where it wasn't possible to trigger multiple notifications with the same kitty notification ID. This is something that works in kitty, and there's no reason why it shouldn't work. The issue was that we track stdout, and the notification helper's PID in the notification struct. Thus, when a notification is being displayed, we can't re-use the same notification struct instance for another notification. This patch fixes this by adding a new notification list, 'active_notifications'. Whenever we detect that we need to track the helper (notification want's to either focus the window on activation, or send an event to the application), we add a copy of the notification to the 'active' list. The notification can then be removed from the 'kitty' list, allowing kitty notifications to re-use the same ID over and over again, even if old notifications are still being displayed. --- notify.c | 36 +++++++++++++++++++++++++----------- notify.h | 2 +- osc.c | 25 +++++++++++++------------ terminal.c | 21 ++++++++++++++++----- terminal.h | 3 ++- 5 files changed, 57 insertions(+), 30 deletions(-) diff --git a/notify.c b/notify.c index 67cf5bd1..af89838b 100644 --- a/notify.c +++ b/notify.c @@ -75,7 +75,7 @@ fdm_notify_stdout(struct fdm *fdm, int fd, int events, void *data) /* Find notification */ - tll_foreach(term->notifications, it) { + tll_foreach(term->active_notifications, it) { if (it->item.stdout_fd == fd) { notif = &it->item; break; @@ -124,7 +124,7 @@ notif_done(struct reaper *reaper, pid_t pid, int status, void *data) { struct terminal *term = data; - tll_foreach(term->notifications, it) { + tll_foreach(term->active_notifications, it) { struct notification *notif = &it->item; if (notif->pid != pid) continue; @@ -148,17 +148,17 @@ notif_done(struct reaper *reaper, pid_t pid, int status, void *data) } notify_free(term, notif); - tll_remove(term->notifications, it); + tll_remove(term->active_notifications, it); return; } } bool -notify_notify(const struct terminal *term, struct notification *notif) +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_fd <= 0); xassert(notif->stdout_data == NULL); notif->pid = -1; @@ -187,6 +187,8 @@ notify_notify(const struct terminal *term, struct notification *notif) } } + bool track_notification = notif->focus || notif->report; + LOG_DBG("notify: title=\"%s\", body=\"%s\"", title, body); xassert(title != NULL); @@ -230,13 +232,25 @@ notify_notify(const struct terminal *term, struct notification *notif) LOG_DBG(" argv[%zu] = \"%s\"", i, argv[i]); int stdout_fds[2] = {-1, -1}; - if ((notif->focus || notif->report) && - pipe2(stdout_fds, O_CLOEXEC | O_NONBLOCK) < 0) - { - LOG_WARN("failed to create stdout pipe"); - /* Non-fatal */ + 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) { xassert(notif->xdg_token == NULL); fdm_add(term->fdm, stdout_fds[0], EPOLLIN, @@ -247,7 +261,7 @@ notify_notify(const struct terminal *term, struct notification *notif) int devnull = open("/dev/null", O_RDONLY); pid_t pid = spawn( term->reaper, NULL, argv, devnull, stdout_fds[1], -1, - ¬if_done, (void *)term, NULL); + track_notification ? ¬if_done : NULL, (void *)term, NULL); if (stdout_fds[1] >= 0) { /* Close write-end of stdout pipe */ diff --git a/notify.h b/notify.h index 55ace52d..90fbf9fc 100644 --- a/notify.h +++ b/notify.h @@ -60,7 +60,7 @@ struct notification_icon { char *tmp_file_on_disk; }; -bool notify_notify(const struct terminal *term, struct notification *notif); +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, diff --git a/osc.c b/osc.c index 5a001b7a..41a0dc68 100644 --- a/osc.c +++ b/osc.c @@ -727,7 +727,7 @@ kitty_notification(struct terminal *term, char *string) /* Search for an existing (d=0) notification to update */ struct notification *notif = NULL; - tll_foreach(term->notifications, it) { + tll_foreach(term->kitty_notifications, it) { if (strcmp(it->item.id, id) == 0) { /* Found existing notification */ notif = &it->item; @@ -736,16 +736,17 @@ kitty_notification(struct terminal *term, char *string) } if (notif == NULL) { - tll_push_front(term->notifications, ((struct notification){ + 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->notifications); + notif = &tll_front(term->kitty_notifications); } if (notif->pid > 0) { @@ -833,15 +834,15 @@ kitty_notification(struct terminal *term, char *string) * 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->notifications, it) { - if (&it->item == notif) { - notify_free(term, &it->item); - tll_remove(term->notifications, it); - break; - } + 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; } } } diff --git a/terminal.c b/terminal.c index eadb9dbc..dc4f37b6 100644 --- a/terminal.c +++ b/terminal.c @@ -1313,7 +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 - .notifications = tll_init(), + .kitty_notifications = tll_init(), + .active_notifications = tll_init(), }; pixman_region32_init(&term->render.last_overlay_clip); @@ -1818,9 +1819,14 @@ term_destroy(struct terminal *term) tll_remove(term->ptmx_paste_buffers, it); } - tll_foreach(term->notifications, it) { + tll_foreach(term->kitty_notifications, it) { notify_free(term, &it->item); - tll_remove(term->notifications, it); + 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++) @@ -2031,9 +2037,14 @@ term_reset(struct terminal *term, bool hard) tll_remove(term->alt.sixel_images, it); } - tll_foreach(term->notifications, it) { + tll_foreach(term->kitty_notifications, it) { notify_free(term, &it->item); - tll_remove(term->notifications, it); + 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++) diff --git a/terminal.h b/terminal.h index 7b726c62..92d1e8f5 100644 --- a/terminal.h +++ b/terminal.h @@ -801,7 +801,8 @@ struct terminal { /* Notifications that either haven't been sent yet, or have been sent but not yet dismissed */ - tll(struct notification) notifications; + tll(struct notification) kitty_notifications; + tll(struct notification) active_notifications; struct notification_icon notification_icons[32]; char *foot_exe; From efa5b9cea639f08ef2b9a9eb0ce6179af98ebd80 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Tue, 23 Jul 2024 12:12:38 +0200 Subject: [PATCH 15/32] osc: cleanup --- osc.c | 19 +++---------------- 1 file changed, 3 insertions(+), 16 deletions(-) diff --git a/osc.c b/osc.c index 41a0dc68..4fde951c 100644 --- a/osc.c +++ b/osc.c @@ -644,7 +644,7 @@ kitty_notification(struct terminal *term, char *string) break; case 'e': - /* base64: 0=utf8, 1=base64(utf8) */ + /* 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') @@ -690,11 +690,13 @@ kitty_notification(struct terminal *term, char *string) 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; @@ -710,21 +712,6 @@ kitty_notification(struct terminal *term, char *string) payload_size = strlen(payload); } - 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, icon != NULL ? icon : "<not set>", - payload_is_title ? "title" : "body", - (when == NOTIFY_ALWAYS - ? "always" - : when == NOTIFY_UNFOCUSED - ? "unfocused" - : "invisible"), - (urgency == NOTIFY_URGENCY_LOW - ? "low" : urgency == NOTIFY_URGENCY_NORMAL - ? "normal" - : "critical"), - payload_is_title ? "title" : "body", payload); - /* Search for an existing (d=0) notification to update */ struct notification *notif = NULL; tll_foreach(term->kitty_notifications, it) { From 9814cf57792f43e95755bcf62237ae759c453645 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Tue, 23 Jul 2024 12:12:50 +0200 Subject: [PATCH 16/32] notify: clean up logging messages --- notify.c | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/notify.c b/notify.c index af89838b..9a9311ee 100644 --- a/notify.c +++ b/notify.c @@ -189,7 +189,8 @@ notify_notify(struct terminal *term, struct notification *notif) bool track_notification = notif->focus || notif->report; - LOG_DBG("notify: title=\"%s\", body=\"%s\"", title, body); + 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) @@ -309,7 +310,7 @@ add_icon(struct notification_icon *icon, const char *id, const char *symbolic_na icon->tmp_file_on_disk = xstrdup(name); } - LOG_DBG("added icon to cache: %s: sym=%s, file=%s", + LOG_DBG("added icon to cache: ID=%s: sym=%s, file=%s", icon->id, icon->symbolic_name, icon->tmp_file_on_disk); } @@ -355,6 +356,7 @@ notify_icon_del(struct terminal *term, const char *id) if (icon->id == NULL || strcmp(icon->id, id) != 0) continue; + LOG_DBG("expelled %s from the notification icon cache", icon->id); notify_icon_free(icon); return; } From d0a542515549a71fa2fb7456326ab99c378d929a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Tue, 23 Jul 2024 12:15:29 +0200 Subject: [PATCH 17/32] notify: add_icon(): check return value of write() --- notify.c | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/notify.c b/notify.c index 9a9311ee..f8c2bfe1 100644 --- a/notify.c +++ b/notify.c @@ -303,11 +303,14 @@ add_icon(struct notification_icon *icon, const char *id, const char *symbolic_na return; } - write(fd, data, data_sz); - close(fd); + if (write(fd, data, data_sz) != (ssize_t)data_sz) { + LOG_ERRNO("failed to write icon data to temporary file"); + } else { + LOG_DBG("wrote icon data to %s", name); + icon->tmp_file_on_disk = xstrdup(name); + } - LOG_DBG("wrote icon data to %s", name); - icon->tmp_file_on_disk = xstrdup(name); + close(fd); } LOG_DBG("added icon to cache: ID=%s: sym=%s, file=%s", From 50efd9726d041b00d3966431b8e6957a3739b33c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Tue, 23 Jul 2024 12:15:37 +0200 Subject: [PATCH 18/32] pgo: updated stubs for notification functions --- pgo/pgo.c | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/pgo/pgo.c b/pgo/pgo.c index 6c13b72a..6dc0dd10 100644 --- a/pgo/pgo.c +++ b/pgo/pgo.c @@ -152,7 +152,7 @@ void ime_disable(struct seat *seat) {} void ime_reset_preedit(struct seat *seat) {} bool -notify_notify(const struct terminal *term, struct notification *notif) +notify_notify(struct terminal *term, struct notification *notif) { return true; } @@ -162,6 +162,22 @@ 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) +{ +} void reaper_add(struct reaper *reaper, pid_t pid, reaper_cb cb, void *cb_data) {} void reaper_del(struct reaper *reaper, pid_t pid) {} From e59efb12332c8e0427a97d22980376fd9e81e7b7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Tue, 23 Jul 2024 15:28:47 +0200 Subject: [PATCH 19/32] osc: remove unused includes --- osc.c | 3 --- 1 file changed, 3 deletions(-) diff --git a/osc.c b/osc.c index 4fde951c..0e092a9f 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" From fabfef9c82192b04a52cdd092a81d8e10ca20fbf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Tue, 23 Jul 2024 15:29:08 +0200 Subject: [PATCH 20/32] notify: consume_stdout(): fix ASAN warning This is an ASAN false positive; size is always 0 when we're passing a NULL pointer. Still, the warning is easy to avoid, so let's do that to reduce the noise level. --- notify.c | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/notify.c b/notify.c index f8c2bfe1..b16b941b 100644 --- a/notify.c +++ b/notify.c @@ -63,8 +63,10 @@ consume_stdout(struct notification *notif, bool eof) left -= len + (eol != NULL ? 1 : 0); } - memmove(notif->stdout_data, data, left); - notif->stdout_sz = left; + if (left > 0) { + memmove(notif->stdout_data, data, left); + notif->stdout_sz = left; + } } static bool From 55a4e59ef96227f23fa00e8c772804f67601567a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Tue, 23 Jul 2024 15:30:01 +0200 Subject: [PATCH 21/32] notify: if there's a symbolic icon name, use it even if there's no graphical ID set. In other words, if there *is* a graphical ID, use the icon cache. Only if there is no graphical ID in the notification request do we fallback to the symbolic name. This means no icon will be displayed if there's no matching icon in the cache. Some examples. You can either pre-load the cache (with inline PNG data, a symbolic name, or both): printf '\e]99;g=123:n=firefox:p=icon:e=1;<PNG data>\e\\' printf '\e]99;g=123;this is a notification\e\\' or printf '\e]99;n=firefox;this is a notification\e\\' --- notify.c | 2 ++ 1 file changed, 2 insertions(+) diff --git a/notify.c b/notify.c index b16b941b..4261268d 100644 --- a/notify.c +++ b/notify.c @@ -187,6 +187,8 @@ notify_notify(struct terminal *term, struct notification *notif) break; } } + } else if (notif->icon_symbolic_name != NULL) { + icon_name_or_path = notif->icon_symbolic_name; } bool track_notification = notif->focus || notif->report; From 045ead985c1e320f289704b6a995d48a5eccf59e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Tue, 23 Jul 2024 16:41:52 +0200 Subject: [PATCH 22/32] notify: don't focus/report on notification dismissal Only do it when the notification was activated. Here, activated means the 'click to activate' notification action was triggered. How do we tie everything together? First, we add a new template parameter, ${action}. It's intended to be used with e.g. notify-send's --action option. When the action is triggered, notify-send prints its name on stdout, on a separate line. Look for this in stdout. Only if we've seen it do we focus/report the notification. --- CHANGELOG.md | 2 +- config.c | 2 +- doc/foot.ini.5.scd | 31 ++++++++++++++++++++++++++----- foot.ini | 2 +- notify.c | 27 ++++++++++++++++----------- notify.h | 1 + 6 files changed, 46 insertions(+), 19 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 861245b2..fe27afc5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -75,7 +75,7 @@ * `desktop-notifications.command` option, replaces `notify`. * `desktop-notifications.inhibit-when-focused` option, replaces `notify-focus-inhibit`. -* `${icon}` and `${urgency}` added to the +* `${icon}`, `${urgency}` and `${action}` added to the `desktop-notifications.command` template. [1707]: https://codeberg.org/dnkl/foot/issues/1707 diff --git a/config.c b/config.c index a624f95c..be465abf 100644 --- a/config.c +++ b/config.c @@ -3225,7 +3225,7 @@ 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 --wait --app-name ${app-id} --icon ${icon} --urgency ${urgency} -- ${title} ${body}", + "notify-send --wait --app-name ${app-id} --icon ${icon} --urgency ${urgency} --action ${action} -- ${title} ${body}", &conf->desktop_notifications.command.argv.args); tokenize_cmdline("xdg-open ${url}", &conf->url.launch.argv.args); diff --git a/doc/foot.ini.5.scd b/doc/foot.ini.5.scd index 5135e080..8e388107 100644 --- a/doc/foot.ini.5.scd +++ b/doc/foot.ini.5.scd @@ -462,11 +462,32 @@ Note: do not set *TERM* here; use the *term* option in the main 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; a line prefixed with - *xdgtoken=* will be recognized as containing the XDG - activation token: + 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 parameter *${action}*. It is intended to be used with + e.g. notify-send's *-A,--action* option. The contents of + *${action}* is not configurable, but will be on the form + 'name=label', where name is a notification internal reference + to the action, and label is what is displayed in the + notification. + + 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: + activate-foot xdgtoken=18179adf579a7a904ce73754964b1ec3 The expected format of stdout may change at any time. Please @@ -491,7 +512,7 @@ Note: do not set *TERM* here; use the *term* option in the main 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}_. + Default: _notify-send --wait --app-name ${app-id} --icon ${icon} --urgency ${urgency} --action ${action} -- ${title} ${body}_. *inhibit-when-focused* Boolean. If enabled, foot will not display notifications if the diff --git a/foot.ini b/foot.ini index 33727dd3..707f09a2 100644 --- a/foot.ini +++ b/foot.ini @@ -47,7 +47,7 @@ # command-focused=no [desktop-notifications] -# command=notify-send --wait --app-name ${app-id} --icon ${icon} --urgency ${urgency} -- ${title} ${body} +# command=notify-send --wait --app-name ${app-id} --icon ${icon} --urgency ${urgency} --action ${action} -- ${title} ${body} # inhibit-when-focused=yes diff --git a/notify.c b/notify.c index 4261268d..d12c3081 100644 --- a/notify.c +++ b/notify.c @@ -54,8 +54,11 @@ consume_stdout(struct notification *notif, bool eof) } else if (!eof) break; + if (strcmp(line, "activate-foot") == 0) + notif->activated = true; + /* Check for 'xdgtoken=xyz' */ - if (len > 9 && memcmp(line, "xdgtoken=", 9) == 0) { + 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); } @@ -133,12 +136,13 @@ notif_done(struct reaper *reaper, pid_t pid, int status, void *data) LOG_DBG("notification %s dismissed", notif->id); - if (notif->focus) { - LOG_DBG("focus window on notification activation: \"%s\"", notif->xdg_token); + 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->report) { + if (notif->activated && notif->report) { xassert(notif->id != NULL); LOG_DBG("sending notification report to client"); @@ -221,13 +225,14 @@ notify_notify(struct terminal *term, struct notification *notif) ? "normal" : "critical"; if (!spawn_expand_template( - &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, icon_name_or_path, title, body, urgency_str}, - &argc, &argv)) + &term->conf->desktop_notifications.command, 7, + (const char *[]){ + "app-id", "window-title", "icon", "title", "body", "urgency", "action"}, + (const char *[]){ + term->app_id ? term->app_id : term->conf->app_id, + term->window_title, icon_name_or_path, title, body, urgency_str, + "activate-foot=Click to activate"}, + &argc, &argv)) { return false; } diff --git a/notify.h b/notify.h index 90fbf9fc..ba017276 100644 --- a/notify.h +++ b/notify.h @@ -45,6 +45,7 @@ struct notification { * Used internally by notify */ + bool activated; /* User 'activated' the notification */ char *xdg_token; /* XDG activation token, from daemon */ pid_t pid; /* Notifier command PID */ From 79832c16e2db2bef79c03aa5eba7cf863436dd67 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Tue, 23 Jul 2024 16:48:15 +0200 Subject: [PATCH 23/32] notify: name the activation action 'default' This is less unique, but also works better with notification daemons that trigger the 'default' action when e.g. clicked. --- notify.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/notify.c b/notify.c index d12c3081..9cf44251 100644 --- a/notify.c +++ b/notify.c @@ -54,7 +54,7 @@ consume_stdout(struct notification *notif, bool eof) } else if (!eof) break; - if (strcmp(line, "activate-foot") == 0) + if (strcmp(line, "default") == 0) notif->activated = true; /* Check for 'xdgtoken=xyz' */ @@ -231,7 +231,7 @@ notify_notify(struct terminal *term, struct notification *notif) (const char *[]){ term->app_id ? term->app_id : term->conf->app_id, term->window_title, icon_name_or_path, title, body, urgency_str, - "activate-foot=Click to activate"}, + "default=Click to activate"}, &argc, &argv)) { return false; From 511d4817d307f75ba68640ab1f394bafaae02844 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Tue, 23 Jul 2024 16:52:18 +0200 Subject: [PATCH 24/32] doc: foot.ini: desktop-notification: remove 'notification dismissal' --- doc/foot.ini.5.scd | 19 +++---------------- 1 file changed, 3 insertions(+), 16 deletions(-) diff --git a/doc/foot.ini.5.scd b/doc/foot.ini.5.scd index 8e388107..40106223 100644 --- a/doc/foot.ini.5.scd +++ b/doc/foot.ini.5.scd @@ -457,7 +457,9 @@ Note: do not set *TERM* here; use the *term* option in the main Window activation (focusing) Foot can focus the window when the notification is - "activated". This typically happens when the default action 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. @@ -497,21 +499,6 @@ Note: do not set *TERM* here; use the *term* option in the main 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} --action ${action} -- ${title} ${body}_. *inhibit-when-focused* From 70b4638a754048eeffc877405222d4750c3ea548 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Tue, 23 Jul 2024 18:32:23 +0200 Subject: [PATCH 25/32] osc: kitty notifications: implement query --- osc.c | 33 +++++++++++++++++++++++++++------ 1 file changed, 27 insertions(+), 6 deletions(-) diff --git a/osc.c b/osc.c index 0e092a9f..6a04386c 100644 --- a/osc.c +++ b/osc.c @@ -567,17 +567,19 @@ kitty_notification(struct terminal *term, char *string) { /* https://sw.kovidgoyal.net/kitty/desktop-notifications */ - char *payload = strchr(string, ';'); - if (payload == NULL) + char *payload_raw = strchr(string, ';'); + if (payload_raw == NULL) return; char *parameters = string; - *payload = '\0'; - payload++; + *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 */ @@ -662,6 +664,25 @@ kitty_notification(struct terminal *term, char *string) payload_type = PAYLOAD_BODY; else if (strcmp(value, "icon") == 0) payload_type = PAYLOAD_ICON; + else if (strcmp(value, "?") == 0) { + /* 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,?: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': @@ -701,11 +722,11 @@ kitty_notification(struct terminal *term, char *string) } if (base64) { - payload = base64_decode(payload, &payload_size); + payload = base64_decode(payload_raw, &payload_size); if (payload == NULL) goto out; } else { - payload = xstrdup(payload); + payload = xstrdup(payload_raw); payload_size = strlen(payload); } From d5c773a58b2cdadf7aee2fe16664c168e750b953 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Tue, 23 Jul 2024 18:48:45 +0200 Subject: [PATCH 26/32] notify: bug: always adjust amount of data left in stdout buffer --- notify.c | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/notify.c b/notify.c index 9cf44251..2dd303db 100644 --- a/notify.c +++ b/notify.c @@ -66,10 +66,10 @@ consume_stdout(struct notification *notif, bool eof) left -= len + (eol != NULL ? 1 : 0); } - if (left > 0) { + if (left > 0) memmove(notif->stdout_data, data, left); - notif->stdout_sz = left; - } + + notif->stdout_sz = left; } static bool From ecbec57a4749cc4162d2bb55dd14e1890b6dff31 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Tue, 23 Jul 2024 19:08:21 +0200 Subject: [PATCH 27/32] notify: split up the ${action} template parameter Split it up into two, ${action-name} and ${action-label}. Dunstify, for example, has a different syntax compared to notify-send: notify-send: default=foobar dunstify: default,foobar --- CHANGELOG.md | 4 ++-- config.c | 2 +- doc/foot.ini.5.scd | 14 ++++++-------- foot.ini | 2 +- notify.c | 10 ++++------ 5 files changed, 14 insertions(+), 18 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fe27afc5..65ddfc3b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -75,8 +75,8 @@ * `desktop-notifications.command` option, replaces `notify`. * `desktop-notifications.inhibit-when-focused` option, replaces `notify-focus-inhibit`. -* `${icon}`, `${urgency}` and `${action}` added to the - `desktop-notifications.command` template. +* `${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 diff --git a/config.c b/config.c index be465abf..307d348d 100644 --- a/config.c +++ b/config.c @@ -3225,7 +3225,7 @@ 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 --wait --app-name ${app-id} --icon ${icon} --urgency ${urgency} --action ${action} -- ${title} ${body}", + "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); diff --git a/doc/foot.ini.5.scd b/doc/foot.ini.5.scd index 40106223..a6c20e6e 100644 --- a/doc/foot.ini.5.scd +++ b/doc/foot.ini.5.scd @@ -470,12 +470,9 @@ Note: do not set *TERM* here; use the *term* option in the main There are two parts to handle this. First, the notification must define an action. For this purpose, foot definse the - template parameter *${action}*. It is intended to be used with - e.g. notify-send's *-A,--action* option. The contents of - *${action}* is not configurable, but will be on the form - 'name=label', where name is a notification internal reference - to the action, and label is what is displayed in the - notification. + 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. @@ -489,7 +486,7 @@ Note: do not set *TERM* here; use the *term* option in the main line, prefixed with *xdgtoken=*. Example: - activate-foot + default xdgtoken=18179adf579a7a904ce73754964b1ec3 The expected format of stdout may change at any time. Please @@ -499,7 +496,8 @@ Note: do not set *TERM* here; use the *term* option in the main reporting the XDG activation token in any way. This means window activation will not work by default. - Default: _notify-send --wait --app-name ${app-id} --icon ${icon} --urgency ${urgency} --action ${action} -- ${title} ${body}_. + 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 diff --git a/foot.ini b/foot.ini index 707f09a2..c743182c 100644 --- a/foot.ini +++ b/foot.ini @@ -47,7 +47,7 @@ # command-focused=no [desktop-notifications] -# command=notify-send --wait --app-name ${app-id} --icon ${icon} --urgency ${urgency} --action ${action} -- ${title} ${body} +# 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 diff --git a/notify.c b/notify.c index 2dd303db..71852c6a 100644 --- a/notify.c +++ b/notify.c @@ -78,7 +78,6 @@ 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) { @@ -225,13 +224,13 @@ notify_notify(struct terminal *term, struct notification *notif) ? "normal" : "critical"; if (!spawn_expand_template( - &term->conf->desktop_notifications.command, 7, + &term->conf->desktop_notifications.command, 8, (const char *[]){ - "app-id", "window-title", "icon", "title", "body", "urgency", "action"}, + "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"}, + "default", "Click to activate"}, &argc, &argv)) { return false; @@ -253,7 +252,7 @@ notify_notify(struct terminal *term, struct notification *notif) notif->title = NULL; notif->body = NULL; notif->icon_id = NULL; - notif->icon_symbolic_name= NULL; + notif->icon_symbolic_name = NULL; notif->icon_data = NULL; notif->icon_data_sz = 0; notif = &tll_back(term->active_notifications); @@ -262,7 +261,6 @@ notify_notify(struct terminal *term, struct notification *notif) if (stdout_fds[0] >= 0) { - xassert(notif->xdg_token == NULL); fdm_add(term->fdm, stdout_fds[0], EPOLLIN, &fdm_notify_stdout, (void *)term); } From 24168ed86ecf20a44f6dce8bdacfcb2c1471844b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Wed, 24 Jul 2024 15:58:19 +0200 Subject: [PATCH 28/32] osc: kitty notifications: don't include '?' in the query reply No need to say we support queries, in the query reply... --- osc.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osc.c b/osc.c index 6a04386c..c5073ff6 100644 --- a/osc.c +++ b/osc.c @@ -677,7 +677,7 @@ kitty_notification(struct terminal *term, char *string) char reply[128]; int n = xsnprintf( reply, sizeof(reply), - "\033]99;i=%s:p=?;p=title,body,?:a=focus,report:o=%s:u=0,1,2%s", + "\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); From 37ab3b1603ce3697d69d28129b5941dac843e992 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Wed, 24 Jul 2024 15:59:52 +0200 Subject: [PATCH 29/32] notify: don't create icon file on disk when we're not going to use it We always prefer the symbolic name. Thus, there's no need to write raw PNG data to disk if we have a symbolic name. Furthermore, keep the file open until the cache entry is evicted. --- notify.c | 37 +++++++++++++++++++++++++------------ notify.h | 3 ++- 2 files changed, 27 insertions(+), 13 deletions(-) diff --git a/notify.c b/notify.c index 71852c6a..4efddec2 100644 --- a/notify.c +++ b/notify.c @@ -186,7 +186,7 @@ notify_notify(struct terminal *term, struct notification *notif) if (icon->id != NULL && strcmp(icon->id, notif->icon_id) == 0) { icon_name_or_path = icon->symbolic_name != NULL ? icon->symbolic_name - : icon->tmp_file_on_disk; + : icon->tmp_file_name; break; } } @@ -299,11 +299,21 @@ add_icon(struct notification_icon *icon, const char *id, const char *symbolic_na { icon->id = xstrdup(id); icon->symbolic_name = symbolic_name != NULL ? xstrdup(symbolic_name) : NULL; - icon->tmp_file_on_disk = NULL; + icon->tmp_file_name = NULL; + icon->tmp_file_fd = -1; - if (data_sz > 0) { + /* + * 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 = mkstemp(name); + int fd = mkostemp(name, O_CLOEXEC); if (fd < 0) { LOG_ERRNO("failed to create temporary file for icon cache"); @@ -312,16 +322,16 @@ add_icon(struct notification_icon *icon, const char *id, const char *symbolic_na 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_on_disk = xstrdup(name); + icon->tmp_file_name = xstrdup(name); + icon->tmp_file_fd = fd; } - - close(fd); } LOG_DBG("added icon to cache: ID=%s: sym=%s, file=%s", - icon->id, icon->symbolic_name, icon->tmp_file_on_disk); + icon->id, icon->symbolic_name, icon->tmp_file_name); } void @@ -375,14 +385,17 @@ notify_icon_del(struct terminal *term, const char *id) void notify_icon_free(struct notification_icon *icon) { - if (icon->tmp_file_on_disk != NULL) - unlink(icon->tmp_file_on_disk); + 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_on_disk); + free(icon->tmp_file_name); icon->id = NULL; icon->symbolic_name = NULL; - icon->tmp_file_on_disk = NULL; + icon->tmp_file_name = NULL; + icon->tmp_file_fd = -1; } diff --git a/notify.h b/notify.h index ba017276..d78af5fa 100644 --- a/notify.h +++ b/notify.h @@ -58,7 +58,8 @@ struct notification { struct notification_icon { char *id; char *symbolic_name; - char *tmp_file_on_disk; + char *tmp_file_name; + int tmp_file_fd; }; bool notify_notify(struct terminal *term, struct notification *notif); From e271027c0c5166994f4f698befefe0b6a5c382f5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Wed, 24 Jul 2024 16:01:42 +0200 Subject: [PATCH 30/32] config: notify-send: it's "action=label", not "action,label" --- config.c | 2 +- doc/foot.ini.5.scd | 2 +- foot.ini | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/config.c b/config.c index 307d348d..664474ca 100644 --- a/config.c +++ b/config.c @@ -3225,7 +3225,7 @@ 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 --wait --app-name ${app-id} --icon ${icon} --urgency ${urgency} --action ${action-name},${action-label} --print-id -- ${title} ${body}", + "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); diff --git a/doc/foot.ini.5.scd b/doc/foot.ini.5.scd index a6c20e6e..ef48323f 100644 --- a/doc/foot.ini.5.scd +++ b/doc/foot.ini.5.scd @@ -496,7 +496,7 @@ Note: do not set *TERM* here; use the *term* option in the main reporting the XDG activation token in any way. This means window activation will not work by default. - Default: _notify-send --wait --app-name ${app-id} --icon ${icon} --urgency ${urgency} --action ${action-name},${action-label} --print-id -- ${title} ${body}_. + Default: _notify-send --wait --app-name ${app-id} --icon ${icon} --urgency ${urgency} --action ${action-name}=${action-label} --print-id -- ${title} ${body}_. *inhibit-when-focused* diff --git a/foot.ini b/foot.ini index c743182c..46b2d5f0 100644 --- a/foot.ini +++ b/foot.ini @@ -47,7 +47,7 @@ # 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} +# 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 From f56da385fe4c4434bfc10c362cbdda58c63d238f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Wed, 24 Jul 2024 16:02:19 +0200 Subject: [PATCH 31/32] notify: try to read the daemon assigned notification ID from stdout And document the things we recognize in the notification helper's stdout. --- doc/foot.ini.5.scd | 15 +++++++++++++++ notify.c | 43 ++++++++++++++++++++++++++++++++++++++++--- notify.h | 1 + 3 files changed, 56 insertions(+), 3 deletions(-) diff --git a/doc/foot.ini.5.scd b/doc/foot.ini.5.scd index ef48323f..72e6d052 100644 --- a/doc/foot.ini.5.scd +++ b/doc/foot.ini.5.scd @@ -496,6 +496,21 @@ Note: do not set *TERM* here; use the *term* option in the main 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}_. diff --git a/notify.c b/notify.c index 4efddec2..986b3b7b 100644 --- a/notify.c +++ b/notify.c @@ -34,6 +34,27 @@ notify_free(struct terminal *term, struct notification *notif) free(notif->stdout_data); } +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'; + } + + *res = maybe_id; + return is_id; +} + static void consume_stdout(struct notification *notif, bool eof) { @@ -54,10 +75,26 @@ consume_stdout(struct notification *notif, bool eof) } else if (!eof) break; - if (strcmp(line, "default") == 0) - notif->activated = true; + uint32_t maybe_id = 0; - /* Check for 'xdgtoken=xyz' */ + /* 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); diff --git a/notify.h b/notify.h index d78af5fa..231495dc 100644 --- a/notify.h +++ b/notify.h @@ -45,6 +45,7 @@ struct notification { * 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 */ From a6bc9cafafd2edb2d88286cbdf4e57e331a86347 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= <daniel@ekloef.se> Date: Wed, 24 Jul 2024 16:04:14 +0200 Subject: [PATCH 32/32] osc+notify: strcmp() -> streq() --- notify.c | 6 +++--- osc.c | 20 ++++++++++---------- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/notify.c b/notify.c index 986b3b7b..05ef6f58 100644 --- a/notify.c +++ b/notify.c @@ -220,7 +220,7 @@ notify_notify(struct terminal *term, struct notification *notif) for (size_t i = 0; i < ALEN(term->notification_icons); i++) { const struct notification_icon *icon = &term->notification_icons[i]; - if (icon->id != NULL && strcmp(icon->id, notif->icon_id) == 0) { + 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; @@ -378,7 +378,7 @@ notify_icon_add(struct terminal *term, const char *id, #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 && strcmp(icon->id, id) == 0) { + if (icon->id != NULL && streq(icon->id, id)) { BUG("notification icon cache already contains \"%s\"", id); } } @@ -410,7 +410,7 @@ 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 || strcmp(icon->id, id) != 0) + if (icon->id == NULL || !streq(icon->id, id)) continue; LOG_DBG("expelled %s from the notification icon cache", icon->id); diff --git a/osc.c b/osc.c index c5073ff6..19d9f097 100644 --- a/osc.c +++ b/osc.c @@ -625,9 +625,9 @@ kitty_notification(struct terminal *term, char *string) if (reverse) v++; - if (strcmp(v, "focus") == 0) + if (streq(v, "focus")) focus = !reverse; - else if (strcmp(v, "report") == 0) + else if (streq(v, "report")) report = !reverse; } @@ -658,13 +658,13 @@ kitty_notification(struct terminal *term, char *string) case 'p': /* payload content: title|body */ - if (strcmp(value, "title") == 0) + if (streq(value, "title")) payload_type = PAYLOAD_TITLE; - else if (strcmp(value, "body") == 0) + else if (streq(value, "body")) payload_type = PAYLOAD_BODY; - else if (strcmp(value, "icon") == 0) + else if (streq(value, "icon")) payload_type = PAYLOAD_ICON; - else if (strcmp(value, "?") == 0) { + else if (streq(value, "?")) { /* Query capabilities */ char when_str[64]; @@ -688,11 +688,11 @@ kitty_notification(struct terminal *term, char *string) case 'o': /* honor when: always|unfocused|invisible */ have_o = true; - if (strcmp(value, "always") == 0) + if (streq(value, "always")) when = NOTIFY_ALWAYS; - else if (strcmp(value, "unfocused") == 0) + else if (streq(value, "unfocused")) when = NOTIFY_UNFOCUSED; - else if (strcmp(value, "invisible") == 0) + else if (streq(value, "invisible")) when = NOTIFY_INVISIBLE; break; @@ -733,7 +733,7 @@ kitty_notification(struct terminal *term, char *string) /* Search for an existing (d=0) notification to update */ struct notification *notif = NULL; tll_foreach(term->kitty_notifications, it) { - if (strcmp(it->item.id, id) == 0) { + if (streq(it->item.id, id)) { /* Found existing notification */ notif = &it->item; break;