Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions include/sway/commands.h
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,7 @@ sway_cmd cmd_exec_validate;
sway_cmd cmd_exec_process;

sway_cmd cmd_allow_tearing;
sway_cmd cmd_append_layout;
sway_cmd cmd_assign;
sway_cmd cmd_bar;
sway_cmd cmd_bindcode;
Expand Down
12 changes: 12 additions & 0 deletions include/sway/criteria.h
Original file line number Diff line number Diff line change
Expand Up @@ -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);

/**
* 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.
*/
Expand Down
10 changes: 10 additions & 0 deletions include/sway/tree/container.h
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@

struct sway_view;
struct sway_seat;
struct json_object;

enum sway_container_layout {
L_NONE,
Expand Down Expand Up @@ -141,11 +142,20 @@ struct sway_container {

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 wl_signal destroy;
} events;
};

void container_init_border_rects(struct sway_container *c, bool *failed);

struct sway_container *container_create(struct sway_view *view);

void container_destroy(struct sway_container *con);
Expand Down
34 changes: 34 additions & 0 deletions include/sway/tree/load_layout.h
Original file line number Diff line number Diff line change
@@ -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
2 changes: 2 additions & 0 deletions meson.build
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,7 @@ if scdoc.found()
'sway/sway-output.5.scd',
'swaybar/swaybar-protocol.7.scd',
'swaymsg/swaymsg.1.scd',
'swaysavetree/sway-save-tree.1.scd',
]

if get_option('swaynag')
Expand Down Expand Up @@ -195,6 +196,7 @@ subdir('protocols')
subdir('common')
subdir('sway')
subdir('swaymsg')
subdir('swaysavetree')

if get_option('swaybar') or get_option('swaynag')
subdir('client')
Expand Down
1 change: 1 addition & 0 deletions sway/commands.c
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ struct cmd_results *checkarg(int argc, const char *name, enum expected_args type

/* Keep alphabetized */
static const struct cmd_handler handlers[] = {
{ "append_layout", cmd_append_layout },
{ "assign", cmd_assign },
{ "bar", cmd_bar },
{ "bindcode", cmd_bindcode },
Expand Down
47 changes: 47 additions & 0 deletions sway/commands/append_layout.c
Original file line number Diff line number Diff line change
@@ -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);
}
59 changes: 39 additions & 20 deletions sway/criteria.c
Original file line number Diff line number Diff line change
Expand Up @@ -190,16 +190,9 @@ static bool criteria_matches_container(struct criteria *criteria,
return true;
}

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;
}

// View-intrinsic checks; does not require view->container.
static bool match_view_intrinsic(struct criteria *criteria,
struct sway_view *view, struct sway_view *focused) {
if (criteria->title) {
const char *title = view_get_title(view);
if (!title) {
Expand Down Expand Up @@ -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 (criteria->id) { // X11 window ID
uint32_t x11_window_id = view_get_x11_window_id(view);
Expand Down Expand Up @@ -419,6 +408,42 @@ static bool criteria_matches_view(struct criteria *criteria,
}
#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 (!container_is_floating(view->container)) {
return false;
Expand Down Expand Up @@ -471,12 +496,6 @@ static bool criteria_matches_view(struct criteria *criteria,
}
}

if (criteria->pid) {
if (criteria->pid != view->pid) {
return false;
}
}

return true;
}

Expand Down
5 changes: 5 additions & 0 deletions sway/ipc-json.c
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
2 changes: 2 additions & 0 deletions sway/meson.build
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ sway_sources = files(
'config/input.c',

'commands/allow_tearing.c',
'commands/append_layout.c',
'commands/assign.c',
'commands/bar.c',
'commands/bind.c',
Expand Down Expand Up @@ -211,6 +212,7 @@ sway_sources = files(

'tree/arrange.c',
'tree/container.c',
'tree/load_layout.c',
'tree/node.c',
'tree/root.c',
'tree/view.c',
Expand Down
46 changes: 46 additions & 0 deletions sway/sway.5.scd
Original file line number Diff line number Diff line change
Expand Up @@ -414,6 +414,52 @@ set|plus|minus|toggle <amount>
The following commands may be used either in the configuration file or at
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>
Assigns windows matching _criteria_ (see *CRITERIA* for details) to
_workspace_. The → (U+2192) is optional and cosmetic. This command is
Expand Down
Loading