#include "notify.h" #include #include #include #include #include #include #include #define LOG_MODULE "notify" #define LOG_ENABLE_DBG 1 #include "log.h" #include "config.h" #include "spawn.h" #include "terminal.h" #include "util.h" #include "wayland.h" #include "xmalloc.h" #include "xsnprintf.h" void notify_free(struct terminal *term, struct notification *notif) { if (notif->pid > 0) fdm_del(term->fdm, notif->stdout_fd); free(notif->id); free(notif->title); free(notif->body); free(notif->category); free(notif->app_id); free(notif->icon_cache_id); free(notif->icon_symbolic_name); free(notif->icon_data); free(notif->xdg_token); free(notif->stdout_data); tll_free_and_free(notif->actions, free); if (notif->icon_path != NULL) { unlink(notif->icon_path); free(notif->icon_path); if (notif->icon_fd >= 0) close(notif->icon_fd); } memset(notif, 0, sizeof(*notif)); } static bool write_icon_file(const void *data, size_t data_sz, int *fd, char **filename, char **symbolic_name) { xassert(*filename == NULL); xassert(*symbolic_name == NULL); char name[64] = "/tmp/foot-notification-icon-XXXXXX"; *filename = NULL; *symbolic_name = NULL; *fd = mkostemp(name, O_CLOEXEC); if (*fd < 0) { LOG_ERRNO("failed to create temporary file for icon cache"); return false; } if (write(*fd, data, data_sz) != (ssize_t)data_sz) { LOG_ERRNO("failed to write icon data to temporary file"); close(*fd); *fd = -1; return false; } LOG_DBG("wrote icon data to %s", name); *filename = xstrdup(name); *symbolic_name = xstrjoin("file://", *filename); return true; } static bool to_integer(const char *line, size_t len, uint32_t *res) { bool is_id = true; uint32_t maybe_id = 0; for (size_t i = 0; i < len; i++) { char digit = line[i]; if (digit < '0' || digit > '9') { is_id = false; break; } maybe_id *= 10; maybe_id += digit - '0'; } *res = maybe_id; return is_id; } 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; uint32_t maybe_button_nr = 0; /* Check for daemon assigned ID, either '123', or 'id=123' */ if ((notif->external_id == 0 && 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"); } else if (len > 7 && memcmp(line, "action=", 7) == 0) { notif->activated = true; if (to_integer(&line[7], len - 7, &maybe_button_nr)) { notif->activated_button = maybe_button_nr; LOG_DBG("custom action %u triggered", notif->activated_button); } else { LOG_DBG("unrecognized action triggered: %.*s", (int)(len - 7), &line[7]); } } else if (notif->external_id > 0 && to_integer(line, len, &maybe_button_nr) && maybe_button_nr > 0 && maybe_button_nr <= notif->button_count) { /* Single integer, appearing *after* the ID, and is within the custom button/action range */ notif->activated = true; notif->activated_button = maybe_button_nr; LOG_DBG("custom action %u triggered", notif->activated_button); } /* 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 closed", notif->id != NULL ? notif->id : ""); if (notif->activated && notif->focus) { LOG_DBG("focus window on notification activation: \"%s\"", notif->xdg_token); if (notif->xdg_token == NULL) LOG_WARN("cannot focus window: no activation token available"); else wayl_activate(term->wl, term->window, notif->xdg_token); } if (notif->activated && notif->report_activated) { LOG_DBG("sending notification activation event to client"); const char *id = notif->id != NULL ? notif->id : "0"; char button_nr[16] = {0}; if (notif->activated_button > 0) { xsnprintf( button_nr, sizeof(button_nr), "%u", notif->activated_button); } char reply[7 + strlen(id) + 1 + strlen(button_nr) + 2 + 1]; int n = xsnprintf( reply, sizeof(reply), "\033]99;i=%s;%s\033\\", id, button_nr); term_to_slave(term, reply, n); } if (notif->report_closed) { LOG_DBG("sending notification close event to client"); const char *id = notif->id != NULL ? notif->id : "0"; char reply[7 + strlen(id) + 1 + 7 + 1 + 2 + 1]; int n = xsnprintf( reply, sizeof(reply), "\033]99;i=%s:p=close;\033\\", id); term_to_slave(term, reply, n); } notify_free(term, notif); tll_remove(term->active_notifications, it); return; } } static bool expand_action_to_argv(struct terminal *term, const char *name, const char *label, size_t *argc, char ***argv) { char **expanded = NULL; size_t count = 0; if (!spawn_expand_template( &term->conf->desktop_notifications.command_action_arg, 2, (const char *[]){"action-name", "action-label"}, (const char *[]){name, label}, &count, &expanded)) { return false; } /* Append to the "global" actions argv */ *argv = xrealloc(*argv, (*argc + count) * sizeof((*argv)[0])); memcpy(&(*argv)[*argc], expanded, count * sizeof(expanded[0])); *argc += count; free(expanded); return true; } bool notify_notify(struct terminal *term, struct notification *notif) { xassert(notif->xdg_token == NULL); xassert(notif->external_id == 0); xassert(notif->pid == 0); xassert(notif->stdout_fd <= 0); xassert(notif->stdout_data == NULL); xassert(notif->icon_path == NULL); xassert(notif->icon_fd <= 0); notif->pid = -1; notif->stdout_fd = -1; notif->icon_fd = -1; if (term->conf->desktop_notifications.command.argv.args == 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; } const char *app_id = notif->app_id != NULL ? notif->app_id : term->app_id != NULL ? term->app_id : term->conf->app_id; const char *title = notif->title != NULL ? notif->title : notif->body; const char *body = notif->title != NULL && notif->body != NULL ? notif->body : ""; /* Icon: symbolic name if present, otherwise a filename */ const char *icon_name_or_path = ""; if (notif->icon_cache_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_cache_id)) { /* For now, we set the symbolic name to 'file:///path' * when using a file based icon. */ xassert(icon->symbolic_name != NULL); icon_name_or_path = icon->symbolic_name; LOG_DBG("using icon from cache (cache ID: %s): %s", icon->id, icon_name_or_path); break; } } } else if (notif->icon_symbolic_name != NULL) { icon_name_or_path = notif->icon_symbolic_name; LOG_DBG("using symbolic icon from notification: %s", icon_name_or_path); } else if (notif->icon_data_sz > 0) { xassert(notif->icon_data != NULL); if (write_icon_file( notif->icon_data, notif->icon_data_sz, ¬if->icon_fd, ¬if->icon_path, ¬if->icon_symbolic_name)) icon_name_or_path = notif->icon_symbolic_name; LOG_DBG("using icon data from notification: %s", icon_name_or_path); } bool track_notification = notif->focus || notif->report_activated || notif->may_be_programatically_closed; uint32_t replaces_id = 0; if (notif->id != NULL) { tll_foreach(term->active_notifications, it) { struct notification *existing = &it->item; if (existing->id == NULL) continue; /* * When replacing/updating a notification, we may have * *multiple* notification helpers running for the "same" * notification. Make sure only the *last* notification's * report closed/activated are honored, to avoid sending * multiple reports. * * This also means we cannot 'break' out of the loop - we * must check *all* notifications. */ if (existing->external_id != 0 && streq(existing->id, notif->id)) { replaces_id = existing->external_id; existing->report_activated = false; existing->report_closed = false; } } } char replaces_id_str[16]; xsnprintf(replaces_id_str, sizeof(replaces_id_str), "%u", replaces_id); const char *urgency_str = notif->urgency == NOTIFY_URGENCY_LOW ? "low" : notif->urgency == NOTIFY_URGENCY_NORMAL ? "normal" : "critical"; LOG_DBG("notify: title=\"%s\", body=\"%s\", app-id=\"%s\", category=\"%s\", " "urgency=\"%s\", icon=\"%s\", expires=%d, replaces=%u, muted=%s " "(tracking: %s)", title, body, app_id, notif->category, urgency_str, icon_name_or_path, notif->expire_time, replaces_id, notif->muted ? "yes" : "no", track_notification ? "yes" : "no"); xassert(title != NULL); if (title == NULL) return false; char **argv = NULL; size_t argc = 0; char **action_argv = NULL; size_t action_argc = 0; char expire_time[16]; xsnprintf(expire_time, sizeof(expire_time), "%d", notif->expire_time); if (term->conf->desktop_notifications.command_action_arg.argv.args) { if (!expand_action_to_argv( term, "default", "Activate", &action_argc, &action_argv)) { return false; } size_t action_idx = 1; tll_foreach(notif->actions, it) { /* Custom actions use a numerical name, starting at 1 */ char name[16]; xsnprintf(name, sizeof(name), "%zu", action_idx++); if (!expand_action_to_argv( term, name, it->item, &action_argc, &action_argv)) { for (size_t i = 0; i < action_argc; i++) free(action_argv[i]); free(action_argv); return false; } } } if (!spawn_expand_template( &term->conf->desktop_notifications.command, 11, (const char *[]){ "app-id", "window-title", "icon", "title", "body", "category", "urgency", "muted", "expire-time", "replace-id", "action-argument"}, (const char *[]){ app_id, term->window_title, icon_name_or_path, title, body, notif->category != NULL ? notif->category : "", urgency_str, notif->muted ? "true" : "false", expire_time, replaces_id_str, /* Custom expansion below, since we need to expand to multiple arguments */ "${action-argument}"}, &argc, &argv)) { return false; } /* Post-process the expanded argv, and patch in all the --action arguments we expanded earlier */ for (size_t i = 0; i < argc; i++) { if (!streq(argv[i], "${action-argument}")) continue; if (action_argc == 0) { free(argv[i]); /* Remove ${command-argument}, but include terminating NULL */ memmove(&argv[i], &argv[i + 1], (argc - i) * sizeof(argv[0])); argc--; break; } /* Remove the "${action-argument}" entry, add all actions argument from earlier, but include terminating NULL */ argv = xrealloc(argv, (argc + action_argc) * sizeof(argv[0])); /* Move remaining arguments to after the action arguments */ memmove(&argv[i + action_argc], &argv[i + 1], (argc - i) * sizeof(argv[0])); /* Include terminating NULL */ free(argv[i]); /* Free xstrdup("${action-argument}"); */ /* Insert the action arguments */ for (size_t j = 0; j < action_argc; j++) { argv[i + j] = action_argv[j]; action_argv[j] = NULL; } argc += action_argc; argc--; /* The ${action-argument} option has been removed */ break; } LOG_DBG("notify command:"); for (size_t i = 0; i < argc; i++) LOG_DBG(" argv[%zu] = \"%s\"", i, argv[i]); xassert(argv[argc] == NULL); 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); /* We've taken over ownership of all data; clear, so that notify_free() doesn't double free */ notif->id = NULL; notif->title = NULL; notif->body = NULL; notif->category = NULL; notif->app_id = NULL; notif->icon_cache_id = NULL; notif->icon_symbolic_name = NULL; notif->icon_data = NULL; notif->icon_data_sz = 0; notif->icon_path = NULL; notif->icon_fd = -1; notif->stdout_fd = -1; struct notification *new_notif = &tll_back(term->active_notifications); /* We don't need these anymore. They'll be free:d by the caller */ new_notif->button_count = tll_length(notif->actions); memset(&new_notif->actions, 0, sizeof(new_notif->actions)); notif = new_notif; } } if (stdout_fds[0] >= 0) { fdm_add(term->fdm, stdout_fds[0], EPOLLIN, &fdm_notify_stdout, (void *)term); } /* Redirect stdin to /dev/null, but ignore failure to open */ int devnull = open("/dev/null", O_RDONLY); pid_t pid = spawn( term->reaper, NULL, argv, devnull, stdout_fds[1], -1, track_notification ? ¬if_done : NULL, (void *)term, NULL); if (stdout_fds[1] >= 0) { /* Close write-end of stdout pipe */ close(stdout_fds[1]); } if (pid < 0 && stdout_fds[0] >= 0) { /* Remove FDM callback if we failed to spawn */ fdm_del(term->fdm, stdout_fds[0]); } if (devnull >= 0) close(devnull); for (size_t i = 0; i < argc; i++) free(argv[i]); free(argv); for (size_t i = 0; i < action_argc; i++) free(action_argv[i]); free(action_argv); notif->pid = pid; notif->stdout_fd = stdout_fds[0]; return true; } void notify_close(struct terminal *term, const char *id) { xassert(id != NULL); LOG_DBG("close notification %s", id); tll_foreach(term->active_notifications, it) { const struct notification *notif = &it->item; if (notif->id == NULL || !streq(notif->id, id)) continue; if (term->conf->desktop_notifications.close.argv.args == NULL) { LOG_DBG( "trying to close notification \"%s\" by sending SIGINT to %u", id, notif->pid); if (notif->pid == 0) { LOG_WARN( "cannot close notification \"%s\": no helper process running", id); } else { /* Best-effort... */ kill(notif->pid, SIGINT); } } else { LOG_DBG( "trying to close notification \"%s\" " "by running user defined command", id); if (notif->external_id == 0) { LOG_WARN("cannot close notification \"%s\": " "no daemon assigned notification ID available", id); return; } char **argv = NULL; size_t argc = 0; char external_id[16]; xsnprintf(external_id, sizeof(external_id), "%u", notif->external_id); if (!spawn_expand_template( &term->conf->desktop_notifications.close, 1, (const char *[]){"id"}, (const char *[]){external_id}, &argc, &argv)) { return; } int devnull = open("/dev/null", O_RDONLY); spawn( term->reaper, NULL, argv, devnull, -1, -1, NULL, (void *)term, NULL); if (devnull >= 0) close(devnull); for (size_t i = 0; i < argc; i++) free(argv[i]); free(argv); } return; } LOG_WARN("cannot close notification \"%s\": no such notification", id); } 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 ). * * 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) { write_icon_file( data, data_sz, &icon->tmp_file_fd, &icon->tmp_file_name, &icon->symbolic_name); } 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_name != NULL) { unlink(icon->tmp_file_name); if (icon->tmp_file_fd >= 0) close(icon->tmp_file_fd); } 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; }