Merge branch 'kitty-notifications'

This commit is contained in:
Daniel Eklöf 2024-07-25 18:32:57 +02:00
commit a213e14ca3
No known key found for this signature in database
GPG key ID: 5BBD4992C116573F
22 changed files with 1087 additions and 99 deletions

View file

@ -70,6 +70,13 @@
* Support for [in-band window resize
notifications](https://gist.github.com/rockorager/e695fb2924d36b2bcf1fff4a3704bd83),
private mode `2048`.
* Support for OSC-99 [_"Kitty desktop
notifications"_](https://sw.kovidgoyal.net/kitty/desktop-notifications/).
* `desktop-notifications.command` option, replaces `notify`.
* `desktop-notifications.inhibit-when-focused` option, replaces
`notify-focus-inhibit`.
* `${icon}`, `${urgency}`,`${action-name}` and `${action-label}` added
to the `desktop-notifications.command` template.
[1707]: https://codeberg.org/dnkl/foot/issues/1707
[1738]: https://codeberg.org/dnkl/foot/issues/1738
@ -116,6 +123,12 @@
### Deprecated
* `notify` option; replaced by `desktop-notifications.command`.
* `notify-focus-inhibit` option; replaced by
`desktop-notifications.inhibit-when-focused`.
### Removed
### Fixed

View file

@ -42,7 +42,7 @@ static const char lookup[64] = {
};
char *
base64_decode(const char *s)
base64_decode(const char *s, size_t *size)
{
const size_t len = strlen(s);
if (unlikely(len % 4 != 0)) {
@ -54,6 +54,9 @@ base64_decode(const char *s)
if (unlikely(ret == NULL))
return NULL;
if (unlikely(size != NULL))
*size = len / 4 * 3;
for (size_t i = 0, o = 0; i < len; i += 4, o += 3) {
unsigned a = reverse_lookup[(unsigned char)s[i + 0]];
unsigned b = reverse_lookup[(unsigned char)s[i + 1]];
@ -68,6 +71,13 @@ base64_decode(const char *s)
if (unlikely(i + 4 != len || (a | b) & P || (c & P && !(d & P))))
goto invalid;
if (unlikely(size != NULL)) {
if (c & P)
*size = len / 4 * 3 - 2;
else
*size = len / 4 * 3 - 1;
}
c &= 63;
d &= 63;
}

View file

@ -3,6 +3,6 @@
#include <stdint.h>
#include <stddef.h>
char *base64_decode(const char *s);
char *base64_decode(const char *s, size_t *out_len);
char *base64_encode(const uint8_t *data, size_t size);
void base64_encode_final(const uint8_t *data, size_t size, char result[4]);

View file

@ -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,
@ -3184,8 +3224,9 @@ config_load(struct config *conf, const char *conf_path,
memcpy(conf->colors.table, default_color_table, sizeof(default_color_table));
parse_modifiers(XKB_MOD_NAME_SHIFT, 5, &conf->mouse.selection_override_modifiers);
tokenize_cmdline("notify-send -a ${app-id} -i ${app-id} ${title} ${body}",
&conf->notify.argv.args);
tokenize_cmdline(
"notify-send --wait --app-name ${app-id} --icon ${icon} --urgency ${urgency} --action ${action-name}=${action-label} --print-id -- ${title} ${body}",
&conf->desktop_notifications.command.argv.args);
tokenize_cmdline("xdg-open ${url}", &conf->url.launch.argv.args);
static const char32_t *url_protocols[] = {
@ -3438,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]);
@ -3520,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);

View file

@ -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;

View file

@ -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

View file

@ -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;<title>;<body>\\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} ${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,106 @@ 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;<title>;<body>\\e\\\\*
- OSC 99: *\\e]99;;<title>\\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'. It can also send an event back to the client
application, notifying it that the notification has been
'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 to know when the notification was
activated (as opposed to just dismissed), and it needs an XDG
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.
Second, foot needs to know when the notification activated,
and it needs to get hold of the XDG activation token.
Both are expected to be printed on stdout.
Foot expects the action name (not label) to be printed on a
single line. No prefix, no postfix.
Foot expects the activation token to be printed on a single
line, prefixed with *xdgtoken=*.
Example:
default
xdgtoken=18179adf579a7a904ce73754964b1ec3
The expected format of stdout may change at any time. Please
read the changelog when upgrading foot.
*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.
Stdout
Foot recognizes the following things from the notification
helper's stdout:
- _nnn_: integer in base 10, daemon assigned notification ID
- _id=nnn_: same as plain _nnn_.
- _default_: the 'default' action was triggered
- _action=default_: same as _default_
- _xdgtoken=xyz_: XDG activation token.
Example:
17++
action=default++
xdgtoken=95ebdfe56e4f47ddb5bba9d7dc3a2c35
Default: _notify-send --wait --app-name ${app-id} --icon ${icon} --urgency ${urgency} --action ${action-name}=${action-label} --print-id -- ${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*

View file

@ -29,8 +29,6 @@
# resize-by-cells=yes
# resize-delay-ms=100
# notify=notify-send -a ${app-id} -i ${app-id} ${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} --action ${action-name}=${action-label} --print-id -- ${title} ${body}
# inhibit-when-focused=yes
[scrollback]
# lines=1000
# multiplier=3.0

View file

@ -306,8 +306,8 @@ execute_binding(struct seat *seat, struct terminal *term,
}
}
if (!spawn(term->reaper, term->cwd, binding->aux->pipe.args,
pipe_fd[0], stdout_fd, stderr_fd, NULL))
if (spawn(term->reaper, term->cwd, binding->aux->pipe.args,
pipe_fd[0], stdout_fd, stderr_fd, NULL, NULL, NULL) < 0)
goto pipe_err;
/* Close read end */

409
notify.c
View file

@ -1,54 +1,322 @@
#include "notify.h"
#include <errno.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/epoll.h>
#include <sys/stat.h>
#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"
#include "terminal.h"
#include "util.h"
#include "wayland.h"
#include "xmalloc.h"
#include "xsnprintf.h"
void
notify_notify(const struct terminal *term, const char *title, const char *body)
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_id);
free(notif->icon_symbolic_name);
free(notif->icon_data);
free(notif->xdg_token);
free(notif->stdout_data);
}
if (term->conf->notify_focus_inhibit && term->kbd_focus) {
/* No notifications while we're focused */
return;
static bool
to_integer(const char *line, size_t len, uint32_t *res)
{
bool is_id = true;
uint32_t maybe_id = 0;
for (size_t i = 0; i < len; i++) {
char digit = line[i];
if (digit < '0' || digit > '9') {
is_id = false;
break;
}
maybe_id *= 10;
maybe_id += digit - '0';
}
if (title == NULL || body == NULL)
return;
*res = maybe_id;
return is_id;
}
if (term->conf->notify.argv.args == NULL)
static void
consume_stdout(struct notification *notif, bool eof)
{
char *data = notif->stdout_data;
const char *line = data;
size_t left = notif->stdout_sz;
/* Process stdout, line-by-line */
while (left > 0) {
line = data;
size_t len = left;
char *eol = memchr(line, '\n', left);
if (eol != NULL) {
*eol = '\0';
len = strlen(line);
data = eol + 1;
} else if (!eof)
break;
uint32_t maybe_id = 0;
/* Check for daemon assigned ID, either '123', or 'id=123' */
if (to_integer(line, len, &maybe_id) ||
(len > 3 && memcmp(line, "id=", 3) == 0 &&
to_integer(&line[3], len - 3, &maybe_id)))
{
notif->external_id = maybe_id;
LOG_DBG("external ID: %u", notif->external_id);
}
/* Check for triggered action, either 'default' or 'action=default' */
else if ((len == 7 && memcmp(line, "default", 7) == 0) ||
(len == 7 + 7 && memcmp(line, "action=default", 7 + 7) == 0))
{
notif->activated = true;
LOG_DBG("notification's default action was triggered");
}
/* Check for XDG activation token, 'xdgtoken=xyz' */
else if (len > 9 && memcmp(line, "xdgtoken=", 9) == 0) {
notif->xdg_token = xstrndup(&line[9], len - 9);
LOG_DBG("XDG token: \"%s\"", notif->xdg_token);
}
left -= len + (eol != NULL ? 1 : 0);
}
if (left > 0)
memmove(notif->stdout_data, data, left);
notif->stdout_sz = left;
}
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->active_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 && notif != NULL) {
if (notif->stdout_data == NULL) {
xassert(notif->stdout_sz == 0);
notif->stdout_data = xmemdup(buf, count);
} else {
notif->stdout_data = xrealloc(notif->stdout_data, notif->stdout_sz + count);
memcpy(&notif->stdout_data[notif->stdout_sz], buf, count);
}
notif->stdout_sz += count;
consume_stdout(notif, false);
}
}
if (events & EPOLLHUP) {
fdm_del(fdm, fd);
if (notif != NULL) {
notif->stdout_fd = -1;
consume_stdout(notif, true);
}
}
return true;
}
static void
notif_done(struct reaper *reaper, pid_t pid, int status, void *data)
{
struct terminal *term = data;
tll_foreach(term->active_notifications, it) {
struct notification *notif = &it->item;
if (notif->pid != pid)
continue;
LOG_DBG("notification %s dismissed", notif->id);
if (notif->activated && notif->focus) {
LOG_DBG("focus window on notification activation: \"%s\"",
notif->xdg_token);
wayl_activate(term->wl, term->window, notif->xdg_token);
}
if (notif->activated && 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->active_notifications, it);
return;
}
}
bool
notify_notify(struct terminal *term, struct notification *notif)
{
xassert(notif->xdg_token == NULL);
xassert(notif->pid == 0);
xassert(notif->stdout_fd <= 0);
xassert(notif->stdout_data == NULL);
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_name_or_path = term->app_id != NULL
? term->app_id
: term->conf->app_id;
if (notif->icon_id != NULL) {
for (size_t i = 0; i < ALEN(term->notification_icons); i++) {
const struct notification_icon *icon = &term->notification_icons[i];
if (icon->id != NULL && streq(icon->id, notif->icon_id)) {
icon_name_or_path = icon->symbolic_name != NULL
? icon->symbolic_name
: icon->tmp_file_name;
break;
}
}
} else if (notif->icon_symbolic_name != NULL) {
icon_name_or_path = notif->icon_symbolic_name;
}
bool track_notification = notif->focus || notif->report;
LOG_DBG("notify: title=\"%s\", body=\"%s\", icon=\"%s\" (tracking: %s)",
title, body, icon_name_or_path, track_notification ? "yes" : "no");
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 false;
}
if (term->conf->desktop_notifications.command.argv.args == NULL)
return false;
char **argv = NULL;
size_t argc = 0;
const char *urgency_str =
notif->urgency == NOTIFY_URGENCY_LOW
? "low"
: notif->urgency == NOTIFY_URGENCY_NORMAL
? "normal" : "critical";
if (!spawn_expand_template(
&term->conf->notify, 4,
(const char *[]){"app-id", "window-title", "title", "body"},
(const char *[]){term->app_id ? term->app_id : term->conf->app_id, term->window_title, title, body},
&argc, &argv))
&term->conf->desktop_notifications.command, 8,
(const char *[]){
"app-id", "window-title", "icon", "title", "body", "urgency", "action-name", "action-label"},
(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"},
&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 (track_notification) {
if (pipe2(stdout_fds, O_CLOEXEC | O_NONBLOCK) < 0) {
LOG_WARN("failed to create stdout pipe");
track_notification = false;
/* Non-fatal */
} else {
tll_push_back(term->active_notifications, *notif);
notif->id = NULL;
notif->title = NULL;
notif->body = 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);
}
}
if (stdout_fds[0] >= 0) {
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);
spawn(term->reaper, NULL, argv, devnull, -1, -1, NULL);
pid_t pid = spawn(
term->reaper, NULL, argv, devnull, stdout_fds[1], -1,
track_notification ? &notif_done : NULL, (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);
@ -56,4 +324,115 @@ 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;
}
static void
add_icon(struct notification_icon *icon, const char *id, const char *symbolic_name,
const uint8_t *data, size_t data_sz)
{
icon->id = xstrdup(id);
icon->symbolic_name = symbolic_name != NULL ? xstrdup(symbolic_name) : NULL;
icon->tmp_file_name = NULL;
icon->tmp_file_fd = -1;
/*
* Dump in-line data to a temporary file. This allows us to pass
* the filename as a parameter to notification helpers
* (i.e. notify-send -i <path>).
*
* Optimization: since we always prefer (i.e. use) the symbolic
* name if present, there's no need to create a file on disk if we
* 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;
}
}
LOG_DBG("added icon to cache: ID=%s: sym=%s, file=%s",
icon->id, icon->symbolic_name, icon->tmp_file_name);
}
void
notify_icon_add(struct terminal *term, const char *id,
const char *symbolic_name, const uint8_t *data, size_t data_sz)
{
#if defined(_DEBUG)
for (size_t i = 0; i < ALEN(term->notification_icons); i++) {
struct notification_icon *icon = &term->notification_icons[i];
if (icon->id != NULL && streq(icon->id, id)) {
BUG("notification icon cache already contains \"%s\"", id);
}
}
#endif
for (size_t i = 0; i < ALEN(term->notification_icons); i++) {
struct notification_icon *icon = &term->notification_icons[i];
if (icon->id == NULL) {
add_icon(icon, id, symbolic_name, data, data_sz);
return;
}
}
/* Cache full - throw out first entry, add new entry last */
notify_icon_free(&term->notification_icons[0]);
memmove(&term->notification_icons[0],
&term->notification_icons[1],
((ALEN(term->notification_icons) - 1) *
sizeof(term->notification_icons[0])));
add_icon(
&term->notification_icons[ALEN(term->notification_icons) - 1],
id, symbolic_name, data, data_sz);
}
void
notify_icon_del(struct terminal *term, const char *id)
{
for (size_t i = 0; i < ALEN(term->notification_icons); i++) {
struct notification_icon *icon = &term->notification_icons[i];
if (icon->id == NULL || !streq(icon->id, id))
continue;
LOG_DBG("expelled %s from the notification icon cache", icon->id);
notify_icon_free(icon);
return;
}
}
void
notify_icon_free(struct notification_icon *icon)
{
if (icon->tmp_file_fd >= 0)
close(icon->tmp_file_fd);
if (icon->tmp_file_name != NULL)
unlink(icon->tmp_file_name);
free(icon->id);
free(icon->symbolic_name);
free(icon->tmp_file_name);
icon->id = NULL;
icon->symbolic_name = NULL;
icon->tmp_file_name = NULL;
icon->tmp_file_fd = -1;
}

