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;
;\\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
+#include
+#include
+
+#include
+#include
+
+#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;;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;