mirror of
https://codeberg.org/dnkl/foot.git
synced 2026-02-04 04:06:06 -05:00
osc/notify: add support for OSC-99, kitty desktop notifications
This adds limited support for OSC-99, kitty desktop notifications[^1]. We
support everything defined by the "protocol", except:
* 'a': action to perform on notification activation. Since we don't
trigger the notification ourselves (over D-Bus), we don't know a)
which ID the notification got, or b) when it is clicked.
* ... and that's it. Everything else is supported
To be explicit, we *do* support:
* Chunked notifications (d=0|1), allowing the application to append
data to a notification in chunks, before it's finally displayed.
* Plain UTF-8, or base64-encoded UTF-8 payload (e=0|1).
* Notification identifier (i=xyz).
* Payload type (p=title|body).
* When to honor the notification (o=always|unfocused|invisible), with
the following quirks:
- we don't know when the window is invisible, thus it's treated as
'unfocused'.
- the foot option 'notify-focus-inhibit' overrides 'always'
* Urgency (u=0|1|2)
[^1]: https://sw.kovidgoyal.net/kitty/desktop-notifications/
This commit is contained in:
parent
45c7cd3f74
commit
b0bf8ca5f7
10 changed files with 322 additions and 15 deletions
|
|
@ -70,6 +70,8 @@
|
|||
* 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/).
|
||||
|
||||
[1707]: https://codeberg.org/dnkl/foot/issues/1707
|
||||
[1738]: https://codeberg.org/dnkl/foot/issues/1738
|
||||
|
|
|
|||
5
config.c
5
config.c
|
|
@ -3184,8 +3184,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 -a ${app-id} -i ${app-id} -u ${urgency} ${title} ${body}",
|
||||
&conf->notify.argv.args);
|
||||
tokenize_cmdline("xdg-open ${url}", &conf->url.launch.argv.args);
|
||||
|
||||
static const char32_t *url_protocols[] = {
|
||||
|
|
|
|||
|
|
@ -360,7 +360,7 @@ empty string to be set, but it must be quoted: *KEY=""*)
|
|||
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}_.
|
||||
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
|
||||
|
|
|
|||
2
foot.ini
2
foot.ini
|
|
@ -29,7 +29,7 @@
|
|||
# resize-by-cells=yes
|
||||
# resize-delay-ms=100
|
||||
|
||||
# notify=notify-send -a ${app-id} -i ${app-id} ${title} ${body}
|
||||
# notify=notify-send -a ${app-id} -i ${app-id} -u ${urgency} ${title} ${body}
|
||||
|
||||
# bold-text-in-bright=no
|
||||
# word-delimiters=,│`|:"'()[]{}<>
|
||||
|
|
|
|||
21
notify.c
21
notify.c
|
|
@ -12,14 +12,18 @@
|
|||
#include "log.h"
|
||||
#include "config.h"
|
||||
#include "spawn.h"
|
||||
#include "terminal.h"
|
||||
#include "xmalloc.h"
|
||||
|
||||
void
|
||||
notify_notify(const struct terminal *term, const char *title, const char *body)
|
||||
notify_notify(const struct terminal *term, const char *title, const char *body,
|
||||
enum notify_when when, enum notify_urgency urgency)
|
||||
{
|
||||
LOG_DBG("notify: title=\"%s\", msg=\"%s\"", title, body);
|
||||
|
||||
if (term->conf->notify_focus_inhibit && term->kbd_focus) {
|
||||
if ((term->conf->notify_focus_inhibit || when != NOTIFY_ALWAYS)
|
||||
&& term->kbd_focus)
|
||||
{
|
||||
/* No notifications while we're focused */
|
||||
return;
|
||||
}
|
||||
|
|
@ -33,10 +37,17 @@ notify_notify(const struct terminal *term, const char *title, const char *body)
|
|||
char **argv = NULL;
|
||||
size_t argc = 0;
|
||||
|
||||
const char *urgency_str =
|
||||
urgency == NOTIFY_URGENCY_LOW
|
||||
? "low"
|
||||
: 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},
|
||||
&term->conf->notify, 5,
|
||||
(const char *[]){"app-id", "window-title", "title", "body", "urgency"},
|
||||
(const char *[]){term->app_id ? term->app_id : term->conf->app_id,
|
||||
term->window_title, title, body, urgency_str},
|
||||
&argc, &argv))
|
||||
{
|
||||
return;
|
||||
|
|
|
|||
28
notify.h
28
notify.h
|
|
@ -1,6 +1,30 @@
|
|||
#pragma once
|
||||
#include <stdbool.h>
|
||||
|
||||
#include "terminal.h"
|
||||
struct terminal;
|
||||
|
||||
enum notify_when {
|
||||
NOTIFY_ALWAYS,
|
||||
NOTIFY_UNFOCUSED,
|
||||
NOTIFY_INVISIBLE
|
||||
};
|
||||
|
||||
enum notify_urgency {
|
||||
NOTIFY_URGENCY_LOW,
|
||||
NOTIFY_URGENCY_NORMAL,
|
||||
NOTIFY_URGENCY_CRITICAL,
|
||||
};
|
||||
|
||||
struct kitty_notification {
|
||||
char *id;
|
||||
char *title;
|
||||
char *body;
|
||||
enum notify_when when;
|
||||
enum notify_urgency urgency;
|
||||
bool focus;
|
||||
bool report;
|
||||
};
|
||||
|
||||
void notify_notify(
|
||||
const struct terminal *term, const char *title, const char *body);
|
||||
const struct terminal *term, const char *title, const char *body,
|
||||
enum notify_when when, enum notify_urgency urgency);
|
||||
|
|
|
|||
249
osc.c
249
osc.c
|
|
@ -560,7 +560,250 @@ osc_notify(struct terminal *term, char *string)
|
|||
return;
|
||||
}
|
||||
|
||||
notify_notify(term, title, msg != NULL ? msg : "");
|
||||
notify_notify(
|
||||
term, title, msg != NULL ? msg : "",
|
||||
NOTIFY_ALWAYS, NOTIFY_URGENCY_NORMAL);
|
||||
}
|
||||
|
||||
static void
|
||||
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");
|
||||
return;
|
||||
}
|
||||
|
||||
char *parameters = string;
|
||||
*payload = '\0';
|
||||
payload++;
|
||||
|
||||
char *id = xstrdup("0"); /* The 'i' parameter */
|
||||
bool focus = true; /* The 'a' parameter */
|
||||
bool report = false; /* The 'a' parameter */
|
||||
bool done = true; /* The 'd' parameter */
|
||||
bool base64 = false; /* The 'e' parameter */
|
||||
bool payload_is_title = true; /* 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] != '=') {
|
||||
LOG_WARN("OSC-99: invalid parameter: \"%s\"", param);
|
||||
continue;
|
||||
}
|
||||
|
||||
char *value = ¶m[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))
|
||||
{
|
||||
LOG_WARN(" a: \"%s\"", v);
|
||||
bool reverse = v[0] == '-';
|
||||
if (reverse)
|
||||
v++;
|
||||
|
||||
if (strcmp(v, "focus") == 0) {
|
||||
focus = !reverse;
|
||||
if (focus)
|
||||
LOG_WARN("unimplemented: OSC-99: focus on notification activation");
|
||||
} 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;
|
||||
}
|
||||
|
||||
case 'd':
|
||||
/* done: 0|1 */
|
||||
if (value[0] == '0' && value[1] == '\0')
|
||||
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':
|
||||
/* base64: 0=utf8, 1=base64(utf8) */
|
||||
if (value[0] == '0' && value[1] == '\0')
|
||||
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':
|
||||
/* id */
|
||||
free(id);
|
||||
id = xstrdup(value);
|
||||
break;
|
||||
|
||||
case 'p':
|
||||
/* payload content: title|body */
|
||||
if (strcmp(value, "title") == 0)
|
||||
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':
|
||||
/* honor when: always|unfocused|invisible */
|
||||
have_o = true;
|
||||
if (strcmp(value, "always") == 0)
|
||||
when = NOTIFY_ALWAYS;
|
||||
else if (strcmp(value, "unfocused") == 0)
|
||||
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':
|
||||
/* 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;
|
||||
else
|
||||
LOG_WARN("OSC-99: unrecognized value for 'u': \"%s\", ignoring", value);
|
||||
break;
|
||||
|
||||
default:
|
||||
LOG_WARN("OSC-99: unrecognized parameter: \"%s\", ignoring", param);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (base64)
|
||||
payload = base64_decode(payload);
|
||||
else
|
||||
payload = xstrdup(payload);
|
||||
|
||||
LOG_DBG("id=%s, done=%d, focus=%d, report=%d, base64=%d, payload: %s, "
|
||||
"honor: %s, urgency: %s, %s: %s",
|
||||
id, done, focus, report, base64,
|
||||
payload_is_title ? "title" : "body",
|
||||
(when == NOTIFY_ALWAYS
|
||||
? "always"
|
||||
: when == NOTIFY_UNFOCUSED
|
||||
? "unfocused"
|
||||
: "invisible"),
|
||||
(urgency == NOTIFY_URGENCY_LOW
|
||||
? "low" : urgency == NOTIFY_URGENCY_NORMAL
|
||||
? "normal"
|
||||
: "critical"),
|
||||
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) {
|
||||
if (strcmp(it->item.id, id) == 0) {
|
||||
/* Found existing notification */
|
||||
LOG_WARN("found existing kitty notification");
|
||||
notif = &it->item;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
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){
|
||||
.id = id,
|
||||
.title = NULL,
|
||||
.body = NULL,
|
||||
.when = when,
|
||||
.urgency = urgency,
|
||||
.focus = focus,
|
||||
.report = report,
|
||||
}));
|
||||
|
||||
id = NULL; /* Prevent double free */
|
||||
notif = &tll_front(term->kitty_notifications);
|
||||
}
|
||||
|
||||
/* 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 (payload_is_title) {
|
||||
if (notif->title == NULL) {
|
||||
notif->title = payload;
|
||||
payload = NULL;
|
||||
} else {
|
||||
char *new_title = xasprintf("%s%s", notif->title, payload);
|
||||
free(notif->title);
|
||||
notif->title = new_title;
|
||||
}
|
||||
} else {
|
||||
if (notif->body == NULL) {
|
||||
notif->body = payload;
|
||||
payload = NULL;
|
||||
} else {
|
||||
char *new_body = xasprintf("%s%s", notif->body, payload);
|
||||
free(notif->body);
|
||||
notif->body = new_body;
|
||||
}
|
||||
}
|
||||
|
||||
free(id);
|
||||
free(payload);
|
||||
|
||||
if (done) {
|
||||
notify_notify(
|
||||
term,
|
||||
notif->title != NULL ? notif->title : "",
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
|
|
@ -780,6 +1023,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) */
|
||||
|
||||
|
|
|
|||
|
|
@ -152,7 +152,9 @@ void ime_disable(struct seat *seat) {}
|
|||
void ime_reset_preedit(struct seat *seat) {}
|
||||
|
||||
void
|
||||
notify_notify(const struct terminal *term, const char *title, const char *body)
|
||||
notify_notify(
|
||||
const struct terminal *term, const char *title, const char *body,
|
||||
enum notify_when when, enum notify_urgency urgency)
|
||||
{
|
||||
}
|
||||
|
||||
|
|
|
|||
21
terminal.c
21
terminal.c
|
|
@ -1313,6 +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(),
|
||||
};
|
||||
|
||||
pixman_region32_init(&term->render.last_overlay_clip);
|
||||
|
|
@ -1817,6 +1818,13 @@ 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);
|
||||
}
|
||||
|
||||
sixel_fini(term);
|
||||
|
||||
term_ime_reset(term);
|
||||
|
|
@ -2022,6 +2030,13 @@ 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);
|
||||
}
|
||||
|
||||
term->grapheme_shaping = term->conf->tweak.grapheme_shaping;
|
||||
|
||||
#if defined(FOOT_IME_ENABLED) && FOOT_IME_ENABLED
|
||||
|
|
@ -3566,8 +3581,10 @@ 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, "Bell", "Bell in terminal",
|
||||
NOTIFY_ALWAYS, NOTIFY_URGENCY_NORMAL);
|
||||
}
|
||||
|
||||
if (term->conf->bell.flash)
|
||||
term_flash(term, 100);
|
||||
|
|
|
|||
|
|
@ -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,8 @@ struct terminal {
|
|||
void *cb_data;
|
||||
} shutdown;
|
||||
|
||||
tll(struct kitty_notification) kitty_notifications;
|
||||
|
||||
char *foot_exe;
|
||||
char *cwd;
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue