diff --git a/CHANGELOG.md b/CHANGELOG.md index 91646cb1..68e67049 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -41,6 +41,12 @@ ## Unreleased ### Added + +* XDG activation support when opening URLs ([#1058][1058]). + +[1058]: https://codeberg.org/dnkl/foot/issues/1058 + + ### Changed ### Deprecated ### Removed diff --git a/input.c b/input.c index 961b73ba..916cbd38 100644 --- a/input.c +++ b/input.c @@ -281,7 +281,8 @@ execute_binding(struct seat *seat, struct terminal *term, } } - if (!spawn(term->reaper, NULL, binding->aux->pipe.args, pipe_fd[0], stdout_fd, stderr_fd)) + if (!spawn(term->reaper, NULL, binding->aux->pipe.args, + pipe_fd[0], stdout_fd, stderr_fd, NULL)) goto pipe_err; /* Close read end */ diff --git a/notify.c b/notify.c index 13cee895..8180477d 100644 --- a/notify.c +++ b/notify.c @@ -48,7 +48,7 @@ notify_notify(const struct terminal *term, const char *title, const char *body) /* 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); + spawn(term->reaper, NULL, argv, devnull, -1, -1, NULL); if (devnull >= 0) close(devnull); diff --git a/spawn.c b/spawn.c index 8c5c33d2..7c6641da 100644 --- a/spawn.c +++ b/spawn.c @@ -17,7 +17,8 @@ bool 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) { int pipe_fds[2] = {-1, -1}; if (pipe2(pipe_fds, O_CLOEXEC) < 0) { @@ -47,14 +48,24 @@ spawn(struct reaper *reaper, const char *cwd, char *const argv[], /* Restore ignored (SIG_IGN) signals */ struct sigaction dfl = {.sa_handler = SIG_DFL}; sigemptyset(&dfl.sa_mask); - if (sigaction(SIGHUP, &dfl, NULL) < 0) + if (sigaction(SIGHUP, &dfl, NULL) < 0 || + sigaction(SIGPIPE, &dfl, NULL) < 0) + { goto child_err; + } if (cwd != NULL && chdir(cwd) < 0) { LOG_WARN("failed to change working directory to %s: %s", cwd, strerror(errno)); } + if (xdg_activation_token != NULL) { + setenv("XDG_ACTIVATION_TOKEN", xdg_activation_token, 1); + + if (getenv("DISPLAY") != NULL) + setenv("DESKTOP_STARTUP_ID", xdg_activation_token, 1); + } + bool close_stderr = stderr_fd >= 0; bool close_stdout = stdout_fd >= 0 && stdout_fd != stderr_fd; bool close_stdin = stdin_fd >= 0 && stdin_fd != stdout_fd && stdin_fd != stderr_fd; diff --git a/spawn.h b/spawn.h index c6f9582e..0fc95041 100644 --- a/spawn.h +++ b/spawn.h @@ -5,7 +5,8 @@ #include "reaper.h" bool 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); bool spawn_expand_template( const struct config_spawn_template *template, diff --git a/terminal.c b/terminal.c index dcaca033..960d6d24 100644 --- a/terminal.c +++ b/terminal.c @@ -3119,7 +3119,8 @@ term_bell(struct terminal *term) (!term->kbd_focus || term->conf->bell.command_focused)) { int devnull = open("/dev/null", O_RDONLY); - spawn(term->reaper, NULL, term->conf->bell.command.argv.args, devnull, -1, -1); + spawn(term->reaper, NULL, term->conf->bell.command.argv.args, + devnull, -1, -1, NULL); if (devnull >= 0) close(devnull); @@ -3131,7 +3132,7 @@ term_spawn_new(const struct terminal *term) { return spawn( term->reaper, term->cwd, (char *const []){term->foot_exe, NULL}, - -1, -1, -1); + -1, -1, -1, NULL); } void diff --git a/url-mode.c b/url-mode.c index 5ae1fd55..538b60f0 100644 --- a/url-mode.c +++ b/url-mode.c @@ -50,8 +50,86 @@ execute_binding(struct seat *seat, struct terminal *term, return true; } +static bool +spawn_url_launcher_with_token(struct terminal *term, + const char *url, + const char *xdg_activation_token) +{ + size_t argc; + char **argv; + + int dev_null = open("/dev/null", O_RDWR); + + if (dev_null < 0) { + LOG_ERRNO("failed to open /dev/null"); + return false; + } + + bool ret = false; + + if (spawn_expand_template( + &term->conf->url.launch, 1, + (const char *[]){"url"}, + (const char *[]){url}, + &argc, &argv)) + { + ret = spawn(term->reaper, term->cwd, argv, + dev_null, dev_null, dev_null, xdg_activation_token); + + for (size_t i = 0; i < argc; i++) + free(argv[i]); + free(argv); + } + + close(dev_null); + return ret; +} + +#if defined(HAVE_XDG_ACTIVATION) +struct spawn_activation_context { + struct terminal *term; + char *url; +}; + static void -activate_url(struct seat *seat, struct terminal *term, const struct url *url) +activation_token_done(const char *token, void *data) +{ + struct spawn_activation_context *ctx = data; + + spawn_url_launcher_with_token(ctx->term, ctx->url, token); + free(ctx->url); + free(ctx); +} +#endif + +static bool +spawn_url_launcher(struct seat *seat, struct terminal *term, const char *url, + uint32_t serial) +{ +#if defined(HAVE_XDG_ACTIVATION) + struct spawn_activation_context *ctx = xmalloc(sizeof(*ctx)); + *ctx = (struct spawn_activation_context){ + .term = term, + .url = xstrdup(url), + }; + + if (wayl_get_activation_token( + seat->wayl, seat, serial, term->window, &activation_token_done, ctx)) + { + /* Context free:d by callback */ + return true; + } + + free(ctx->url); + free(ctx); +#endif + + return spawn_url_launcher_with_token(term, url, NULL); +} + +static void +activate_url(struct seat *seat, struct terminal *term, const struct url *url, + uint32_t serial) { char *url_string = NULL; @@ -87,30 +165,7 @@ activate_url(struct seat *seat, struct terminal *term, const struct url *url) case URL_ACTION_LAUNCH: case URL_ACTION_PERSISTENT: { - size_t argc; - char **argv; - - int dev_null = open("/dev/null", O_RDWR); - - if (dev_null < 0) { - LOG_ERRNO("failed to open /dev/null"); - break; - } - - if (spawn_expand_template( - &term->conf->url.launch, 1, - (const char *[]){"url"}, - (const char *[]){url_string}, - &argc, &argv)) - { - spawn(term->reaper, term->cwd, argv, dev_null, dev_null, dev_null); - - for (size_t i = 0; i < argc; i++) - free(argv[i]); - free(argv); - } - - close(dev_null); + spawn_url_launcher(seat, term, url_string, serial); break; } } @@ -207,7 +262,7 @@ urls_input(struct seat *seat, struct terminal *term, } if (match) { - activate_url(seat, term, match); + activate_url(seat, term, match, serial); switch (match->action) { case URL_ACTION_COPY: diff --git a/wayland.c b/wayland.c index d65a571b..591fa4b7 100644 --- a/wayland.c +++ b/wayland.c @@ -1566,8 +1566,12 @@ wayl_win_destroy(struct wl_window *win) shm_purge(term->render.chains.csd); #if defined(HAVE_XDG_ACTIVATION) - if (win->xdg_activation_token != NULL) - xdg_activation_token_v1_destroy(win->xdg_activation_token); + tll_foreach(win->xdg_tokens, it) { + xdg_activation_token_v1_destroy(it->item->xdg_token); + free(it->item); + + tll_remove(win->xdg_tokens, it); + } #endif if (win->frame_callback != NULL) wl_callback_destroy(win->frame_callback); @@ -1706,51 +1710,21 @@ wayl_roundtrip(struct wayland *wayl) #if defined(HAVE_XDG_ACTIVATION) static void -activation_token_done(void *data, struct xdg_activation_token_v1 *xdg_token, - const char *token) +activation_token_for_urgency_done(const char *token, void *data) { struct wl_window *win = data; struct wayland *wayl = win->term->wl; - LOG_DBG("activation token: %s", token); - xdg_activation_v1_activate(wayl->xdg_activation, token, win->surface); - - xassert(win->xdg_activation_token == xdg_token); - xdg_activation_token_v1_destroy(xdg_token); - win->xdg_activation_token = NULL; } - -static const struct xdg_activation_token_v1_listener activation_token_listener = { - .done = &activation_token_done, -}; #endif /* HAVE_XDG_ACTIVATION */ bool wayl_win_set_urgent(struct wl_window *win) { #if defined(HAVE_XDG_ACTIVATION) - struct wayland *wayl = win->term->wl; - - if (wayl->xdg_activation == NULL) - return false; - - if (win->xdg_activation_token != NULL) - return true; - - struct xdg_activation_token_v1 *token = - xdg_activation_v1_get_activation_token(wayl->xdg_activation); - - if (token == NULL) { - LOG_ERR("failed to retrieve XDG activation token"); - return false; - } - - xdg_activation_token_v1_add_listener(token, &activation_token_listener, win); - xdg_activation_token_v1_set_surface(token, win->surface); - xdg_activation_token_v1_commit(token); - win->xdg_activation_token = token; - return true; + return wayl_get_activation_token( + win->term->wl, NULL, 0, win, &activation_token_for_urgency_done, win); #else return false; #endif @@ -1833,3 +1807,72 @@ wayl_win_subsurface_destroy(struct wl_surf_subsurf *surf) surf->surf = NULL; surf->sub = NULL; } + +#if defined(HAVE_XDG_ACTIVATION) + +static void +activation_token_done(void *data, struct xdg_activation_token_v1 *xdg_token, + const char *token) +{ + LOG_DBG("XDG activation token done: %s", token); + + struct xdg_activation_token_context *ctx = data; + struct wl_window *win = ctx->win; + + ctx->cb(token, ctx->cb_data); + + tll_foreach(win->xdg_tokens, it) { + if (it->item->xdg_token != xdg_token) + continue; + + xassert(win == it->item->win); + + free(ctx); + xdg_activation_token_v1_destroy(xdg_token); + tll_remove(win->xdg_tokens, it); + return; + } + + xassert(false); +} + +static const struct +xdg_activation_token_v1_listener activation_token_listener = { + .done = &activation_token_done, +}; + +bool +wayl_get_activation_token( + struct wayland *wayl, struct seat *seat, uint32_t serial, + struct wl_window *win, + void (*cb)(const char *token, void *data), void *cb_data) +{ + if (wayl->xdg_activation == NULL) + return false; + + struct xdg_activation_token_v1 *token = + xdg_activation_v1_get_activation_token(wayl->xdg_activation); + + if (token == NULL) { + LOG_ERR("failed to retrieve XDG activation token"); + return false; + } + + struct xdg_activation_token_context *ctx = xmalloc(sizeof(*ctx)); + *ctx = (struct xdg_activation_token_context){ + .win = win, + .xdg_token = token, + .cb = cb, + .cb_data = cb_data, + }; + tll_push_back(win->xdg_tokens, ctx); + + if (seat != NULL && serial != 0) + xdg_activation_token_v1_set_serial(token, serial, seat->wl_seat); + + xdg_activation_token_v1_set_surface(token, win->surface); + xdg_activation_token_v1_add_listener(token, &activation_token_listener, ctx); + xdg_activation_token_v1_commit(token); + return true; +} +#endif diff --git a/wayland.h b/wayland.h index 2e8dcd98..8f59eb98 100644 --- a/wayland.h +++ b/wayland.h @@ -295,6 +295,22 @@ struct wl_url { enum csd_mode {CSD_UNKNOWN, CSD_NO, CSD_YES}; +#if defined(HAVE_XDG_ACTIVATION) +typedef void (*activation_token_cb_t)(const char *token, void *data); + +/* + * This context holds data used both in the token::done callback, and + * when cleaning up created, by not-yet-done tokens in + * wayl_win_destroy(). + */ +struct xdg_activation_token_context { + struct wl_window *win; /* Need for win->xdg_tokens */ + struct xdg_activation_token_v1 *xdg_token; /* Used to match token in done() */ + activation_token_cb_t cb; /* User provided callback */ + void *cb_data; /* Callback user pointer */ +}; +#endif + struct wayland; struct wl_window { struct terminal *term; @@ -302,7 +318,7 @@ struct wl_window { struct xdg_surface *xdg_surface; struct xdg_toplevel *xdg_toplevel; #if defined(HAVE_XDG_ACTIVATION) - struct xdg_activation_token_v1 *xdg_activation_token; + tll(struct xdg_activation_token_context *) xdg_tokens; #endif struct zxdg_toplevel_decoration_v1 *xdg_toplevel_decoration; @@ -417,3 +433,9 @@ bool wayl_win_subsurface_new_with_custom_parent( struct wl_window *win, struct wl_surface *parent, struct wl_surf_subsurf *surf, bool allow_pointer_input); void wayl_win_subsurface_destroy(struct wl_surf_subsurf *surf); + +#if defined(HAVE_XDG_ACTIVATION) +bool wayl_get_activation_token( + struct wayland *wayl, struct seat *seat, uint32_t serial, + struct wl_window *win, activation_token_cb_t cb, void *cb_data); +#endif