mirror of
https://codeberg.org/dnkl/foot.git
synced 2026-02-06 04:06:06 -05:00
Merge branch 'kitty-notifications'
This commit is contained in:
commit
a213e14ca3
22 changed files with 1087 additions and 99 deletions
13
CHANGELOG.md
13
CHANGELOG.md
|
|
@ -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
|
||||
|
||||
|
|
|
|||
12
base64.c
12
base64.c
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
2
base64.h
2
base64.h
|
|
@ -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]);
|
||||
|
|
|
|||
64
config.c
64
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,
|
||||
|
|
@ -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);
|
||||
|
|
|
|||
6
config.h
6
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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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*
|
||||
|
|
|
|||
7
foot.ini
7
foot.ini
|
|
@ -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
|
||||
|
|
|
|||
4
input.c
4
input.c
|
|
@ -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
409
notify.c
|
|
@ -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(¬if->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 ? ¬if_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;
|
||||
}
|
||||
|
|
|
|||
73
notify.h
73
notify.h
|
|
@ -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
310
osc.c
|
|
@ -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 = ¶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))
|
||||
{
|
||||
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
|
||||
? ¬if->title
|
||||
: ¬if->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(¬if->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) */
|
||||
|
||||
|
|
|
|||
31
pgo/pgo.c
31
pgo/pgo.c
|
|
@ -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
14
spawn.c
|
|
@ -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
|
||||
|
|
|
|||
8
spawn.h
8
spawn.h
|
|
@ -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,
|
||||
|
|
|
|||
43
terminal.c
43
terminal.c
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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]);
|
||||
|
|
|
|||
15
wayland.c
15
wayland.c
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue