diff --git a/CHANGELOG.md b/CHANGELOG.md index 3394c0ce..82ad7f9d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -75,8 +75,11 @@ * `desktop-notifications.command` option, replaces `notify`. * `desktop-notifications.inhibit-when-focused` option, replaces `notify-focus-inhibit`. -* `${icon}`, `${urgency}`,`${action-name}` and `${action-label}` added +* `${icon}`, `${urgency}` and `${action-argument}` added to the `desktop-notifications.command` template. +* `desktop-notifications.command-action-argument` option, defining how + `${action-argument}` (in `desktop-notifications.command`) should be + expanded. * `desktop-notifications.close` option, defining what to execute when an application wants to close an existing notification (via an OSC-99 escape sequence). diff --git a/config.c b/config.c index e45a02a7..46b3d2d2 100644 --- a/config.c +++ b/config.c @@ -356,9 +356,9 @@ open_config(void) /* First, check XDG_CONFIG_HOME (or .config, if unset) */ if (xdg_config_home != NULL && xdg_config_home[0] != '\0') - path = xstrjoin(xdg_config_home, "/foot/foot.ini"); + path = xstrjoin(xdg_config_home, "/foot/foot.ini", 0); else if (home_dir != NULL) - path = xstrjoin(home_dir, "/.config/foot/foot.ini"); + path = xstrjoin(home_dir, "/.config/foot/foot.ini", 0); if (path != NULL) { LOG_DBG("checking for %s", path); @@ -383,7 +383,7 @@ open_config(void) conf_dir = strtok(NULL, ":")) { free(path); - path = xstrjoin(conf_dir, "/foot/foot.ini"); + path = xstrjoin(conf_dir, "/foot/foot.ini", 0); LOG_DBG("checking for %s", path); int fd = open(path, O_RDONLY | O_CLOEXEC); @@ -1115,7 +1115,7 @@ parse_section_desktop_notifications(struct context *ctx) if (streq(key, "command")) return value_to_spawn_template( ctx, &conf->desktop_notifications.command); - else if (streq(key, "command-action-arg")) + else if (streq(key, "command-action-argument")) return value_to_spawn_template( ctx, &conf->desktop_notifications.command_action_arg); else if (streq(key, "close")) @@ -2931,7 +2931,7 @@ get_server_socket_path(void) const char *wayland_display = getenv("WAYLAND_DISPLAY"); if (wayland_display == NULL) { - return xstrjoin(xdg_runtime, "/foot.sock"); + return xstrjoin(xdg_runtime, "/foot.sock", 0); } return xasprintf("%s/foot-%s.sock", xdg_runtime, wayland_display); @@ -3242,7 +3242,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 ${app-id} --category ${category} --urgency ${urgency} --expire-time ${expire-time} --hint STRING:image-path:${icon} --replace-id ${replace-id} ${action-arg} --print-id -- ${title} ${body}", + "notify-send --wait --app-name ${app-id} --icon ${app-id} --category ${category} --urgency ${urgency} --expire-time ${expire-time} --hint STRING:image-path:${icon} --replace-id ${replace-id} ${action-argument} --print-id -- ${title} ${body}", &conf->desktop_notifications.command.argv.args); tokenize_cmdline("--action ${action-name}=${action-label}", &conf->desktop_notifications.command_action_arg.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 f310e228..11114feb 100644 --- a/doc/foot.ini.5.scd +++ b/doc/foot.ini.5.scd @@ -459,14 +459,15 @@ Note: do not set *TERM* here; use the *term* option in the main below. Can be used together with e.g. notify-send's *--replace-id* option. - _${action-arg}_ will be expanded to the *command-action-arg* - option, for each notification action. There will always be at - least one action, the "default" action. Foot uses this to - enable window focusing, and reporting notification activation - to applications that requested such events. + _${action-argument}_ will be expanded to the + *command-action-argument* option, for each notification + action. There will always be at least one action, the + "default" action. Foot uses this to enable window focusing, + and reporting notification activation to applications that + requested such events. Applications can also define their own custom notification - actions. See the *command-action-arg* option for details. + actions. See the *command-action-argument* option for details. Ways to trigger notifications Applications can trigger notifications in the following ways: @@ -496,7 +497,7 @@ 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 will add a "default" action to the notification (see the - *command-action-arg* option). + *command-action-argument* option). Second, foot needs to know when the notification is activated, and it needs to get hold of the XDG activation token. @@ -524,18 +525,41 @@ xdgtoken=18179adf579a7a904ce73754964b1ec3 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_. + - _id_: integer in base 10, daemon assigned notification ID + - *id=*_id_: same as plain _nnn_. - *default*: the 'default' action was triggered - *action=*_default_: same as _default_ - *action=*_n_: application custom action _n_ triggered + - _n_: integer in base 10, appearing after the ID; application + custom action _n_ triggered - *xdgtoken=*_xyz_: XDG activation token. - Example: + Example #1: 17++ action=default++ xdgtoken=95ebdfe56e4f47ddb5bba9d7dc3a2c35 - + + Foot recognizes this as: + - notification has the daemon assigned ID 17 + - the user triggered the default action + - the notification send an XDG activation token + + Example #2: + 17++ +1 + + Foot recognizes this as: + - notification has the daemon assigned ID 17 + - the user triggered the first custom action, "1" + + Example #3: + id=17++ +1 + + Foot recognizes this as: + - notification has the daemon assigned ID 17 + - the user triggered the first custom action, "1 + Default: _notify-send++ --wait++ --app-name ${app-id}++ @@ -545,11 +569,11 @@ xdgtoken=95ebdfe56e4f47ddb5bba9d7dc3a2c35 --expire-time ${expire-time}++ --hint STRING:image-path:${icon}++ --replace-id ${replace-id}++ - ${action-arg}++ + ${action-argument}++ --print-id++ -- ${title} ${body}_. -*command-action-arg* +*command-action-argument* String to use with *command* to enable passing action/button names to the notification helper. @@ -561,32 +585,32 @@ xdgtoken=95ebdfe56e4f47ddb5bba9d7dc3a2c35 Furhermore, the OSC-99 notifications protocol allows applications to define their own actions. Foot uses a combination of the - *command* option, and the *command-action-arg* option to pass the - names of the actions to the notification helper. + *command* option, and the *command-action-argument* option to pass + the names of the actions to the notification helper. This option has the following template arguments: - _${action-name}_: the name of the action; *default* for the default action configured by foot, and _n_, where _n_ is an integer >= 1, for application defined actions. - - _${action-label}_: *Click to activate* for the default action, - and a free-form string for application defined actions. + - _${action-label}_: *Activate* for the default action, and a + free-form string for application defined actions. For each notification action (remember, there will always be at - least one), *command-action-arg* will be expanded with the + least one), *command-action-argument* will be expanded with the action's name and label. - Then, _${action-arg}_ is expanded *command* to the full list of - actions. + Then, _${action-argument}_ is expanded *command* to the full list + of actions. - If *command-action-arg* is set to the empty string, no actions - will be passed to *command*. That is, _${action-arg}_ will be - replaced with the empty string. + If *command-action-argument* is set to the empty string, no + actions will be passed to *command*. That is, _${action-argument}_ + will be replaced with the empty string. Example: - *command-action-arg=--action ${action-name}=${action-label}* - *command=notify-send ${action-arg} ...* + *command-action-argument=--action ${action-name}=${action-label}*++ +*command=notify-send ${action-argument} ...* Assume the application defined two custom actions: *OK* and *Cancel*. diff --git a/foot.ini b/foot.ini index 0ae0b52f..029daa9b 100644 --- a/foot.ini +++ b/foot.ini @@ -47,8 +47,8 @@ # command-focused=no [desktop-notifications] -# command=notify-send --wait --app-name ${app-id} --icon ${app-id} --category ${category} --urgency ${urgency} --expire-time ${expire-time} --hint STRING:image-path:${icon} --replace-id ${replace-id} ${action-arg} --print-id -- ${title} ${body} -# command-action-arg=--action ${action-name}=${action-label} +# command=notify-send --wait --app-name ${app-id} --icon ${app-id} --category ${category} --urgency ${urgency} --expire-time ${expire-time} --hint STRING:image-path:${icon} --replace-id ${replace-id} ${action-argument} --print-id -- ${title} ${body} +# command-action-argument=--action ${action-name}=${action-label} # close="" # inhibit-when-focused=yes diff --git a/main.c b/main.c index 207e6eb7..47bccf2b 100644 --- a/main.c +++ b/main.c @@ -261,7 +261,7 @@ main(int argc, char *const *argv) break; case 't': - tll_push_back(overrides, xstrjoin("term=", optarg)); + tll_push_back(overrides, xstrjoin("term=", optarg, 0)); break; case 'L': @@ -269,11 +269,11 @@ main(int argc, char *const *argv) break; case 'T': - tll_push_back(overrides, xstrjoin("title=", optarg)); + tll_push_back(overrides, xstrjoin("title=", optarg, 0)); break; case 'a': - tll_push_back(overrides, xstrjoin("app-id=", optarg)); + tll_push_back(overrides, xstrjoin("app-id=", optarg, 0)); break; case 'D': { @@ -287,7 +287,7 @@ main(int argc, char *const *argv) } case 'f': { - char *font_override = xstrjoin("font=", optarg); + char *font_override = xstrjoin("font=", optarg, 0); tll_push_back(overrides, font_override); break; } @@ -658,3 +658,18 @@ out: log_deinit(); return ret == EXIT_SUCCESS && !as_server ? shutdown_ctx.exit_code : ret; } + +UNITTEST +{ + char *s = xstrjoin("foo", "bar", 0); + xassert(streq(s, "foobar")); + free(s); + + s = xstrjoin("foo", "bar", ' '); + xassert(streq(s, "foo bar")); + free(s); + + s = xstrjoin("foo", "bar", ','); + xassert(streq(s, "foo,bar")); + free(s); +} diff --git a/notify.c b/notify.c index e109e6a8..6315e61f 100644 --- a/notify.c +++ b/notify.c @@ -23,13 +23,15 @@ void notify_free(struct terminal *term, struct notification *notif) { - fdm_del(term->fdm, notif->stdout_fd); + if (notif->pid > 0) + fdm_del(term->fdm, notif->stdout_fd); + free(notif->id); free(notif->title); free(notif->body); free(notif->category); free(notif->app_id); - free(notif->icon_id); + free(notif->icon_cache_id); free(notif->icon_symbolic_name); free(notif->icon_data); free(notif->xdg_token); @@ -44,6 +46,8 @@ notify_free(struct terminal *term, struct notification *notif) if (notif->icon_fd >= 0) close(notif->icon_fd); } + + memset(notif, 0, sizeof(*notif)); } static bool @@ -119,9 +123,10 @@ consume_stdout(struct notification *notif, bool eof) break; uint32_t maybe_id = 0; + uint32_t maybe_button_nr = 0; /* Check for daemon assigned ID, either '123', or 'id=123' */ - if (to_integer(line, len, &maybe_id) || + if ((notif->external_id == 0 && to_integer(line, len, &maybe_id)) || (len > 3 && memcmp(line, "id=", 3) == 0 && to_integer(&line[3], len - 3, &maybe_id))) { @@ -140,7 +145,6 @@ consume_stdout(struct notification *notif, bool eof) else if (len > 7 && memcmp(line, "action=", 7) == 0) { notif->activated = true; - uint32_t maybe_button_nr; if (to_integer(&line[7], len - 7, &maybe_button_nr)) { notif->activated_button = maybe_button_nr; LOG_DBG("custom action %u triggered", notif->activated_button); @@ -150,6 +154,18 @@ consume_stdout(struct notification *notif, bool eof) } } + else if (notif->external_id > 0 && + to_integer(line, len, &maybe_button_nr) && + maybe_button_nr > 0 && + maybe_button_nr <= notif->button_count) + { + /* Single integer, appearing *after* the ID, and is within + the custom button/action range */ + notif->activated = true; + notif->activated_button = maybe_button_nr; + LOG_DBG("custom action %u triggered", notif->activated_button); + } + /* Check for XDG activation token, 'xdgtoken=xyz' */ else if (len > 9 && memcmp(line, "xdgtoken=", 9) == 0) { notif->xdg_token = xstrndup(&line[9], len - 9); @@ -272,6 +288,31 @@ notif_done(struct reaper *reaper, pid_t pid, int status, void *data) } } +static bool +expand_action_to_argv(struct terminal *term, const char *name, const char *label, + size_t *argc, char ***argv) +{ + char **expanded = NULL; + size_t count = 0; + + if (!spawn_expand_template( + &term->conf->desktop_notifications.command_action_arg, 2, + (const char *[]){"action-name", "action-label"}, + (const char *[]){name, label}, + &count, &expanded)) + { + return false; + } + + /* Append to the "global" actions argv */ + *argv = xrealloc(*argv, (*argc + count) * sizeof((*argv)[0])); + memcpy(&(*argv)[*argc], expanded, count * sizeof(expanded[0])); + *argc += count; + + free(expanded); + return true; +} + bool notify_notify(struct terminal *term, struct notification *notif) { @@ -309,11 +350,11 @@ notify_notify(struct terminal *term, struct notification *notif) /* Icon: symbolic name if present, otherwise a filename */ const char *icon_name_or_path = ""; - if (notif->icon_id != NULL) { + if (notif->icon_cache_id != NULL) { for (size_t i = 0; i < ALEN(term->notification_icons); i++) { const struct notification_icon *icon = &term->notification_icons[i]; - if (icon->id != NULL && streq(icon->id, notif->icon_id)) { + if (icon->id != NULL && streq(icon->id, notif->icon_cache_id)) { /* For now, we set the symbolic name to 'file:///path' * when using a file based icon. */ xassert(icon->symbolic_name != NULL); @@ -397,59 +438,27 @@ notify_notify(struct terminal *term, struct notification *notif) xsnprintf(expire_time, sizeof(expire_time), "%d", notif->expire_time); if (term->conf->desktop_notifications.command_action_arg.argv.args) { - struct action { - const char *name; - const char *label; - }; - - tll(struct action) actions = tll_init(); - tll_push_back(actions, ((struct action){"default", "Click to activate"})); - - tll_foreach(notif->actions, it) { - tll_push_back(actions, ((struct action){NULL, it->item})); + if (!expand_action_to_argv( + term, "default", "Activate", &action_argc, &action_argv)) + { + return false; } - size_t action_idx = 0; - tll_foreach(actions, it) { - const char *name = it->item.name; - const char *label = it->item.label; + size_t action_idx = 1; + tll_foreach(notif->actions, it) { - /* - * Custom actions (buttons) start at 1. - * - * We always insert our own default action first, causing - * all custom actions to start at index 1 in our list. - */ - char numerical_name[16]; - xsnprintf(numerical_name, sizeof(numerical_name), "%zu", action_idx); + /* Custom actions use a numerical name, starting at 1 */ + char name[16]; + xsnprintf(name, sizeof(name), "%zu", action_idx++); - if (name == NULL) - name = numerical_name; - - char **expanded = NULL; - size_t count = 0; - - if (!spawn_expand_template( - &term->conf->desktop_notifications.command_action_arg, 2, - (const char *[]){"action-name", "action-label"}, - (const char *[]){name, label}, - &count, &expanded)) + if (!expand_action_to_argv( + term, name, it->item, &action_argc, &action_argv)) { + for (size_t i = 0; i < action_argc; i++) + free(action_argv[i]); + free(action_argv); return false; } - - /* Append to the "global" actions argv */ - action_argv = xrealloc( - action_argv, (action_argc + count) * sizeof(action_argv[0])); - - for (size_t i = 0; i < count; i++) - action_argv[action_argc + i] = expanded[i]; - - action_argc += count; - - free(expanded); - action_idx++; - tll_remove(actions, it); } } @@ -457,33 +466,35 @@ notify_notify(struct terminal *term, struct notification *notif) &term->conf->desktop_notifications.command, 10, (const char *[]){ "app-id", "window-title", "icon", "title", "body", "category", - "urgency", "expire-time", "replace-id", "action-arg"}, + "urgency", "expire-time", "replace-id", "action-argument"}, (const char *[]){ app_id, term->window_title, icon_name_or_path, title, body, notif->category != NULL ? notif->category : "", urgency_str, expire_time, replaces_id_str, /* Custom expansion below, since we need to expand to multiple arguments */ - "${action-arg}"}, + "${action-argument}"}, &argc, &argv)) { return false; } + /* Post-process the expanded argv, and patch in all the --action + arguments we expanded earlier */ for (size_t i = 0; i < argc; i++) { - if (!streq(argv[i], "${action-arg}")) + if (!streq(argv[i], "${action-argument}")) continue; if (action_argc == 0) { free(argv[i]); - /* Remove ${command-arg}, but include terminating NULL */ + /* Remove ${command-argument}, but include terminating NULL */ memmove(&argv[i], &argv[i + 1], (argc - i) * sizeof(argv[0])); argc--; break; } - /* Remove the "${action-arg}" entry, add all actions argument + /* Remove the "${action-argument}" entry, add all actions argument from earlier, but include terminating NULL */ argv = xrealloc(argv, (argc + action_argc) * sizeof(argv[0])); @@ -492,7 +503,7 @@ notify_notify(struct terminal *term, struct notification *notif) &argv[i + 1], (argc - i) * sizeof(argv[0])); /* Include terminating NULL */ - free(argv[i]); /* Free xstrdup("${action-arg}"); */ + free(argv[i]); /* Free xstrdup("${action-argument}"); */ /* Insert the action arguments */ for (size_t j = 0; j < action_argc; j++) { @@ -501,7 +512,7 @@ notify_notify(struct terminal *term, struct notification *notif) } argc += action_argc; - argc--; /* The ${action-arg} option has been removed */ + argc--; /* The ${action-argument} option has been removed */ break; } @@ -518,12 +529,15 @@ notify_notify(struct terminal *term, struct notification *notif) /* Non-fatal */ } else { tll_push_back(term->active_notifications, *notif); + + /* We've taken over ownership of all data; clear, so that + notify_free() doesn't double free */ notif->id = NULL; notif->title = NULL; notif->body = NULL; notif->category = NULL; notif->app_id = NULL; - notif->icon_id = NULL; + notif->icon_cache_id = NULL; notif->icon_symbolic_name = NULL; notif->icon_data = NULL; notif->icon_data_sz = 0; @@ -533,6 +547,7 @@ notify_notify(struct terminal *term, struct notification *notif) struct notification *new_notif = &tll_back(term->active_notifications); /* We don't need these anymore. They'll be free:d by the caller */ + new_notif->button_count = tll_length(notif->actions); memset(&new_notif->actions, 0, sizeof(new_notif->actions)); notif = new_notif; } diff --git a/notify.h b/notify.h index 2a7c5ca1..85de209e 100644 --- a/notify.h +++ b/notify.h @@ -29,26 +29,28 @@ struct notification { /* * Set by caller of notify_notify() */ - char *id; - char *title; + char *id; /* Internal notification ID */ + + char *app_id; /* Custom app-id, overrides the terminal's app-id if set */ + char *title; /* Required */ char *body; char *category; - char *app_id; /* Custm app-id, overrides the terminal's app-id */ - char *icon_id; - char *icon_symbolic_name; - uint8_t *icon_data; - size_t icon_data_sz; - enum notify_when when; enum notify_urgency urgency; int32_t expire_time; + tll(char *) actions; - bool focus; - bool may_be_programatically_closed; - bool report_activated; - bool report_closed; + char *icon_cache_id; + char *icon_symbolic_name; + uint8_t *icon_data; + size_t icon_data_sz; + + bool focus; /* Focus the foot window when notification is activated */ + bool may_be_programatically_closed; /* OSC-99: notification may be programatically closed by the client */ + bool report_activated; /* OSC-99: report notification activation to client */ + bool report_closed; /* OSC-99: report notification closed to client */ /* * Used internally by notify @@ -56,6 +58,7 @@ struct notification { uint32_t external_id; /* Daemon assigned notification ID */ bool activated; /* User 'activated' the notification */ + uint32_t button_count; /* Number of buttons (custom actions) in notification */ uint32_t activated_button; /* User activated one of the custom actions */ char *xdg_token; /* XDG activation token, from daemon */ diff --git a/osc.c b/osc.c index 360ea6ee..eb5e9718 100644 --- a/osc.c +++ b/osc.c @@ -579,7 +579,7 @@ kitty_notification(struct terminal *term, char *string) char *id = NULL; /* The 'i' parameter */ char *app_id = NULL; /* The 'f' parameter */ - char *icon_id = NULL; /* The 'g' parameter */ + char *icon_cache_id = NULL; /* The 'g' parameter */ char *symbolic_icon = NULL; /* The 'n' parameter */ char *category = NULL; /* The 't' parameter */ char *payload = NULL; @@ -693,18 +693,24 @@ kitty_notification(struct terminal *term, char *string) else if (streq(value, "?")) { /* Query capabilities */ - char when_str[64]; - strcpy(when_str, "unfocused"); + const char *reply_id = id != NULL ? id : "0"; + + const char *p_caps = "title,body,?,close,alive,icon,buttons"; + const char *a_caps = "focus,report"; + const char *u_caps = "0,1,2"; + + char when_caps[64]; + strcpy(when_caps, "unfocused"); if (!term->conf->desktop_notifications.inhibit_when_focused) - strcat(when_str, ",always"); + strcat(when_caps, ",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,?,close,alive,icon,buttons:a=focus,report:o=%s:u=0,1,2:c=1:w=1%s", - id != NULL ? id : "0", when_str, terminator); + "\033]99;i=%s:p=?;p=%s:a=%s:o=%s:u=%s:c=1:w=1%s", + reply_id, p_caps, a_caps, when_caps, u_caps, terminator); xassert(n < sizeof(reply)); term_to_slave(term, reply, n); @@ -759,60 +765,79 @@ kitty_notification(struct terminal *term, char *string) if (category == NULL) category = decoded; else { - const size_t old_len = strlen(category); - const size_t new_len = strlen(decoded); - /* Append, comma separated */ - category = xrealloc(category, old_len + 1 + new_len + 1); - category[old_len] = ','; - memcpy(&category[old_len + 1], decoded, new_len); - category[old_len + 1 + new_len] = '\0'; + char *old_category = category; + category = xstrjoin(old_category, decoded, ','); free(decoded); + free(old_category); } } break; } case 'g': - /* graphical ID */ - free(icon_id); - icon_id = xstrdup(value); + /* graphical ID (see 'n' and 'p=icon') */ + free(icon_cache_id); + icon_cache_id = xstrdup(value); break; - case 'n': - /* Symbolic icon name, used with 'g' */ - free(symbolic_icon); - symbolic_icon = base64_decode(value, NULL); + case 'n': { + /* Symbolic icon name, may used with 'g' */ - /* Translate OSC-99 "special" names */ - if (symbolic_icon != NULL) { - const char *translated_name = NULL; + /* + * Sigh, protocol says 'n' can be used multiple times, and + * that the terminal picks the first one that it can + * resolve. + * + * We can't resolve any icons at all. So, enter + * heuristics... let's pick the *shortest* symbolic + * name. The idea is that icon *names* are typically + * shorter than .desktop names, and macOS bundle + * identifiers. + */ + char *maybe_new_symbolic_icon = base64_decode(value, NULL); + if (maybe_new_symbolic_icon == NULL) + break; - if (streq(symbolic_icon, "error")) - translated_name = "dialog-error"; - else if (streq(symbolic_icon, "warn") || - streq(symbolic_icon, "warning")) - translated_name = "dialog-warning"; - else if (streq(symbolic_icon, "info")) - translated_name = "dialog-information"; - else if (streq(symbolic_icon, "question")) - translated_name = "dialog-question"; - else if (streq(symbolic_icon, "help")) - translated_name = "system-help"; - else if (streq(symbolic_icon, "file-manager")) - translated_name = "system-file-manager"; - else if (streq(symbolic_icon, "system-monitor")) - translated_name = "utilities-system-monitor"; - else if (streq(symbolic_icon, "text-editor")) - translated_name = "text-editor"; + if (symbolic_icon == NULL || + strlen(maybe_new_symbolic_icon) < strlen(symbolic_icon)) + { + free(symbolic_icon); + symbolic_icon = maybe_new_symbolic_icon; - if (translated_name != NULL) { - free(symbolic_icon); - symbolic_icon = xstrdup(translated_name); + /* Translate OSC-99 "special" names */ + if (symbolic_icon != NULL) { + const char *translated_name = NULL; + + if (streq(symbolic_icon, "error")) + translated_name = "dialog-error"; + else if (streq(symbolic_icon, "warn") || + streq(symbolic_icon, "warning")) + translated_name = "dialog-warning"; + else if (streq(symbolic_icon, "info")) + translated_name = "dialog-information"; + else if (streq(symbolic_icon, "question")) + translated_name = "dialog-question"; + else if (streq(symbolic_icon, "help")) + translated_name = "system-help"; + else if (streq(symbolic_icon, "file-manager")) + translated_name = "system-file-manager"; + else if (streq(symbolic_icon, "system-monitor")) + translated_name = "utilities-system-monitor"; + else if (streq(symbolic_icon, "text-editor")) + translated_name = "text-editor"; + + if (translated_name != NULL) { + free(symbolic_icon); + symbolic_icon = xstrdup(translated_name); + } } + } else { + free(maybe_new_symbolic_icon); } break; } + } } if (base64) { @@ -824,42 +849,28 @@ kitty_notification(struct terminal *term, char *string) payload_size = strlen(payload); } - /* Search for an existing (d=0) notification to update */ - struct notification *notif = NULL; - tll_foreach(term->kitty_notifications, it) { - if ((id == NULL && it->item.id == NULL) || - (id != NULL && it->item.id != NULL && streq(it->item.id, id))) - { - /* Found existing notification */ - notif = &it->item; - break; - } + /* Append metadata to previous notification chunk */ + struct notification *notif = &term->kitty_notification; + + if (!((id == NULL && notif->id == NULL) || + (id != NULL && notif->id != NULL && streq(id, notif->id))) || + !notif->may_be_programatically_closed) /* Free:d notification has this as false... */ + { + /* ID mismatch, ignore previous notification state */ + notify_free(term, notif); + + notif->id = id; + notif->when = when; + notif->urgency = urgency; + notif->expire_time = expire_time; + notif->focus = focus; + notif->may_be_programatically_closed = true; + notif->report_activated = report_activated; + notif->report_closed = report_closed; + + id = NULL; /* Prevent double free */ } - if (notif == NULL) { - tll_push_front(term->kitty_notifications, ((struct notification){ - .id = id, - .when = when, - .urgency = urgency, - .expire_time = expire_time, - .actions = tll_init(), - .focus = focus, - .may_be_programatically_closed = true, - .report_activated = report_activated, - .report_closed = report_closed, - .stdout_fd = -1, - })); - - id = NULL; /* Prevent double free */ - notif = &tll_front(term->kitty_notifications); - } - - if (notif->pid > 0) { - /* Notification has already been completed, ignore new metadata */ - goto out; - } - - /* Update notification metadata */ if (have_a) { notif->focus = focus; notif->report_activated = report_activated; @@ -875,10 +886,10 @@ kitty_notification(struct terminal *term, char *string) if (have_w) notif->expire_time = expire_time; - if (icon_id != NULL) { - free(notif->icon_id); - notif->icon_id = icon_id; - icon_id = NULL; /* Prevent double free */ + if (icon_cache_id != NULL) { + free(notif->icon_cache_id); + notif->icon_cache_id = icon_cache_id; + icon_cache_id = NULL; /* Prevent double free */ } if (symbolic_icon != NULL) { @@ -898,15 +909,10 @@ kitty_notification(struct terminal *term, char *string) notif->category = category; category = NULL; /* Prevent double free */ } else { - const size_t old_len = strlen(notif->category); - const size_t new_len = strlen(category); - /* Append, comma separated */ - notif->category = - xrealloc(notif->category, old_len + 1 + new_len + 1); - notif->category[old_len] = ','; - memcpy(¬if->category[old_len + 1], category, new_len); - notif->category[old_len + 1 + new_len] = '\0'; + char *new_category = xstrjoin(notif->category, category, ','); + free(notif->category); + notif->category = new_category; } } @@ -923,7 +929,7 @@ kitty_notification(struct terminal *term, char *string) payload = NULL; } else { char *old = *ptr; - *ptr = xstrjoin(old, payload); + *ptr = xstrjoin(old, payload, 0); free(old); } break; @@ -964,11 +970,11 @@ kitty_notification(struct terminal *term, char *string) if (done) { /* Update icon cache, if necessary */ - if (notif->icon_id != NULL && + if (notif->icon_cache_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, + notify_icon_del(term, notif->icon_cache_id); + notify_icon_add(term, notif->icon_cache_id, notif->icon_symbolic_name, notif->icon_data, notif->icon_data_sz); @@ -985,27 +991,19 @@ kitty_notification(struct terminal *term, char *string) notify_close(term, notif->id); } else if (payload_type == PAYLOAD_ALIVE) { char *alive_ids = NULL; - size_t alive_ids_len = 0; tll_foreach(term->active_notifications, it) { /* TODO: check with kitty: use "0" for all notifications with no ID? */ const char *item_id = it->item.id != NULL ? it->item.id : "0"; - const size_t id_len = strlen(item_id); - if (alive_ids == NULL) { + if (alive_ids == NULL) alive_ids = xstrdup(item_id); - alive_ids_len = id_len; - } else { - alive_ids = xrealloc(alive_ids, alive_ids_len + 1 + id_len + 1); - - /* Append "," */ - alive_ids[alive_ids_len] = ','; - memcpy(&alive_ids[alive_ids_len + 1], item_id, id_len); - - alive_ids_len += 1 + id_len; - alive_ids[alive_ids_len] = '\0'; + else { + char *old_alive_ids = alive_ids; + alive_ids = xstrjoin(old_alive_ids, item_id, ','); + free(old_alive_ids); } } @@ -1029,19 +1027,13 @@ kitty_notification(struct terminal *term, char *string) } } - tll_foreach(term->kitty_notifications, it) { - if (&it->item == notif) { - notify_free(term, &it->item); - tll_remove(term->kitty_notifications, it); - break; - } - } + notify_free(term, notif); } out: free(id); free(app_id); - free(icon_id); + free(icon_cache_id); free(symbolic_icon); free(payload); free(category); diff --git a/terminal.c b/terminal.c index bcd2651e..16de2d65 100644 --- a/terminal.c +++ b/terminal.c @@ -994,7 +994,7 @@ reload_fonts(struct terminal *term, bool resize_grid) snprintf(size, sizeof(size), ":size=%.2f", term->font_sizes[i][j].pt_size * scale); - names[i][j] = xstrjoin(font->pattern, size); + names[i][j] = xstrjoin(font->pattern, size, 0); } } @@ -1021,9 +1021,9 @@ reload_fonts(struct terminal *term, bool resize_grid) char *attrs[4] = { [0] = dpi, /* Takes ownership */ - [1] = xstrjoin(dpi, !custom_bold ? ":weight=bold" : ""), - [2] = xstrjoin(dpi, !custom_italic ? ":slant=italic" : ""), - [3] = xstrjoin(dpi, !custom_bold_italic ? ":weight=bold:slant=italic" : ""), + [1] = xstrjoin(dpi, !custom_bold ? ":weight=bold" : "", 0), + [2] = xstrjoin(dpi, !custom_italic ? ":slant=italic" : "", 0), + [3] = xstrjoin(dpi, !custom_bold_italic ? ":weight=bold:slant=italic" : "", 0), }; struct fcft_font *fonts[4]; @@ -1313,7 +1313,6 @@ term_init(const struct config *conf, struct fdm *fdm, struct reaper *reaper, #if defined(FOOT_IME_ENABLED) && FOOT_IME_ENABLED .ime_enabled = true, #endif - .kitty_notifications = tll_init(), .active_notifications = tll_init(), }; @@ -1823,11 +1822,7 @@ term_destroy(struct terminal *term) tll_remove(term->ptmx_paste_buffers, it); } - tll_foreach(term->kitty_notifications, it) { - notify_free(term, &it->item); - tll_remove(term->kitty_notifications, it); - } - + notify_free(term, &term->kitty_notification); tll_foreach(term->active_notifications, it) { notify_free(term, &it->item); tll_remove(term->active_notifications, it); @@ -2041,11 +2036,7 @@ term_reset(struct terminal *term, bool hard) tll_remove(term->alt.sixel_images, it); } - tll_foreach(term->kitty_notifications, it) { - notify_free(term, &it->item); - tll_remove(term->kitty_notifications, it); - } - + notify_free(term, &term->kitty_notification); tll_foreach(term->active_notifications, it) { notify_free(term, &it->item); tll_remove(term->active_notifications, it); diff --git a/terminal.h b/terminal.h index 92d1e8f5..a87a125b 100644 --- a/terminal.h +++ b/terminal.h @@ -799,9 +799,11 @@ struct terminal { void *cb_data; } shutdown; - /* Notifications that either haven't been sent yet, or have been - sent but not yet dismissed */ - tll(struct notification) kitty_notifications; + /* State, to handle chunked notifications */ + struct notification kitty_notification; + + /* Currently active notifications, from foot's perspective (their + notification helper processes are still running) */ tll(struct notification) active_notifications; struct notification_icon notification_icons[32]; diff --git a/tests/test-config.c b/tests/test-config.c index ae111446..a41e8536 100644 --- a/tests/test-config.c +++ b/tests/test-config.c @@ -580,7 +580,7 @@ test_section_desktop_notifications(void) 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); - test_spawn_template(&ctx, &parse_section_desktop_notifications, "command-action-arg", &conf.desktop_notifications.command_action_arg); + test_spawn_template(&ctx, &parse_section_desktop_notifications, "command-action-argument", &conf.desktop_notifications.command_action_arg); test_spawn_template(&ctx, &parse_section_desktop_notifications, "close", &conf.desktop_notifications.close); config_free(&conf); diff --git a/xmalloc.h b/xmalloc.h index 67fa5c43..76db7e1b 100644 --- a/xmalloc.h +++ b/xmalloc.h @@ -25,12 +25,16 @@ xmemdup(const void *ptr, size_t size) } static inline char * -xstrjoin(const char *s1, const char *s2) +xstrjoin(const char *s1, const char *s2, char delim) { size_t n1 = strlen(s1); - size_t n2 = strlen(s2); - char *joined = xmalloc(n1 + n2 + 1); + size_t n2 = delim > 0 ? 1 : 0; + size_t n3 = strlen(s2); + + char *joined = xmalloc(n1 + n2 + n3 + 1); memcpy(joined, s1, n1); - memcpy(joined + n1, s2, n2 + 1); + if (delim > 0) + joined[n1] = delim; + memcpy(joined + n1 + n2, s2, n3 + 1); return joined; }