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;