View file

@ -1,6 +1,73 @@
#pragma once
#include <stdbool.h>
#include <stdint.h>
#include <unistd.h>
#include "terminal.h"
struct terminal;
void notify_notify(
const struct terminal *term, const char *title, const char *body);
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 {
/* 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 notification {
/*
* Set by caller of notify_notify()
*/
char *id;
char *title;
char *body;
char *icon_id;
char *icon_symbolic_name;
uint8_t *icon_data;
size_t icon_data_sz;
enum notify_when when;
enum notify_urgency urgency;
bool focus;
bool report;
/*
* Used internally by notify
*/
uint32_t external_id; /* Daemon assigned notification ID */
bool activated; /* User 'activated' the notification */
char *xdg_token; /* XDG activation token, from daemon */
pid_t pid; /* Notifier command PID */
int stdout_fd; /* Notifier command's stdout */
char *stdout_data; /* Data we've reado from command's stdout */
size_t stdout_sz;
};
struct notification_icon {
char *id;
char *symbolic_name;
char *tmp_file_name;
int tmp_file_fd;
};
bool notify_notify(struct terminal *term, struct notification *notif);
void notify_free(struct terminal *term, struct notification *notif);
void notify_icon_add(struct terminal *term, const char *id,
const char *symbolic_name, const uint8_t *data,
size_t data_sz);
void notify_icon_del(struct terminal *term, const char *id);
void notify_icon_free(struct notification_icon *icon);

