diff --git a/CHANGELOG.md b/CHANGELOG.md index 62a7a90a..13e82dbe 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -86,6 +86,12 @@ means foot can be PGO:d in e.g. sandboxed build scripts. See * `DECSET` escape to modify the `escape` key to send `\E[27;1;27~` instead of `\E`: `CSI ? 27127 h` enables the new behavior, `CSI ? 27127 l` disables it (the default). +* OSC 777;notify: desktop notifications. Use in combination with the + new **notify** option in `foot.ini` + (https://codeberg.org/dnkl/foot/issues/224). +* **bell** option can now be set to `notify`, in which case a desktop + notification is emitted when foot receives `BEL` in an unfocused + window. ### Changed diff --git a/config.c b/config.c index e28937a9..085c9f7e 100644 --- a/config.c +++ b/config.c @@ -483,14 +483,17 @@ parse_section_main(const char *key, const char *value, struct config *conf, else if (strcmp(key, "bell") == 0) { if (strcmp(value, "set-urgency") == 0) - conf->bell_is_urgent = true; + conf->bell_action = BELL_ACTION_URGENT; + else if (strcmp(value, "notify") == 0) + conf->bell_action = BELL_ACTION_NOTIFY; else if (strcmp(value, "none") == 0) - conf->bell_is_urgent = false; + conf->bell_action = BELL_ACTION_NONE; else { LOG_AND_NOTIFY_ERR( "%s:%d: [default]: bell: " - "expected either 'set-urgency' or 'none'", path, lineno); - conf->bell_is_urgent = false; + "expected either 'set-urgency', 'notify' or 'none'", + path, lineno); + conf->bell_action = BELL_ACTION_NONE; return false; } } @@ -566,6 +569,27 @@ parse_section_main(const char *key, const char *value, struct config *conf, mbstowcs(conf->word_delimiters, value, chars + 1); } + else if (strcmp(key, "notify") == 0) { + free(conf->notify.raw_cmd); + free(conf->notify.argv); + + conf->notify.raw_cmd = NULL; + conf->notify.argv = NULL; + + char *raw_cmd = xstrdup(value); + char **argv = NULL; + + if (!tokenize_cmdline(raw_cmd, &argv)) { + LOG_AND_NOTIFY_ERR( + "%s:%d: [default]: notify: syntax error in command line", + path, lineno); + return false; + } + + conf->notify.raw_cmd = raw_cmd; + conf->notify.argv = argv; + } + else { LOG_AND_NOTIFY_ERR("%s:%u: [default]: %s: invalid key", path, lineno, key); return false; @@ -1924,7 +1948,7 @@ config_load(struct config *conf, const char *conf_path, .pad_x = 2, .pad_y = 2, .bold_in_bright = false, - .bell_is_urgent = false, + .bell_action = BELL_ACTION_NONE, .startup_mode = STARTUP_WINDOWED, .fonts = {tll_init(), tll_init(), tll_init(), tll_init()}, .dpi_aware = true, /* Use DPI by default, not scale factor */ @@ -1989,6 +2013,10 @@ config_load(struct config *conf, const char *conf_path, .server_socket_path = get_server_socket_path(), .presentation_timings = false, .hold_at_exit = false, + .notify = { + .raw_cmd = NULL, + .argv = NULL, + }, .tweak = { .fcft_filter = FCFT_SCALING_FILTER_LANCZOS3, @@ -2004,6 +2032,10 @@ config_load(struct config *conf, const char *conf_path, .notifications = tll_init(), }; + conf->notify.raw_cmd = xstrdup( + "notify-send -a foot -i foot ${title} ${body}"); + tokenize_cmdline(conf->notify.raw_cmd, &conf->notify.argv); + tll_foreach(*initial_user_notifications, it) tll_push_back(conf->notifications, it->item); tll_free(*initial_user_notifications); @@ -2070,6 +2102,8 @@ config_free(struct config conf) free(conf.app_id); free(conf.word_delimiters); free(conf.scrollback.indicator.text); + free(conf.notify.raw_cmd); + free(conf.notify.argv); for (size_t i = 0; i < ALEN(conf.fonts); i++) { tll_foreach(conf.fonts[i], it) config_font_destroy(&it->item); diff --git a/config.h b/config.h index e69e5376..9e5e8b4e 100644 --- a/config.h +++ b/config.h @@ -72,7 +72,11 @@ struct config { unsigned pad_y; bool bold_in_bright; - bool bell_is_urgent; + enum { + BELL_ACTION_NONE, + BELL_ACTION_URGENT, + BELL_ACTION_NOTIFY, + } bell_action; enum { STARTUP_WINDOWED, STARTUP_MAXIMIZED, STARTUP_FULLSCREEN } startup_mode; @@ -163,6 +167,11 @@ struct config { bool presentation_timings; bool hold_at_exit; + struct { + char *raw_cmd; + char **argv; + } notify; + struct { enum fcft_scaling_filter fcft_filter; bool allow_overflowing_double_width_glyphs; diff --git a/csi.c b/csi.c index 232f7b1d..eaa527a0 100644 --- a/csi.c +++ b/csi.c @@ -482,7 +482,7 @@ decset_decrst(struct terminal *term, unsigned param, bool enable) break; case 1042: - term->bell_is_urgent = enable; + term->bell_action_enabled = enable; break; #if 0 @@ -591,7 +591,7 @@ xtsave(struct terminal *term, unsigned param) case 1034: term->xtsave.meta_eight_bit = term->meta.eight_bit; break; case 1035: term->xtsave.num_lock_modifier = term->num_lock_modifier; break; case 1036: term->xtsave.meta_esc_prefix = term->meta.esc_prefix; break; - case 1042: term->xtsave.bell_is_urgent = term->bell_is_urgent; break; + case 1042: term->xtsave.bell_action_enabled = term->bell_action_enabled; break; case 1049: term->xtsave.alt_screen = term->grid == &term->alt; break; case 2004: term->xtsave.bracketed_paste = term->bracketed_paste; break; case 27127: term->xtsave.modify_escape_key = term->modify_escape_key; break; @@ -626,7 +626,7 @@ xtrestore(struct terminal *term, unsigned param) case 1034: enable = term->xtsave.meta_eight_bit; break; case 1035: enable = term->xtsave.num_lock_modifier; break; case 1036: enable = term->xtsave.meta_esc_prefix; break; - case 1042: enable = term->xtsave.bell_is_urgent; break; + case 1042: enable = term->xtsave.bell_action_enabled; break; case 1049: enable = term->xtsave.alt_screen; break; case 2004: enable = term->xtsave.bracketed_paste; break; case 27127: enable = term->xtsave.modify_escape_key; break; diff --git a/doc/foot.ini.5.scd b/doc/foot.ini.5.scd index 14cd6d16..8397b118 100644 --- a/doc/foot.ini.5.scd +++ b/doc/foot.ini.5.scd @@ -120,7 +120,7 @@ in this order: *bell* Action to perform when receiving a *BEL* character. Can be set to - either *set-urgency* or *none*. + either *set-urgency*, *notify* or *none*. When set to *set-urgency*, the margins will be painted in red whenever *BEL* is received while the window does *not* have @@ -136,6 +136,9 @@ in this order: _Note_: expect this feature to be *replaced* with proper compositor urgency support once/if that gets implemented. + When set to *notify*, foot will emit a desktop notification (see + the *notify* option). + When set to *none*, no special action is taken when receiving *BEL*. Default: _none_. @@ -145,6 +148,17 @@ in this order: 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). + + Applications can trigger notifications in the following ways: + + - OSC 777: *\\e]777;notify;;<body>\\e\\\\* + + Default: _notify-send -a foot -i foot ${title} ${body}_. + # SECTION: scrollback diff --git a/foot.ini b/foot.ini index abf1353d..29ab2922 100644 --- a/foot.ini +++ b/foot.ini @@ -16,6 +16,7 @@ # bold-text-in-bright=no # bell=none # word-delimiters=,│`|:"'()[]{}<> +# notify=notify-send -a foot -i foot ${title} ${body} [scrollback] # lines=1000 diff --git a/meson.build b/meson.build index 36c5807e..e6a5c3d4 100644 --- a/meson.build +++ b/meson.build @@ -155,6 +155,7 @@ executable( 'ime.c', 'ime.h', 'input.c', 'input.h', 'main.c', + 'notify.c', 'notify.h', 'quirks.c', 'quirks.h', 'reaper.c', 'reaper.h', 'render.c', 'render.h', diff --git a/notify.c b/notify.c new file mode 100644 index 00000000..6f294458 --- /dev/null +++ b/notify.c @@ -0,0 +1,108 @@ +#include "notify.h" + +#include <stdlib.h> +#include <string.h> +#include <unistd.h> + +#include <sys/stat.h> +#include <fcntl.h> + +#define LOG_MODULE "notify" +#define LOG_ENABLE_DBG 0 +#include "log.h" +#include "config.h" +#include "spawn.h" +#include "xmalloc.h" + +void +notify_notify(const struct terminal *term, const char *title, const char *body) +{ + LOG_DBG("notify: title=\"%s\", msg=\"%s\"", title, msg); + + if (term->kbd_focus) { + /* No notifications while we’re focused */ + return; + } + + if (title == NULL || body == NULL) + return; + + if (term->conf->notify.argv == NULL) + return; + + size_t argv_size = 0; + for (; term->conf->notify.argv[argv_size] != NULL; argv_size++) + ; + +#define append(s, n) \ + do { \ + expanded = xrealloc(expanded, len + (n) + 1); \ + memcpy(&expanded[len], s, n); \ + len += n; \ + expanded[len] = '\0'; \ + } while (0) + + char **argv = malloc((argv_size + 1) * sizeof(argv[0])); + + /* Expand ${title} and ${body} */ + for (size_t i = 0; i < argv_size; i++) { + size_t len = 0; + char *expanded = NULL; + + char *start = NULL; + char *last_end = term->conf->notify.argv[i]; + + while ((start = strstr(last_end, "${")) != NULL) { + /* Append everything from the last template's end to this + * one's beginning */ + append(last_end, start - last_end); + + /* Find end of template */ + start += 2; + char *end = strstr(start, "}"); + + if (end == NULL) { + /* Ensure final append() copies the unclosed '${' */ + last_end = start - 2; + LOG_WARN("notify: unclosed template: %s", last_end); + break; + } + + /* Expand template */ + if (strncmp(start, "title", end - start) == 0) + append(title, strlen(title)); + else if (strncmp(start, "body", end - start) == 0) + append(body, strlen(body)); + else { + /* Unrecognized template - append it as-is */ + start -= 2; + append(start, end + 1 - start); + LOG_WARN("notify: unrecognized template: %.*s", + (int)(end + 1 - start), start); + } + + last_end = end + 1;; + } + + append(last_end, term->conf->notify.argv[i] + strlen(term->conf->notify.argv[i]) - last_end); + argv[i] = expanded; + } + argv[argv_size] = NULL; + +#undef append + + LOG_DBG("notify command:"); + for (size_t i = 0; i < argv_size; i++) + LOG_DBG(" argv[%zu] = \"%s\"", i, argv[i]); + + /* 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); + + if (devnull >= 0) + close(devnull); + + for (size_t i = 0; i < argv_size; i++) + free(argv[i]); + free(argv); +} diff --git a/notify.h b/notify.h new file mode 100644 index 00000000..ce60562f --- /dev/null +++ b/notify.h @@ -0,0 +1,6 @@ +#pragma once + +#include "terminal.h" + +void notify_notify( + const struct terminal *term, const char *title, const char *body); diff --git a/osc.c b/osc.c index f89becfd..d24a4603 100644 --- a/osc.c +++ b/osc.c @@ -10,6 +10,7 @@ #include "base64.h" #include "config.h" #include "grid.h" +#include "notify.h" #include "render.h" #include "selection.h" #include "terminal.h" @@ -374,22 +375,37 @@ osc_set_pwd(struct terminal *term, char *string) free(host); } -#if 0 static void osc_notify(struct terminal *term, char *string) { + /* + * The 'notify' perl extension + * (https://pub.phyks.me/scripts/urxvt/notify) is very simple: + * + * #!/usr/bin/perl + * + * sub on_osc_seq_perl { + * my ($term, $osc, $resp) = @_; + * if ($osc =~ /^notify;(\S+);(.*)$/) { + * system("notify-send '$1' '$2'"); + * } + * } + * + * As can be seen, the notification text is not encoded in any + * way. The regex does a greedy match of the ';' separator. Thus, + * any extra ';' will end up being part of the title. There's no + * way to have a ';' in the message body. + * + * I've changed that behavior slightly in; we split the title from + * body on the *first* ';', allowing us to have semicolons in the + * message body, but *not* in the title. + */ char *ctx = NULL; - const char *cmd = strtok_r(string, ";", &ctx); - const char *title = strtok_r(NULL, ";", &ctx); - const char *msg = strtok_r(NULL, ";", &ctx); + const char *title = strtok_r(string, ";", &ctx); + const char *msg = strtok_r(NULL, "\x00", &ctx); - LOG_DBG("cmd: \"%s\", title: \"%s\", msg: \"%s\"", - cmd, title, msg); - - if (cmd == NULL || strcmp(cmd, "notify") != 0 || title == NULL || msg == NULL) - return; + notify_notify(term, title, msg); } -#endif static void update_color_in_grids(struct terminal *term, uint32_t old_color, @@ -673,11 +689,27 @@ osc_dispatch(struct terminal *term) osc_flash(term); break; -#if 0 - case 777: - osc_notify(term, string); + case 777: { + /* + * OSC 777 is an URxvt generic escape used to send commands to + * perl extensions. The generic syntax is: \E]777;<command>;<string>ST + * + * We only recognize the 'notify' command, which is, if not + * well established, at least fairly well known. + */ + + char *param_brk = strchr(string, ';'); + if (param_brk == NULL) { + UNHANDLED(); + return; + } + + if (strncmp(string, "notify", param_brk - string) == 0) + osc_notify(term, param_brk + 1); + else + UNHANDLED(); break; -#endif + } default: UNHANDLED(); diff --git a/pgo/pgo.c b/pgo/pgo.c index 0dc08f61..7874cc8d 100644 --- a/pgo/pgo.c +++ b/pgo/pgo.c @@ -129,6 +129,11 @@ void cmd_scrollback_down(struct terminal *term, int rows) {} void ime_enable(struct seat *seat) {} void ime_disable(struct seat *seat) {} +void +notify_notify(const struct terminal *term, const char *title, const char *body) +{ +} + int main(int argc, const char *const *argv) { diff --git a/terminal.c b/terminal.c index a626b88d..e9ccbcd0 100644 --- a/terminal.c +++ b/terminal.c @@ -25,6 +25,7 @@ #include "extract.h" #include "grid.h" #include "ime.h" +#include "notify.h" #include "quirks.h" #include "reaper.h" #include "render.h" @@ -1091,7 +1092,7 @@ term_init(const struct config *conf, struct fdm *fdm, struct reaper *reaper, .eight_bit = true, }, .num_lock_modifier = true, - .bell_is_urgent = conf->bell_is_urgent, + .bell_action_enabled = true, .tab_stops = tll_init(), .wl = wayl, .render = { @@ -1532,6 +1533,8 @@ term_reset(struct terminal *term, bool hard) term->bracketed_paste = false; term->focus_events = false; term->modify_escape_key = false; + term->num_lock_modifier = true; + term->bell_action_enabled = true; term->mouse_tracking = MOUSE_NONE; term->mouse_reporting = MOUSE_NORMAL; term->charsets.selected = 0; @@ -2512,12 +2515,24 @@ term_flash(struct terminal *term, unsigned duration_ms) void term_bell(struct terminal *term) { - if (term->kbd_focus || !term->bell_is_urgent) + if (term->kbd_focus || !term->bell_action_enabled) return; - /* There's no 'urgency' hint in Wayland - we just paint the margins red */ - term->render.urgency = true; - term_damage_margins(term); + switch (term->conf->bell_action) { + case BELL_ACTION_NONE: + break; + + case BELL_ACTION_URGENT: + /* There's no 'urgency' hint in Wayland - we just paint the + * margins red */ + term->render.urgency = true; + term_damage_margins(term); + break; + + case BELL_ACTION_NOTIFY: + notify_notify(term, "Bell", "Bell in terminal"); + break; + } } bool diff --git a/terminal.h b/terminal.h index 07723e1e..0b0e2685 100644 --- a/terminal.h +++ b/terminal.h @@ -271,7 +271,7 @@ struct terminal { } meta; bool num_lock_modifier; - bool bell_is_urgent; + bool bell_action_enabled; /* Saved DECSET modes - we save the SET state */ struct { @@ -295,7 +295,7 @@ struct terminal { uint32_t meta_eight_bit:1; uint32_t meta_esc_prefix:1; uint32_t num_lock_modifier:1; - uint32_t bell_is_urgent:1; + uint32_t bell_action_enabled:1; uint32_t alt_screen:1; uint32_t modify_escape_key:1; uint32_t ime:1;