mirror of
https://codeberg.org/dnkl/foot.git
synced 2026-02-04 04:06:06 -05:00
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:
parent
d87b81dd52
commit
76ac910b11
9 changed files with 580 additions and 78 deletions
17
config.c
17
config.c
|
|
@ -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]);
|
||||
|
|
|
|||
1
config.h
1
config.h
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
3
foot.ini
3
foot.ini
|
|
@ -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
322
notify.c
|
|
@ -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,
|
||||
¬if->icon_fd,
|
||||
¬if->icon_path,
|
||||
¬if->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",
|
||||
|
|
|
|||
13
notify.h
13
notify.h
|
|
@ -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
176
osc.c
|
|
@ -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(¬if->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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue