osc: kitty notifications: buttons, icons, app-name, categories etc

First, icons have been finalized in the specification. There were only
three things we needed to adjust:

* symbolic names are base64 encoded
* there are a couple of OSC-99 defined symbolic names that need to be
  translated to the corresponding XDG icon name.
* allow in-band icons without a cache ID (that is, allow applications
  to use p=icon without having to cache the icon first).

Second, add support for the following new additions to the protocol:

* 'f': custom app-name, overrides the terminal's app-id
* 't': categories
* 'p=alive': lets applications poll for currently active notifications
* 'id' is now 'unset' by default, rather than "0"
* 'w': expire time (i.e. notification timeout)
* "buttons": aka actions. This lets applications add additional (to
  the terminal defined "default" action) actions. The 'activated' event
  has been updated to report which button/action was used to activate
  the notification.

To support button/actions, desktop-notifications.command had to be
reworked a bit.

There's now a new config option:
desktop-notifications.command-action-arg. It has two template
arguments ${action-name} and ${action-label}.

command-action-arg gets expanded for *each* action.

${action-name} and ${action-label} has been replaced by ${action-arg}
in command. This is a somewhat special template, in that it gets
replaced by *all* instances of the expanded actions.
This commit is contained in:
Daniel Eklöf 2024-07-31 16:22:17 +02:00
parent d87b81dd52
commit 76ac910b11
No known key found for this signature in database
GPG key ID: 5BBD4992C116573F
9 changed files with 580 additions and 78 deletions

View file

