osc: kitty notifications: implement focus|report

This patch adds support for window focusing, and sending events back
to the client application when a notification is closed.

* Refactor notification related configuration options:
    - add desktop-notifications sub-section
    - deprecate 'notify' in favor of 'desktop-notifications.command'
    - deprecate 'notify-focus-inhibit' in favor of
      'desktop-notifications.inhibit-when-focused'
* Refactor: rename 'struct kitty_notification' to 'struct
  notification'
* Pass a 'struct notification' to notify_notify(), instead of many
  arguments.
* notify_notify() now registers a reaper callback. When the notifier
  process has terminated, the notification is considered closed, and we
  either try to focus (activate) the window, or send an event to the
  client application, depending on the notification setting.
* For the window activation, we need an XDG activation token. For now,
  assume *everything* written on stdout is part of the token.
* Refactor: remove much of the warnings from OSC-99; we don't
  typically log anything when an OSC/CSI has invalid values.
* Add icon support to OSC-99. This isn't part of the upstream
  spec. Foot's implementation:
    - uses the 'I' parameter
    - the value is expected to be a symbolic icon name
    - a quick check for absolute paths is done, and such icon requests
      are ignored.
* Added ${icon} to the 'desktop-notifications.command' template. Uses
  the icon specified in the notification, or ${app-id} if not set.
This commit is contained in:
Daniel Eklöf 2024-07-23 06:59:46 +02:00
parent 12152a8ae4
commit 5905ea0d84
No known key found for this signature in database
GPG key ID: 5BBD4992C116573F
11 changed files with 410 additions and 137 deletions

185
notify.c
View file

@ -1,9 +1,11 @@
#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>
@ -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,
&notif_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;
}