mirror of
https://codeberg.org/dnkl/foot.git
synced 2026-03-18 05:34:02 -04:00
url-mode: add support for XDG activation when opening URLs
First, add a ‘token’ argument to spawn(). When non-NULL, spawn() will set the ‘XDG_ACTIVATION_TOKEN’ environment variable in the forked process. If DISPLAY is non-NULL, we also set DESKTOP_STARTUP_ID, for compatibility with X11 applications. Note that failing to set either of these environment variables are considered non-fatal - i.e. we ignore failures. Next, add a helper function, wayl_get_activation_token(), to generate an XDG activation token, and call a user-provided callback when it’s ‘done (since token generation is asynchronous). This function takes an optional ‘seat’ and ‘serial’ arguments - when both are non-NULL/zero, we set the serial on the token. ‘win’ is a required argument, used to set the surface on the token. Re-write wayl_win_set_urgent() to use the new helper function. Finally, rewrite activate_url() to first try to get an activation token (and spawn the URL launcher in the token callback). If that fails, or if we don’t have XDG activation support, spawn the URL launcher immediately (like before this patch). Closes #1058
This commit is contained in:
parent
bd8dd9ff7e
commit
ea1aac88db
9 changed files with 209 additions and 69 deletions
|
|
@ -41,6 +41,12 @@
|
||||||
|
|
||||||
## Unreleased
|
## Unreleased
|
||||||
### Added
|
### Added
|
||||||
|
|
||||||
|
* XDG activation support when opening URLs ([#1058][1058]).
|
||||||
|
|
||||||
|
[1058]: https://codeberg.org/dnkl/foot/issues/1058
|
||||||
|
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
### Deprecated
|
### Deprecated
|
||||||
### Removed
|
### Removed
|
||||||
|
|
|
||||||
3
input.c
3
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;
|
goto pipe_err;
|
||||||
|
|
||||||
/* Close read end */
|
/* Close read end */
|
||||||
|
|
|
||||||
2
notify.c
2
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 */
|
/* 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);
|
spawn(term->reaper, NULL, argv, devnull, -1, -1, NULL);
|
||||||
|
|
||||||
if (devnull >= 0)
|
if (devnull >= 0)
|
||||||
close(devnull);
|
close(devnull);
|
||||||
|
|
|
||||||
15
spawn.c
15
spawn.c
|
|
@ -17,7 +17,8 @@
|
||||||
|
|
||||||
bool
|
bool
|
||||||
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)
|
||||||
{
|
{
|
||||||
int pipe_fds[2] = {-1, -1};
|
int pipe_fds[2] = {-1, -1};
|
||||||
if (pipe2(pipe_fds, O_CLOEXEC) < 0) {
|
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 */
|
/* Restore ignored (SIG_IGN) signals */
|
||||||
struct sigaction dfl = {.sa_handler = SIG_DFL};
|
struct sigaction dfl = {.sa_handler = SIG_DFL};
|
||||||
sigemptyset(&dfl.sa_mask);
|
sigemptyset(&dfl.sa_mask);
|
||||||
if (sigaction(SIGHUP, &dfl, NULL) < 0)
|
if (sigaction(SIGHUP, &dfl, NULL) < 0 ||
|
||||||
|
sigaction(SIGPIPE, &dfl, NULL) < 0)
|
||||||
|
{
|
||||||
goto child_err;
|
goto child_err;
|
||||||
|
}
|
||||||
|
|
||||||
if (cwd != NULL && chdir(cwd) < 0) {
|
if (cwd != NULL && chdir(cwd) < 0) {
|
||||||
LOG_WARN("failed to change working directory to %s: %s",
|
LOG_WARN("failed to change working directory to %s: %s",
|
||||||
cwd, strerror(errno));
|
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_stderr = stderr_fd >= 0;
|
||||||
bool close_stdout = stdout_fd >= 0 && stdout_fd != stderr_fd;
|
bool close_stdout = stdout_fd >= 0 && stdout_fd != stderr_fd;
|
||||||
bool close_stdin = stdin_fd >= 0 && stdin_fd != stdout_fd && stdin_fd != stderr_fd;
|
bool close_stdin = stdin_fd >= 0 && stdin_fd != stdout_fd && stdin_fd != stderr_fd;
|
||||||
|
|
|
||||||
3
spawn.h
3
spawn.h
|
|
@ -5,7 +5,8 @@
|
||||||
#include "reaper.h"
|
#include "reaper.h"
|
||||||
|
|
||||||
bool spawn(struct reaper *reaper, const char *cwd, char *const argv[],
|
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(
|
bool spawn_expand_template(
|
||||||
const struct config_spawn_template *template,
|
const struct config_spawn_template *template,
|
||||||
|
|
|
||||||
|
|
@ -3119,7 +3119,8 @@ term_bell(struct terminal *term)
|
||||||
(!term->kbd_focus || term->conf->bell.command_focused))
|
(!term->kbd_focus || term->conf->bell.command_focused))
|
||||||
{
|
{
|
||||||
int devnull = open("/dev/null", O_RDONLY);
|
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)
|
if (devnull >= 0)
|
||||||
close(devnull);
|
close(devnull);
|
||||||
|
|
@ -3131,7 +3132,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);
|
-1, -1, -1, NULL);
|
||||||
}
|
}
|
||||||
|
|
||||||
void
|
void
|
||||||
|
|
|
||||||
107
url-mode.c
107
url-mode.c
|
|
@ -50,8 +50,86 @@ execute_binding(struct seat *seat, struct terminal *term,
|
||||||
return true;
|
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
|
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;
|
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_LAUNCH:
|
||||||
case URL_ACTION_PERSISTENT: {
|
case URL_ACTION_PERSISTENT: {
|
||||||
size_t argc;
|
spawn_url_launcher(seat, term, url_string, serial);
|
||||||
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);
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -207,7 +262,7 @@ urls_input(struct seat *seat, struct terminal *term,
|
||||||
}
|
}
|
||||||
|
|
||||||
if (match) {
|
if (match) {
|
||||||
activate_url(seat, term, match);
|
activate_url(seat, term, match, serial);
|
||||||
|
|
||||||
switch (match->action) {
|
switch (match->action) {
|
||||||
case URL_ACTION_COPY:
|
case URL_ACTION_COPY:
|
||||||
|
|
|
||||||
113
wayland.c
113
wayland.c
|
|
@ -1566,8 +1566,12 @@ wayl_win_destroy(struct wl_window *win)
|
||||||
shm_purge(term->render.chains.csd);
|
shm_purge(term->render.chains.csd);
|
||||||
|
|
||||||
#if defined(HAVE_XDG_ACTIVATION)
|
#if defined(HAVE_XDG_ACTIVATION)
|
||||||
if (win->xdg_activation_token != NULL)
|
tll_foreach(win->xdg_tokens, it) {
|
||||||
xdg_activation_token_v1_destroy(win->xdg_activation_token);
|
xdg_activation_token_v1_destroy(it->item->xdg_token);
|
||||||
|
free(it->item);
|
||||||
|
|
||||||
|
tll_remove(win->xdg_tokens, it);
|
||||||
|
}
|
||||||
#endif
|
#endif
|
||||||
if (win->frame_callback != NULL)
|
if (win->frame_callback != NULL)
|
||||||
wl_callback_destroy(win->frame_callback);
|
wl_callback_destroy(win->frame_callback);
|
||||||
|
|
@ -1706,51 +1710,21 @@ wayl_roundtrip(struct wayland *wayl)
|
||||||
|
|
||||||
#if defined(HAVE_XDG_ACTIVATION)
|
#if defined(HAVE_XDG_ACTIVATION)
|
||||||
static void
|
static void
|
||||||
activation_token_done(void *data, struct xdg_activation_token_v1 *xdg_token,
|
activation_token_for_urgency_done(const char *token, void *data)
|
||||||
const char *token)
|
|
||||||
{
|
{
|
||||||
struct wl_window *win = data;
|
struct wl_window *win = data;
|
||||||
struct wayland *wayl = win->term->wl;
|
struct wayland *wayl = win->term->wl;
|
||||||
|
|
||||||
LOG_DBG("activation token: %s", token);
|
|
||||||
|
|
||||||
xdg_activation_v1_activate(wayl->xdg_activation, token, win->surface);
|
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 */
|
#endif /* HAVE_XDG_ACTIVATION */
|
||||||
|
|
||||||
bool
|
bool
|
||||||
wayl_win_set_urgent(struct wl_window *win)
|
wayl_win_set_urgent(struct wl_window *win)
|
||||||
{
|
{
|
||||||
#if defined(HAVE_XDG_ACTIVATION)
|
#if defined(HAVE_XDG_ACTIVATION)
|
||||||
struct wayland *wayl = win->term->wl;
|
return wayl_get_activation_token(
|
||||||
|
win->term->wl, NULL, 0, win, &activation_token_for_urgency_done, win);
|
||||||
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;
|
|
||||||
#else
|
#else
|
||||||
return false;
|
return false;
|
||||||
#endif
|
#endif
|
||||||
|
|
@ -1833,3 +1807,72 @@ wayl_win_subsurface_destroy(struct wl_surf_subsurf *surf)
|
||||||
surf->surf = NULL;
|
surf->surf = NULL;
|
||||||
surf->sub = 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
|
||||||
|
|
|
||||||
24
wayland.h
24
wayland.h
|
|
@ -295,6 +295,22 @@ struct wl_url {
|
||||||
|
|
||||||
enum csd_mode {CSD_UNKNOWN, CSD_NO, CSD_YES};
|
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 wayland;
|
||||||
struct wl_window {
|
struct wl_window {
|
||||||
struct terminal *term;
|
struct terminal *term;
|
||||||
|
|
@ -302,7 +318,7 @@ struct wl_window {
|
||||||
struct xdg_surface *xdg_surface;
|
struct xdg_surface *xdg_surface;
|
||||||
struct xdg_toplevel *xdg_toplevel;
|
struct xdg_toplevel *xdg_toplevel;
|
||||||
#if defined(HAVE_XDG_ACTIVATION)
|
#if defined(HAVE_XDG_ACTIVATION)
|
||||||
struct xdg_activation_token_v1 *xdg_activation_token;
|
tll(struct xdg_activation_token_context *) xdg_tokens;
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
struct zxdg_toplevel_decoration_v1 *xdg_toplevel_decoration;
|
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_window *win, struct wl_surface *parent,
|
||||||
struct wl_surf_subsurf *surf, bool allow_pointer_input);
|
struct wl_surf_subsurf *surf, bool allow_pointer_input);
|
||||||
void wayl_win_subsurface_destroy(struct wl_surf_subsurf *surf);
|
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
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue