Add support for xdg-session-management-v1

See https://gitlab.freedesktop.org/wlroots/wlroots/-/merge_requests/5321
This commit is contained in:
Simon Ser 2026-04-11 18:19:43 +02:00
parent 1606311553
commit c542c69170
9 changed files with 500 additions and 2 deletions

View file

@ -130,6 +130,12 @@ struct sway_server {
struct wlr_ext_workspace_manager_v1 *workspace_manager_v1;
struct wl_listener workspace_manager_v1_commit;
struct {
char *dir;
FILE *urandom;
struct wl_listener new_session;
} xdg_session_manager_v1;
struct wl_list pending_launcher_ctxs; // launcher_ctx::link
// The timeout for transactions, after which a transaction is applied

View file

@ -59,6 +59,7 @@ struct sway_view_impl {
struct sway_view *ancestor);
void (*close)(struct sway_view *view);
void (*close_popups)(struct sway_view *view);
void (*notify_state_update)(struct sway_view *view);
void (*destroy)(struct sway_view *view);
};
@ -113,6 +114,12 @@ struct sway_view {
#endif
};
struct {
bool pending;
char *workspace;
bool floating;
} session_restore;
struct {
struct wl_signal unmap;
} events;
@ -131,6 +138,12 @@ struct sway_xdg_shell_view {
struct wlr_scene_tree *image_capture_tree;
char *tag;
struct {
struct sway_xdg_session_v1 *session;
char *name;
struct wl_list link; // sway_xdg_session_v1.toplevels
} xdg_session_v1;
struct wl_listener commit;
struct wl_listener request_move;
struct wl_listener request_resize;
@ -366,4 +379,6 @@ bool view_can_tear(struct sway_view *view);
void xdg_toplevel_tag_manager_v1_handle_set_tag(struct wl_listener *listener, void *data);
void view_notify_state_update(struct sway_view *view);
#endif

View file

@ -0,0 +1,12 @@
#ifndef _SWAY_XDG_SESSION_MANAGEMENT_V1_H
#define _SWAY_XDG_SESSION_MANAGEMENT_V1_H
struct sway_xdg_session_v1;
bool init_xdg_session_management_v1(struct sway_server *server);
void finish_xdg_session_management_v1(struct sway_server *server);
void notify_xdg_session_management_v1_toplevel_initial_configure(struct sway_xdg_shell_view *view);
void notify_xdg_session_management_v1_toplevel_update(struct sway_xdg_shell_view *view);
#endif

View file

@ -18,6 +18,7 @@
#include "sway/tree/view.h"
#include "sway/tree/workspace.h"
#include "sway/xdg_decoration.h"
#include "sway/xdg_session_management_v1.h"
static struct sway_xdg_popup *popup_create(
struct wlr_xdg_popup *wlr_popup, struct sway_view *view,
@ -193,7 +194,8 @@ static void set_activated(struct sway_view *view, bool activated) {
}
static void set_tiled(struct sway_view *view, bool tiled) {
if (xdg_shell_view_from_view(view) == NULL) {
struct sway_xdg_shell_view *xdg_shell_view = xdg_shell_view_from_view(view);
if (xdg_shell_view == NULL) {
return;
}
if (wl_resource_get_version(view->wlr_xdg_toplevel->resource) >=
@ -209,6 +211,8 @@ static void set_tiled(struct sway_view *view, bool tiled) {
// to stop the client from drawing decorations outside of the toplevel geometry.
wlr_xdg_toplevel_set_maximized(view->wlr_xdg_toplevel, tiled);
}
notify_xdg_session_management_v1_toplevel_update(xdg_shell_view);
}
static void set_fullscreen(struct sway_view *view, bool fullscreen) {
@ -263,6 +267,11 @@ static void close_popups(struct sway_view *view) {
}
}
static void notify_state_update(struct sway_view *view) {
struct sway_xdg_shell_view *xdg_shell_view = xdg_shell_view_from_view(view);
notify_xdg_session_management_v1_toplevel_update(xdg_shell_view);
}
static void destroy(struct sway_view *view) {
struct sway_xdg_shell_view *xdg_shell_view =
xdg_shell_view_from_view(view);
@ -285,6 +294,7 @@ static const struct sway_view_impl view_impl = {
.is_transient_for = is_transient_for,
.close = _close,
.close_popups = close_popups,
.notify_state_update = notify_state_update,
.destroy = destroy,
};
@ -303,6 +313,8 @@ static void handle_commit(struct wl_listener *listener, void *data) {
wlr_xdg_toplevel_set_wm_capabilities(view->wlr_xdg_toplevel,
WLR_XDG_TOPLEVEL_WM_CAPABILITIES_FULLSCREEN);
// TODO: wlr_xdg_toplevel_set_bounds()
notify_xdg_session_management_v1_toplevel_initial_configure(xdg_shell_view);
return;
}
@ -528,6 +540,8 @@ static void handle_map(struct wl_listener *listener, void *data) {
xdg_shell_view->set_app_id.notify = handle_set_app_id;
wl_signal_add(&toplevel->events.set_app_id,
&xdg_shell_view->set_app_id);
notify_xdg_session_management_v1_toplevel_update(xdg_shell_view);
}
static void handle_destroy(struct wl_listener *listener, void *data) {
@ -545,6 +559,8 @@ static void handle_destroy(struct wl_listener *listener, void *data) {
if (view->xdg_decoration) {
view->xdg_decoration->view = NULL;
}
wl_list_remove(&xdg_shell_view->xdg_session_v1.link);
free(xdg_shell_view->xdg_session_v1.name);
view_begin_destroy(view);
}
@ -572,6 +588,8 @@ void handle_xdg_shell_toplevel(struct wl_listener *listener, void *data) {
}
xdg_shell_view->view.wlr_xdg_toplevel = xdg_toplevel;
wl_list_init(&xdg_shell_view->xdg_session_v1.link);
xdg_shell_view->map.notify = handle_map;
wl_signal_add(&xdg_toplevel->base->surface->events.map, &xdg_shell_view->map);

View file

@ -14,6 +14,7 @@ sway_sources = files(
'swaynag.c',
'xdg_activation_v1.c',
'xdg_decoration.c',
'xdg_session_management_v1.c',
'desktop/idle_inhibit_v1.c',
'desktop/layer_shell.c',

View file

@ -64,6 +64,7 @@
#include "sway/input/cursor.h"
#include "sway/tree/root.h"
#include "sway/tree/workspace.h"
#include "sway/xdg_session_management_v1.h"
#if WLR_HAS_XWAYLAND
#include <wlr/xwayland/shell.h>
@ -662,6 +663,11 @@ bool server_init(struct sway_server *server) {
return false;
}
if (!init_xdg_session_management_v1(server)) {
sway_log(SWAY_ERROR, "Failed to create XDG session manager");
return false;
}
wl_list_init(&server->pending_launcher_ctxs);
// Avoid using "wayland-0" as display socket
@ -739,6 +745,7 @@ void server_fini(struct sway_server *server) {
wl_list_remove(&server->request_set_cursor_shape.link);
wl_list_remove(&server->new_foreign_toplevel_capture_request.link);
wl_list_remove(&server->workspace_manager_v1_commit.link);
finish_xdg_session_management_v1(server);
input_manager_finish(server->input);
// TODO: free sway-specific resources

View file

@ -86,6 +86,7 @@ void view_destroy(struct sway_view *view) {
}
wl_list_remove(&view->events.unmap.listener_list);
list_free(view->executed_criteria);
free(view->session_restore.workspace);
view_assign_ctx(view, NULL);
wlr_scene_node_destroy(&view->image_capture_scene->tree.node);
@ -618,6 +619,15 @@ static struct sway_workspace *select_workspace(struct sway_view *view) {
return ws;
}
// Check session restoration
if (view->session_restore.pending && view->session_restore.workspace != NULL) {
ws = workspace_by_name(view->session_restore.workspace);
if (ws) {
view_assign_ctx(view, NULL);
return ws;
}
}
// Check if there's a PID mapping
ws = view->ctx ? launcher_ctx_get_workspace(view->ctx) : NULL;
if (ws) {
@ -859,7 +869,14 @@ void view_map(struct sway_view *view, struct wlr_surface *wlr_surface,
view_update_csd_from_client(view, decoration);
}
if (view->impl->wants_floating && view->impl->wants_floating(view)) {
bool floating;
if (view->session_restore.pending) {
floating = view->session_restore.floating;
} else {
floating = view->impl->wants_floating && view->impl->wants_floating(view);
}
if (floating) {
view->container->pending.border = config->floating_border;
view->container->pending.border_thickness = config->floating_border_thickness;
container_set_floating(view->container, true);
@ -909,6 +926,10 @@ void view_map(struct sway_view *view, struct wlr_surface *wlr_surface,
input_manager_set_focus(&view->container->node);
}
view->session_restore.pending = false;
free(view->session_restore.workspace);
view->session_restore.workspace = NULL;
if (view->ext_foreign_toplevel) {
update_ext_foreign_toplevel(view);
}
@ -1278,3 +1299,9 @@ void view_send_frame_done(struct sway_view *view) {
wlr_scene_node_for_each_buffer(node, send_frame_done_iterator, &when);
}
}
void view_notify_state_update(struct sway_view *view) {
if (view->impl->notify_state_update) {
view->impl->notify_state_update(view);
}
}

View file

@ -881,7 +881,11 @@ struct sway_container *workspace_find_container(struct sway_workspace *ws,
}
static void set_workspace(struct sway_container *container, void *data) {
bool changed = container->pending.workspace != container->pending.parent->pending.workspace;
container->pending.workspace = container->pending.parent->pending.workspace;
if (changed && container->view) {
view_notify_state_update(container->view);
}
}
static void workspace_attach_tiling(struct sway_workspace *ws,

View file

@ -0,0 +1,408 @@
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <json_object.h>
#include <json_util.h>
#include <wlr/types/wlr_security_context_v1.h>
#include <wlr/types/wlr_xdg_session_management_v1.h>
#include <wlr/types/wlr_xdg_shell.h>
#include "log.h"
#include "stringop.h"
#include "sway/server.h"
#include "sway/tree/view.h"
#include "sway/tree/workspace.h"
#include "sway/xdg_session_management_v1.h"
struct sway_xdg_session_v1 {
struct wlr_xdg_session_v1 *wlr; // may be NULL
char *path;
json_object *restorable_toplevels;
struct wl_list toplevels; // sway_xdg_shell_view.xdg_session_v1_link
bool save_pending;
struct wl_event_source *save_timer;
struct wl_listener destroy;
struct wl_listener remove;
struct wl_listener add_toplevel;
struct wl_listener restore_toplevel;
struct wl_listener remove_toplevel;
};
static char *get_directory(void) {
const char *home = getenv("HOME");
const char *xdg_state_home = getenv("XDG_STATE_HOME");
char *xdg_state_home_default = NULL;
if (xdg_state_home == NULL && home != NULL) {
xdg_state_home_default = format_str("%s/.local/state", home);
xdg_state_home = xdg_state_home_default;
}
if (xdg_state_home == NULL) {
return NULL;
}
char *path = format_str("%s/sway", xdg_state_home);
free(xdg_state_home_default);
return path;
}
static char *get_session_path(struct wl_client *client, const char *session_id) {
char *prefix = NULL;
const struct wlr_security_context_v1_state *security_context =
wlr_security_context_manager_v1_lookup_client(server.security_context_manager_v1, client);
if (security_context != NULL &&
strchr(security_context->sandbox_engine, '/') == NULL &&
strchr(security_context->app_id, '/') == NULL) {
prefix = format_str("%s_%s_", security_context->sandbox_engine, security_context->app_id);
}
char *path = format_str("%s/%s%s.json", server.xdg_session_manager_v1.dir,
prefix ? prefix : "", session_id);
free(prefix);
return path;
}
static FILE *open_urandom(void) {
int fd = open("/dev/urandom", O_RDONLY | O_CLOEXEC);
if (fd < 0) {
sway_log_errno(SWAY_ERROR, "Failed to open /dev/urandom");
return NULL;
}
FILE *f = fdopen(fd, "r");
if (f == NULL) {
sway_log_errno(SWAY_ERROR, "fdopen() failed");
close(fd);
return NULL;
}
return f;
}
#define TOKEN_SIZE 33
static bool generate_token(char out[static TOKEN_SIZE]) {
FILE *urandom = server.xdg_session_manager_v1.urandom;
uint64_t data[2];
if (fread(data, sizeof(data), 1, urandom) != 1) {
sway_log_errno(SWAY_ERROR, "Failed to read from random device");
return false;
}
if (snprintf(out, TOKEN_SIZE, "%016" PRIx64 "%016" PRIx64, data[0], data[1]) != TOKEN_SIZE - 1) {
sway_log_errno(SWAY_ERROR, "Failed to format hex string token");
return false;
}
return true;
}
static json_object *session_to_json(struct sway_xdg_session_v1 *session) {
json_object *toplevels_obj = json_object_new_object();
struct sway_xdg_shell_view *view;
wl_list_for_each(view, &session->toplevels, xdg_session_v1.link) {
struct sway_container *container = view->view.container;
json_object *toplevel_obj = json_object_new_object();
json_object_object_add(toplevel_obj, "floating",
json_object_new_boolean(container_is_floating(container)));
json_object_object_add(toplevel_obj, "floating",
json_object_new_string(container->pending.workspace->name));
// TODO: more
json_object_object_add(toplevels_obj, view->xdg_session_v1.name, toplevel_obj);
}
json_object *session_obj = json_object_new_object();
json_object_object_add(session_obj, "toplevels", toplevels_obj);
return session_obj;
}
static void session_save(struct sway_xdg_session_v1 *session) {
json_object *session_obj = session_to_json(session);
int ret = json_object_to_file(session->path, session_obj);
json_object_put(session_obj);
if (ret < 0) {
sway_log(SWAY_ERROR, "Failed to save XDG session to '%s'", session->path);
}
}
int session_handle_save_timer(void *data) {
struct sway_xdg_session_v1 *session = data;
session->save_pending = false;
session_save(session);
return 0;
}
static void session_schedule_save(struct sway_xdg_session_v1 *session) {
if (!session->save_pending) {
wl_event_source_timer_update(session->save_timer, 30 * 1000);
}
}
static json_object *load_session(const char *path) {
int fd = open(path, O_RDONLY | O_CLOEXEC);
if (fd < 0) {
if (errno != ENOENT) {
sway_log_errno(SWAY_ERROR, "Failed to read XDG session from '%s'", path);
}
return NULL;
}
json_object *session_obj = json_object_from_fd(fd);
close(fd);
if (session_obj == NULL) {
sway_log(SWAY_ERROR, "Failed to load XDG session from '%s'", path);
}
return session_obj;
}
static void session_consider_destroy(struct sway_xdg_session_v1 *session) {
// TODO: call this function on toplevel destroy
if (session->wlr || !wl_list_empty(&session->toplevels)) {
return;
}
if (session->save_pending) {
session_save(session);
}
wl_event_source_remove(session->save_timer);
json_object_put(session->restorable_toplevels);
free(session->path);
free(session);
}
static void session_handle_destroy(struct wl_listener *listener, void *data) {
struct sway_xdg_session_v1 *session = wl_container_of(listener, session, destroy);
wl_list_remove(&session->destroy.link);
wl_list_remove(&session->remove.link);
wl_list_remove(&session->add_toplevel.link);
wl_list_remove(&session->restore_toplevel.link);
wl_list_remove(&session->remove_toplevel.link);
session->wlr = NULL;
session_consider_destroy(session);
}
static void session_handle_remove(struct wl_listener *listener, void *data) {
struct sway_xdg_session_v1 *session = wl_container_of(listener, session, remove);
if (unlink(session->path) != 0 && errno != ENOENT) {
sway_log_errno(SWAY_ERROR, "Failed to delete XDG session '%s'", session->path);
}
}
static void session_add_toplevel(struct sway_xdg_session_v1 *session,
struct wlr_xdg_toplevel_session_v1 *toplevel_session, bool restore) {
struct sway_xdg_shell_view *view = toplevel_session->toplevel->base->data;
char *name = strdup(toplevel_session->name);
if (name == NULL) {
wl_resource_post_no_memory(toplevel_session->resource);
return;
}
// TODO: send error on duplicate session or name
view->xdg_session_v1.session = session;
view->xdg_session_v1.name = name;
wl_list_remove(&view->xdg_session_v1.link);
wl_list_insert(&session->toplevels, &view->xdg_session_v1.link);
// TODO: listen to rename event
if (!restore) {
session_schedule_save(session);
}
}
static void session_handle_add_toplevel(struct wl_listener *listener, void *data) {
struct sway_xdg_session_v1 *session = wl_container_of(listener, session, add_toplevel);
struct wlr_xdg_toplevel_session_v1 *toplevel_session = data;
session_add_toplevel(session, toplevel_session, false);
}
static void session_handle_restore_toplevel(struct wl_listener *listener, void *data) {
struct sway_xdg_session_v1 *session = wl_container_of(listener, session, restore_toplevel);
struct wlr_xdg_toplevel_session_v1 *toplevel_session = data;
session_add_toplevel(session, toplevel_session, true);
}
static void session_handle_remove_toplevel(struct wl_listener *listener, void *data) {
struct sway_xdg_session_v1 *session = wl_container_of(listener, session, remove_toplevel);
const struct wlr_xdg_session_v1_remove_toplevel_event *event = data;
json_object_object_del(session->restorable_toplevels, event->name);
bool found;
struct sway_xdg_shell_view *view;
wl_list_for_each(view, &session->toplevels, xdg_session_v1.link) {
if (strcmp(view->xdg_session_v1.name, event->name) == 0) {
found = true;
break;
}
}
if (!found) {
return;
}
// TODO: deduplicate
view->xdg_session_v1.session = NULL;
free(view->xdg_session_v1.name);
view->xdg_session_v1.name = NULL;
wl_list_remove(&view->xdg_session_v1.link);
wl_list_init(&view->xdg_session_v1.link);
session_schedule_save(session);
}
static void handle_new_session(struct wl_listener *listener, void *data) {
struct wlr_xdg_session_v1 *wlr_session = data;
struct wl_client *client = wl_resource_get_client(wlr_session->resource);
char *path = NULL;
json_object *restorable_toplevels = NULL;
if (wlr_session->id != NULL) {
path = get_session_path(client, wlr_session->id);
if (path == NULL) {
wl_resource_post_no_memory(wlr_session->resource);
return;
}
json_object *session_obj = load_session(path);
restorable_toplevels = json_object_get(json_object_object_get(session_obj, "toplevels"));
json_object_put(session_obj);
}
char new_session_id[TOKEN_SIZE];
if (restorable_toplevels == NULL) {
free(path);
path = NULL;
if (!generate_token(new_session_id)) {
wl_resource_post_no_memory(wlr_session->resource);
return;
}
path = get_session_path(client, new_session_id);
if (path == NULL) {
wl_resource_post_no_memory(wlr_session->resource);
return;
}
}
struct sway_xdg_session_v1 *session = calloc(1, sizeof(*session));
if (session == NULL) {
wl_resource_post_no_memory(wlr_session->resource);
free(path);
return;
}
session->save_timer = wl_event_loop_add_timer(server.wl_event_loop,
session_handle_save_timer, session);
if (session->save_timer == NULL) {
wl_resource_post_no_memory(wlr_session->resource);
free(session);
free(path);
return;
}
session->wlr = wlr_session;
session->path = path;
session->restorable_toplevels = restorable_toplevels;
session->destroy.notify = session_handle_destroy;
wl_signal_add(&session->wlr->events.destroy, &session->destroy);
session->remove.notify = session_handle_remove;
wl_signal_add(&session->wlr->events.remove, &session->remove);
session->add_toplevel.notify = session_handle_add_toplevel;
wl_signal_add(&session->wlr->events.add_toplevel, &session->add_toplevel);
session->restore_toplevel.notify = session_handle_restore_toplevel;
wl_signal_add(&session->wlr->events.restore_toplevel, &session->restore_toplevel);
session->remove_toplevel.notify = session_handle_remove_toplevel;
wl_signal_add(&session->wlr->events.remove_toplevel, &session->remove_toplevel);
if (restorable_toplevels != NULL) {
wlr_xdg_session_v1_notify_restored(session->wlr);
} else {
wlr_xdg_session_v1_notify_created(session->wlr, new_session_id);
}
}
bool init_xdg_session_management_v1(struct sway_server *server) {
char *dir = get_directory();
if (dir == NULL) {
sway_log(SWAY_ERROR, "Failed to pick XDG session management directory");
return false;
}
server->xdg_session_manager_v1.dir = dir;
FILE *urandom = open_urandom();
if (urandom == NULL) {
return false;
}
server->xdg_session_manager_v1.urandom = urandom;
struct wlr_xdg_session_manager_v1 *xdg_session_manager_v1 =
wlr_xdg_session_manager_v1_create(server->wl_display, 1);
if (xdg_session_manager_v1 == NULL) {
return false;
}
server->xdg_session_manager_v1.new_session.notify = handle_new_session;
wl_signal_add(&xdg_session_manager_v1->events.new_session,
&server->xdg_session_manager_v1.new_session);
// TODO: regularly clean up stale files
return true;
}
void finish_xdg_session_management_v1(struct sway_server *server) {
wl_list_remove(&server->xdg_session_manager_v1.new_session.link);
free(server->xdg_session_manager_v1.dir);
fclose(server->xdg_session_manager_v1.urandom);
}
void notify_xdg_session_management_v1_toplevel_update(struct sway_xdg_shell_view *view) {
if (view->xdg_session_v1.session) {
session_schedule_save(view->xdg_session_v1.session);
}
}
void notify_xdg_session_management_v1_toplevel_initial_configure(struct sway_xdg_shell_view *view) {
struct sway_xdg_session_v1 *session = view->xdg_session_v1.session;
if (session == NULL) {
return;
}
json_object *toplevel_obj = json_object_get(json_object_object_get(
session->restorable_toplevels, view->xdg_session_v1.name));
json_object_object_del(session->restorable_toplevels, view->xdg_session_v1.name);
if (toplevel_obj == NULL) {
return;
}
if (view->view.session_restore.pending) {
return;
}
view->view.session_restore.pending = true;
view->view.session_restore.floating =
json_object_get_boolean(json_object_object_get(toplevel_obj, "floating"));
view->view.session_restore.workspace =
strdup(json_object_get_string(json_object_object_get(toplevel_obj, "workspace")));
if (session->wlr != NULL) {
struct wlr_xdg_toplevel_session_v1 *toplevel_session;
wl_list_for_each(toplevel_session, &session->wlr->toplevels, link) {
if (toplevel_session->toplevel == view->view.wlr_xdg_toplevel) {
wlr_xdg_toplevel_session_v1_notify_restored(toplevel_session);
break;
}
}
}
json_object_put(toplevel_obj);
}