@ -806,6 +806,11 @@ value_to_spawn_template(struct context *ctx,
char **argv = NULL;
if (ctx->value[0] == '"' && ctx->value[1] == '"' && ctx->value[2] == '\0') {
template->argv.args = NULL;
return true;
}
if (!tokenize_cmdline(ctx->value, &argv)) {
LOG_CONTEXTUAL_ERR("syntax error in command line");
return false;
@ -1110,6 +1115,9 @@ 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"))
return value_to_spawn_template(
ctx, &conf->desktop_notifications.command_action_arg);
else if (streq(key, "close"))
return value_to_spawn_template(
ctx, &conf->desktop_notifications.close);
@ -3189,6 +3197,9 @@ config_load(struct config *conf, const char *conf_path,
.command = {
.argv = {.args = NULL},
},
.command_action_arg = {
.argv = {.args = NULL},
},
.close = {
.argv = {.args = NULL},
},
@ -3231,8 +3242,9 @@ 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} --urgency ${urgency} --hint STRING:image-path:${icon} --action ${action-name}=${action-label} --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-arg} --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);
static const char32_t *url_protocols[] = {
@ -3487,6 +3499,8 @@ config_clone(const struct config *old)
spawn_template_clone(&conf->bell.command, &old->bell.command);
spawn_template_clone(&conf->desktop_notifications.command,
&old->desktop_notifications.command);
spawn_template_clone(&conf->desktop_notifications.command_action_arg,
&old->desktop_notifications.command_action_arg);
spawn_template_clone(&conf->desktop_notifications.close,
&old->desktop_notifications.close);
@ -3571,6 +3585,7 @@ config_free(struct config *conf)
spawn_template_free(&conf->bell.command);
free(conf->scrollback.indicator.text);
spawn_template_free(&conf->desktop_notifications.command);
spawn_template_free(&conf->desktop_notifications.command_action_arg);
spawn_template_free(&conf->desktop_notifications.close);
for (size_t i = 0; i < ALEN(conf->fonts); i++)
config_font_list_destroy(&conf->fonts[i]);

View file

@ -340,6 +340,7 @@ struct config {
struct {
struct config_spawn_template command;
struct config_spawn_template command_action_arg;
struct config_spawn_template close;
bool inhibit_when_focused;
} desktop_notifications;

View file

@ -436,12 +436,37 @@ Note: do not set *TERM* here; use the *term* option in the main
_${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.
notification request, or the empty string if no icon was
specified. Can be used with e.g. notify-send's *--icon*
option, or preferably, by setting the *image-path* hint (with
e.g. notify-send's *--hint* option).
_${category}_ is replaced by the notification's catogory. Can
be used together with e.g. notify-send's *--category* option.
_${urgency}_ is replaced with the notifications urgency;
*low*, *normal* or *critical*.
*low*, *normal* or *critical*. Can be used together with
e.g. notify-send's *--urgency* option.
_${expire-time}_ is replaced with the notification specified
notification timeout. Can be used together with
e.g. notify-send's *--expire-time* option.
_${replace-id}_ is replaced by the notification daemon
assigned ID that the notification replaces/updates. For this
to work, foot needs to know the externally assigned IDs of
previously emitted notifications, see the 'stdout' section
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.
Applications can also define their own custom notification
actions. See the *command-action-arg* option for details.
Ways to trigger notifications
Applications can trigger notifications in the following ways:
@ -469,12 +494,11 @@ Note: do not set *TERM* here; use the *term* option in the main
activation token.
There are two parts to handle this. First, the notification
must define an action. For this purpose, foot definse the
template parameters *${action-name}* and
*${action-label}*. They are intended to be used with
e.g. notify-send's *-A,--action* option.
must define an action. For this purpose, foot will add a
"default" action to the notification (see the
*command-action-arg* option).
Second, foot needs to know when the notification activated,
Second, foot needs to know when the notification is activated,
and it needs to get hold of the XDG activation token.
Both are expected to be printed on stdout.
@ -486,8 +510,8 @@ Note: do not set *TERM* here; use the *term* option in the main
line, prefixed with *xdgtoken=*.
Example:
default
xdgtoken=18179adf579a7a904ce73754964b1ec3
default++
xdgtoken=18179adf579a7a904ce73754964b1ec3
The expected format of stdout may change at any time. Please
read the changelog when upgrading foot.
@ -501,17 +525,81 @@ Note: do not set *TERM* here; use the *term* option in the main
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.
- *id=*_nnn_: same as plain _nnn_.
- *default*: the 'default' action was triggered
- *action=*_default_: same as _default_
- *action=*_n_: application custom action _n_ triggered
- *xdgtoken=*_xyz_: XDG activation token.
Example:
17++
action=default++
xdgtoken=95ebdfe56e4f47ddb5bba9d7dc3a2c35
Default: _notify-send --wait --app-name ${app-id} --icon ${app-id} --urgency ${urgency} --hint STRING:image-path:${icon} --action ${action-name}=${action-label} --print-id -- ${title} ${body}_.
Default: _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*
String to use with *command* to enable passing action/button names
to the notification helper.
Foot will always configure a "default" action that can be used to
"activate" the notification, which in turn can cause the foot
window to be focused, or an escape to be sent to the terminal
application (depending on how the application generated the
notification).
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.
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.
For each notification action (remember, there will always be at
least one), *command-action-arg* will be expanded with the
action's name and label.
Then, _${action-arg}_ 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.
Example:
*command-action-arg=--action ${action-name}=${action-label}*
*command=notify-send ${action-arg} ...*
Assume the application defined two custom actions: *OK* and
*Cancel*.
Given the above, foot will execute:
notify-send++
--action default='Click to activate'++
--action 1=OK++
--action 2=Cancel++
...
Default: _--action ${action-name}=${action-label}_
*close*
Command to execute to close an existing notification.

View file

@ -47,7 +47,8 @@
# command-focused=no
[desktop-notifications]
# command=notify-send --wait --app-name ${app-id} --icon ${app-id} --urgency ${urgency} --hint STRING:image-path:${icon} --action ${action-name}=${action-label} --print-id -- ${title} ${body}
# 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}
# close=""
# inhibit-when-focused=yes

322
notify.c
View file

@ -10,7 +10,7 @@
#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"
@ -27,11 +27,54 @@ notify_free(struct terminal *term, struct notification *notif)
free(notif->id);
free(notif->title);
free(notif->body);
free(notif->category);
free(notif->app_id);
free(notif->icon_id);
free(notif->icon_symbolic_name);
free(notif->icon_data);
free(notif->xdg_token);
free(notif->stdout_data);
tll_free_and_free(notif->actions, free);
if (notif->icon_path != NULL) {
unlink(notif->icon_path);
free(notif->icon_path);
if (notif->icon_fd >= 0)
close(notif->icon_fd);
}
}
static bool
write_icon_file(const void *data, size_t data_sz, int *fd, char **filename,
char **symbolic_name)
{
xassert(*filename == NULL);
xassert(*symbolic_name == NULL);
char name[64] = "/tmp/foot-notification-icon-XXXXXX";
*filename = NULL;
*symbolic_name = NULL;
*fd = mkostemp(name, O_CLOEXEC);
if (*fd < 0) {
LOG_ERRNO("failed to create temporary file for icon cache");
return false;
}
if (write(*fd, data, data_sz) != (ssize_t)data_sz) {
LOG_ERRNO("failed to write icon data to temporary file");
close(*fd);
*fd = -1;
return false;
}
LOG_DBG("wrote icon data to %s", name);
*filename = xstrdup(name);
*symbolic_name = xasprintf("file://%s", *filename);
return true;
}
static bool
@ -94,6 +137,19 @@ consume_stdout(struct notification *notif, bool eof)
LOG_DBG("notification's default action was triggered");
}
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);
} else {
LOG_DBG("unrecognized action triggered: %.*s",
(int)(len - 7), &line[7]);
}
}
/* Check for XDG activation token, 'xdgtoken=xyz' */
else if (len > 9 && memcmp(line, "xdgtoken=", 9) == 0) {
notif->xdg_token = xstrndup(&line[9], len - 9);
@ -170,7 +226,8 @@ notif_done(struct reaper *reaper, pid_t pid, int status, void *data)
if (notif->pid != pid)
continue;
LOG_DBG("notification %s closed", notif->id);
LOG_DBG("notification %s closed",
notif->id != NULL ? notif->id : "<unset>");
if (notif->activated && notif->focus) {
LOG_DBG("focus window on notification activation: \"%s\"",
@ -183,22 +240,29 @@ notif_done(struct reaper *reaper, pid_t pid, int status, void *data)
}
if (notif->activated && notif->report_activated) {
xassert(notif->id != NULL);
LOG_DBG("sending notification activation event to client");
char reply[7 + strlen(notif->id) + 1 + 2 + 1];
const char *id = notif->id != NULL ? notif->id : "0";
char button_nr[16] = {0};
if (notif->activated_button > 0) {
xsnprintf(
button_nr, sizeof(button_nr), "%u", notif->activated_button);
}
char reply[7 + strlen(id) + 1 + strlen(button_nr) + 2 + 1];
int n = xsnprintf(
reply, sizeof(reply), "\033]99;i=%s;\033\\", notif->id);
reply, sizeof(reply), "\033]99;i=%s;%s\033\\", id, button_nr);
term_to_slave(term, reply, n);
}
if (notif->report_closed) {
LOG_DBG("sending notification close event to client");
char reply[7 + strlen(notif->id) + 1 + 7 + 1 + 2 + 1];
const char *id = notif->id != NULL ? notif->id : "0";
char reply[7 + strlen(id) + 1 + 7 + 1 + 2 + 1];
int n = xsnprintf(
reply, sizeof(reply), "\033]99;i=%s:p=close;\033\\", notif->id);
reply, sizeof(reply), "\033]99;i=%s:p=close;\033\\", id);
term_to_slave(term, reply, n);
}
@ -212,14 +276,33 @@ bool
notify_notify(struct terminal *term, struct notification *notif)
{
xassert(notif->xdg_token == NULL);
xassert(notif->external_id == 0);
xassert(notif->pid == 0);
xassert(notif->stdout_fd <= 0);
xassert(notif->stdout_data == NULL);
xassert(notif->icon_path == NULL);
xassert(notif->icon_fd <= 0);
notif->pid = -1;
notif->stdout_fd = -1;
notif->icon_fd = -1;
/* Use body as title, if title is unset */
if (term->conf->desktop_notifications.command.argv.args == NULL)
return false;
if ((term->conf->desktop_notifications.inhibit_when_focused ||
notif->when != NOTIFY_ALWAYS)
&& term->kbd_focus)
{
/* No notifications while we're focused */
return false;
}
const char *app_id = notif->app_id != NULL
? notif->app_id
: term->app_id != NULL
? term->app_id
: term->conf->app_id;
const char *title = notif->title != NULL ? notif->title : notif->body;
const char *body = notif->title != NULL && notif->body != NULL ? notif->body : "";
@ -231,40 +314,64 @@ notify_notify(struct terminal *term, struct notification *notif)
const struct notification_icon *icon = &term->notification_icons[i];
if (icon->id != NULL && streq(icon->id, notif->icon_id)) {
icon_name_or_path = icon->symbolic_name != NULL
? icon->symbolic_name
: icon->tmp_file_name;
/* For now, we set the symbolic name to 'file:///path'
* when using a file based icon. */
xassert(icon->symbolic_name != NULL);
icon_name_or_path = icon->symbolic_name;
LOG_DBG("using icon from cache (cache ID: %s): %s",
icon->id, icon_name_or_path);
break;
}
}
} else if (notif->icon_symbolic_name != NULL) {
icon_name_or_path = notif->icon_symbolic_name;
LOG_DBG("using symbolic icon from notification: %s", icon_name_or_path);
} else if (notif->icon_data_sz > 0) {
xassert(notif->icon_data != NULL);
if (write_icon_file(
notif->icon_data, notif->icon_data_sz,
&notif->icon_fd,
&notif->icon_path,
&notif->icon_symbolic_name))
icon_name_or_path = notif->icon_symbolic_name;
LOG_DBG("using icon data from notification: %s", icon_name_or_path);
}
bool track_notification = notif->focus ||
notif->report_activated ||
notif->may_be_programatically_closed;
LOG_DBG("notify: title=\"%s\", body=\"%s\", icon=\"%s\" (tracking: %s)",
title, body, icon_name_or_path, track_notification ? "yes" : "no");
uint32_t replaces_id = 0;
if (notif->id != NULL) {
tll_foreach(term->active_notifications, it) {
struct notification *existing = &it->item;
xassert(title != NULL);
if (title == NULL)
return false;
if (existing->id == NULL)
continue;
if ((term->conf->desktop_notifications.inhibit_when_focused ||
notif->when != NOTIFY_ALWAYS)
&& term->kbd_focus)
{
/* No notifications while we're focused */
return false;
/*
* When replacing/updating a notificaton, we may have
* *multiple* notification helpers running for the "same"
* notification. Make sure only the *last* notification's
* report closed/activated are honored, to avoid sending
* multiple reports.
*
* This also means we cannot 'break' out of the loop - we
* must check *all* notifications.
*/
if (existing->external_id != 0 && streq(existing->id, notif->id)) {
replaces_id = existing->external_id;
existing->report_activated = false;
existing->report_closed = false;
}
}
}
if (term->conf->desktop_notifications.command.argv.args == NULL)
return false;
char **argv = NULL;
size_t argc = 0;
char replaces_id_str[16];
xsnprintf(replaces_id_str, sizeof(replaces_id_str), "%u", replaces_id);
const char *urgency_str =
notif->urgency == NOTIFY_URGENCY_LOW
@ -272,19 +379,129 @@ notify_notify(struct terminal *term, struct notification *notif)
: notif->urgency == NOTIFY_URGENCY_NORMAL
? "normal" : "critical";
LOG_DBG("notify: title=\"%s\", body=\"%s\", app-id=\"%s\", category=\"%s\", "
"urgency=\"%s\", icon=\"%s\", expires=%d, replaces=%u (tracking: %s)",
title, body, app_id, notif->category, urgency_str, icon_name_or_path,
notif->expire_time, replaces_id, track_notification ? "yes" : "no");
xassert(title != NULL);
if (title == NULL)
return false;
char **argv = NULL;
size_t argc = 0;
char **action_argv = NULL;
size_t action_argc = 0;
char expire_time[16];
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}));
}
size_t action_idx = 0;
tll_foreach(actions, it) {
const char *name = it->item.name;
const char *label = it->item.label;
/*
* 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);
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))
{
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);
}
}
if (!spawn_expand_template(
&term->conf->desktop_notifications.command, 8,
&term->conf->desktop_notifications.command, 10,
(const char *[]){
"app-id", "window-title", "icon", "title", "body", "urgency", "action-name", "action-label"},
"app-id", "window-title", "icon", "title", "body", "category",
"urgency", "expire-time", "replace-id", "action-arg"},
(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"},
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}"},
&argc, &argv))
{
return false;
}
for (size_t i = 0; i < argc; i++) {
if (!streq(argv[i], "${action-arg}"))
continue;
if (action_argc == 0) {
free(argv[i]);
memmove(&argv[i], &argv[i + 1], (argc - i - 1) * sizeof(argv[0]));
argv[argc--] = NULL;
break;
}
/* Remove the "${action-arg}" entry, add all actions argument from earlier */
argv = xrealloc(argv, (argc + action_argc - 1) * sizeof(argv[0]));
/* Move remaining arguments to after the action arguments */
memmove(&argv[i + action_argc],
&argv[i + 1],
(argc - (i + 1)) * sizeof(argv[0]));
free(argv[i]); /* Free xstrdup("${action-arg}"); */
/* Insert the action arguments */
for (size_t j = 0; j < action_argc; j++) {
argv[i + j] = action_argv[j];
action_argv[j] = NULL;
}
argc += action_argc;
argc--; /* The ${action-arg} option has been removed */
break;
}
LOG_DBG("notify command:");
for (size_t i = 0; i < argc; i++)
LOG_DBG(" argv[%zu] = \"%s\"", i, argv[i]);
@ -300,15 +517,23 @@ notify_notify(struct terminal *term, struct notification *notif)
notif->id = NULL;
notif->title = NULL;
notif->body = NULL;
notif->category = NULL;
notif->app_id = 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);
notif->icon_path = NULL;
notif->icon_fd = -1;
notif->stdout_fd = -1;
struct notification *new_notif = &tll_back(term->active_notifications);
/* We don't need these anymore. They'll be free:d by the caller */
memset(&new_notif->actions, 0, sizeof(new_notif->actions));
notif = new_notif;
}
}
if (stdout_fds[0] >= 0) {
fdm_add(term->fdm, stdout_fds[0], EPOLLIN,
&fdm_notify_stdout, (void *)term);
@ -336,6 +561,9 @@ notify_notify(struct terminal *term, struct notification *notif)
for (size_t i = 0; i < argc; i++)
free(argv[i]);
free(argv);
for (size_t i = 0; i < action_argc; i++)
free(action_argv[i]);
free(action_argv);
notif->pid = pid;
notif->stdout_fd = stdout_fds[0];
@ -345,11 +573,12 @@ notify_notify(struct terminal *term, struct notification *notif)
void
notify_close(struct terminal *term, const char *id)
{
xassert(id != NULL);
LOG_DBG("close notification %s", id);
tll_foreach(term->active_notifications, it) {
const struct notification *notif = &it->item;
if (notif->id == 0 || !streq(notif->id, id))
if (notif->id == NULL || !streq(notif->id, id))
continue;
if (term->conf->desktop_notifications.close.argv.args == NULL) {
@ -429,22 +658,11 @@ add_icon(struct notification_icon *icon, const char *id, const char *symbolic_na
* have a symbolic name.
*/
if (symbolic_name == NULL && data_sz > 0) {
char name[64] = "/tmp/foot-notification-icon-cache-XXXXXX";
int fd = mkostemp(name, O_CLOEXEC);
if (fd < 0) {
LOG_ERRNO("failed to create temporary file for icon cache");
return;
}
if (write(fd, data, data_sz) != (ssize_t)data_sz) {
LOG_ERRNO("failed to write icon data to temporary file");
close(fd);
} else {
LOG_DBG("wrote icon data to %s", name);
icon->tmp_file_name = xstrdup(name);
icon->tmp_file_fd = fd;
}
write_icon_file(
data, data_sz,
&icon->tmp_file_fd,
&icon->tmp_file_name,
&icon->symbolic_name);
}
LOG_DBG("added icon to cache: ID=%s: sym=%s, file=%s",

View file

@ -3,6 +3,8 @@
#include <stdint.h>
#include <unistd.h>
#include <tllist.h>
struct terminal;
enum notify_when {
@ -30,7 +32,9 @@ struct notification {
char *id;
char *title;
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;
@ -38,6 +42,9 @@ struct notification {
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;
@ -49,6 +56,7 @@ struct notification {
uint32_t external_id; /* Daemon assigned notification ID */
bool activated; /* User 'activated' the notification */
uint32_t activated_button; /* User activated one of the custom actions */
char *xdg_token; /* XDG activation token, from daemon */
pid_t pid; /* Notifier command PID */
@ -56,6 +64,11 @@ struct notification {
char *stdout_data; /* Data we've reado from command's stdout */
size_t stdout_sz;
/* Used when notification provides raw icon data, and it's
bypassing the icon cache */
char *icon_path;
int icon_fd;
};
struct notification_icon {

176
osc.c
View file

@ -559,7 +559,9 @@ osc_notify(struct terminal *term, char *string)
notify_notify(term, &(struct notification){
.title = (char *)title,
.body = (char *)msg});
.body = (char *)msg,
.expire_time = -1,
});
}
static void
@ -575,9 +577,11 @@ kitty_notification(struct terminal *term, char *string)
*payload_raw = '\0';
payload_raw++;
char *id = xstrdup("0"); /* The 'i' parameter */
char *id = NULL; /* The 'i' parameter */
char *app_id = NULL; /* The 'f' parameter */
char *icon_id = NULL; /* The 'g' parameter */
char *symbolic_icon = NULL; /* The 'n' parameter */
char *category = NULL; /* The 't' parameter */
char *payload = NULL;
bool focus = true; /* The 'a' parameter */
@ -586,12 +590,16 @@ kitty_notification(struct terminal *term, char *string)
bool done = true; /* The 'd' parameter */
bool base64 = false; /* The 'e' parameter */
int32_t expire_time = -1; /* The 'w' parameter */
size_t payload_size;
enum {
PAYLOAD_TITLE,
PAYLOAD_BODY,
PAYLOAD_CLOSE,
PAYLOAD_ALIVE,
PAYLOAD_ICON,
PAYLOAD_BUTTON,
} payload_type = PAYLOAD_TITLE; /* The 'p' parameter */
enum notify_when when = NOTIFY_ALWAYS;
@ -601,6 +609,7 @@ kitty_notification(struct terminal *term, char *string)
bool have_c = false;
bool have_o = false;
bool have_u = false;
bool have_w = false;
char *ctx = NULL;
for (char *param = strtok_r(parameters, ":", &ctx);
@ -675,8 +684,12 @@ kitty_notification(struct terminal *term, char *string)
payload_type = PAYLOAD_BODY;
else if (streq(value, "close"))
payload_type = PAYLOAD_CLOSE;
else if (streq(value, "alive"))
payload_type = PAYLOAD_ALIVE;
else if (streq(value, "icon"))
payload_type = PAYLOAD_ICON;
else if (streq(value, "buttons"))
payload_type = PAYLOAD_BUTTON;
else if (streq(value, "?")) {
/* Query capabilities */
@ -690,9 +703,10 @@ kitty_notification(struct terminal *term, char *string)
char reply[128];
int n = xsnprintf(
reply, sizeof(reply),
"\033]99;i=%s:p=?;p=title,body,close,icon:a=focus,report:o=%s:u=0,1,2:c=1%s",
id, when_str, terminator);
"\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);
xassert(n < sizeof(reply));
term_to_slave(term, reply, n);
goto out;
}
@ -720,6 +734,45 @@ kitty_notification(struct terminal *term, char *string)
urgency = NOTIFY_URGENCY_CRITICAL;
break;
case 'w': {
/* Notification timeout */
errno = 0;
char *end = NULL;
long timeout = strtol(value, &end, 10);
if (errno == 0 && *end == '\0' && timeout <= INT32_MAX) {
expire_time = timeout;
have_w = true;
}
break;
}
case 'f':
free(app_id);
app_id = base64_decode(value, NULL);
break;
case 't': {
/* Type (category) */
char *decoded = base64_decode(value, NULL);
if (decoded != NULL) {
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';
free(decoded);
}
}
break;
}
case 'g':
/* graphical ID */
free(icon_id);
@ -729,7 +782,35 @@ kitty_notification(struct terminal *term, char *string)
case 'n':
/* Symbolic icon name, used with 'g' */
free(symbolic_icon);
symbolic_icon = xstrdup(value);
symbolic_icon = base64_decode(value, NULL);
/* 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);
}
}
break;
}
}
@ -746,7 +827,9 @@ 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 (streq(it->item.id, id)) {
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;
@ -758,6 +841,8 @@ kitty_notification(struct terminal *term, char *string)
.id = id,
.when = when,
.urgency = urgency,
.expire_time = expire_time,
.actions = tll_init(),
.focus = focus,
.may_be_programatically_closed = true,
.report_activated = report_activated,
@ -787,6 +872,8 @@ kitty_notification(struct terminal *term, char *string)
notif->when = when;
if (have_u)
notif->urgency = urgency;
if (have_w)
notif->expire_time = expire_time;
if (icon_id != NULL) {
free(notif->icon_id);
@ -800,6 +887,29 @@ kitty_notification(struct terminal *term, char *string)
symbolic_icon = NULL;
}
if (app_id != NULL) {
free(notif->app_id);
notif->app_id = app_id;
app_id = NULL; /* Prevent double free */
}
if (category != NULL) {
if (notif->category == NULL) {
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(&notif->category[old_len + 1], category, new_len);
notif->category[old_len + 1 + new_len] = '\0';
}
}
/* Handled chunked payload - append to existing metadata */
switch (payload_type) {
case PAYLOAD_TITLE:
@ -820,6 +930,7 @@ kitty_notification(struct terminal *term, char *string)
}
case PAYLOAD_CLOSE:
case PAYLOAD_ALIVE:
/* Ignore payload */
break;
@ -835,6 +946,20 @@ kitty_notification(struct terminal *term, char *string)
notif->icon_data_sz += payload_size;
}
break;
case PAYLOAD_BUTTON: {
char *ctx = NULL;
for (const char *button = strtok_r(payload, "\u2028", &ctx);
button != NULL;
button = strtok_r(NULL, "\u2028", &ctx))
{
if (button[0] != '\0') {
tll_push_back(notif->actions, xstrdup(button));
}
}
break;
}
}
if (done) {
@ -856,7 +981,42 @@ kitty_notification(struct terminal *term, char *string)
}
if (payload_type == PAYLOAD_CLOSE) {
notify_close(term, notif->id);
if (notif->id != NULL)
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) {
alive_ids = xstrdup(item_id);
alive_ids_len = id_len;
} else {
alive_ids = xrealloc(alive_ids, alive_ids_len + 1 + id_len + 1);
/* Append ",<id>" */
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';
}
}
char *reply = xasprintf(
"\033]99;i=%s:p=alive;%s\033\\",
notif->id != NULL ? notif->id : "0",
alive_ids != NULL ? alive_ids : "");
term_to_slave(term, reply, strlen(reply));
free(reply);
free(alive_ids);
} else {
/*
* Show notification.
@ -880,9 +1040,11 @@ kitty_notification(struct terminal *term, char *string)
out:
free(id);
free(app_id);
free(icon_id);
free(symbolic_icon);
free(payload);
free(category);
}
void

View file

@ -3601,7 +3601,9 @@ term_bell(struct terminal *term)
if (term->conf->bell.notify) {
notify_notify(term, &(struct notification){
.title = (char *)"Bell",
.body = (char *)"Bell in terminal"});
.body = (char *)"Bell in terminal",
.expire_time = -1,
});
}
if (term->conf->bell.flash)

View file

@ -580,6 +580,8 @@ 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, "close", &conf.desktop_notifications.close);
config_free(&conf);
}