diff --git a/config.c b/config.c
index d34df132..a624f95c 100644
--- a/config.c
+++ b/config.c
@@ -1024,11 +1024,29 @@ parse_section_main(struct context *ctx)
else if (streq(key, "word-delimiters"))
return value_to_wchars(ctx, &conf->word_delimiters);
- else if (streq(key, "notify"))
- return value_to_spawn_template(ctx, &conf->notify);
+ else if (streq(key, "notify")) {
+ user_notification_add(
+ &conf->notifications, USER_NOTIFICATION_DEPRECATED,
+ xstrdup("notify: use desktop-notifications.command instead"));
+ log_msg(
+ LOG_CLASS_WARNING, LOG_MODULE, __FILE__, __LINE__,
+ "deprecated: notify: use desktop-notifications.command instead");
+ return value_to_spawn_template(
+ ctx, &conf->desktop_notifications.command);
+ }
- else if (streq(key, "notify-focus-inhibit"))
- return value_to_bool(ctx, &conf->notify_focus_inhibit);
+ else if (streq(key, "notify-focus-inhibit")) {
+ user_notification_add(
+ &conf->notifications, USER_NOTIFICATION_DEPRECATED,
+ xstrdup("notify-focus-inhibit: "
+ "use desktop-notifications.inhibit-when-focused instead"));
+ log_msg(
+ LOG_CLASS_WARNING, LOG_MODULE, __FILE__, __LINE__,
+ "deprecrated: notify-focus-inhibit: "
+ "use desktop-notifications.inhibit-when-focused instead");
+ return value_to_bool(
+ ctx, &conf->desktop_notifications.inhibit_when_focused);
+ }
else if (streq(key, "selection-target")) {
_Static_assert(sizeof(conf->selection_target) == sizeof(int),
@@ -1083,6 +1101,24 @@ parse_section_bell(struct context *ctx)
}
}
+static bool
+parse_section_desktop_notifications(struct context *ctx)
+{
+ struct config *conf = ctx->conf;
+ const char *key = ctx->key;
+
+ if (streq(key, "command"))
+ return value_to_spawn_template(
+ ctx, &conf->desktop_notifications.command);
+ else if (streq(key, "inhibit-when-focused"))
+ return value_to_bool(
+ ctx, &conf->desktop_notifications.inhibit_when_focused);
+ else {
+ LOG_CONTEXTUAL_ERR("not a valid option: %s", key);
+ return false;
+ }
+}
+
static bool
parse_section_scrollback(struct context *ctx)
{
@@ -2662,6 +2698,7 @@ parse_key_value(char *kv, const char **section, const char **key, const char **v
enum section {
SECTION_MAIN,
SECTION_BELL,
+ SECTION_DESKTOP_NOTIFICATIONS,
SECTION_SCROLLBACK,
SECTION_URL,
SECTION_COLORS,
@@ -2688,6 +2725,7 @@ static const struct {
} section_info[] = {
[SECTION_MAIN] = {&parse_section_main, "main"},
[SECTION_BELL] = {&parse_section_bell, "bell"},
+ [SECTION_DESKTOP_NOTIFICATIONS] = {&parse_section_desktop_notifications, "desktop-notifications"},
[SECTION_SCROLLBACK] = {&parse_section_scrollback, "scrollback"},
[SECTION_URL] = {&parse_section_url, "url"},
[SECTION_COLORS] = {&parse_section_colors, "colors"},
@@ -3144,10 +3182,12 @@ config_load(struct config *conf, const char *conf_path,
.presentation_timings = false,
.selection_target = SELECTION_TARGET_PRIMARY,
.hold_at_exit = false,
- .notify = {
- .argv = {.args = NULL},
+ .desktop_notifications = {
+ .command = {
+ .argv = {.args = NULL},
+ },
+ .inhibit_when_focused = true,
},
- .notify_focus_inhibit = true,
.tweak = {
.fcft_filter = FCFT_SCALING_FILTER_LANCZOS3,
@@ -3185,8 +3225,8 @@ 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 -a ${app-id} -i ${app-id} -u ${urgency} ${title} ${body}",
- &conf->notify.argv.args);
+ "notify-send --wait --app-name ${app-id} --icon ${icon} --urgency ${urgency} -- ${title} ${body}",
+ &conf->desktop_notifications.command.argv.args);
tokenize_cmdline("xdg-open ${url}", &conf->url.launch.argv.args);
static const char32_t *url_protocols[] = {
@@ -3439,7 +3479,8 @@ config_clone(const struct config *old)
conf->scrollback.indicator.text = xc32dup(old->scrollback.indicator.text);
conf->server_socket_path = xstrdup(old->server_socket_path);
spawn_template_clone(&conf->bell.command, &old->bell.command);
- spawn_template_clone(&conf->notify, &old->notify);
+ spawn_template_clone(&conf->desktop_notifications.command,
+ &old->desktop_notifications.command);
for (size_t i = 0; i < ALEN(conf->fonts); i++)
config_font_list_clone(&conf->fonts[i], &old->fonts[i]);
@@ -3521,7 +3562,7 @@ config_free(struct config *conf)
free(conf->word_delimiters);
spawn_template_free(&conf->bell.command);
free(conf->scrollback.indicator.text);
- spawn_template_free(&conf->notify);
+ spawn_template_free(&conf->desktop_notifications.command);
for (size_t i = 0; i < ALEN(conf->fonts); i++)
config_font_list_destroy(&conf->fonts[i]);
free(conf->server_socket_path);
diff --git a/config.h b/config.h
index 4ce36486..b3688f28 100644
--- a/config.h
+++ b/config.h
@@ -338,8 +338,10 @@ struct config {
SELECTION_TARGET_BOTH
} selection_target;
- struct config_spawn_template notify;
- bool notify_focus_inhibit;
+ struct {
+ struct config_spawn_template command;
+ bool inhibit_when_focused;
+ } desktop_notifications;
env_var_list_t env_vars;
diff --git a/doc/foot-ctlseqs.7.scd b/doc/foot-ctlseqs.7.scd
index 5c611c92..998b6843 100644
--- a/doc/foot-ctlseqs.7.scd
+++ b/doc/foot-ctlseqs.7.scd
@@ -718,6 +718,10 @@ All _OSC_ sequences begin with *\\E]*, sometimes abbreviated _OSC_.
: Copy _Pd_ (base64 encoded text) to the clipboard. _Pc_ denotes the
target: *c* targets the clipboard and *s* and *p* the primary
selection.
+| \\E] 99 ; _params_ ; _payload_ \\E\\
+: kitty
+: Desktop notification; uses *desktop-notifications.command* in
+ *foot.ini*(5).
| \\E] 104 ; _c_ \\E\\
: xterm
: Reset color number _c_ (multiple semicolon separated _c_ values may
@@ -757,7 +761,8 @@ All _OSC_ sequences begin with *\\E]*, sometimes abbreviated _OSC_.
: Flash the entire terminal (foot extension)
| \\E] 777;notify;_title_;_msg_ \\E\\
: urxvt
-: Desktop notification, uses *notify* in *foot.ini*(5).
+: Desktop notification, uses *desktop-notifications.command* in
+ *foot.ini*(5).
# DCS
diff --git a/doc/foot.ini.5.scd b/doc/foot.ini.5.scd
index fe6ff57b..e6b554bb 100644
--- a/doc/foot.ini.5.scd
+++ b/doc/foot.ini.5.scd
@@ -342,32 +342,6 @@ empty string to be set, but it must be quoted: *KEY=""*)
text. Note that whitespace characters are _always_ word
delimiters, regardless of this setting. Default: _,│`|:"'()[]{}<>_
-*notify*
- Command to execute to display a notification. _${title}_ and
- _${body}_ will be replaced with the notification's actual _title_
- and _body_ (message content).
-
- _${app-id}_ is replaced with the value of the command line option
- _--app-id_, and defaults to *foot* (normal mode), or
- *footclient* (server mode).
-
- _${window-title}_ is replaced with the current window title.
-
- Applications can trigger notifications in the following ways:
-
- - OSC 777: *\\e]777;notify;
;\\e\\\\*
-
- By default, notifications are *inhibited* if the foot window
- has keyboard focus. See _notify-focus-inhibit_.
-
- Default: _notify-send -a ${app-id} -i ${app-id} -u ${urgency} ${title} ${body}_.
-
-*notify-focus-inhibit*
- Boolean. If enabled, foot will not display notifications if the
- terminal window has keyboard focus.
-
- Default: _yes_
-
*selection-target*
Clipboard target to automatically copy selected text to. One of
*none*, *primary*, *clipboard* or *both*. Default: _primary_.
@@ -426,10 +400,11 @@ Note: do not set *TERM* here; use the *term* option in the main
Default: _no_
*notify*
- When set to _yes_, foot will emit a desktop notification using
- the command specified in the *notify* option whenever *BEL* is
+ When set to _yes_, foot will emit a desktop notification using the
+ command specified in the *notify* option whenever *BEL* is
received. By default, bell notifications are shown only when the
- window does *not* have keyboard focus. See _notify-focus-inhibit_.
+ window does *not* have keyboard focus. See
+ _desktop-notifications.inhibit-when-focused_.
Default: _no_
@@ -445,6 +420,82 @@ Note: do not set *TERM* here; use the *term* option in the main
Whether to run the command on *BEL* even while focused. Default:
_no_
+# SECTION: desktop-notifications
+
+*command*
+ Command to execute to display a notification.
+
+ Template arguments
+ _${title}_ and _${body}_ will be replaced with the
+ notification's actual _title_ and _body_ (message content).
+
+ _${app-id}_ is replaced with the value of the command line
+ option _--app-id_, and defaults to *foot* (normal mode), or
+ *footclient* (server mode).
+
+ _${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.
+
+ _${urgency}_ is replaced with the notifications urgency;
+ *low*, *normal* or *critical*.
+
+ Ways to trigger notifications
+ Applications can trigger notifications in the following ways:
+
+ - OSC 777: *\\e]777;notify;;\\e\\\\*
+ - OSC 99: *\\e]99;;\\e\\\\* (this is just a bare bones
+ example; this protocol has lots of features, see
+ https://sw.kovidgoyal.net/kitty/desktop-notifications)
+
+ By default, notifications are *inhibited* if the foot window
+ has keyboard focus. See
+ _desktop-notifications.inhibit-when-focused_.
+
+ Window activation (focusing)
+ Foot can focus the window when the notification is
+ "activated". This typically happens when the default action is
+ invoked, and/or when the notification is clicked, but exact
+ behavior depends on the notification daemon in use, and how it
+ has been configured.
+
+ For this to work, foot needs an XDG activation token. To this
+ end, foot will read the command's stdout; everything printed
+ there, not including trailing newlines, are assumed to be part
+ of the activation token. There is no harm in printing
+ something else on stdout - it will simply result in the
+ activation failing (i.e. the window will not be focused).
+
+ *Note*: notify-send does not, out of the box, support
+ reporting the XDG activation token in any way. This means
+ window activation will not work by default.
+
+ Notification dismissal
+ The kitty desktop notifications protocol (OSC-99) allows the
+ terminal application to request an event be sent to it when
+ the notification has been dismissed (by setting *a=report* in
+ the notification request).
+
+ To be able to send this event, foot needs to know when the
+ notification is dismissed. This is handled in a very simple
+ manner; the command signals notification dismissal by
+ exiting. That is, as soon as the command returns, foot
+ considers the notification dismissed.
+
+ For *notify-send*, this can be achieved with the *--wait*
+ option.
+
+ Default: _notify-send --wait --app-name ${app-id} --icon ${icon} --urgency ${urgency} -- ${title} ${body}_.
+
+*inhibit-when-focused*
+ Boolean. If enabled, foot will not display notifications if the
+ terminal window has keyboard focus.
+
+ Default: _yes_
+
# SECTION: scrollback
*lines*
diff --git a/foot.ini b/foot.ini
index 7a1db9ba..33727dd3 100644
--- a/foot.ini
+++ b/foot.ini
@@ -29,8 +29,6 @@
# resize-by-cells=yes
# resize-delay-ms=100
-# notify=notify-send -a ${app-id} -i ${app-id} -u ${urgency} ${title} ${body}
-
# bold-text-in-bright=no
# word-delimiters=,│`|:"'()[]{}<>
# selection-target=primary
@@ -48,6 +46,11 @@
# command=
# command-focused=no
+[desktop-notifications]
+# command=notify-send --wait --app-name ${app-id} --icon ${icon} --urgency ${urgency} -- ${title} ${body}
+# inhibit-when-focused=yes
+
+
[scrollback]
# lines=1000
# multiplier=3.0
diff --git a/notify.c b/notify.c
index 7cd22ccb..043f41a5 100644
--- a/notify.c
+++ b/notify.c
@@ -1,9 +1,11 @@
#include "notify.h"
+#include
#include
#include
#include
+#include
#include
#include
@@ -13,50 +15,189 @@
#include "config.h"
#include "spawn.h"
#include "terminal.h"
+#include "wayland.h"
#include "xmalloc.h"
+#include "xsnprintf.h"
void
-notify_notify(const struct terminal *term, const char *title, const char *body,
- enum notify_when when, enum notify_urgency urgency)
+notify_free(struct terminal *term, struct notification *notif)
{
- LOG_DBG("notify: title=\"%s\", msg=\"%s\"", title, body);
+ fdm_del(term->fdm, notif->stdout_fd);
+ free(notif->id);
+ free(notif->title);
+ free(notif->body);
+ free(notif->icon);
+ free(notif->xdg_token);
+}
- if ((term->conf->notify_focus_inhibit || when != NOTIFY_ALWAYS)
+static bool
+fdm_notify_stdout(struct fdm *fdm, int fd, int events, void *data)
+{
+ const struct terminal *term = data;
+ struct notification *notif = NULL;
+
+ /* Find notification */
+ tll_foreach(term->notifications, it) {
+ if (it->item.stdout_fd == fd) {
+ notif = &it->item;
+ break;
+ }
+ }
+
+ if (events & EPOLLIN) {
+ char buf[512];
+ ssize_t count = read(fd, buf, sizeof(buf) - 1);
+
+ if (count < 0) {
+ if (errno == EINTR)
+ return true;
+
+ LOG_ERRNO("failed to read notification activation token");
+ return false;
+ }
+
+ if (count > 0) {
+ buf[count - 1] = '\0';
+
+ if (notif != NULL) {
+ if (notif->xdg_token == NULL) {
+ notif->xdg_token = xstrdup(buf);
+ } else {
+ char *new_token = xstrjoin(notif->xdg_token, buf);
+ free(notif->xdg_token);
+ notif->xdg_token = new_token;
+ }
+ }
+ }
+ }
+
+ if (events & EPOLLHUP) {
+ fdm_del(fdm, fd);
+ if (notif != NULL)
+ notif->stdout_fd = -1;
+
+ /* Strip trailing newlines */
+ if (notif != NULL && notif->xdg_token != NULL) {
+ size_t len = strlen(notif->xdg_token);
+
+ while (len > 0 && notif->xdg_token[len - 1] == '\n')
+ len--;
+
+ notif->xdg_token[len] = '\0';
+ }
+ }
+
+ return true;
+}
+
+static void
+notif_done(struct reaper *reaper, pid_t pid, int status, void *data)
+{
+ struct terminal *term = data;
+
+ tll_foreach(term->notifications, it) {
+ struct notification *notif = &it->item;
+ if (notif->pid != pid)
+ continue;
+
+ LOG_DBG("notification %s dismissed", notif->id);
+
+ if (notif->focus) {
+ LOG_DBG("focus window on notification activation: \"%s\"", notif->xdg_token);
+ wayl_activate(term->wl, term->window, notif->xdg_token);
+ }
+
+ if (notif->report) {
+ xassert(notif->id != NULL);
+
+ LOG_DBG("sending notification report to client");
+
+ char reply[5 + strlen(notif->id) + 1 + 2 + 1];
+ int n = xsnprintf(
+ reply, sizeof(reply), "\033]99;%s;\033\\", notif->id);
+ term_to_slave(term, reply, n);
+ }
+
+ notify_free(term, notif);
+ tll_remove(term->notifications, it);
+ return;
+ }
+}
+
+bool
+notify_notify(const struct terminal *term, struct notification *notif)
+{
+ xassert(notif->xdg_token == NULL);
+ xassert(notif->pid == 0);
+ xassert(notif->stdout_fd == 0);
+
+ notif->pid = -1;
+ notif->stdout_fd = -1;
+
+ /* Use body as title, if title is unset */
+ const char *title = notif->title != NULL ? notif->title : notif->body;
+ const char *body = notif->title != NULL && notif->body != NULL ? notif->body : "";
+
+ /* Icon: use symbolic name from notification, if present,
+ otherwise fallback to the application ID */
+ const char *icon = notif->icon != NULL
+ ? notif->icon
+ : term->app_id != NULL
+ ? term->app_id
+ : term->conf->app_id;
+
+ LOG_DBG("notify: title=\"%s\", body=\"%s\"", title, body);
+
+ xassert(title != NULL);
+ if (title == 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;
+ return false;
}
- if (title == NULL || body == NULL)
- return;
-
- if (term->conf->notify.argv.args == NULL)
- return;
+ if (term->conf->desktop_notifications.command.argv.args == NULL)
+ return false;
char **argv = NULL;
size_t argc = 0;
const char *urgency_str =
- urgency == NOTIFY_URGENCY_LOW
+ notif->urgency == NOTIFY_URGENCY_LOW
? "low"
- : urgency == NOTIFY_URGENCY_NORMAL
+ : notif->urgency == NOTIFY_URGENCY_NORMAL
? "normal" : "critical";
if (!spawn_expand_template(
- &term->conf->notify, 5,
- (const char *[]){"app-id", "window-title", "title", "body", "urgency"},
+ &term->conf->desktop_notifications.command, 6,
+ (const char *[]){"app-id", "window-title", "icon", "title", "body", "urgency"},
(const char *[]){term->app_id ? term->app_id : term->conf->app_id,
- term->window_title, title, body, urgency_str},
+ term->window_title, icon, title, body, urgency_str},
&argc, &argv))
{
- return;
+ return false;
}
LOG_DBG("notify command:");
for (size_t i = 0; i < argc; i++)
LOG_DBG(" argv[%zu] = \"%s\"", i, argv[i]);
+ int stdout_fds[2] = {-1, -1};
+ if (notif->focus && pipe2(stdout_fds, O_CLOEXEC | O_NONBLOCK) < 0) {
+ LOG_WARN("failed to create stdout pipe");
+ /* Non-fatal */
+ }
+
+ if (stdout_fds[0] >= 0) {
+ xassert(notif->xdg_token == NULL);
+ fdm_add(term->fdm, stdout_fds[0], EPOLLIN,
+ &fdm_notify_stdout, (void *)term);
+ }
+
/* Redirect stdin to /dev/null, but ignore failure to open */
int devnull = open("/dev/null", O_RDONLY);
pid_t pid = spawn(
@@ -64,6 +205,14 @@ notify_notify(const struct terminal *term, const char *title, const char *body,
¬if_done, (void *)term, NULL);
if (stdout_fds[1] >= 0) {
+ /* Close write-end of stdout pipe */
+ close(stdout_fds[1]);
+ }
+
+ if (pid < 0 && stdout_fds[0] >= 0) {
+ /* Remove FDM callback if we failed to spawn */
+ fdm_del(term->fdm, stdout_fds[0]);
+ }
if (devnull >= 0)
close(devnull);
@@ -71,4 +220,8 @@ notify_notify(const struct terminal *term, const char *title, const char *body,
for (size_t i = 0; i < argc; i++)
free(argv[i]);
free(argv);
+
+ notif->pid = pid;
+ notif->stdout_fd = stdout_fds[0];
+ return true;
}
diff --git a/notify.h b/notify.h
index be79b41a..6c43e294 100644
--- a/notify.h
+++ b/notify.h
@@ -1,30 +1,41 @@
#pragma once
#include
+#include
struct terminal;
enum notify_when {
+ /* First, so that it can be left out of initializer and still be
+ the default */
NOTIFY_ALWAYS,
+
NOTIFY_UNFOCUSED,
NOTIFY_INVISIBLE
};
enum notify_urgency {
- NOTIFY_URGENCY_LOW,
+ /* First, so that it can be left out of initializer and still be
+ the default */
NOTIFY_URGENCY_NORMAL,
+
+ NOTIFY_URGENCY_LOW,
NOTIFY_URGENCY_CRITICAL,
};
-struct kitty_notification {
+struct notification {
char *id;
char *title;
char *body;
+ char *icon;
+ char *xdg_token;
enum notify_when when;
enum notify_urgency urgency;
bool focus;
bool report;
+
+ pid_t pid;
+ int stdout_fd;
};
-void notify_notify(
- const struct terminal *term, const char *title, const char *body,
- enum notify_when when, enum notify_urgency urgency);
+bool notify_notify(const struct terminal *term, struct notification *notif);
+void notify_free(struct terminal *term, struct notification *notif);
diff --git a/osc.c b/osc.c
index 7485cada..6926c3ca 100644
--- a/osc.c
+++ b/osc.c
@@ -560,9 +560,9 @@ osc_notify(struct terminal *term, char *string)
return;
}
- notify_notify(
- term, title, msg != NULL ? msg : "",
- NOTIFY_ALWAYS, NOTIFY_URGENCY_NORMAL);
+ notify_notify(term, &(struct notification){
+ .title = (char *)title,
+ .body = (char *)msg});
}
static void
@@ -571,16 +571,15 @@ kitty_notification(struct terminal *term, char *string)
/* https://sw.kovidgoyal.net/kitty/desktop-notifications */
char *payload = strchr(string, ';');
- if (payload == NULL) {
- LOG_ERR("OSC-99: payload missing");
+ if (payload == NULL)
return;
- }
char *parameters = string;
*payload = '\0';
payload++;
char *id = xstrdup("0"); /* The 'i' parameter */
+ char *icon = NULL; /* The 'I' parameter */
bool focus = true; /* The 'a' parameter */
bool report = false; /* The 'a' parameter */
bool done = true; /* The 'd' parameter */
@@ -601,10 +600,8 @@ kitty_notification(struct terminal *term, char *string)
{
/* All parameters are on the form X=value, where X is always
exactly one character */
- if (param[0] == '\0' || param[1] != '=') {
- LOG_WARN("OSC-99: invalid parameter: \"%s\"", param);
+ if (param[0] == '\0' || param[1] != '=')
continue;
- }
char *value = ¶m[2];
@@ -613,25 +610,19 @@ kitty_notification(struct terminal *term, char *string)
/* notification activation action: focus|report|-focus|-report */
have_a = true;
char *a_ctx = NULL;
+
for (const char *v = strtok_r(value, ",", &a_ctx);
v != NULL;
v = strtok_r(NULL, ",", &a_ctx))
{
- LOG_WARN(" a: \"%s\"", v);
bool reverse = v[0] == '-';
if (reverse)
v++;
- if (strcmp(v, "focus") == 0) {
+ if (strcmp(v, "focus") == 0)
focus = !reverse;
- if (focus)
- LOG_WARN("unimplemented: OSC-99: focus on notification activation");
- } else if (strcmp(v, "report") == 0) {
+ else if (strcmp(v, "report") == 0)
report = !reverse;
- if (report)
- LOG_WARN("unimplemented: OSC-99: report on notification activation");
- } else
- LOG_WARN("OSC-99: unrecognized value for 'a': \"%s\", ignoring", v);
}
break;
@@ -643,8 +634,6 @@ kitty_notification(struct terminal *term, char *string)
done = false;
else if (value[0] == '1' && value[1] == '\0')
done = true;
- else
- LOG_WARN("OSC-99: unrecognized value for 'd': \"%s\", ignoring", value);
break;
case 'e':
@@ -653,8 +642,6 @@ kitty_notification(struct terminal *term, char *string)
base64 = false;
else if (value[0] == '1' && value[1] == '\0')
base64 = true;
- else
- LOG_WARN("OSC-99: unrecognized value for 'e': \"%s\", ignoring", value);
break;
case 'i':
@@ -669,8 +656,6 @@ kitty_notification(struct terminal *term, char *string)
payload_is_title = true;
else if (strcmp(value, "body") == 0)
payload_is_title = false;
- else
- LOG_WARN("OSC-99: unrecognized value for 'p': \"%s\", ignoring", value);
break;
case 'o':
@@ -682,8 +667,6 @@ kitty_notification(struct terminal *term, char *string)
when = NOTIFY_UNFOCUSED;
else if (strcmp(value, "invisible") == 0)
when = NOTIFY_INVISIBLE;
- else
- LOG_WARN("OSC-99: unrecognized value for 'o': \"%s\", ignoring", value);
break;
case 'u':
@@ -695,13 +678,16 @@ kitty_notification(struct terminal *term, char *string)
urgency = NOTIFY_URGENCY_NORMAL;
else if (value[0] == '2' && value[1] == '\0')
urgency = NOTIFY_URGENCY_CRITICAL;
- else
- LOG_WARN("OSC-99: unrecognized value for 'u': \"%s\", ignoring", value);
break;
- default:
- LOG_WARN("OSC-99: unrecognized parameter: \"%s\", ignoring", param);
- break;
+ /*
+ * The options below are not (yet) part of the official spec.
+ */
+ case 'I':
+ /* icon: only symbolic names allowed; absolute paths are ignored */
+ if (value[0] != '/')
+ icon = xstrdup(value);
+ break;
}
}
@@ -710,9 +696,9 @@ kitty_notification(struct terminal *term, char *string)
else
payload = xstrdup(payload);
- LOG_DBG("id=%s, done=%d, focus=%d, report=%d, base64=%d, payload: %s, "
+ LOG_DBG("id=%s, done=%d, focus=%d, report=%d, base64=%d, icon=%s, payload: %s, "
"honor: %s, urgency: %s, %s: %s",
- id, done, focus, report, base64,
+ id, done, focus, report, base64, icon != NULL ? icon : "",
payload_is_title ? "title" : "body",
(when == NOTIFY_ALWAYS
? "always"
@@ -726,11 +712,10 @@ kitty_notification(struct terminal *term, char *string)
payload_is_title ? "title" : "body", payload);
/* Search for an existing (d=0) notification to update */
- struct kitty_notification *notif = NULL;
- tll_foreach(term->kitty_notifications, it) {
+ struct notification *notif = NULL;
+ tll_foreach(term->notifications, it) {
if (strcmp(it->item.id, id) == 0) {
/* Found existing notification */
- LOG_WARN("found existing kitty notification");
notif = &it->item;
break;
}
@@ -739,8 +724,9 @@ kitty_notification(struct terminal *term, char *string)
if (notif == NULL) {
/* Somewhat unoptimized... this will be free:d and removed
immediately if d=1 */
- tll_push_front(term->kitty_notifications, ((struct kitty_notification){
+ tll_push_front(term->notifications, ((struct notification){
.id = id,
+ .icon = NULL,
.title = NULL,
.body = NULL,
.when = when,
@@ -749,8 +735,13 @@ kitty_notification(struct terminal *term, char *string)
.report = report,
}));
- id = NULL; /* Prevent double free */
- notif = &tll_front(term->kitty_notifications);
+ id = NULL; /* Prevent double free */
+ notif = &tll_front(term->notifications);
+ }
+
+ if (notif->pid > 0) {
+ /* Notification has already been completed, ignore new metadata */
+ goto out;
}
/* Update notification metadata */
@@ -764,6 +755,12 @@ kitty_notification(struct terminal *term, char *string)
if (have_u)
notif->urgency = urgency;
+ if (icon != NULL) {
+ free(notif->icon);
+ notif->icon = icon;
+ icon = NULL; /* Prevent double free */
+ }
+
if (payload_is_title) {
if (notif->title == NULL) {
notif->title = payload;
@@ -784,26 +781,22 @@ kitty_notification(struct terminal *term, char *string)
}
}
- free(id);
- free(payload);
-
if (done) {
- notify_notify(
- term,
- notif->title != NULL ? notif->title : notif->body,
- notif->title != NULL && notif->body != NULL ? notif->body : "",
- notif->when, notif->urgency);
-
- tll_foreach(term->kitty_notifications, it) {
- if (&it->item == notif) {
- free(it->item.id);
- free(it->item.title);
- free(it->item.body);
- tll_remove(term->kitty_notifications, it);
- break;
+ if (!notify_notify(term, notif)) {
+ tll_foreach(term->notifications, it) {
+ if (&it->item == notif) {
+ notify_free(term, &it->item);
+ tll_remove(term->notifications, it);
+ break;
+ }
}
}
}
+
+out:
+ free(id);
+ free(icon);
+ free(payload);
}
void
diff --git a/terminal.c b/terminal.c
index bf0c78dc..a0be4518 100644
--- a/terminal.c
+++ b/terminal.c
@@ -1313,7 +1313,7 @@ term_init(const struct config *conf, struct fdm *fdm, struct reaper *reaper,
#if defined(FOOT_IME_ENABLED) && FOOT_IME_ENABLED
.ime_enabled = true,
#endif
- .kitty_notifications = tll_init(),
+ .notifications = tll_init(),
};
pixman_region32_init(&term->render.last_overlay_clip);
@@ -1818,11 +1818,9 @@ term_destroy(struct terminal *term)
tll_remove(term->ptmx_paste_buffers, it);
}
- tll_foreach(term->kitty_notifications, it) {
- free(it->item.id);
- free(it->item.body);
- free(it->item.title);
- tll_remove(term->kitty_notifications, it);
+ tll_foreach(term->notifications, it) {
+ notify_free(term, &it->item);
+ tll_remove(term->notifications, it);
}
sixel_fini(term);
@@ -2030,11 +2028,9 @@ term_reset(struct terminal *term, bool hard)
tll_remove(term->alt.sixel_images, it);
}
- tll_foreach(term->kitty_notifications, it) {
- free(it->item.id);
- free(it->item.title);
- free(it->item.body);
- tll_remove(term->kitty_notifications, it);
+ tll_foreach(term->notifications, it) {
+ notify_free(term, &it->item);
+ tll_remove(term->notifications, it);
}
term->grapheme_shaping = term->conf->tweak.grapheme_shaping;
@@ -3582,8 +3578,9 @@ term_bell(struct terminal *term)
}
if (term->conf->bell.notify) {
- notify_notify(term, "Bell", "Bell in terminal",
- NOTIFY_ALWAYS, NOTIFY_URGENCY_NORMAL);
+ notify_notify(term, &(struct notification){
+ .title = (char *)"Bell",
+ .body = (char *)"Bell in terminal"});
}
if (term->conf->bell.flash)
diff --git a/terminal.h b/terminal.h
index 65dde151..0967bf14 100644
--- a/terminal.h
+++ b/terminal.h
@@ -799,7 +799,9 @@ struct terminal {
void *cb_data;
} shutdown;
- tll(struct kitty_notification) kitty_notifications;
+ /* Notifications that either haven't been sent yet, or have been
+ sent but not yet dismissed */
+ tll(struct notification) notifications;
char *foot_exe;
char *cwd;
diff --git a/tests/test-config.c b/tests/test-config.c
index 4a0fd755..ec718c24 100644
--- a/tests/test-config.c
+++ b/tests/test-config.c
@@ -511,7 +511,7 @@ test_section_main(void)
test_boolean(&ctx, &parse_section_main, "login-shell", &conf.login_shell);
test_boolean(&ctx, &parse_section_main, "box-drawings-uses-font-glyphs", &conf.box_drawings_uses_font_glyphs);
test_boolean(&ctx, &parse_section_main, "locked-title", &conf.locked_title);
- test_boolean(&ctx, &parse_section_main, "notify-focus-inhibit", &conf.notify_focus_inhibit);
+ test_boolean(&ctx, &parse_section_main, "notify-focus-inhibit", &conf.desktop_notifications.inhibit_when_focused); /* Deprecated */
test_boolean(&ctx, &parse_section_main, "dpi-aware", &conf.dpi_aware);
test_pt_or_px(&ctx, &parse_section_main, "font-size-adjustment", &conf.font_size_adjustment.pt_or_px); /* TODO: test ‘N%’ values too */
@@ -524,7 +524,7 @@ test_section_main(void)
test_uint16(&ctx, &parse_section_main, "resize-delay-ms", &conf.resize_delay_ms);
test_uint16(&ctx, &parse_section_main, "workers", &conf.render_worker_count);
- test_spawn_template(&ctx, &parse_section_main, "notify", &conf.notify);
+ test_spawn_template(&ctx, &parse_section_main, "notify", &conf.desktop_notifications.command); /* Deprecated */
test_enum(&ctx, &parse_section_main, "selection-target",
4,
@@ -570,6 +570,20 @@ test_section_bell(void)
config_free(&conf);
}
+static void
+test_section_desktop_notifications(void)
+{
+ struct config conf = {0};
+ struct context ctx = {.conf = &conf, .section = "desktop-notifications", .path = "unittest"};
+
+ test_invalid_key(&ctx, &parse_section_desktop_notifications, "invalid-key");
+
+ 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);
+
+ config_free(&conf);
+}
+
static void
test_section_scrollback(void)
{
@@ -1391,6 +1405,7 @@ main(int argc, const char *const *argv)
log_init(LOG_COLORIZE_AUTO, false, 0, LOG_CLASS_ERROR);
test_section_main();
test_section_bell();
+ test_section_desktop_notifications();
test_section_scrollback();
test_section_url();
test_section_cursor();