310
osc.c
View file

@ -12,15 +12,12 @@
#include "log.h"
#include "base64.h"
#include "config.h"
#include "grid.h"
#include "macros.h"
#include "notify.h"
#include "render.h"
#include "selection.h"
#include "terminal.h"
#include "uri.h"
#include "util.h"
#include "vt.h"
#include "xmalloc.h"
#include "xsnprintf.h"
@ -67,7 +64,7 @@ osc_to_clipboard(struct terminal *term, const char *target,
return;
}
char *decoded = base64_decode(base64_data);
char *decoded = base64_decode(base64_data, NULL);
if (decoded == NULL) {
if (errno == EINVAL)
LOG_WARN("OSC: invalid clipboard data: %s", base64_data);
@ -560,7 +557,306 @@ osc_notify(struct terminal *term, char *string)
return;
}
notify_notify(term, title, msg != NULL ? msg : "");
notify_notify(term, &(struct notification){
.title = (char *)title,
.body = (char *)msg});
}
static void
kitty_notification(struct terminal *term, char *string)
{
/* https://sw.kovidgoyal.net/kitty/desktop-notifications */
char *payload_raw = strchr(string, ';');
if (payload_raw == NULL)
return;
char *parameters = string;
*payload_raw = '\0';
payload_raw++;
char *id = xstrdup("0"); /* The 'i' parameter */
char *icon_id = NULL; /* The 'g' parameter */
char *symbolic_icon = NULL; /* The 'n' parameter */
char *payload = NULL;
bool focus = true; /* The 'a' parameter */
bool report = false; /* The 'a' parameter */
bool done = true; /* The 'd' parameter */
bool base64 = false; /* The 'e' parameter */
size_t payload_size;
enum {
PAYLOAD_TITLE,
PAYLOAD_BODY,
PAYLOAD_ICON,
} payload_type = PAYLOAD_TITLE; /* The 'p' parameter */
enum notify_when when = NOTIFY_ALWAYS;
enum notify_urgency urgency = NOTIFY_URGENCY_NORMAL;
bool have_a = false;
bool have_o = false;
bool have_u = false;
char *ctx = NULL;
for (char *param = strtok_r(parameters, ":", &ctx);
param != NULL;
param = strtok_r(NULL, ":", &ctx))
{
/* All parameters are on the form X=value, where X is always
exactly one character */
if (param[0] == '\0' || param[1] != '=')
continue;
char *value = &param[2];
switch (param[0]) {
case 'a': {
/* 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))
{
bool reverse = v[0] == '-';
if (reverse)
v++;
if (streq(v, "focus"))
focus = !reverse;
else if (streq(v, "report"))
report = !reverse;
}
break;
}
case 'd':
/* done: 0|1 */
if (value[0] == '0' && value[1] == '\0')
done = false;
else if (value[0] == '1' && value[1] == '\0')
done = true;
break;
case 'e':
/* base64 (payload encoding): 0=utf8, 1=base64(utf8) */
if (value[0] == '0' && value[1] == '\0')
base64 = false;
else if (value[0] == '1' && value[1] == '\0')
base64 = true;
break;
case 'i':
/* id */
free(id);
id = xstrdup(value);
break;
case 'p':
/* payload content: title|body */
if (streq(value, "title"))
payload_type = PAYLOAD_TITLE;
else if (streq(value, "body"))
payload_type = PAYLOAD_BODY;
else if (streq(value, "icon"))
payload_type = PAYLOAD_ICON;
else if (streq(value, "?")) {
/* Query capabilities */
char when_str[64];
strcpy(when_str, "unfocused");
if (!term->conf->desktop_notifications.inhibit_when_focused)
strcat(when_str, ",always");
const char *terminator = term->vt.osc.bel ? "\a" : "\033\\";
char reply[128];
int n = xsnprintf(
reply, sizeof(reply),
"\033]99;i=%s:p=?;p=title,body,icon:a=focus,report:o=%s:u=0,1,2%s",
id, when_str, terminator);
term_to_slave(term, reply, n);
goto out;
}
break;
case 'o':
/* honor when: always|unfocused|invisible */
have_o = true;
if (streq(value, "always"))
when = NOTIFY_ALWAYS;
else if (streq(value, "unfocused"))
when = NOTIFY_UNFOCUSED;
else if (streq(value, "invisible"))
when = NOTIFY_INVISIBLE;
break;
case 'u':
/* urgency: 0=low, 1=normal, 2=critical */
have_u = true;
if (value[0] == '0' && value[1] == '\0')
urgency = NOTIFY_URGENCY_LOW;
else if (value[0] == '1' && value[1] == '\0')
urgency = NOTIFY_URGENCY_NORMAL;
else if (value[0] == '2' && value[1] == '\0')
urgency = NOTIFY_URGENCY_CRITICAL;
break;
case 'g':
/* graphical ID */
free(icon_id);
icon_id = xstrdup(value);
break;
case 'n':
/* Symbolic icon name, used with 'g' */
free(symbolic_icon);
symbolic_icon = xstrdup(value);
break;
}
}
if (base64) {
payload = base64_decode(payload_raw, &payload_size);
if (payload == NULL)
goto out;
} else {
payload = xstrdup(payload_raw);
payload_size = strlen(payload);
}
/* 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)) {
/* Found existing notification */
notif = &it->item;
break;
}
}
if (notif == NULL) {
tll_push_front(term->kitty_notifications, ((struct notification){
.id = id,
.when = when,
.urgency = urgency,
.focus = focus,
.report = report,
.stdout_fd = -1,
}));
id = NULL; /* Prevent double free */
notif = &tll_front(term->kitty_notifications);
}
if (notif->pid > 0) {
/* Notification has already been completed, ignore new metadata */
goto out;
}
/* Update notification metadata */
if (have_a) {
notif->focus = focus;
notif->report = report;
}
if (have_o)
notif->when = when;
if (have_u)
notif->urgency = urgency;
if (icon_id != NULL) {
free(notif->icon_id);
notif->icon_id = icon_id;
icon_id = NULL; /* Prevent double free */
}
if (symbolic_icon != NULL) {
free(notif->icon_symbolic_name);
notif->icon_symbolic_name = symbolic_icon;
symbolic_icon = NULL;
}
/* Handled chunked payload - append to existing metadata */
switch (payload_type) {
case PAYLOAD_TITLE:
case PAYLOAD_BODY: {
char **ptr = payload_type == PAYLOAD_TITLE
? &notif->title
: &notif->body;
if (*ptr == NULL) {
*ptr = payload;
payload = NULL;
} else {
char *old = *ptr;
*ptr = xstrjoin(old, payload);
free(old);
}
break;
}
case PAYLOAD_ICON:
if (notif->icon_data == NULL) {
notif->icon_data = (uint8_t *)payload;
notif->icon_data_sz = payload_size;
payload = NULL;
} else {
notif->icon_data = xrealloc(
notif->icon_data, notif->icon_data_sz + payload_size);
memcpy(&notif->icon_data[notif->icon_data_sz], payload, payload_size);
notif->icon_data_sz += payload_size;
}
break;
}
if (done) {
/* Update icon cache, if necessary */
if (notif->icon_id != NULL &&
(notif->icon_symbolic_name != NULL || notif->icon_data != NULL))
{
notify_icon_del(term, notif->icon_id);
notify_icon_add(term, notif->icon_id,
notif->icon_symbolic_name,
notif->icon_data, notif->icon_data_sz);
/* Don't need this anymore */
free(notif->icon_symbolic_name);
free(notif->icon_data);
notif->icon_symbolic_name = NULL;
notif->icon_data = NULL;
notif->icon_data_sz = 0;
}
/*
* Show notification.
*
* The checks for title|body is to handle notifications that
* only load icon data into the icon cache
*/
if (notif->title != NULL || notif->body != NULL) {
notify_notify(term, notif);
}
tll_foreach(term->kitty_notifications, it) {
if (&it->item == notif) {
notify_free(term, &it->item);
tll_remove(term->kitty_notifications, it);
break;
}
}
}
out:
free(id);
free(icon_id);
free(symbolic_icon);
free(payload);
}
void
@ -780,6 +1076,10 @@ osc_dispatch(struct terminal *term)
osc_selection(term, string);
break;
case 99: /* Kitty notifications */
kitty_notification(term, string);
break;
case 104: {
/* Reset Color Number 'c' (whole table if no parameter) */

View file

@ -100,12 +100,12 @@ void wayl_win_alpha_changed(struct wl_window *win) {}
bool wayl_win_set_urgent(struct wl_window *win) { return true; }
bool wayl_fractional_scaling(const struct wayland *wayl) { return true; }
bool
pid_t
spawn(struct reaper *reaper, const char *cwd, char *const argv[],
int stdin_fd, int stdout_fd, int stderr_fd,
const char *xdg_activation_token)
reaper_cb cb, void *cb_data, const char *xdg_activation_token)
{
return true;
return 2;
}
pid_t
@ -151,8 +151,31 @@ void ime_enable(struct seat *seat) {}
void ime_disable(struct seat *seat) {}
void ime_reset_preedit(struct seat *seat) {}
bool
notify_notify(struct terminal *term, struct notification *notif)
{
return true;
}
void
notify_notify(const struct terminal *term, const char *title, const char *body)
notify_free(struct terminal *term, struct notification *notif)
{
}
void
notify_icon_add(struct terminal *term, const char *id,
const char *symbolic_name, const uint8_t *data,
size_t data_sz)
{
}
void
notify_icon_del(struct terminal *term, const char *id)
{
}
void
notify_icon_free(struct notification_icon *icon)
{
}

