mirror of
https://github.com/swaywm/sway.git
synced 2026-06-13 14:33:19 -04:00
Merge 93931c37a6 into 97c342f9e1
This commit is contained in:
commit
e7da535183
17 changed files with 1324 additions and 54 deletions
|
|
@ -105,6 +105,7 @@ sway_cmd cmd_exec_validate;
|
||||||
sway_cmd cmd_exec_process;
|
sway_cmd cmd_exec_process;
|
||||||
|
|
||||||
sway_cmd cmd_allow_tearing;
|
sway_cmd cmd_allow_tearing;
|
||||||
|
sway_cmd cmd_append_layout;
|
||||||
sway_cmd cmd_assign;
|
sway_cmd cmd_assign;
|
||||||
sway_cmd cmd_bar;
|
sway_cmd cmd_bar;
|
||||||
sway_cmd cmd_bindcode;
|
sway_cmd cmd_bindcode;
|
||||||
|
|
|
||||||
|
|
@ -83,6 +83,18 @@ struct criteria *criteria_parse(char *raw, char **error);
|
||||||
*/
|
*/
|
||||||
list_t *criteria_for_view(struct sway_view *view, enum criteria_type types);
|
list_t *criteria_for_view(struct sway_view *view, enum criteria_type types);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Match a criteria against a view without requiring view->container to be
|
||||||
|
* set. Used by append_layout's swallow matching, which runs before a view
|
||||||
|
* is attached to a container in the tree. Only view-intrinsic fields are
|
||||||
|
* considered: title, shell, app_id, sandbox_*, tag, pid, and (when xwayland
|
||||||
|
* is enabled) class, instance, window_role, window_type, X11 id. Criteria
|
||||||
|
* fields that depend on container state (con_mark, con_id, floating, tiling,
|
||||||
|
* workspace, urgent) are ignored.
|
||||||
|
*/
|
||||||
|
bool criteria_matches_view_unmapped(struct criteria *criteria,
|
||||||
|
struct sway_view *view);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Compile a list of containers matching the given criteria.
|
* Compile a list of containers matching the given criteria.
|
||||||
*/
|
*/
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@
|
||||||
|
|
||||||
struct sway_view;
|
struct sway_view;
|
||||||
struct sway_seat;
|
struct sway_seat;
|
||||||
|
struct json_object;
|
||||||
|
|
||||||
enum sway_container_layout {
|
enum sway_container_layout {
|
||||||
L_NONE,
|
L_NONE,
|
||||||
|
|
@ -141,11 +142,20 @@ struct sway_container {
|
||||||
|
|
||||||
list_t *marks; // char *
|
list_t *marks; // char *
|
||||||
|
|
||||||
|
// append_layout placeholder: view==NULL, swallows holds matchers for the
|
||||||
|
// view that should be installed here. swallows_json retains the original
|
||||||
|
// i3-shaped array so IPC can echo it verbatim for round-trip.
|
||||||
|
bool is_placeholder;
|
||||||
|
list_t *swallows; // struct criteria *
|
||||||
|
struct json_object *swallows_json;
|
||||||
|
|
||||||
struct {
|
struct {
|
||||||
struct wl_signal destroy;
|
struct wl_signal destroy;
|
||||||
} events;
|
} events;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
void container_init_border_rects(struct sway_container *c, bool *failed);
|
||||||
|
|
||||||
struct sway_container *container_create(struct sway_view *view);
|
struct sway_container *container_create(struct sway_view *view);
|
||||||
|
|
||||||
void container_destroy(struct sway_container *con);
|
void container_destroy(struct sway_container *con);
|
||||||
|
|
|
||||||
34
include/sway/tree/load_layout.h
Normal file
34
include/sway/tree/load_layout.h
Normal file
|
|
@ -0,0 +1,34 @@
|
||||||
|
#ifndef _SWAY_LOAD_LAYOUT_H
|
||||||
|
#define _SWAY_LOAD_LAYOUT_H
|
||||||
|
|
||||||
|
#include <stdbool.h>
|
||||||
|
|
||||||
|
struct sway_workspace;
|
||||||
|
struct sway_view;
|
||||||
|
struct sway_container;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Append the container tree described by the JSON file at `path` to the given
|
||||||
|
* workspace. The file may be either strict JSON (a single object or array) or
|
||||||
|
* the i3-save-tree concatenated-object form (}\n{ between siblings). On error
|
||||||
|
* leaves the workspace unmodified, returns false, and writes an allocated
|
||||||
|
* error string to *error_out. Caller frees the error string.
|
||||||
|
*
|
||||||
|
* Currently tiling-only. Top-level `floating_nodes` (and any nested
|
||||||
|
* floating_nodes) are skipped with a debug log.
|
||||||
|
*/
|
||||||
|
bool load_layout_from_file(struct sway_workspace *ws, const char *path,
|
||||||
|
char **error_out);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Walk the tree looking for a placeholder container (is_placeholder == true)
|
||||||
|
* whose swallows list contains a criteria that matches the given view. Walks
|
||||||
|
* tiling lists only, depth-first, document order. Returns the first match, or
|
||||||
|
* NULL if none.
|
||||||
|
*
|
||||||
|
* Used by view_map to decide whether an incoming view should be installed
|
||||||
|
* into a pre-existing placeholder slot rather than a freshly created one.
|
||||||
|
*/
|
||||||
|
struct sway_container *find_swallow_match(struct sway_view *view);
|
||||||
|
|
||||||
|
#endif
|
||||||
|
|
@ -127,6 +127,7 @@ if scdoc.found()
|
||||||
'sway/sway-output.5.scd',
|
'sway/sway-output.5.scd',
|
||||||
'swaybar/swaybar-protocol.7.scd',
|
'swaybar/swaybar-protocol.7.scd',
|
||||||
'swaymsg/swaymsg.1.scd',
|
'swaymsg/swaymsg.1.scd',
|
||||||
|
'swaysavetree/sway-save-tree.1.scd',
|
||||||
]
|
]
|
||||||
|
|
||||||
if get_option('swaynag')
|
if get_option('swaynag')
|
||||||
|
|
@ -195,6 +196,7 @@ subdir('protocols')
|
||||||
subdir('common')
|
subdir('common')
|
||||||
subdir('sway')
|
subdir('sway')
|
||||||
subdir('swaymsg')
|
subdir('swaymsg')
|
||||||
|
subdir('swaysavetree')
|
||||||
|
|
||||||
if get_option('swaybar') or get_option('swaynag')
|
if get_option('swaybar') or get_option('swaynag')
|
||||||
subdir('client')
|
subdir('client')
|
||||||
|
|
|
||||||
|
|
@ -42,6 +42,7 @@ struct cmd_results *checkarg(int argc, const char *name, enum expected_args type
|
||||||
|
|
||||||
/* Keep alphabetized */
|
/* Keep alphabetized */
|
||||||
static const struct cmd_handler handlers[] = {
|
static const struct cmd_handler handlers[] = {
|
||||||
|
{ "append_layout", cmd_append_layout },
|
||||||
{ "assign", cmd_assign },
|
{ "assign", cmd_assign },
|
||||||
{ "bar", cmd_bar },
|
{ "bar", cmd_bar },
|
||||||
{ "bindcode", cmd_bindcode },
|
{ "bindcode", cmd_bindcode },
|
||||||
|
|
|
||||||
47
sway/commands/append_layout.c
Normal file
47
sway/commands/append_layout.c
Normal file
|
|
@ -0,0 +1,47 @@
|
||||||
|
#include "log.h"
|
||||||
|
#include "stringop.h"
|
||||||
|
#include "sway/commands.h"
|
||||||
|
#include "sway/config.h"
|
||||||
|
#include "sway/input/input-manager.h"
|
||||||
|
#include "sway/input/seat.h"
|
||||||
|
#include "sway/tree/load_layout.h"
|
||||||
|
#include "sway/tree/workspace.h"
|
||||||
|
|
||||||
|
struct cmd_results *cmd_append_layout(int argc, char **argv) {
|
||||||
|
struct cmd_results *error = NULL;
|
||||||
|
if ((error = checkarg(argc, "append_layout", EXPECTED_AT_LEAST, 1))) {
|
||||||
|
return error;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (config->reading || !config->active) {
|
||||||
|
return cmd_results_new(CMD_DEFER, NULL);
|
||||||
|
}
|
||||||
|
|
||||||
|
struct sway_seat *seat = input_manager_current_seat();
|
||||||
|
struct sway_workspace *ws = seat_get_focused_workspace(seat);
|
||||||
|
if (!ws) {
|
||||||
|
return cmd_results_new(CMD_FAILURE,
|
||||||
|
"append_layout: no focused workspace");
|
||||||
|
}
|
||||||
|
|
||||||
|
char *path = join_args(argv, argc);
|
||||||
|
if (!path) {
|
||||||
|
return cmd_results_new(CMD_FAILURE, "append_layout: out of memory");
|
||||||
|
}
|
||||||
|
if (!expand_path(&path)) {
|
||||||
|
free(path);
|
||||||
|
return cmd_results_new(CMD_FAILURE,
|
||||||
|
"append_layout: failed to expand path");
|
||||||
|
}
|
||||||
|
|
||||||
|
char *err = NULL;
|
||||||
|
bool ok = load_layout_from_file(ws, path, &err);
|
||||||
|
free(path);
|
||||||
|
if (!ok) {
|
||||||
|
struct cmd_results *res = cmd_results_new(CMD_FAILURE, "%s",
|
||||||
|
err ? err : "append_layout: unknown error");
|
||||||
|
free(err);
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
return cmd_results_new(CMD_SUCCESS, NULL);
|
||||||
|
}
|
||||||
|
|
@ -190,16 +190,9 @@ static bool criteria_matches_container(struct criteria *criteria,
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
static bool criteria_matches_view(struct criteria *criteria,
|
// View-intrinsic checks; does not require view->container.
|
||||||
struct sway_view *view) {
|
static bool match_view_intrinsic(struct criteria *criteria,
|
||||||
struct sway_seat *seat = input_manager_current_seat();
|
struct sway_view *view, struct sway_view *focused) {
|
||||||
struct sway_container *focus = seat_get_focused_container(seat);
|
|
||||||
struct sway_view *focused = focus ? focus->view : NULL;
|
|
||||||
|
|
||||||
if (!view->container) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (criteria->title) {
|
if (criteria->title) {
|
||||||
const char *title = view_get_title(view);
|
const char *title = view_get_title(view);
|
||||||
if (!title) {
|
if (!title) {
|
||||||
|
|
@ -340,10 +333,6 @@ static bool criteria_matches_view(struct criteria *criteria,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!criteria_matches_container(criteria, view->container)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
#if WLR_HAS_XWAYLAND
|
#if WLR_HAS_XWAYLAND
|
||||||
if (criteria->id) { // X11 window ID
|
if (criteria->id) { // X11 window ID
|
||||||
uint32_t x11_window_id = view_get_x11_window_id(view);
|
uint32_t x11_window_id = view_get_x11_window_id(view);
|
||||||
|
|
@ -419,6 +408,42 @@ static bool criteria_matches_view(struct criteria *criteria,
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
if (criteria->pid) {
|
||||||
|
if (criteria->pid != view->pid) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool criteria_matches_view_unmapped(struct criteria *criteria,
|
||||||
|
struct sway_view *view) {
|
||||||
|
struct sway_seat *seat = input_manager_current_seat();
|
||||||
|
struct sway_container *focus = seat_get_focused_container(seat);
|
||||||
|
struct sway_view *focused = focus ? focus->view : NULL;
|
||||||
|
|
||||||
|
return match_view_intrinsic(criteria, view, focused);
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool criteria_matches_view(struct criteria *criteria,
|
||||||
|
struct sway_view *view) {
|
||||||
|
struct sway_seat *seat = input_manager_current_seat();
|
||||||
|
struct sway_container *focus = seat_get_focused_container(seat);
|
||||||
|
struct sway_view *focused = focus ? focus->view : NULL;
|
||||||
|
|
||||||
|
if (!view->container) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!match_view_intrinsic(criteria, view, focused)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!criteria_matches_container(criteria, view->container)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
if (criteria->floating) {
|
if (criteria->floating) {
|
||||||
if (!container_is_floating(view->container)) {
|
if (!container_is_floating(view->container)) {
|
||||||
return false;
|
return false;
|
||||||
|
|
@ -471,12 +496,6 @@ static bool criteria_matches_view(struct criteria *criteria,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (criteria->pid) {
|
|
||||||
if (criteria->pid != view->pid) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -776,6 +776,11 @@ static void ipc_json_describe_container(struct sway_container *c, json_object *o
|
||||||
if (c->view) {
|
if (c->view) {
|
||||||
ipc_json_describe_view(c, object);
|
ipc_json_describe_view(c, object);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (c->is_placeholder && c->swallows_json) {
|
||||||
|
json_object_object_add(object, "swallows",
|
||||||
|
json_object_get(c->swallows_json));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct focus_inactive_data {
|
struct focus_inactive_data {
|
||||||
|
|
|
||||||
|
|
@ -43,6 +43,7 @@ sway_sources = files(
|
||||||
'config/input.c',
|
'config/input.c',
|
||||||
|
|
||||||
'commands/allow_tearing.c',
|
'commands/allow_tearing.c',
|
||||||
|
'commands/append_layout.c',
|
||||||
'commands/assign.c',
|
'commands/assign.c',
|
||||||
'commands/bar.c',
|
'commands/bar.c',
|
||||||
'commands/bind.c',
|
'commands/bind.c',
|
||||||
|
|
@ -211,6 +212,7 @@ sway_sources = files(
|
||||||
|
|
||||||
'tree/arrange.c',
|
'tree/arrange.c',
|
||||||
'tree/container.c',
|
'tree/container.c',
|
||||||
|
'tree/load_layout.c',
|
||||||
'tree/node.c',
|
'tree/node.c',
|
||||||
'tree/root.c',
|
'tree/root.c',
|
||||||
'tree/view.c',
|
'tree/view.c',
|
||||||
|
|
|
||||||
|
|
@ -414,6 +414,52 @@ set|plus|minus|toggle <amount>
|
||||||
The following commands may be used either in the configuration file or at
|
The following commands may be used either in the configuration file or at
|
||||||
runtime.
|
runtime.
|
||||||
|
|
||||||
|
*append_layout* <path>
|
||||||
|
Appends the container tree described by the JSON file at _path_ to the
|
||||||
|
currently focused workspace. Useful for restoring a saved layout: each
|
||||||
|
leaf container in the JSON is a _placeholder_ with a *swallows* array of
|
||||||
|
matchers; when a window opens whose properties match an unmatched
|
||||||
|
placeholder, sway installs that window into the placeholder's slot
|
||||||
|
instead of the usual focus-based placement.
|
||||||
|
|
||||||
|
The format mirrors i3's, with one extension: in addition to i3's
|
||||||
|
*class*, *instance*, *title*, *window_role*, and *window_type* keys
|
||||||
|
(which target xwayland views), each *swallows* entry may include
|
||||||
|
*app_id* to match Wayland views. The *machine* key is logged and
|
||||||
|
ignored. *class*, *instance*, *title*, *window_role* and *app_id*
|
||||||
|
are PCRE2 regex strings. *window_type* must be a literal atom name
|
||||||
|
(*normal*, *dialog*, *utility*, *toolbar*, *splash*, *menu*,
|
||||||
|
*dropdown_menu*, *popup_menu*, *tooltip*, *notification*); the
|
||||||
|
i3-save-tree anchored form _^name$_ is accepted, but regex
|
||||||
|
alternation such as _^(normal|dialog)$_ is rejected, split such
|
||||||
|
patterns into multiple swallows entries (which match as OR).
|
||||||
|
|
||||||
|
A minimal example targeting one Wayland and one xwayland window in
|
||||||
|
a vertical split:
|
||||||
|
|
||||||
|
```
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"layout": "splitv",
|
||||||
|
"nodes": [
|
||||||
|
{ "swallows": [{ "app_id": "^foot$" }] },
|
||||||
|
{ "swallows": [{ "class": "^Firefox$" }] }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
The companion command-line tool *sway-save-tree*(1) generates such
|
||||||
|
files from a live workspace. The i3-save-tree(1) concatenated-object
|
||||||
|
output form is also accepted.
|
||||||
|
|
||||||
|
Floating placeholders (the *floating_nodes* JSON array) are not
|
||||||
|
supported in this release; entries are skipped with a debug-log entry.
|
||||||
|
Placeholder match runs before *assign* rules: an explicit slot wins
|
||||||
|
over workspace assignment. *for_window* rules still run on the
|
||||||
|
swallowed view after placement. If multiple placeholders match the
|
||||||
|
same incoming view, the depth-first, document-order first match wins.
|
||||||
|
|
||||||
*assign* <criteria> [→] [workspace] [number] <workspace>
|
*assign* <criteria> [→] [workspace] [number] <workspace>
|
||||||
Assigns windows matching _criteria_ (see *CRITERIA* for details) to
|
Assigns windows matching _criteria_ (see *CRITERIA* for details) to
|
||||||
_workspace_. The → (U+2192) is optional and cosmetic. This command is
|
_workspace_. The → (U+2192) is optional and cosmetic. This command is
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,9 @@
|
||||||
#include <wlr/types/wlr_linux_dmabuf_v1.h>
|
#include <wlr/types/wlr_linux_dmabuf_v1.h>
|
||||||
#include <wlr/types/wlr_output_layout.h>
|
#include <wlr/types/wlr_output_layout.h>
|
||||||
#include <wlr/types/wlr_subcompositor.h>
|
#include <wlr/types/wlr_subcompositor.h>
|
||||||
|
#include <json.h>
|
||||||
#include "sway/config.h"
|
#include "sway/config.h"
|
||||||
|
#include "sway/criteria.h"
|
||||||
#include "sway/desktop/transaction.h"
|
#include "sway/desktop/transaction.h"
|
||||||
#include "sway/input/input-manager.h"
|
#include "sway/input/input-manager.h"
|
||||||
#include "sway/input/seat.h"
|
#include "sway/input/seat.h"
|
||||||
|
|
@ -43,6 +45,13 @@ static struct wlr_scene_rect *alloc_rect_node(struct wlr_scene_tree *parent,
|
||||||
return rect;
|
return rect;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void container_init_border_rects(struct sway_container *c, bool *failed) {
|
||||||
|
c->border.top = alloc_rect_node(c->border.tree, failed);
|
||||||
|
c->border.bottom = alloc_rect_node(c->border.tree, failed);
|
||||||
|
c->border.left = alloc_rect_node(c->border.tree, failed);
|
||||||
|
c->border.right = alloc_rect_node(c->border.tree, failed);
|
||||||
|
}
|
||||||
|
|
||||||
struct sway_container *container_create(struct sway_view *view) {
|
struct sway_container *container_create(struct sway_view *view) {
|
||||||
struct sway_container *c = calloc(1, sizeof(struct sway_container));
|
struct sway_container *c = calloc(1, sizeof(struct sway_container));
|
||||||
if (!c) {
|
if (!c) {
|
||||||
|
|
@ -87,10 +96,7 @@ struct sway_container *container_create(struct sway_view *view) {
|
||||||
|
|
||||||
if (view) {
|
if (view) {
|
||||||
// only containers with views can have borders
|
// only containers with views can have borders
|
||||||
c->border.top = alloc_rect_node(c->border.tree, &failed);
|
container_init_border_rects(c, &failed);
|
||||||
c->border.bottom = alloc_rect_node(c->border.tree, &failed);
|
|
||||||
c->border.left = alloc_rect_node(c->border.tree, &failed);
|
|
||||||
c->border.right = alloc_rect_node(c->border.tree, &failed);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!failed && !scene_descriptor_assign(&c->scene_tree->node,
|
if (!failed && !scene_descriptor_assign(&c->scene_tree->node,
|
||||||
|
|
@ -460,6 +466,16 @@ void container_destroy(struct sway_container *con) {
|
||||||
|
|
||||||
list_free_items_and_destroy(con->marks);
|
list_free_items_and_destroy(con->marks);
|
||||||
|
|
||||||
|
if (con->swallows) {
|
||||||
|
for (int i = 0; i < con->swallows->length; ++i) {
|
||||||
|
criteria_destroy(con->swallows->items[i]);
|
||||||
|
}
|
||||||
|
list_free(con->swallows);
|
||||||
|
}
|
||||||
|
if (con->swallows_json) {
|
||||||
|
json_object_put(con->swallows_json);
|
||||||
|
}
|
||||||
|
|
||||||
if (con->view && con->view->container == con) {
|
if (con->view && con->view->container == con) {
|
||||||
con->view->container = NULL;
|
con->view->container = NULL;
|
||||||
if (con->view->destroying) {
|
if (con->view->destroying) {
|
||||||
|
|
@ -509,11 +525,18 @@ void container_reap_empty(struct sway_container *con) {
|
||||||
if (con->view) {
|
if (con->view) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
// Placeholders are intentionally view-less; do not reap them.
|
||||||
|
if (con->is_placeholder) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
struct sway_workspace *ws = con->pending.workspace;
|
struct sway_workspace *ws = con->pending.workspace;
|
||||||
while (con) {
|
while (con) {
|
||||||
if (con->pending.children->length) {
|
if (con->pending.children->length) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (con->is_placeholder) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
struct sway_container *parent = con->pending.parent;
|
struct sway_container *parent = con->pending.parent;
|
||||||
container_begin_destroy(con);
|
container_begin_destroy(con);
|
||||||
con = parent;
|
con = parent;
|
||||||
|
|
|
||||||
636
sway/tree/load_layout.c
Normal file
636
sway/tree/load_layout.c
Normal file
|
|
@ -0,0 +1,636 @@
|
||||||
|
#include <ctype.h>
|
||||||
|
#include <errno.h>
|
||||||
|
#include <json.h>
|
||||||
|
#include <stdbool.h>
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <string.h>
|
||||||
|
#include <strings.h>
|
||||||
|
#include "list.h"
|
||||||
|
#include "log.h"
|
||||||
|
#include "stringop.h"
|
||||||
|
#include "sway/criteria.h"
|
||||||
|
#include "sway/desktop/transaction.h"
|
||||||
|
#include "sway/output.h"
|
||||||
|
#include "sway/tree/arrange.h"
|
||||||
|
#include "sway/tree/container.h"
|
||||||
|
#include "sway/tree/load_layout.h"
|
||||||
|
#include "sway/tree/root.h"
|
||||||
|
#include "sway/tree/view.h"
|
||||||
|
#include "sway/tree/workspace.h"
|
||||||
|
|
||||||
|
static char *slurp_file(const char *path, char **error_out) {
|
||||||
|
FILE *f = fopen(path, "rb");
|
||||||
|
if (!f) {
|
||||||
|
*error_out = format_str("append_layout: cannot open %s: %s",
|
||||||
|
path, strerror(errno));
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
if (fseek(f, 0, SEEK_END) != 0) {
|
||||||
|
*error_out = format_str("append_layout: seek failed on %s", path);
|
||||||
|
fclose(f);
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
long size = ftell(f);
|
||||||
|
if (size < 0) {
|
||||||
|
*error_out = format_str("append_layout: ftell failed on %s", path);
|
||||||
|
fclose(f);
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
rewind(f);
|
||||||
|
char *buf = malloc(size + 1);
|
||||||
|
if (!buf) {
|
||||||
|
*error_out = format_str("append_layout: out of memory");
|
||||||
|
fclose(f);
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
size_t n = fread(buf, 1, size, f);
|
||||||
|
fclose(f);
|
||||||
|
if ((long)n != size) {
|
||||||
|
free(buf);
|
||||||
|
*error_out = format_str("append_layout: short read on %s", path);
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
buf[size] = '\0';
|
||||||
|
return buf;
|
||||||
|
}
|
||||||
|
|
||||||
|
// i3-save-tree prepends a vim modeline; the leading `/` would otherwise
|
||||||
|
// confuse preprocess_i3_concat's array-vs-object check.
|
||||||
|
static char *strip_header_comments(char *buf) {
|
||||||
|
char *p = buf;
|
||||||
|
for (;;) {
|
||||||
|
while (*p && isspace((unsigned char)*p)) {
|
||||||
|
p++;
|
||||||
|
}
|
||||||
|
if (p[0] == '/' && p[1] == '/') {
|
||||||
|
while (*p && *p != '\n') {
|
||||||
|
p++;
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (p[0] == '/' && p[1] == '*') {
|
||||||
|
p += 2;
|
||||||
|
while (*p && !(p[0] == '*' && p[1] == '/')) {
|
||||||
|
p++;
|
||||||
|
}
|
||||||
|
if (*p) {
|
||||||
|
p += 2;
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (p != buf) {
|
||||||
|
memmove(buf, p, strlen(p) + 1);
|
||||||
|
}
|
||||||
|
return buf;
|
||||||
|
}
|
||||||
|
|
||||||
|
// i3-save-tree emits a sequence of top-level objects separated by `}\n{`
|
||||||
|
// rather than wrapping them in an array. Wrap into a strict JSON array.
|
||||||
|
// Same string-literal caveat as i3's own loader.
|
||||||
|
static char *preprocess_i3_concat(char *buf) {
|
||||||
|
const char *p = buf;
|
||||||
|
while (*p && isspace((unsigned char)*p)) {
|
||||||
|
p++;
|
||||||
|
}
|
||||||
|
if (*p == '[' || *p != '{') {
|
||||||
|
return buf;
|
||||||
|
}
|
||||||
|
|
||||||
|
size_t in_len = strlen(buf);
|
||||||
|
size_t cap = in_len + 16;
|
||||||
|
char *out = malloc(cap + 1);
|
||||||
|
if (!out) {
|
||||||
|
return buf;
|
||||||
|
}
|
||||||
|
size_t pos = 0;
|
||||||
|
out[pos++] = '[';
|
||||||
|
|
||||||
|
for (size_t i = 0; i < in_len; i++) {
|
||||||
|
out[pos++] = buf[i];
|
||||||
|
if (pos + 4 >= cap) {
|
||||||
|
cap = cap * 2 + 16;
|
||||||
|
char *grown = realloc(out, cap + 1);
|
||||||
|
if (!grown) {
|
||||||
|
free(out);
|
||||||
|
return buf;
|
||||||
|
}
|
||||||
|
out = grown;
|
||||||
|
}
|
||||||
|
if (buf[i] == '}') {
|
||||||
|
size_t j = i + 1;
|
||||||
|
while (j < in_len && isspace((unsigned char)buf[j])) {
|
||||||
|
j++;
|
||||||
|
}
|
||||||
|
if (j < in_len && buf[j] == '{') {
|
||||||
|
out[pos++] = ',';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
out[pos++] = ']';
|
||||||
|
out[pos] = '\0';
|
||||||
|
free(buf);
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
static enum sway_container_layout parse_layout_name(const char *s) {
|
||||||
|
if (!s) {
|
||||||
|
return L_NONE;
|
||||||
|
}
|
||||||
|
if (strcasecmp(s, "splith") == 0) {
|
||||||
|
return L_HORIZ;
|
||||||
|
}
|
||||||
|
if (strcasecmp(s, "splitv") == 0) {
|
||||||
|
return L_VERT;
|
||||||
|
}
|
||||||
|
if (strcasecmp(s, "tabbed") == 0) {
|
||||||
|
return L_TABBED;
|
||||||
|
}
|
||||||
|
// i3-save-tree emits "stacked", sway's `layout` command spells it
|
||||||
|
// "stacking"; accept both.
|
||||||
|
if (strcasecmp(s, "stacked") == 0 || strcasecmp(s, "stacking") == 0) {
|
||||||
|
return L_STACKED;
|
||||||
|
}
|
||||||
|
return L_NONE;
|
||||||
|
}
|
||||||
|
|
||||||
|
static enum sway_container_border parse_border_name(const char *s) {
|
||||||
|
if (!s) {
|
||||||
|
return B_NORMAL;
|
||||||
|
}
|
||||||
|
if (strcasecmp(s, "none") == 0) {
|
||||||
|
return B_NONE;
|
||||||
|
}
|
||||||
|
if (strcasecmp(s, "pixel") == 0) {
|
||||||
|
return B_PIXEL;
|
||||||
|
}
|
||||||
|
if (strcasecmp(s, "csd") == 0) {
|
||||||
|
return B_CSD;
|
||||||
|
}
|
||||||
|
return B_NORMAL;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Escape `"` so the value survives the criteria tokenizer. Backslashes are
|
||||||
|
// left alone since PCRE consumes them and criteria's unescape only collapses
|
||||||
|
// `\"`.
|
||||||
|
static char *escape_criteria_value(const char *value) {
|
||||||
|
size_t n = strlen(value);
|
||||||
|
char *out = malloc(n * 2 + 1);
|
||||||
|
if (!out) {
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
size_t pos = 0;
|
||||||
|
for (size_t i = 0; i < n; i++) {
|
||||||
|
if (value[i] == '"') {
|
||||||
|
out[pos++] = '\\';
|
||||||
|
}
|
||||||
|
out[pos++] = value[i];
|
||||||
|
}
|
||||||
|
out[pos] = '\0';
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool append_key_value(char **buf, const char *key, const char *value) {
|
||||||
|
char *escaped = escape_criteria_value(value);
|
||||||
|
if (!escaped) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
size_t old = *buf ? strlen(*buf) : 0;
|
||||||
|
size_t add = strlen(key) + strlen(escaped) + 5; // ` k="v"`
|
||||||
|
char *grown = realloc(*buf, old + add + 1);
|
||||||
|
if (!grown) {
|
||||||
|
free(escaped);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
*buf = grown;
|
||||||
|
int written = snprintf(grown + old, add + 1, "%s%s=\"%s\"",
|
||||||
|
old ? " " : "", key, escaped);
|
||||||
|
free(escaped);
|
||||||
|
if (written < 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool append_bare(char **buf, const char *key, const char *value) {
|
||||||
|
size_t old = *buf ? strlen(*buf) : 0;
|
||||||
|
size_t add = strlen(key) + strlen(value) + 3; // ` k=v`
|
||||||
|
char *grown = realloc(*buf, old + add + 1);
|
||||||
|
if (!grown) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
*buf = grown;
|
||||||
|
int written = snprintf(grown + old, add + 1, "%s%s=%s",
|
||||||
|
old ? " " : "", key, value);
|
||||||
|
return written >= 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// window_type is parsed as an enum, not a regex. Accept i3-save-tree's
|
||||||
|
// "^name$" form and bare names; reject anything else (including alternation)
|
||||||
|
// rather than letting the criteria parser silently fall through to ATOM_LAST.
|
||||||
|
static const char *known_window_types[] = {
|
||||||
|
"normal", "dialog", "utility", "toolbar", "splash", "menu",
|
||||||
|
"dropdown_menu", "popup_menu", "tooltip", "notification", NULL,
|
||||||
|
};
|
||||||
|
|
||||||
|
static char *parse_window_type_value(const char *value, char **error_out) {
|
||||||
|
size_t n = strlen(value);
|
||||||
|
size_t start = (n > 0 && value[0] == '^') ? 1 : 0;
|
||||||
|
size_t end = (n > start && value[n - 1] == '$') ? n - 1 : n;
|
||||||
|
size_t bare_len = end - start;
|
||||||
|
char *bare = malloc(bare_len + 1);
|
||||||
|
if (!bare) {
|
||||||
|
*error_out = format_str("append_layout: out of memory");
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
memcpy(bare, value + start, bare_len);
|
||||||
|
bare[bare_len] = '\0';
|
||||||
|
for (int i = 0; known_window_types[i]; i++) {
|
||||||
|
if (strcasecmp(bare, known_window_types[i]) == 0) {
|
||||||
|
return bare;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
*error_out = format_str("append_layout: window_type %s is not a "
|
||||||
|
"supported literal value (use one of: normal, dialog, "
|
||||||
|
"utility, toolbar, splash, menu, dropdown_menu, popup_menu, "
|
||||||
|
"tooltip, notification; regex alternation is not supported, "
|
||||||
|
"split into multiple swallow entries instead)", value);
|
||||||
|
free(bare);
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
// app_id is a sway extension over i3's swallows schema; machine is ignored.
|
||||||
|
static struct criteria *build_swallow_criteria(struct json_object *entry,
|
||||||
|
char **error_out) {
|
||||||
|
if (!json_object_is_type(entry, json_type_object)) {
|
||||||
|
*error_out = format_str("append_layout: swallows entry is not an object");
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
static const char *regex_keys[] = {
|
||||||
|
"class", "instance", "title", "window_role", "app_id", NULL,
|
||||||
|
};
|
||||||
|
char *body = NULL;
|
||||||
|
for (int i = 0; regex_keys[i]; i++) {
|
||||||
|
struct json_object *v;
|
||||||
|
if (!json_object_object_get_ex(entry, regex_keys[i], &v)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (!json_object_is_type(v, json_type_string)) {
|
||||||
|
free(body);
|
||||||
|
*error_out = format_str("append_layout: swallows.%s is not a string",
|
||||||
|
regex_keys[i]);
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
if (!append_key_value(&body, regex_keys[i],
|
||||||
|
json_object_get_string(v))) {
|
||||||
|
free(body);
|
||||||
|
*error_out = format_str("append_layout: out of memory");
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
struct json_object *wt;
|
||||||
|
if (json_object_object_get_ex(entry, "window_type", &wt)) {
|
||||||
|
if (!json_object_is_type(wt, json_type_string)) {
|
||||||
|
free(body);
|
||||||
|
*error_out = format_str(
|
||||||
|
"append_layout: swallows.window_type is not a string");
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
char *bare = parse_window_type_value(json_object_get_string(wt),
|
||||||
|
error_out);
|
||||||
|
if (!bare) {
|
||||||
|
free(body);
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
if (!append_bare(&body, "window_type", bare)) {
|
||||||
|
free(bare);
|
||||||
|
free(body);
|
||||||
|
*error_out = format_str("append_layout: out of memory");
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
free(bare);
|
||||||
|
}
|
||||||
|
struct json_object *machine;
|
||||||
|
if (json_object_object_get_ex(entry, "machine", &machine)) {
|
||||||
|
sway_log(SWAY_DEBUG,
|
||||||
|
"append_layout: ignoring 'machine' key in swallows entry");
|
||||||
|
}
|
||||||
|
if (!body) {
|
||||||
|
free(body);
|
||||||
|
*error_out = format_str("append_layout: empty swallows entry");
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
size_t body_len = strlen(body);
|
||||||
|
char *raw = malloc(body_len + 3);
|
||||||
|
if (!raw) {
|
||||||
|
free(body);
|
||||||
|
*error_out = format_str("append_layout: out of memory");
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
raw[0] = '[';
|
||||||
|
memcpy(raw + 1, body, body_len);
|
||||||
|
raw[body_len + 1] = ']';
|
||||||
|
raw[body_len + 2] = '\0';
|
||||||
|
free(body);
|
||||||
|
|
||||||
|
char *parse_err = NULL;
|
||||||
|
struct criteria *c = criteria_parse(raw, &parse_err);
|
||||||
|
free(raw);
|
||||||
|
if (!c) {
|
||||||
|
*error_out = format_str("append_layout: invalid swallows pattern: %s",
|
||||||
|
parse_err ? parse_err : "(no detail)");
|
||||||
|
free(parse_err);
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
return c;
|
||||||
|
}
|
||||||
|
|
||||||
|
static list_t *parse_swallows(struct json_object *arr, char **error_out) {
|
||||||
|
if (!json_object_is_type(arr, json_type_array)) {
|
||||||
|
*error_out = format_str("append_layout: swallows is not an array");
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
list_t *out = create_list();
|
||||||
|
size_t n = json_object_array_length(arr);
|
||||||
|
for (size_t i = 0; i < n; i++) {
|
||||||
|
struct json_object *entry = json_object_array_get_idx(arr, i);
|
||||||
|
struct criteria *c = build_swallow_criteria(entry, error_out);
|
||||||
|
if (!c) {
|
||||||
|
for (int j = 0; j < out->length; j++) {
|
||||||
|
criteria_destroy(out->items[j]);
|
||||||
|
}
|
||||||
|
list_free(out);
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
list_add(out, c);
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tear down a parsed subtree that we never attached to a workspace.
|
||||||
|
static void free_transient_subtree(struct sway_container *con) {
|
||||||
|
if (!con) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (con->pending.children) {
|
||||||
|
while (con->pending.children->length) {
|
||||||
|
struct sway_container *child =
|
||||||
|
con->pending.children->items[0];
|
||||||
|
free_transient_subtree(child);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
container_begin_destroy(con);
|
||||||
|
}
|
||||||
|
|
||||||
|
static struct sway_container *build_node(struct json_object *obj,
|
||||||
|
char **error_out);
|
||||||
|
|
||||||
|
static struct sway_container *build_node(struct json_object *obj,
|
||||||
|
char **error_out) {
|
||||||
|
if (!json_object_is_type(obj, json_type_object)) {
|
||||||
|
*error_out = format_str("append_layout: node is not an object");
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
struct sway_container *c = container_create(NULL);
|
||||||
|
if (!c) {
|
||||||
|
*error_out = format_str("append_layout: container_create failed");
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
struct json_object *layout_v;
|
||||||
|
if (json_object_object_get_ex(obj, "layout", &layout_v) &&
|
||||||
|
json_object_is_type(layout_v, json_type_string)) {
|
||||||
|
c->pending.layout = parse_layout_name(json_object_get_string(layout_v));
|
||||||
|
}
|
||||||
|
|
||||||
|
struct json_object *border_v;
|
||||||
|
if (json_object_object_get_ex(obj, "border", &border_v) &&
|
||||||
|
json_object_is_type(border_v, json_type_string)) {
|
||||||
|
c->pending.border = parse_border_name(json_object_get_string(border_v));
|
||||||
|
} else {
|
||||||
|
c->pending.border = B_NORMAL;
|
||||||
|
}
|
||||||
|
|
||||||
|
struct json_object *bw_v;
|
||||||
|
if (json_object_object_get_ex(obj, "current_border_width", &bw_v) &&
|
||||||
|
json_object_is_type(bw_v, json_type_int)) {
|
||||||
|
c->pending.border_thickness = json_object_get_int(bw_v);
|
||||||
|
}
|
||||||
|
|
||||||
|
struct json_object *percent_v;
|
||||||
|
if (json_object_object_get_ex(obj, "percent", &percent_v)) {
|
||||||
|
// Normalised by the next arrange pass.
|
||||||
|
double p = json_object_get_double(percent_v);
|
||||||
|
c->width_fraction = p;
|
||||||
|
c->height_fraction = p;
|
||||||
|
}
|
||||||
|
|
||||||
|
struct json_object *name_v;
|
||||||
|
if (json_object_object_get_ex(obj, "name", &name_v) &&
|
||||||
|
json_object_is_type(name_v, json_type_string)) {
|
||||||
|
const char *s = json_object_get_string(name_v);
|
||||||
|
free(c->title);
|
||||||
|
c->title = strdup(s ? s : "");
|
||||||
|
}
|
||||||
|
|
||||||
|
struct json_object *marks_v;
|
||||||
|
if (json_object_object_get_ex(obj, "marks", &marks_v) &&
|
||||||
|
json_object_is_type(marks_v, json_type_array)) {
|
||||||
|
size_t n = json_object_array_length(marks_v);
|
||||||
|
for (size_t i = 0; i < n; i++) {
|
||||||
|
struct json_object *m = json_object_array_get_idx(marks_v, i);
|
||||||
|
if (json_object_is_type(m, json_type_string)) {
|
||||||
|
container_add_mark(c, (char *)json_object_get_string(m));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct json_object *floating_v;
|
||||||
|
if (json_object_object_get_ex(obj, "floating_nodes", &floating_v) &&
|
||||||
|
json_object_is_type(floating_v, json_type_array) &&
|
||||||
|
json_object_array_length(floating_v) > 0) {
|
||||||
|
sway_log(SWAY_DEBUG, "append_layout: skipping floating_nodes "
|
||||||
|
"(tiling-only support in this release)");
|
||||||
|
}
|
||||||
|
|
||||||
|
struct json_object *nodes_v;
|
||||||
|
bool has_children = json_object_object_get_ex(obj, "nodes", &nodes_v) &&
|
||||||
|
json_object_is_type(nodes_v, json_type_array) &&
|
||||||
|
json_object_array_length(nodes_v) > 0;
|
||||||
|
|
||||||
|
struct json_object *swallows_v;
|
||||||
|
bool has_swallows = json_object_object_get_ex(obj, "swallows", &swallows_v);
|
||||||
|
|
||||||
|
if (has_children) {
|
||||||
|
// i3 ignores swallows on non-leaves; mirror that.
|
||||||
|
size_t n = json_object_array_length(nodes_v);
|
||||||
|
for (size_t i = 0; i < n; i++) {
|
||||||
|
struct json_object *child_obj =
|
||||||
|
json_object_array_get_idx(nodes_v, i);
|
||||||
|
struct sway_container *child = build_node(child_obj, error_out);
|
||||||
|
if (!child) {
|
||||||
|
free_transient_subtree(c);
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
container_add_child(c, child);
|
||||||
|
}
|
||||||
|
} else if (has_swallows) {
|
||||||
|
list_t *sw = parse_swallows(swallows_v, error_out);
|
||||||
|
if (!sw) {
|
||||||
|
free_transient_subtree(c);
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
c->is_placeholder = true;
|
||||||
|
c->swallows = sw;
|
||||||
|
// Retained so IPC can echo it verbatim for round-trip.
|
||||||
|
c->swallows_json = json_object_get(swallows_v);
|
||||||
|
// arrange_children asserts on L_NONE; placeholders end up there
|
||||||
|
// via the view-less branch even with no children.
|
||||||
|
if (c->pending.layout == L_NONE) {
|
||||||
|
c->pending.layout = L_HORIZ;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
*error_out = format_str("append_layout: node has neither nodes nor "
|
||||||
|
"swallows");
|
||||||
|
free_transient_subtree(c);
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
return c;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool load_layout_from_file(struct sway_workspace *ws, const char *path,
|
||||||
|
char **error_out) {
|
||||||
|
if (!ws) {
|
||||||
|
*error_out = format_str("append_layout: no target workspace");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
char *buf = slurp_file(path, error_out);
|
||||||
|
if (!buf) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
buf = strip_header_comments(buf);
|
||||||
|
buf = preprocess_i3_concat(buf);
|
||||||
|
|
||||||
|
struct json_tokener *tok = json_tokener_new();
|
||||||
|
if (!tok) {
|
||||||
|
free(buf);
|
||||||
|
*error_out = format_str("append_layout: json_tokener_new failed");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
struct json_object *root_obj = json_tokener_parse_ex(tok, buf, strlen(buf));
|
||||||
|
enum json_tokener_error err = json_tokener_get_error(tok);
|
||||||
|
json_tokener_free(tok);
|
||||||
|
free(buf);
|
||||||
|
if (!root_obj || err != json_tokener_success) {
|
||||||
|
*error_out = format_str("append_layout: json parse error: %s",
|
||||||
|
json_tokener_error_desc(err));
|
||||||
|
if (root_obj) {
|
||||||
|
json_object_put(root_obj);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// All-or-nothing: build the whole tree before attaching anything.
|
||||||
|
list_t *children = create_list();
|
||||||
|
bool ok = true;
|
||||||
|
if (json_object_is_type(root_obj, json_type_array)) {
|
||||||
|
size_t n = json_object_array_length(root_obj);
|
||||||
|
for (size_t i = 0; i < n; i++) {
|
||||||
|
struct json_object *entry =
|
||||||
|
json_object_array_get_idx(root_obj, i);
|
||||||
|
struct sway_container *child = build_node(entry, error_out);
|
||||||
|
if (!child) {
|
||||||
|
ok = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
list_add(children, child);
|
||||||
|
}
|
||||||
|
} else if (json_object_is_type(root_obj, json_type_object)) {
|
||||||
|
struct sway_container *child = build_node(root_obj, error_out);
|
||||||
|
if (!child) {
|
||||||
|
ok = false;
|
||||||
|
} else {
|
||||||
|
list_add(children, child);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
*error_out = format_str("append_layout: unexpected JSON root type");
|
||||||
|
ok = false;
|
||||||
|
}
|
||||||
|
json_object_put(root_obj);
|
||||||
|
|
||||||
|
if (!ok) {
|
||||||
|
for (int i = 0; i < children->length; i++) {
|
||||||
|
free_transient_subtree(children->items[i]);
|
||||||
|
}
|
||||||
|
list_free(children);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// workspace_add_tiling would wrap each child with container_split when
|
||||||
|
// default_layout is set, which mutates the parsed tree.
|
||||||
|
for (int i = 0; i < children->length; i++) {
|
||||||
|
workspace_insert_tiling_direct(ws, children->items[i],
|
||||||
|
ws->tiling->length);
|
||||||
|
}
|
||||||
|
list_free(children);
|
||||||
|
|
||||||
|
arrange_workspace(ws);
|
||||||
|
transaction_commit_dirty();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool placeholder_matches_view(struct sway_container *placeholder,
|
||||||
|
struct sway_view *view) {
|
||||||
|
if (!placeholder->is_placeholder || !placeholder->swallows) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
for (int i = 0; i < placeholder->swallows->length; i++) {
|
||||||
|
struct criteria *c = placeholder->swallows->items[i];
|
||||||
|
if (criteria_matches_view_unmapped(c, view)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
static struct sway_container *search_swallow(struct sway_container *con,
|
||||||
|
struct sway_view *view) {
|
||||||
|
if (placeholder_matches_view(con, view)) {
|
||||||
|
return con;
|
||||||
|
}
|
||||||
|
if (con->pending.children) {
|
||||||
|
for (int i = 0; i < con->pending.children->length; i++) {
|
||||||
|
struct sway_container *child = con->pending.children->items[i];
|
||||||
|
struct sway_container *match = search_swallow(child, view);
|
||||||
|
if (match) {
|
||||||
|
return match;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
struct sway_container *find_swallow_match(struct sway_view *view) {
|
||||||
|
if (!view) {
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
for (int o = 0; o < root->outputs->length; o++) {
|
||||||
|
struct sway_output *output = root->outputs->items[o];
|
||||||
|
for (int w = 0; w < output->workspaces->length; w++) {
|
||||||
|
struct sway_workspace *ws = output->workspaces->items[w];
|
||||||
|
for (int t = 0; t < ws->tiling->length; t++) {
|
||||||
|
struct sway_container *con = ws->tiling->items[t];
|
||||||
|
struct sway_container *match = search_swallow(con, view);
|
||||||
|
if (match) {
|
||||||
|
return match;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
122
sway/tree/view.c
122
sway/tree/view.c
|
|
@ -16,6 +16,7 @@
|
||||||
#if WLR_HAS_XWAYLAND
|
#if WLR_HAS_XWAYLAND
|
||||||
#include <wlr/xwayland.h>
|
#include <wlr/xwayland.h>
|
||||||
#endif
|
#endif
|
||||||
|
#include <json.h>
|
||||||
#include "list.h"
|
#include "list.h"
|
||||||
#include "log.h"
|
#include "log.h"
|
||||||
#include "sway/criteria.h"
|
#include "sway/criteria.h"
|
||||||
|
|
@ -32,6 +33,7 @@
|
||||||
#include "sway/sway_text_node.h"
|
#include "sway/sway_text_node.h"
|
||||||
#include "sway/tree/arrange.h"
|
#include "sway/tree/arrange.h"
|
||||||
#include "sway/tree/container.h"
|
#include "sway/tree/container.h"
|
||||||
|
#include "sway/tree/load_layout.h"
|
||||||
#include "sway/tree/view.h"
|
#include "sway/tree/view.h"
|
||||||
#include "sway/tree/workspace.h"
|
#include "sway/tree/workspace.h"
|
||||||
#include "sway/config.h"
|
#include "sway/config.h"
|
||||||
|
|
@ -809,6 +811,45 @@ static void handle_foreign_destroy(
|
||||||
wl_list_remove(&view->foreign_destroy.link);
|
wl_list_remove(&view->foreign_destroy.link);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Promote a placeholder into a view-backed container in place, fixing up
|
||||||
|
// the bits container_create normally branches on `view` for: border rects
|
||||||
|
// (only allocated for view-backed) and children lists (only allocated for
|
||||||
|
// view-less). The transaction reparents view->scene_tree into content_tree.
|
||||||
|
static void promote_placeholder(struct sway_container *placeholder,
|
||||||
|
struct sway_view *view) {
|
||||||
|
if (placeholder->swallows) {
|
||||||
|
for (int i = 0; i < placeholder->swallows->length; i++) {
|
||||||
|
criteria_destroy(placeholder->swallows->items[i]);
|
||||||
|
}
|
||||||
|
list_free(placeholder->swallows);
|
||||||
|
placeholder->swallows = NULL;
|
||||||
|
}
|
||||||
|
if (placeholder->swallows_json) {
|
||||||
|
json_object_put(placeholder->swallows_json);
|
||||||
|
placeholder->swallows_json = NULL;
|
||||||
|
}
|
||||||
|
placeholder->is_placeholder = false;
|
||||||
|
|
||||||
|
if (placeholder->pending.children) {
|
||||||
|
list_free(placeholder->pending.children);
|
||||||
|
placeholder->pending.children = NULL;
|
||||||
|
}
|
||||||
|
if (placeholder->current.children) {
|
||||||
|
list_free(placeholder->current.children);
|
||||||
|
placeholder->current.children = NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool failed = false;
|
||||||
|
container_init_border_rects(placeholder, &failed);
|
||||||
|
if (failed) {
|
||||||
|
sway_log(SWAY_ERROR, "promote_placeholder: border rect alloc failed");
|
||||||
|
}
|
||||||
|
|
||||||
|
placeholder->view = view;
|
||||||
|
view->container = placeholder;
|
||||||
|
node_set_dirty(&placeholder->node);
|
||||||
|
}
|
||||||
|
|
||||||
void view_map(struct sway_view *view, struct wlr_surface *wlr_surface,
|
void view_map(struct sway_view *view, struct wlr_surface *wlr_surface,
|
||||||
bool fullscreen, struct wlr_output *fullscreen_output,
|
bool fullscreen, struct wlr_output *fullscreen_output,
|
||||||
bool decoration) {
|
bool decoration) {
|
||||||
|
|
@ -817,7 +858,15 @@ void view_map(struct sway_view *view, struct wlr_surface *wlr_surface,
|
||||||
}
|
}
|
||||||
view->surface = wlr_surface;
|
view->surface = wlr_surface;
|
||||||
view_populate_pid(view);
|
view_populate_pid(view);
|
||||||
view->container = container_create(view);
|
|
||||||
|
// On a swallow match, the placeholder is already in the tree; skip the
|
||||||
|
// workspace/target-sibling placement path below.
|
||||||
|
struct sway_container *placeholder = find_swallow_match(view);
|
||||||
|
if (placeholder) {
|
||||||
|
promote_placeholder(placeholder, view);
|
||||||
|
} else {
|
||||||
|
view->container = container_create(view);
|
||||||
|
}
|
||||||
|
|
||||||
if (view->ctx == NULL) {
|
if (view->ctx == NULL) {
|
||||||
struct launcher_ctx *ctx = launcher_ctx_find_pid(view->pid);
|
struct launcher_ctx *ctx = launcher_ctx_find_pid(view->pid);
|
||||||
|
|
@ -828,14 +877,19 @@ void view_map(struct sway_view *view, struct wlr_surface *wlr_surface,
|
||||||
|
|
||||||
// If there is a request to be opened fullscreen on a specific output, try
|
// If there is a request to be opened fullscreen on a specific output, try
|
||||||
// to honor that request. Otherwise, fallback to assigns, pid mappings,
|
// to honor that request. Otherwise, fallback to assigns, pid mappings,
|
||||||
// focused workspace, etc
|
// focused workspace, etc. A swallowed view stays on the placeholder's
|
||||||
|
// workspace.
|
||||||
struct sway_workspace *ws = NULL;
|
struct sway_workspace *ws = NULL;
|
||||||
if (fullscreen_output && fullscreen_output->data) {
|
if (placeholder) {
|
||||||
struct sway_output *output = fullscreen_output->data;
|
ws = view->container->pending.workspace;
|
||||||
ws = output_get_active_workspace(output);
|
} else {
|
||||||
}
|
if (fullscreen_output && fullscreen_output->data) {
|
||||||
if (!ws) {
|
struct sway_output *output = fullscreen_output->data;
|
||||||
ws = select_workspace(view);
|
ws = output_get_active_workspace(output);
|
||||||
|
}
|
||||||
|
if (!ws) {
|
||||||
|
ws = select_workspace(view);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (ws && ws->output) {
|
if (ws && ws->output) {
|
||||||
|
|
@ -846,28 +900,30 @@ void view_map(struct sway_view *view, struct wlr_surface *wlr_surface,
|
||||||
wlr_surface_set_preferred_buffer_scale(wlr_surface, ceil(scale));
|
wlr_surface_set_preferred_buffer_scale(wlr_surface, ceil(scale));
|
||||||
}
|
}
|
||||||
|
|
||||||
struct sway_seat *seat = input_manager_current_seat();
|
|
||||||
struct sway_node *node =
|
|
||||||
seat_get_focus_inactive(seat, ws ? &ws->node : &root->node);
|
|
||||||
struct sway_container *target_sibling = NULL;
|
struct sway_container *target_sibling = NULL;
|
||||||
if (node && node->type == N_CONTAINER) {
|
if (!placeholder) {
|
||||||
if (container_is_floating(node->sway_container)) {
|
struct sway_seat *seat = input_manager_current_seat();
|
||||||
// If we're about to launch the view into the floating container, then
|
struct sway_node *node =
|
||||||
// launch it as a tiled view instead.
|
seat_get_focus_inactive(seat, ws ? &ws->node : &root->node);
|
||||||
if (ws) {
|
if (node && node->type == N_CONTAINER) {
|
||||||
target_sibling = seat_get_focus_inactive_tiling(seat, ws);
|
if (container_is_floating(node->sway_container)) {
|
||||||
if (target_sibling) {
|
// If we're about to launch the view into the floating container, then
|
||||||
struct sway_container *con =
|
// launch it as a tiled view instead.
|
||||||
seat_get_focus_inactive_view(seat, &target_sibling->node);
|
if (ws) {
|
||||||
if (con) {
|
target_sibling = seat_get_focus_inactive_tiling(seat, ws);
|
||||||
target_sibling = con;
|
if (target_sibling) {
|
||||||
|
struct sway_container *con =
|
||||||
|
seat_get_focus_inactive_view(seat, &target_sibling->node);
|
||||||
|
if (con) {
|
||||||
|
target_sibling = con;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
ws = seat_get_last_known_workspace(seat);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
ws = seat_get_last_known_workspace(seat);
|
target_sibling = node->sway_container;
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
target_sibling = node->sway_container;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -895,10 +951,12 @@ void view_map(struct sway_view *view, struct wlr_surface *wlr_surface,
|
||||||
&view->foreign_destroy);
|
&view->foreign_destroy);
|
||||||
|
|
||||||
struct sway_container *container = view->container;
|
struct sway_container *container = view->container;
|
||||||
if (target_sibling) {
|
if (!placeholder) {
|
||||||
container_add_sibling(target_sibling, container, 1);
|
if (target_sibling) {
|
||||||
} else if (ws) {
|
container_add_sibling(target_sibling, container, 1);
|
||||||
container = workspace_add_tiling(ws, container);
|
} else if (ws) {
|
||||||
|
container = workspace_add_tiling(ws, container);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
ipc_event_window(view->container, "new");
|
ipc_event_window(view->container, "new");
|
||||||
|
|
||||||
|
|
@ -906,7 +964,11 @@ void view_map(struct sway_view *view, struct wlr_surface *wlr_surface,
|
||||||
view_update_csd_from_client(view, decoration);
|
view_update_csd_from_client(view, decoration);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (view->impl->wants_floating && view->impl->wants_floating(view)) {
|
if (placeholder) {
|
||||||
|
// Keep the JSON-loaded border; ignore wants_floating since the user
|
||||||
|
// explicitly placed a tiling slot.
|
||||||
|
view_set_tiled(view, true);
|
||||||
|
} else if (view->impl->wants_floating && view->impl->wants_floating(view)) {
|
||||||
view->container->pending.border = config->floating_border;
|
view->container->pending.border = config->floating_border;
|
||||||
view->container->pending.border_thickness = config->floating_border_thickness;
|
view->container->pending.border_thickness = config->floating_border_thickness;
|
||||||
container_set_floating(view->container, true);
|
container_set_floating(view->container, true);
|
||||||
|
|
|
||||||
308
swaysavetree/main.c
Normal file
308
swaysavetree/main.c
Normal file
|
|
@ -0,0 +1,308 @@
|
||||||
|
// sway-save-tree: dump a workspace's tiling tree as JSON for `swaymsg
|
||||||
|
// append_layout`. Counterpart to i3-save-tree(1).
|
||||||
|
|
||||||
|
#include <getopt.h>
|
||||||
|
#include <limits.h>
|
||||||
|
#include <stdint.h>
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <string.h>
|
||||||
|
#include <strings.h>
|
||||||
|
#include <sys/time.h>
|
||||||
|
#include <json.h>
|
||||||
|
#include "ipc-client.h"
|
||||||
|
#include "ipc.h"
|
||||||
|
#include "log.h"
|
||||||
|
|
||||||
|
static void usage(void) {
|
||||||
|
fprintf(stderr,
|
||||||
|
"Usage: sway-save-tree --workspace <name|number>\n"
|
||||||
|
"\n"
|
||||||
|
"Dump the tiling tree of the named workspace as JSON suitable for\n"
|
||||||
|
"`swaymsg append_layout`. Output is sent to stdout.\n"
|
||||||
|
"\n"
|
||||||
|
"Floating windows are skipped in this release.\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Anchored, metachar-escaped copy so swallows match the value literally.
|
||||||
|
static char *anchored_regex(const char *s) {
|
||||||
|
if (!s) {
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
size_t n = strlen(s);
|
||||||
|
char *out = malloc(n * 2 + 3);
|
||||||
|
if (!out) {
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
size_t pos = 0;
|
||||||
|
out[pos++] = '^';
|
||||||
|
for (size_t i = 0; i < n; i++) {
|
||||||
|
char c = s[i];
|
||||||
|
if (strchr(".\\+*?()[]{}|^$", c)) {
|
||||||
|
out[pos++] = '\\';
|
||||||
|
}
|
||||||
|
out[pos++] = c;
|
||||||
|
}
|
||||||
|
out[pos++] = '$';
|
||||||
|
out[pos] = '\0';
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns NULL if the view has nothing identifiable; caller drops the leaf.
|
||||||
|
static struct json_object *synth_swallows(struct json_object *view_obj) {
|
||||||
|
struct json_object *app_id = NULL, *wp = NULL;
|
||||||
|
const char *class = NULL, *instance = NULL;
|
||||||
|
|
||||||
|
json_object_object_get_ex(view_obj, "app_id", &app_id);
|
||||||
|
|
||||||
|
if (json_object_object_get_ex(view_obj, "window_properties", &wp)) {
|
||||||
|
struct json_object *cls, *inst;
|
||||||
|
if (json_object_object_get_ex(wp, "class", &cls)) {
|
||||||
|
class = json_object_get_string(cls);
|
||||||
|
}
|
||||||
|
if (json_object_object_get_ex(wp, "instance", &inst)) {
|
||||||
|
instance = json_object_get_string(inst);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct json_object *entry = json_object_new_object();
|
||||||
|
bool added = false;
|
||||||
|
|
||||||
|
if (app_id && json_object_get_type(app_id) == json_type_string) {
|
||||||
|
const char *s = json_object_get_string(app_id);
|
||||||
|
if (s && *s) {
|
||||||
|
char *re = anchored_regex(s);
|
||||||
|
if (re) {
|
||||||
|
json_object_object_add(entry, "app_id",
|
||||||
|
json_object_new_string(re));
|
||||||
|
free(re);
|
||||||
|
added = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (class) {
|
||||||
|
char *re = anchored_regex(class);
|
||||||
|
if (re) {
|
||||||
|
json_object_object_add(entry, "class",
|
||||||
|
json_object_new_string(re));
|
||||||
|
free(re);
|
||||||
|
added = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (instance) {
|
||||||
|
char *re = anchored_regex(instance);
|
||||||
|
if (re) {
|
||||||
|
json_object_object_add(entry, "instance",
|
||||||
|
json_object_new_string(re));
|
||||||
|
free(re);
|
||||||
|
added = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!added) {
|
||||||
|
json_object_put(entry);
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
struct json_object *arr = json_object_new_array();
|
||||||
|
json_object_array_add(arr, entry);
|
||||||
|
return arr;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void copy_key(struct json_object *src, struct json_object *dst,
|
||||||
|
const char *key) {
|
||||||
|
struct json_object *v;
|
||||||
|
if (json_object_object_get_ex(src, key, &v)) {
|
||||||
|
json_object_object_add(dst, key, json_object_get(v));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static struct json_object *build_layout_node(struct json_object *src) {
|
||||||
|
struct json_object *out = json_object_new_object();
|
||||||
|
|
||||||
|
copy_key(src, out, "layout");
|
||||||
|
copy_key(src, out, "name");
|
||||||
|
copy_key(src, out, "border");
|
||||||
|
copy_key(src, out, "current_border_width");
|
||||||
|
copy_key(src, out, "percent");
|
||||||
|
copy_key(src, out, "marks");
|
||||||
|
|
||||||
|
// Unfilled placeholders carry their original swallows; pass it through.
|
||||||
|
struct json_object *swallows;
|
||||||
|
if (json_object_object_get_ex(src, "swallows", &swallows) &&
|
||||||
|
json_object_is_type(swallows, json_type_array) &&
|
||||||
|
json_object_array_length(swallows) > 0) {
|
||||||
|
json_object_object_add(out, "swallows", json_object_get(swallows));
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
struct json_object *nodes;
|
||||||
|
bool has_nodes = json_object_object_get_ex(src, "nodes", &nodes) &&
|
||||||
|
json_object_is_type(nodes, json_type_array) &&
|
||||||
|
json_object_array_length(nodes) > 0;
|
||||||
|
|
||||||
|
if (has_nodes) {
|
||||||
|
struct json_object *out_nodes = json_object_new_array();
|
||||||
|
size_t n = json_object_array_length(nodes);
|
||||||
|
for (size_t i = 0; i < n; i++) {
|
||||||
|
struct json_object *child = json_object_array_get_idx(nodes, i);
|
||||||
|
struct json_object *built = build_layout_node(child);
|
||||||
|
if (built) {
|
||||||
|
json_object_array_add(out_nodes, built);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (json_object_array_length(out_nodes) == 0) {
|
||||||
|
json_object_put(out_nodes);
|
||||||
|
json_object_put(out);
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
json_object_object_add(out, "nodes", out_nodes);
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
struct json_object *synth = synth_swallows(src);
|
||||||
|
if (!synth) {
|
||||||
|
json_object_put(out);
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
json_object_object_add(out, "swallows", synth);
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Matches by name; if numeric >= 0, also matches num so "1" finds "1: web".
|
||||||
|
static struct json_object *find_workspace(struct json_object *node,
|
||||||
|
const char *name, int numeric) {
|
||||||
|
struct json_object *type;
|
||||||
|
if (json_object_object_get_ex(node, "type", &type) &&
|
||||||
|
json_object_get_type(type) == json_type_string &&
|
||||||
|
strcmp(json_object_get_string(type), "workspace") == 0) {
|
||||||
|
struct json_object *ws_name;
|
||||||
|
if (json_object_object_get_ex(node, "name", &ws_name) &&
|
||||||
|
strcmp(json_object_get_string(ws_name), name) == 0) {
|
||||||
|
return node;
|
||||||
|
}
|
||||||
|
if (numeric >= 0) {
|
||||||
|
struct json_object *num;
|
||||||
|
if (json_object_object_get_ex(node, "num", &num) &&
|
||||||
|
json_object_get_int(num) == numeric) {
|
||||||
|
return node;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
struct json_object *nodes;
|
||||||
|
if (json_object_object_get_ex(node, "nodes", &nodes) &&
|
||||||
|
json_object_is_type(nodes, json_type_array)) {
|
||||||
|
size_t n = json_object_array_length(nodes);
|
||||||
|
for (size_t i = 0; i < n; i++) {
|
||||||
|
struct json_object *child = json_object_array_get_idx(nodes, i);
|
||||||
|
struct json_object *match = find_workspace(child, name, numeric);
|
||||||
|
if (match) {
|
||||||
|
return match;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
int main(int argc, char **argv) {
|
||||||
|
const char *workspace = NULL;
|
||||||
|
static const struct option long_opts[] = {
|
||||||
|
{"workspace", required_argument, NULL, 'w'},
|
||||||
|
{"help", no_argument, NULL, 'h'},
|
||||||
|
{0, 0, 0, 0 },
|
||||||
|
};
|
||||||
|
int opt;
|
||||||
|
while ((opt = getopt_long(argc, argv, "w:h", long_opts, NULL)) != -1) {
|
||||||
|
switch (opt) {
|
||||||
|
case 'w':
|
||||||
|
workspace = optarg;
|
||||||
|
break;
|
||||||
|
case 'h':
|
||||||
|
default:
|
||||||
|
usage();
|
||||||
|
return opt == 'h' ? 0 : 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!workspace) {
|
||||||
|
usage();
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
char *socket_path = get_socketpath();
|
||||||
|
if (!socket_path) {
|
||||||
|
fprintf(stderr, "sway-save-tree: cannot find sway IPC socket\n");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
int fd = ipc_open_socket(socket_path);
|
||||||
|
free(socket_path);
|
||||||
|
struct timeval timeout = {.tv_sec = 3, .tv_usec = 0};
|
||||||
|
ipc_set_recv_timeout(fd, timeout);
|
||||||
|
uint32_t len = 0;
|
||||||
|
char *resp = ipc_single_command(fd, IPC_GET_TREE, "", &len);
|
||||||
|
if (!resp) {
|
||||||
|
fprintf(stderr, "sway-save-tree: GET_TREE IPC failed\n");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
struct json_tokener *tok = json_tokener_new();
|
||||||
|
struct json_object *root = json_tokener_parse_ex(tok, resp, len);
|
||||||
|
json_tokener_free(tok);
|
||||||
|
free(resp);
|
||||||
|
if (!root) {
|
||||||
|
fprintf(stderr, "sway-save-tree: failed to parse GET_TREE response\n");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
int numeric = -1;
|
||||||
|
if (workspace[0] != '\0') {
|
||||||
|
char *end = NULL;
|
||||||
|
long n = strtol(workspace, &end, 10);
|
||||||
|
if (end != workspace && *end == '\0' && n >= 0 && n <= INT_MAX) {
|
||||||
|
numeric = (int)n;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
struct json_object *ws = find_workspace(root, workspace, numeric);
|
||||||
|
if (!ws) {
|
||||||
|
fprintf(stderr, "sway-save-tree: workspace '%s' not found\n",
|
||||||
|
workspace);
|
||||||
|
json_object_put(root);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
struct json_object *floating;
|
||||||
|
if (json_object_object_get_ex(ws, "floating_nodes", &floating) &&
|
||||||
|
json_object_is_type(floating, json_type_array) &&
|
||||||
|
json_object_array_length(floating) > 0) {
|
||||||
|
fprintf(stderr, "sway-save-tree: ignoring %zu floating window(s) on "
|
||||||
|
"workspace '%s' (tiling-only in this release)\n",
|
||||||
|
json_object_array_length(floating), workspace);
|
||||||
|
}
|
||||||
|
|
||||||
|
struct json_object *nodes;
|
||||||
|
if (!json_object_object_get_ex(ws, "nodes", &nodes) ||
|
||||||
|
!json_object_is_type(nodes, json_type_array) ||
|
||||||
|
json_object_array_length(nodes) == 0) {
|
||||||
|
fprintf(stderr, "sway-save-tree: workspace '%s' has no tiling "
|
||||||
|
"children\n", workspace);
|
||||||
|
json_object_put(root);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
struct json_object *out = json_object_new_array();
|
||||||
|
size_t n = json_object_array_length(nodes);
|
||||||
|
for (size_t i = 0; i < n; i++) {
|
||||||
|
struct json_object *child = json_object_array_get_idx(nodes, i);
|
||||||
|
struct json_object *built = build_layout_node(child);
|
||||||
|
if (built) {
|
||||||
|
json_object_array_add(out, built);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const char *str = json_object_to_json_string_ext(out,
|
||||||
|
JSON_C_TO_STRING_PRETTY);
|
||||||
|
puts(str);
|
||||||
|
|
||||||
|
json_object_put(out);
|
||||||
|
json_object_put(root);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
8
swaysavetree/meson.build
Normal file
8
swaysavetree/meson.build
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
executable(
|
||||||
|
'sway-save-tree',
|
||||||
|
'main.c',
|
||||||
|
include_directories: [sway_inc],
|
||||||
|
dependencies: [jsonc],
|
||||||
|
link_with: [lib_sway_common],
|
||||||
|
install: true
|
||||||
|
)
|
||||||
54
swaysavetree/sway-save-tree.1.scd
Normal file
54
swaysavetree/sway-save-tree.1.scd
Normal file
|
|
@ -0,0 +1,54 @@
|
||||||
|
sway-save-tree(1)
|
||||||
|
|
||||||
|
# NAME
|
||||||
|
|
||||||
|
sway-save-tree - Dump a workspace's tiling tree as append_layout-compatible JSON.
|
||||||
|
|
||||||
|
# SYNOPSIS
|
||||||
|
|
||||||
|
_sway-save-tree_ --workspace <name>
|
||||||
|
|
||||||
|
# DESCRIPTION
|
||||||
|
|
||||||
|
Reads the live tree of the running sway instance over the IPC socket, walks
|
||||||
|
the named workspace, prunes runtime fields, and emits a JSON document
|
||||||
|
suitable for *swaymsg append_layout*(5). Each leaf view becomes a
|
||||||
|
placeholder entry whose *swallows* array contains a regex-anchored match on
|
||||||
|
the view's *app_id* (Wayland) or *class* / *instance* (xwayland).
|
||||||
|
|
||||||
|
The output is sent to standard output. Pre-existing placeholders that were
|
||||||
|
loaded by *append_layout* and have not yet swallowed a window are echoed
|
||||||
|
verbatim from their *swallows* array.
|
||||||
|
|
||||||
|
This is the sway counterpart of i3-save-tree(1).
|
||||||
|
|
||||||
|
# OPTIONS
|
||||||
|
|
||||||
|
*--workspace* <name>
|
||||||
|
Name or number of the workspace to dump. Required. If the argument is
|
||||||
|
all digits, it is also matched against the workspace's number, so
|
||||||
|
*--workspace 1* finds a workspace named "1: web".
|
||||||
|
|
||||||
|
*-h, --help*
|
||||||
|
Show help and exit.
|
||||||
|
|
||||||
|
# LIMITATIONS
|
||||||
|
|
||||||
|
Floating windows are skipped in this release; only tiling children of the
|
||||||
|
workspace are emitted. A warning is printed to stderr when floating windows
|
||||||
|
are present.
|
||||||
|
|
||||||
|
# EXAMPLES
|
||||||
|
|
||||||
|
Save the layout of workspace 1 to a file, kill the running windows, and
|
||||||
|
restore the layout:
|
||||||
|
|
||||||
|
```
|
||||||
|
$ sway-save-tree --workspace 1 > /tmp/layout.json
|
||||||
|
$ swaymsg [workspace=1] kill
|
||||||
|
$ swaymsg append_layout /tmp/layout.json
|
||||||
|
```
|
||||||
|
|
||||||
|
# SEE ALSO
|
||||||
|
|
||||||
|
*sway*(5), *sway*(1), *swaymsg*(1)
|
||||||
Loading…
Add table
Add a link
Reference in a new issue