From ccb184ae645cd92a6e3aba429ab88a94a09bbf8c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Tue, 23 Jul 2024 11:29:05 +0200 Subject: [PATCH] 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;\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;\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 #include -char *base64_decode(const char *s); +char *base64_decode(const char *s, size_t *out_len); char *base64_encode(const uint8_t *data, size_t size); void base64_encode_final(const uint8_t *data, size_t size, char result[4]); diff --git a/notify.c b/notify.c index d6d1d8a0..67cf5bd1 100644 --- a/notify.c +++ b/notify.c @@ -10,11 +10,12 @@ #include #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 +#include #include 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;