14
spawn.c
View file

@ -15,9 +15,9 @@
#include "debug.h"
#include "xmalloc.h"
bool
pid_t
spawn(struct reaper *reaper, const char *cwd, char *const argv[],
int stdin_fd, int stdout_fd, int stderr_fd,
int stdin_fd, int stdout_fd, int stderr_fd, reaper_cb cb, void *cb_data,
const char *xdg_activation_token)
{
int pipe_fds[2] = {-1, -1};
@ -104,16 +104,16 @@ spawn(struct reaper *reaper, const char *cwd, char *const argv[],
close(pipe_fds[0]);
if (ret == 0) {
reaper_add(reaper, pid, NULL, NULL);
return true;
reaper_add(reaper, pid, cb, cb_data);
return pid;
} else if (ret < 0) {
LOG_ERRNO("failed to read from pipe");
return false;
return -1;
} else {
LOG_ERRNO_P(errno_copy, "%s: failed to spawn", argv[0]);
errno = errno_copy;
waitpid(pid, NULL, 0);
return false;
return -1;
}
err:
@ -121,7 +121,7 @@ err:
close(pipe_fds[0]);
if (pipe_fds[1] != -1)
close(pipe_fds[1]);
return false;
return -1;
}
bool

View file

@ -1,12 +1,14 @@
#pragma once
#include <stdbool.h>
#include <unistd.h>
#include "config.h"
#include "reaper.h"
bool spawn(struct reaper *reaper, const char *cwd, char *const argv[],
int stdin_fd, int stdout_fd, int stderr_fd,
const char *xdg_activation_token);
pid_t spawn(struct reaper *reaper, const char *cwd, char *const argv[],
int stdin_fd, int stdout_fd, int stderr_fd,
reaper_cb cb, void *cb_data, const char *xdg_activation_token);
bool spawn_expand_template(
const struct config_spawn_template *template,

View file

@ -198,7 +198,7 @@ add_utmp_record(const struct config *conf, struct reaper *reaper, int ptmx)
return true;
char *const argv[] = {conf->utmp_helper_path, UTMP_ADD, getenv("WAYLAND_DISPLAY"), NULL};
return spawn(reaper, NULL, argv, ptmx, ptmx, -1, NULL);
return spawn(reaper, NULL, argv, ptmx, ptmx, -1, NULL, NULL, NULL) >= 0;
#else
return true;
#endif
@ -222,7 +222,7 @@ del_utmp_record(const struct config *conf, struct reaper *reaper, int ptmx)
;
char *const argv[] = {conf->utmp_helper_path, UTMP_DEL, del_argument, NULL};
return spawn(reaper, NULL, argv, ptmx, ptmx, -1, NULL);
return spawn(reaper, NULL, argv, ptmx, ptmx, -1, NULL, NULL, NULL) >= 0;
#else
return true;
#endif
@ -1313,6 +1313,8 @@ term_init(const struct config *conf, struct fdm *fdm, struct reaper *reaper,
#if defined(FOOT_IME_ENABLED) && FOOT_IME_ENABLED
.ime_enabled = true,
#endif
.kitty_notifications = tll_init(),
.active_notifications = tll_init(),
};
pixman_region32_init(&term->render.last_overlay_clip);
@ -1817,6 +1819,19 @@ term_destroy(struct terminal *term)
tll_remove(term->ptmx_paste_buffers, it);
}
tll_foreach(term->kitty_notifications, it) {
notify_free(term, &it->item);
tll_remove(term->kitty_notifications, it);
}
tll_foreach(term->active_notifications, it) {
notify_free(term, &it->item);
tll_remove(term->active_notifications, it);
}
for (size_t i = 0; i < ALEN(term->notification_icons); i++)
notify_icon_free(&term->notification_icons[i]);
sixel_fini(term);
term_ime_reset(term);
@ -2022,6 +2037,19 @@ term_reset(struct terminal *term, bool hard)
tll_remove(term->alt.sixel_images, it);
}
tll_foreach(term->kitty_notifications, it) {
notify_free(term, &it->item);
tll_remove(term->kitty_notifications, it);
}
tll_foreach(term->active_notifications, it) {
notify_free(term, &it->item);
tll_remove(term->active_notifications, it);
}
for (size_t i = 0; i < ALEN(term->notification_icons); i++)
notify_icon_free(&term->notification_icons[i]);
term->grapheme_shaping = term->conf->tweak.grapheme_shaping;
#if defined(FOOT_IME_ENABLED) && FOOT_IME_ENABLED
@ -3566,8 +3594,11 @@ term_bell(struct terminal *term)
}
}
if (term->conf->bell.notify)
notify_notify(term, "Bell", "Bell in terminal");
if (term->conf->bell.notify) {
notify_notify(term, &(struct notification){
.title = (char *)"Bell",
.body = (char *)"Bell in terminal"});
}
if (term->conf->bell.flash)
term_flash(term, 100);
@ -3577,7 +3608,7 @@ term_bell(struct terminal *term)
{
int devnull = open("/dev/null", O_RDONLY);
spawn(term->reaper, NULL, term->conf->bell.command.argv.args,
devnull, -1, -1, NULL);
devnull, -1, -1, NULL, NULL, NULL);
if (devnull >= 0)
close(devnull);
@ -3589,7 +3620,7 @@ term_spawn_new(const struct terminal *term)
{
return spawn(
term->reaper, term->cwd, (char *const []){term->foot_exe, NULL},
-1, -1, -1, NULL);
-1, -1, -1, NULL, NULL, NULL) >= 0;
}
void

