mirror of
https://codeberg.org/dnkl/foot.git
synced 2026-03-14 05:33:59 -04: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
|
* Support for [in-band window resize
|
||||||
notifications](https://gist.github.com/rockorager/e695fb2924d36b2bcf1fff4a3704bd83),
|
notifications](https://gist.github.com/rockorager/e695fb2924d36b2bcf1fff4a3704bd83),
|
||||||
private mode `2048`.
|
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
|
[1707]: https://codeberg.org/dnkl/foot/issues/1707
|
||||||
[1738]: https://codeberg.org/dnkl/foot/issues/1738
|
[1738]: https://codeberg.org/dnkl/foot/issues/1738
|
||||||
|
|
@ -116,6 +123,12 @@
|
||||||
|
|
||||||
|
|
||||||
### Deprecated
|
### Deprecated
|
||||||
|
|
||||||
|
* `notify` option; replaced by `desktop-notifications.command`.
|
||||||
|
* `notify-focus-inhibit` option; replaced by
|
||||||
|
`desktop-notifications.inhibit-when-focused`.
|
||||||
|
|
||||||
|
|
||||||
### Removed
|
### Removed
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|
||||||
|
|
|
||||||
12
base64.c
12
base64.c
|
|
@ -42,7 +42,7 @@ static const char lookup[64] = {
|
||||||
};
|
};
|
||||||
|
|
||||||
char *
|
char *
|
||||||
base64_decode(const char *s)
|
base64_decode(const char *s, size_t *size)
|
||||||
{
|
{
|
||||||
const size_t len = strlen(s);
|
const size_t len = strlen(s);
|
||||||
if (unlikely(len % 4 != 0)) {
|
if (unlikely(len % 4 != 0)) {
|
||||||
|
|
@ -54,6 +54,9 @@ base64_decode(const char *s)
|
||||||
if (unlikely(ret == NULL))
|
if (unlikely(ret == NULL))
|
||||||
return NULL;
|
return NULL;
|
||||||
|
|
||||||
|
if (unlikely(size != NULL))
|
||||||
|
*size = len / 4 * 3;
|
||||||
|
|
||||||
for (size_t i = 0, o = 0; i < len; i += 4, o += 3) {
|
for (size_t i = 0, o = 0; i < len; i += 4, o += 3) {
|
||||||
unsigned a = reverse_lookup[(unsigned char)s[i + 0]];
|
unsigned a = reverse_lookup[(unsigned char)s[i + 0]];
|
||||||
unsigned b = reverse_lookup[(unsigned char)s[i + 1]];
|
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))))
|
if (unlikely(i + 4 != len || (a | b) & P || (c & P && !(d & P))))
|
||||||
goto invalid;
|
goto invalid;
|
||||||
|
|
||||||
|
if (unlikely(size != NULL)) {
|
||||||
|
if (c & P)
|
||||||
|
*size = len / 4 * 3 - 2;
|
||||||
|
else
|
||||||
|
*size = len / 4 * 3 - 1;
|
||||||
|
}
|
||||||
|
|
||||||
c &= 63;
|
c &= 63;
|
||||||
d &= 63;
|
d &= 63;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
2
base64.h
2
base64.h
|
|
@ -3,6 +3,6 @@
|
||||||
#include <stdint.h>
|
#include <stdint.h>
|
||||||
#include <stddef.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);
|
char *base64_encode(const uint8_t *data, size_t size);
|
||||||
void base64_encode_final(const uint8_t *data, size_t size, char result[4]);
|
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"))
|
else if (streq(key, "word-delimiters"))
|
||||||
return value_to_wchars(ctx, &conf->word_delimiters);
|
return value_to_wchars(ctx, &conf->word_delimiters);
|
||||||
|
|
||||||
else if (streq(key, "notify"))
|
else if (streq(key, "notify")) {
|
||||||
return value_to_spawn_template(ctx, &conf->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"))
|
else if (streq(key, "notify-focus-inhibit")) {
|
||||||
return value_to_bool(ctx, &conf->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")) {
|
else if (streq(key, "selection-target")) {
|
||||||
_Static_assert(sizeof(conf->selection_target) == sizeof(int),
|
_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
|
static bool
|
||||||
parse_section_scrollback(struct context *ctx)
|
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 {
|
enum section {
|
||||||
SECTION_MAIN,
|
SECTION_MAIN,
|
||||||
SECTION_BELL,
|
SECTION_BELL,
|
||||||
|
SECTION_DESKTOP_NOTIFICATIONS,
|
||||||
SECTION_SCROLLBACK,
|
SECTION_SCROLLBACK,
|
||||||
SECTION_URL,
|
SECTION_URL,
|
||||||
SECTION_COLORS,
|
SECTION_COLORS,
|
||||||
|
|
@ -2688,6 +2725,7 @@ static const struct {
|
||||||
} section_info[] = {
|
} section_info[] = {
|
||||||
[SECTION_MAIN] = {&parse_section_main, "main"},
|
[SECTION_MAIN] = {&parse_section_main, "main"},
|
||||||
[SECTION_BELL] = {&parse_section_bell, "bell"},
|
[SECTION_BELL] = {&parse_section_bell, "bell"},
|
||||||
|
[SECTION_DESKTOP_NOTIFICATIONS] = {&parse_section_desktop_notifications, "desktop-notifications"},
|
||||||
[SECTION_SCROLLBACK] = {&parse_section_scrollback, "scrollback"},
|
[SECTION_SCROLLBACK] = {&parse_section_scrollback, "scrollback"},
|
||||||
[SECTION_URL] = {&parse_section_url, "url"},
|
[SECTION_URL] = {&parse_section_url, "url"},
|
||||||
[SECTION_COLORS] = {&parse_section_colors, "colors"},
|
[SECTION_COLORS] = {&parse_section_colors, "colors"},
|
||||||
|
|
@ -3144,10 +3182,12 @@ config_load(struct config *conf, const char *conf_path,
|
||||||
.presentation_timings = false,
|
.presentation_timings = false,
|
||||||
.selection_target = SELECTION_TARGET_PRIMARY,
|
.selection_target = SELECTION_TARGET_PRIMARY,
|
||||||
.hold_at_exit = false,
|
.hold_at_exit = false,
|
||||||
.notify = {
|
.desktop_notifications = {
|
||||||
.argv = {.args = NULL},
|
.command = {
|
||||||
|
.argv = {.args = NULL},
|
||||||
|
},
|
||||||
|
.inhibit_when_focused = true,
|
||||||
},
|
},
|
||||||
.notify_focus_inhibit = true,
|
|
||||||
|
|
||||||
.tweak = {
|
.tweak = {
|
||||||
.fcft_filter = FCFT_SCALING_FILTER_LANCZOS3,
|
.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));
|
memcpy(conf->colors.table, default_color_table, sizeof(default_color_table));
|
||||||
parse_modifiers(XKB_MOD_NAME_SHIFT, 5, &conf->mouse.selection_override_modifiers);
|
parse_modifiers(XKB_MOD_NAME_SHIFT, 5, &conf->mouse.selection_override_modifiers);
|
||||||
|
|
||||||
tokenize_cmdline("notify-send -a ${app-id} -i ${app-id} ${title} ${body}",
|
tokenize_cmdline(
|
||||||
&conf->notify.argv.args);
|
"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);
|
tokenize_cmdline("xdg-open ${url}", &conf->url.launch.argv.args);
|
||||||
|
|
||||||
static const char32_t *url_protocols[] = {
|
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->scrollback.indicator.text = xc32dup(old->scrollback.indicator.text);
|
||||||
conf->server_socket_path = xstrdup(old->server_socket_path);
|
conf->server_socket_path = xstrdup(old->server_socket_path);
|
||||||
spawn_template_clone(&conf->bell.command, &old->bell.command);
|
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++)
|
for (size_t i = 0; i < ALEN(conf->fonts); i++)
|
||||||
config_font_list_clone(&conf->fonts[i], &old->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);
|
free(conf->word_delimiters);
|
||||||
spawn_template_free(&conf->bell.command);
|
spawn_template_free(&conf->bell.command);
|
||||||
free(conf->scrollback.indicator.text);
|
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++)
|
for (size_t i = 0; i < ALEN(conf->fonts); i++)
|
||||||
config_font_list_destroy(&conf->fonts[i]);
|
config_font_list_destroy(&conf->fonts[i]);
|
||||||
free(conf->server_socket_path);
|
free(conf->server_socket_path);
|
||||||
|
|
|
||||||
6
config.h
6
config.h
|
|
@ -338,8 +338,10 @@ struct config {
|
||||||
SELECTION_TARGET_BOTH
|
SELECTION_TARGET_BOTH
|
||||||
} selection_target;
|
} selection_target;
|
||||||
|
|
||||||
struct config_spawn_template notify;
|
struct {
|
||||||
bool notify_focus_inhibit;
|
struct config_spawn_template command;
|
||||||
|
bool inhibit_when_focused;
|
||||||
|
} desktop_notifications;
|
||||||
|
|
||||||
env_var_list_t env_vars;
|
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
|
: Copy _Pd_ (base64 encoded text) to the clipboard. _Pc_ denotes the
|
||||||
target: *c* targets the clipboard and *s* and *p* the primary
|
target: *c* targets the clipboard and *s* and *p* the primary
|
||||||
selection.
|
selection.
|
||||||
|
| \\E] 99 ; _params_ ; _payload_ \\E\\
|
||||||
|
: kitty
|
||||||
|
: Desktop notification; uses *desktop-notifications.command* in
|
||||||
|
*foot.ini*(5).
|
||||||
| \\E] 104 ; _c_ \\E\\
|
| \\E] 104 ; _c_ \\E\\
|
||||||
: xterm
|
: xterm
|
||||||
: Reset color number _c_ (multiple semicolon separated _c_ values may
|
: 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)
|
: Flash the entire terminal (foot extension)
|
||||||
| \\E] 777;notify;_title_;_msg_ \\E\\
|
| \\E] 777;notify;_title_;_msg_ \\E\\
|
||||||
: urxvt
|
: urxvt
|
||||||
: Desktop notification, uses *notify* in *foot.ini*(5).
|
: Desktop notification, uses *desktop-notifications.command* in
|
||||||
|
*foot.ini*(5).
|
||||||
|
|
||||||
# DCS
|
# DCS
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -342,32 +342,6 @@ empty string to be set, but it must be quoted: *KEY=""*)
|
||||||
text. Note that whitespace characters are _always_ word
|
text. Note that whitespace characters are _always_ word
|
||||||
delimiters, regardless of this setting. Default: _,│`|:"'()[]{}<>_
|
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*
|
*selection-target*
|
||||||
Clipboard target to automatically copy selected text to. One of
|
Clipboard target to automatically copy selected text to. One of
|
||||||
*none*, *primary*, *clipboard* or *both*. Default: _primary_.
|
*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_
|
Default: _no_
|
||||||
|
|
||||||
*notify*
|
*notify*
|
||||||
When set to _yes_, foot will emit a desktop notification using
|
When set to _yes_, foot will emit a desktop notification using the
|
||||||
the command specified in the *notify* option whenever *BEL* is
|
command specified in the *notify* option whenever *BEL* is
|
||||||
received. By default, bell notifications are shown only when the
|
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_
|
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:
|
Whether to run the command on *BEL* even while focused. Default:
|
||||||
_no_
|
_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
|
# SECTION: scrollback
|
||||||
|
|
||||||
*lines*
|
*lines*
|
||||||
|
|
|
||||||
7
foot.ini
7
foot.ini
|
|
@ -29,8 +29,6 @@
|
||||||
# resize-by-cells=yes
|
# resize-by-cells=yes
|
||||||
# resize-delay-ms=100
|
# resize-delay-ms=100
|
||||||
|
|
||||||
# notify=notify-send -a ${app-id} -i ${app-id} ${title} ${body}
|
|
||||||
|
|
||||||
# bold-text-in-bright=no
|
# bold-text-in-bright=no
|
||||||
# word-delimiters=,│`|:"'()[]{}<>
|
# word-delimiters=,│`|:"'()[]{}<>
|
||||||
# selection-target=primary
|
# selection-target=primary
|
||||||
|
|
@ -48,6 +46,11 @@
|
||||||
# command=
|
# command=
|
||||||
# command-focused=no
|
# 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]
|
[scrollback]
|
||||||
# lines=1000
|
# lines=1000
|
||||||
# multiplier=3.0
|
# 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,
|
if (spawn(term->reaper, term->cwd, binding->aux->pipe.args,
|
||||||
pipe_fd[0], stdout_fd, stderr_fd, NULL))
|
pipe_fd[0], stdout_fd, stderr_fd, NULL, NULL, NULL) < 0)
|
||||||
goto pipe_err;
|
goto pipe_err;
|
||||||
|
|
||||||
/* Close read end */
|
/* Close read end */
|
||||||
|
|
|
||||||
409
notify.c
409
notify.c
|
|
@ -1,54 +1,322 @@
|
||||||
#include "notify.h"
|
#include "notify.h"
|
||||||
|
|
||||||
|
#include <errno.h>
|
||||||
#include <stdlib.h>
|
#include <stdlib.h>
|
||||||
#include <string.h>
|
#include <string.h>
|
||||||
#include <unistd.h>
|
#include <unistd.h>
|
||||||
|
|
||||||
|
#include <sys/epoll.h>
|
||||||
#include <sys/stat.h>
|
#include <sys/stat.h>
|
||||||
#include <fcntl.h>
|
#include <fcntl.h>
|
||||||
|
|
||||||
#define LOG_MODULE "notify"
|
#define LOG_MODULE "notify"
|
||||||
#define LOG_ENABLE_DBG 0
|
#define LOG_ENABLE_DBG 1
|
||||||
#include "log.h"
|
#include "log.h"
|
||||||
#include "config.h"
|
#include "config.h"
|
||||||
#include "spawn.h"
|
#include "spawn.h"
|
||||||
|
#include "terminal.h"
|
||||||
|
#include "util.h"
|
||||||
|
#include "wayland.h"
|
||||||
#include "xmalloc.h"
|
#include "xmalloc.h"
|
||||||
|
#include "xsnprintf.h"
|
||||||
|
|
||||||
void
|
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) {
|
static bool
|
||||||
/* No notifications while we're focused */
|
to_integer(const char *line, size_t len, uint32_t *res)
|
||||||
return;
|
{
|
||||||
|
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)
|
*res = maybe_id;
|
||||||
return;
|
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;
|
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;
|
char **argv = NULL;
|
||||||
size_t argc = 0;
|
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(
|
if (!spawn_expand_template(
|
||||||
&term->conf->notify, 4,
|
&term->conf->desktop_notifications.command, 8,
|
||||||
(const char *[]){"app-id", "window-title", "title", "body"},
|
(const char *[]){
|
||||||
(const char *[]){term->app_id ? term->app_id : term->conf->app_id, term->window_title, title, body},
|
"app-id", "window-title", "icon", "title", "body", "urgency", "action-name", "action-label"},
|
||||||
&argc, &argv))
|
(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:");
|
LOG_DBG("notify command:");
|
||||||
for (size_t i = 0; i < argc; i++)
|
for (size_t i = 0; i < argc; i++)
|
||||||
LOG_DBG(" argv[%zu] = \"%s\"", i, argv[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 */
|
/* Redirect stdin to /dev/null, but ignore failure to open */
|
||||||
int devnull = open("/dev/null", O_RDONLY);
|
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)
|
if (devnull >= 0)
|
||||||
close(devnull);
|
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++)
|
for (size_t i = 0; i < argc; i++)
|
||||||
free(argv[i]);
|
free(argv[i]);
|
||||||
free(argv);
|
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
|
#pragma once
|
||||||
|
#include <stdbool.h>
|
||||||
|
#include <stdint.h>
|
||||||
|
#include <unistd.h>
|
||||||
|
|
||||||
#include "terminal.h"
|
struct terminal;
|
||||||
|
|
||||||
void notify_notify(
|
enum notify_when {
|
||||||
const struct terminal *term, const char *title, const char *body);
|
/* 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 "log.h"
|
||||||
#include "base64.h"
|
#include "base64.h"
|
||||||
#include "config.h"
|
#include "config.h"
|
||||||
#include "grid.h"
|
|
||||||
#include "macros.h"
|
#include "macros.h"
|
||||||
#include "notify.h"
|
#include "notify.h"
|
||||||
#include "render.h"
|
|
||||||
#include "selection.h"
|
#include "selection.h"
|
||||||
#include "terminal.h"
|
#include "terminal.h"
|
||||||
#include "uri.h"
|
#include "uri.h"
|
||||||
#include "util.h"
|
#include "util.h"
|
||||||
#include "vt.h"
|
|
||||||
#include "xmalloc.h"
|
#include "xmalloc.h"
|
||||||
#include "xsnprintf.h"
|
#include "xsnprintf.h"
|
||||||
|
|
||||||
|
|
@ -67,7 +64,7 @@ osc_to_clipboard(struct terminal *term, const char *target,
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
char *decoded = base64_decode(base64_data);
|
char *decoded = base64_decode(base64_data, NULL);
|
||||||
if (decoded == NULL) {
|
if (decoded == NULL) {
|
||||||
if (errno == EINVAL)
|
if (errno == EINVAL)
|
||||||
LOG_WARN("OSC: invalid clipboard data: %s", base64_data);
|
LOG_WARN("OSC: invalid clipboard data: %s", base64_data);
|
||||||
|
|
@ -560,7 +557,306 @@ osc_notify(struct terminal *term, char *string)
|
||||||
return;
|
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
|
void
|
||||||
|
|
@ -780,6 +1076,10 @@ osc_dispatch(struct terminal *term)
|
||||||
osc_selection(term, string);
|
osc_selection(term, string);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case 99: /* Kitty notifications */
|
||||||
|
kitty_notification(term, string);
|
||||||
|
break;
|
||||||
|
|
||||||
case 104: {
|
case 104: {
|
||||||
/* Reset Color Number 'c' (whole table if no parameter) */
|
/* 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_win_set_urgent(struct wl_window *win) { return true; }
|
||||||
bool wayl_fractional_scaling(const struct wayland *wayl) { 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[],
|
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,
|
||||||
const char *xdg_activation_token)
|
reaper_cb cb, void *cb_data, const char *xdg_activation_token)
|
||||||
{
|
{
|
||||||
return true;
|
return 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
pid_t
|
pid_t
|
||||||
|
|
@ -151,8 +151,31 @@ void ime_enable(struct seat *seat) {}
|
||||||
void ime_disable(struct seat *seat) {}
|
void ime_disable(struct seat *seat) {}
|
||||||
void ime_reset_preedit(struct seat *seat) {}
|
void ime_reset_preedit(struct seat *seat) {}
|
||||||
|
|
||||||
|
bool
|
||||||
|
notify_notify(struct terminal *term, struct notification *notif)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
void
|
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 "debug.h"
|
||||||
#include "xmalloc.h"
|
#include "xmalloc.h"
|
||||||
|
|
||||||
bool
|
pid_t
|
||||||
spawn(struct reaper *reaper, const char *cwd, char *const argv[],
|
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)
|
const char *xdg_activation_token)
|
||||||
{
|
{
|
||||||
int pipe_fds[2] = {-1, -1};
|
int pipe_fds[2] = {-1, -1};
|
||||||
|
|
@ -104,16 +104,16 @@ spawn(struct reaper *reaper, const char *cwd, char *const argv[],
|
||||||
close(pipe_fds[0]);
|
close(pipe_fds[0]);
|
||||||
|
|
||||||
if (ret == 0) {
|
if (ret == 0) {
|
||||||
reaper_add(reaper, pid, NULL, NULL);
|
reaper_add(reaper, pid, cb, cb_data);
|
||||||
return true;
|
return pid;
|
||||||
} else if (ret < 0) {
|
} else if (ret < 0) {
|
||||||
LOG_ERRNO("failed to read from pipe");
|
LOG_ERRNO("failed to read from pipe");
|
||||||
return false;
|
return -1;
|
||||||
} else {
|
} else {
|
||||||
LOG_ERRNO_P(errno_copy, "%s: failed to spawn", argv[0]);
|
LOG_ERRNO_P(errno_copy, "%s: failed to spawn", argv[0]);
|
||||||
errno = errno_copy;
|
errno = errno_copy;
|
||||||
waitpid(pid, NULL, 0);
|
waitpid(pid, NULL, 0);
|
||||||
return false;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
err:
|
err:
|
||||||
|
|
@ -121,7 +121,7 @@ err:
|
||||||
close(pipe_fds[0]);
|
close(pipe_fds[0]);
|
||||||
if (pipe_fds[1] != -1)
|
if (pipe_fds[1] != -1)
|
||||||
close(pipe_fds[1]);
|
close(pipe_fds[1]);
|
||||||
return false;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool
|
bool
|
||||||
|
|
|
||||||
8
spawn.h
8
spawn.h
|
|
@ -1,12 +1,14 @@
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include <stdbool.h>
|
#include <stdbool.h>
|
||||||
|
#include <unistd.h>
|
||||||
|
|
||||||
#include "config.h"
|
#include "config.h"
|
||||||
#include "reaper.h"
|
#include "reaper.h"
|
||||||
|
|
||||||
bool spawn(struct reaper *reaper, const char *cwd, char *const argv[],
|
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,
|
||||||
const char *xdg_activation_token);
|
reaper_cb cb, void *cb_data, const char *xdg_activation_token);
|
||||||
|
|
||||||
bool spawn_expand_template(
|
bool spawn_expand_template(
|
||||||
const struct config_spawn_template *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;
|
return true;
|
||||||
|
|
||||||
char *const argv[] = {conf->utmp_helper_path, UTMP_ADD, getenv("WAYLAND_DISPLAY"), NULL};
|
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
|
#else
|
||||||
return true;
|
return true;
|
||||||
#endif
|
#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};
|
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
|
#else
|
||||||
return true;
|
return true;
|
||||||
#endif
|
#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
|
#if defined(FOOT_IME_ENABLED) && FOOT_IME_ENABLED
|
||||||
.ime_enabled = true,
|
.ime_enabled = true,
|
||||||
#endif
|
#endif
|
||||||
|
.kitty_notifications = tll_init(),
|
||||||
|
.active_notifications = tll_init(),
|
||||||
};
|
};
|
||||||
|
|
||||||
pixman_region32_init(&term->render.last_overlay_clip);
|
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_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);
|
sixel_fini(term);
|
||||||
|
|
||||||
term_ime_reset(term);
|
term_ime_reset(term);
|
||||||
|
|
@ -2022,6 +2037,19 @@ term_reset(struct terminal *term, bool hard)
|
||||||
tll_remove(term->alt.sixel_images, it);
|
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;
|
term->grapheme_shaping = term->conf->tweak.grapheme_shaping;
|
||||||
|
|
||||||
#if defined(FOOT_IME_ENABLED) && FOOT_IME_ENABLED
|
#if defined(FOOT_IME_ENABLED) && FOOT_IME_ENABLED
|
||||||
|
|
@ -3566,8 +3594,11 @@ term_bell(struct terminal *term)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (term->conf->bell.notify)
|
if (term->conf->bell.notify) {
|
||||||
notify_notify(term, "Bell", "Bell in terminal");
|
notify_notify(term, &(struct notification){
|
||||||
|
.title = (char *)"Bell",
|
||||||
|
.body = (char *)"Bell in terminal"});
|
||||||
|
}
|
||||||
|
|
||||||
if (term->conf->bell.flash)
|
if (term->conf->bell.flash)
|
||||||
term_flash(term, 100);
|
term_flash(term, 100);
|
||||||
|
|
@ -3577,7 +3608,7 @@ term_bell(struct terminal *term)
|
||||||
{
|
{
|
||||||
int devnull = open("/dev/null", O_RDONLY);
|
int devnull = open("/dev/null", O_RDONLY);
|
||||||
spawn(term->reaper, NULL, term->conf->bell.command.argv.args,
|
spawn(term->reaper, NULL, term->conf->bell.command.argv.args,
|
||||||
devnull, -1, -1, NULL);
|
devnull, -1, -1, NULL, NULL, NULL);
|
||||||
|
|
||||||
if (devnull >= 0)
|
if (devnull >= 0)
|
||||||
close(devnull);
|
close(devnull);
|
||||||
|
|
@ -3589,7 +3620,7 @@ term_spawn_new(const struct terminal *term)
|
||||||
{
|
{
|
||||||
return spawn(
|
return spawn(
|
||||||
term->reaper, term->cwd, (char *const []){term->foot_exe, NULL},
|
term->reaper, term->cwd, (char *const []){term->foot_exe, NULL},
|
||||||
-1, -1, -1, NULL);
|
-1, -1, -1, NULL, NULL, NULL) >= 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
void
|
void
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,7 @@
|
||||||
#include "fdm.h"
|
#include "fdm.h"
|
||||||
#include "key-binding.h"
|
#include "key-binding.h"
|
||||||
#include "macros.h"
|
#include "macros.h"
|
||||||
|
#include "notify.h"
|
||||||
#include "reaper.h"
|
#include "reaper.h"
|
||||||
#include "shm.h"
|
#include "shm.h"
|
||||||
#include "wayland.h"
|
#include "wayland.h"
|
||||||
|
|
@ -798,6 +799,12 @@ struct terminal {
|
||||||
void *cb_data;
|
void *cb_data;
|
||||||
} shutdown;
|
} 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 *foot_exe;
|
||||||
char *cwd;
|
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, "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, "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, "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_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 */
|
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, "resize-delay-ms", &conf.resize_delay_ms);
|
||||||
test_uint16(&ctx, &parse_section_main, "workers", &conf.render_worker_count);
|
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",
|
test_enum(&ctx, &parse_section_main, "selection-target",
|
||||||
4,
|
4,
|
||||||
|
|
@ -570,6 +570,20 @@ test_section_bell(void)
|
||||||
config_free(&conf);
|
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
|
static void
|
||||||
test_section_scrollback(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);
|
log_init(LOG_COLORIZE_AUTO, false, 0, LOG_CLASS_ERROR);
|
||||||
test_section_main();
|
test_section_main();
|
||||||
test_section_bell();
|
test_section_bell();
|
||||||
|
test_section_desktop_notifications();
|
||||||
test_section_scrollback();
|
test_section_scrollback();
|
||||||
test_section_url();
|
test_section_url();
|
||||||
test_section_cursor();
|
test_section_cursor();
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
#include "unicode-mode.h"
|
#include "unicode-mode.h"
|
||||||
|
|
||||||
#define LOG_MODULE "unicode-input"
|
#define LOG_MODULE "unicode-input"
|
||||||
#define LOG_ENABLE_DBG 1
|
#define LOG_ENABLE_DBG 0
|
||||||
#include "log.h"
|
#include "log.h"
|
||||||
#include "render.h"
|
#include "render.h"
|
||||||
#include "search.h"
|
#include "search.h"
|
||||||
|
|
|
||||||
|
|
@ -74,8 +74,9 @@ spawn_url_launcher_with_token(struct terminal *term,
|
||||||
(const char *[]){url},
|
(const char *[]){url},
|
||||||
&argc, &argv))
|
&argc, &argv))
|
||||||
{
|
{
|
||||||
ret = spawn(term->reaper, term->cwd, argv,
|
ret = spawn(
|
||||||
dev_null, dev_null, dev_null, xdg_activation_token);
|
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++)
|
for (size_t i = 0; i < argc; i++)
|
||||||
free(argv[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);
|
wl_surface_commit(win->surface.surf);
|
||||||
|
|
||||||
/* Complete XDG startup notification */
|
/* Complete XDG startup notification */
|
||||||
if (token && wayl->xdg_activation != NULL)
|
wayl_activate(wayl, win, token);
|
||||||
xdg_activation_v1_activate(wayl->xdg_activation, token, win->surface.surf);
|
|
||||||
|
|
||||||
if (!wayl_win_subsurface_new(win, &win->overlay, false)) {
|
if (!wayl_win_subsurface_new(win, &win->overlay, false)) {
|
||||||
LOG_ERR("failed to create overlay surface");
|
LOG_ERR("failed to create overlay surface");
|
||||||
|
|
@ -2377,3 +2376,15 @@ wayl_get_activation_token(
|
||||||
xdg_activation_token_v1_commit(token);
|
xdg_activation_token_v1_commit(token);
|
||||||
return true;
|
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(
|
bool wayl_get_activation_token(
|
||||||
struct wayland *wayl, struct seat *seat, uint32_t serial,
|
struct wayland *wayl, struct seat *seat, uint32_t serial,
|
||||||
struct wl_window *win, activation_token_cb_t cb, void *cb_data);
|
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