diff --git a/include/sway/tree/container.h b/include/sway/tree/container.h index 9b2571c1a..1a38d678b 100644 --- a/include/sway/tree/container.h +++ b/include/sway/tree/container.h @@ -9,6 +9,7 @@ struct sway_view; struct sway_seat; +struct json_object; enum sway_container_layout { L_NONE, @@ -146,6 +147,7 @@ struct sway_container { // 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 wl_signal destroy; diff --git a/meson.build b/meson.build index 17d65c334..98afc98fd 100644 --- a/meson.build +++ b/meson.build @@ -195,6 +195,7 @@ subdir('protocols') subdir('common') subdir('sway') subdir('swaymsg') +subdir('swaysavetree') if get_option('swaybar') or get_option('swaynag') subdir('client') diff --git a/sway/ipc-json.c b/sway/ipc-json.c index 3b69ad384..e6438304e 100644 --- a/sway/ipc-json.c +++ b/sway/ipc-json.c @@ -776,6 +776,11 @@ static void ipc_json_describe_container(struct sway_container *c, json_object *o if (c->view) { 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 { diff --git a/sway/tree/container.c b/sway/tree/container.c index c76d4f32d..b63aaebdc 100644 --- a/sway/tree/container.c +++ b/sway/tree/container.c @@ -7,6 +7,7 @@ #include #include #include +#include #include "sway/config.h" #include "sway/criteria.h" #include "sway/desktop/transaction.h" @@ -471,6 +472,9 @@ void container_destroy(struct sway_container *con) { } list_free(con->swallows); } + if (con->swallows_json) { + json_object_put(con->swallows_json); + } if (con->view && con->view->container == con) { con->view->container = NULL; diff --git a/sway/tree/load_layout.c b/sway/tree/load_layout.c index 247c4e01b..8a9493823 100644 --- a/sway/tree/load_layout.c +++ b/sway/tree/load_layout.c @@ -363,6 +363,8 @@ static struct sway_container *build_node(struct json_object *obj, } 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); } else { // Leaf without swallows is an empty split with no children. i3 does // not produce these; treat as an error to avoid silently leaving diff --git a/sway/tree/view.c b/sway/tree/view.c index b1e3c6daa..8cc019bca 100644 --- a/sway/tree/view.c +++ b/sway/tree/view.c @@ -16,6 +16,7 @@ #if WLR_HAS_XWAYLAND #include #endif +#include #include "list.h" #include "log.h" #include "sway/criteria.h" @@ -823,6 +824,10 @@ static void promote_placeholder(struct sway_container *placeholder, 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) { diff --git a/swaysavetree/main.c b/swaysavetree/main.c new file mode 100644 index 000000000..da64cce69 --- /dev/null +++ b/swaysavetree/main.c @@ -0,0 +1,292 @@ +// sway-save-tree: dump a workspace's tiling tree as JSON for `swaymsg +// append_layout`. Counterpart to i3-save-tree(1). + +#include +#include +#include +#include +#include +#include +#include +#include +#include "ipc-client.h" +#include "ipc.h" +#include "log.h" + +static void usage(void) { + fprintf(stderr, + "Usage: sway-save-tree --workspace \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; +} + +// Recursive search of the tree for the requested workspace. +static struct json_object *find_workspace(struct json_object *node, + const char *name) { + 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; + } + } + 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); + 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; + } + + struct json_object *ws = find_workspace(root, workspace); + 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; +} diff --git a/swaysavetree/meson.build b/swaysavetree/meson.build new file mode 100644 index 000000000..0ec86f6e1 --- /dev/null +++ b/swaysavetree/meson.build @@ -0,0 +1,8 @@ +executable( + 'sway-save-tree', + 'main.c', + include_directories: [sway_inc], + dependencies: [jsonc], + link_with: [lib_sway_common], + install: true +)