View file

@ -20,6 +20,7 @@
#include "fdm.h"
#include "key-binding.h"
#include "macros.h"
#include "notify.h"
#include "reaper.h"
#include "shm.h"
#include "wayland.h"
@ -798,6 +799,12 @@ struct terminal {
void *cb_data;
} shutdown;
/* Notifications that either haven't been sent yet, or have been
sent but not yet dismissed */
tll(struct notification) kitty_notifications;
tll(struct notification) active_notifications;
struct notification_icon notification_icons[32];
char *foot_exe;
char *cwd;

View file

@ -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();

View file

@ -1,7 +1,7 @@
#include "unicode-mode.h"
#define LOG_MODULE "unicode-input"
#define LOG_ENABLE_DBG 1
#define LOG_ENABLE_DBG 0
#include "log.h"
#include "render.h"
#include "search.h"

View file

@ -74,8 +74,9 @@ spawn_url_launcher_with_token(struct terminal *term,
(const char *[]){url},
&argc, &argv))
{
ret = spawn(term->reaper, term->cwd, argv,
dev_null, dev_null, dev_null, xdg_activation_token);
ret = spawn(
term->reaper, term->cwd, argv,
dev_null, dev_null, dev_null, NULL, NULL, xdg_activation_token) >= 0;
for (size_t i = 0; i < argc; i++)
free(argv[i]);

View file

@ -1824,8 +1824,7 @@ wayl_win_init(struct terminal *term, const char *token)
wl_surface_commit(win->surface.surf);
/* Complete XDG startup notification */
if (token && wayl->xdg_activation != NULL)
xdg_activation_v1_activate(wayl->xdg_activation, token, win->surface.surf);
wayl_activate(wayl, win, token);
if (!wayl_win_subsurface_new(win, &win->overlay, false)) {
LOG_ERR("failed to create overlay surface");
@ -2377,3 +2376,15 @@ wayl_get_activation_token(
xdg_activation_token_v1_commit(token);
return true;
}
void
wayl_activate(struct wayland *wayl, struct wl_window *win, const char *token)
{
if (wayl->xdg_activation == NULL)
return;
if (token == NULL)
return;
xdg_activation_v1_activate(wayl->xdg_activation, token, win->surface.surf);
}

View file

@ -499,3 +499,5 @@ void wayl_win_subsurface_destroy(struct wayl_sub_surface *surf);
bool wayl_get_activation_token(
struct wayland *wayl, struct seat *seat, uint32_t serial,
struct wl_window *win, activation_token_cb_t cb, void *cb_data);
void wayl_activate(struct wayland *wayl, struct wl_window *win, const char *token);