diff --git a/include/sway/server.h b/include/sway/server.h index 8c8114882..d3d439516 100644 --- a/include/sway/server.h +++ b/include/sway/server.h @@ -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 diff --git a/include/sway/tree/view.h b/include/sway/tree/view.h index ae81c5bbf..e190df7e1 100644 --- a/include/sway/tree/view.h +++ b/include/sway/tree/view.h @@ -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 diff --git a/include/sway/xdg_session_management_v1.h b/include/sway/xdg_session_management_v1.h new file mode 100644 index 000000000..89ce4a59b --- /dev/null +++ b/include/sway/xdg_session_management_v1.h @@ -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 diff --git a/sway/desktop/xdg_shell.c b/sway/desktop/xdg_shell.c index 7217e1369..5afd805c0 100644 --- a/sway/desktop/xdg_shell.c +++ b/sway/desktop/xdg_shell.c @@ -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); diff --git a/sway/meson.build b/sway/meson.build index cb03a4d28..dcca2662d 100644 --- a/sway/meson.build +++ b/sway/meson.build @@ -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', diff --git a/sway/server.c b/sway/server.c index 8bdafb674..a31c91366 100644 --- a/sway/server.c +++ b/sway/server.c @@ -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 @@ -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 diff --git a/sway/tree/view.c b/sway/tree/view.c index 6dde3f63c..31da033ab 100644 --- a/sway/tree/view.c +++ b/sway/tree/view.c @@ -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); + } +} diff --git a/sway/tree/workspace.c b/sway/tree/workspace.c index 23311a456..a28439584 100644 --- a/sway/tree/workspace.c +++ b/sway/tree/workspace.c @@ -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, diff --git a/sway/xdg_session_management_v1.c b/sway/xdg_session_management_v1.c new file mode 100644 index 000000000..917bdc211 --- /dev/null +++ b/sway/xdg_session_management_v1.c @@ -0,0 +1,408 @@ +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +#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); +}