Layout save/restore is an i3 feature that serializes your layout to JSON and attempts to arrange windows in the same way in a later session. First step is to research how i3 does it and propose some strategies for implementing it in sway.
I did a bit of research using the documentation as a reference. Effectively the way this is implemented (I don't use this feature at all) is this:
When generating the i3-save-tree output it includes all of the obvious information about container placement and so on -- which for us would just be a matter of recursively walking down the tree.
However, it includes "swallowing" information which is effectively a set of regular expressions that matches WM_CLASS (which is a tuple of (instance, class)), _NET_WM_NAME (title) and WM_WINDOW_ROLE (window role). When a program is started in an i3 session and it matches all of the "swallowing" information, it will place that program in the loaded pane.
The swallowing information is actually an array, so you can create OR'd matches.
Note that the output of i3-save-tree is not actually standard-conformant JSON. IMO we should not follow their lead, and we should always output standard compliant JSON.
So from what I can see there are a few things we need to implement and/or consider:
WM_CLASS, _NET_WM_NAME and WM_WINDOW_ROLE in the Wayland (and wlc) protocols?append_layout (source a JSON blob and add it to the current layout).I've decided that we are just not going to support this i3 feature, ever.
@SirCmpwn Any specific reason why?
It's too complicated and hacky for too little benefit. I don't like the design constraints it imposes upon sway.
Would you reconsider your decision? This feature is actually quite useful in some (could even say rare) professional settings, e.g. setting up a music production with a midi mixer, synthesizer, sequencer, and composition programs open in their specific layout every time, or just general software development with editors, compilers, debuggers and test windows.
While there is no point in being i3 compliant on this, I think a good discussion in trying to come up with a better solution would be quite beneficial.
No, I won't reconsider. However, you should be able to accomplish something similar with a script that starts up the software you need and arranges it appropriately via IPC.
I would love to say "Would you like to reconsider whether you would like to reconsider 'No, I won't reconsider' ", but that might easily overflow the buffer if not implemented correctly. Anyway, I do think writing a script for that should be straightforward so thanks!
it's a shame that this great i3 feature is dropped simply because "I'm too lazy to implement something I personally do not use". Please tell me how am I suppose to do swallowing in a script? How do I assign a program window to a specific layout window using IPC? If I start a program I only know its process id and what is available in the /proc/proc_id.
It's too complicated and hacky for too little benefit. I don't like the design constraints it imposes upon sway.
This is why it was dropped, not because, quote, "I'm too lazy to implement something I personally do not use." You can use IPC to move a window to a specific workspace over IPC with criteria. You can split the window and move things around, too. The pid associated with a window is included in the IPC_GET_TREE output, along with a unique ID you can use for criteria.
give an IPC command example please of moving a program window into a specific layout window on a workspaceX
To arrange three windows like this on an empty workspace:
xz
yz
IPC_GET_TREE and find the container IDs which map to those pids[con_id=$id] move to workspace $ws commands until they are[con_id=x] splitv[con_id=y] move leftright, why there can't be a method that does exactly this if it is so easy? It would just have to parse a layout file additionally, right? Now using your approach what would I do if I wanted to change my layout a bit, like resize here and there provided that my layout is a little bit more complex rather that just 3 windows? Currently I just have to resize and overwrite my layout file. And btw I'm inside of a bash script so how do I get an IPC notification?
right, why there can't be a method that does exactly this if it is so easy?
See
It's too complicated and hacky for too little benefit. I don't like the design constraints it imposes upon sway.
If you want to write a script which implements i3 layout save/restore as an external program, you should do so, but no one is going to write it for you. This project is run by volunteers, and no one is volunteering to do this.
And btw I'm inside of a bash script so how do I get an IPC notification?
By not being in a bash script
nope, I'm not going to implement a script because
It's too complicated and hacky
without proper support from underlying framework. Your script "example" could only work for very simple layouts. It's because if you have a generic layout description you'd have to start with a specific program window traversing the layout up the tree trying to figure out where that window would land in the end and how the inverted IPC command chain would look like.
btw feature that allows restoring a complex working environment (up to starting terminals with specific commands) just by a keyboard shortcut brings rather huge benefit than "too little benefit". Lacking such a feature is a road block for me => no switching from i3. Chusikowski
I agree that i3's implementation of this feature has its own set of shortcomings. However, the proposed alternative of using IPC feels rather clunky since all it allows is akin to replaying a keyboard macro.
A more powerful alternative would be to extend the API to allow passing a tree of existing windows referenced by ID or criteria, with optional hints for layout and sizing, and have sway apply that atomically.
This would feel much cleaner than i3's swallow windows (which can only capture windows that do not yet exist), while providing equivalent, if not more flexible functionality.
Note that my proposal differs from i3 in two ways:
append_layout, i3 spawns placeholder windows, and stores the criteria in the container tree. When a new window is mapped and i3 attempts to manage it, it searches the tree for a matching placeholder and substitutes the first positive match. This requires special handling in various components of the window manager, and can't operate on existing windows.I would consider a patch, but I'm not interested in doing this myself and I'm worried about the complexity/value tradeoff.
That's all I needed to hear. I would gladly write this feature myself, but I'm currently focusing on other projects so i3 is still my daily driver for now. I'll probably come back to it later, unless someone else is interested in doing it before me.
I agree that i3's implementation of this feature has its own set of shortcomings. However, the proposed alternative of using IPC feels rather clunky since all it allows is akin to replaying a keyboard macro.
A more powerful alternative would be to extend the API to allow passing a tree of _existing_ windows referenced by ID or criteria, with optional hints for layout and sizing, and have sway apply that atomically.
This would feel much cleaner than i3's swallow windows (which can only capture windows that do not yet exist), while providing equivalent, if not more flexible functionality.Note that my proposal differs from i3 in two ways:
- i3 expects a path to a JSON file containing the layout. This is dumb, the entire layout should be passed over IPC instead. Clients may choose to read a layout from a file, but that's up to them.
- When you call
append_layout, i3 spawns placeholder windows, and stores the criteria in the container tree. When a new window is mapped and i3 attempts to manage it, it searches the tree for a matching placeholder and substitutes the first positive match. This requires special handling in various components of the window manager, and can't operate on existing windows.
My proposal is rather self-contained: since it should operate on existing windows, everything can be done immediately in the command handler, without having to spawn placeholders and/or store additional metadata in the tree.
I completely agree with all of this. The placeholder system in i3 is not very good and you have to use hacks like unmapping and remapping windows with xdotool to get a layout to apply to existing windows.
Let me know when you get time to start working on it. I'd be eager to help out.
I've recently been looking at the code that i3 uses when restoring a layout.
json_content_t content = json_determine_content(buf, len);
LOG("JSON content = %d\n", content);
if (content == JSON_CONTENT_UNKNOWN) {
ELOG("Could not determine the contents of \"%s\", not loading.\n", path);
yerror("Could not determine the contents of \"%s\".", path);
goto out;
}
Con *parent = focused;
if (content == JSON_CONTENT_WORKSPACE) {
parent = output_get_content(con_get_output(parent));
} else {
/* We need to append the layout to a split container, since a leaf
* container must not have any children (by definition).
* Note that we explicitly check for workspaces, since they are okay for
* this purpose, but con_accepts_window() returns false for workspaces. */
while (parent->type != CT_WORKSPACE && !con_accepts_window(parent))
parent = parent->parent;
}
DLOG("Appending to parent=%p instead of focused=%p\n", parent, focused);
char *errormsg = NULL;
tree_append_json(parent, buf, len, &errormsg);
if (errormsg != NULL) {
yerror(errormsg);
free(errormsg);
/* Note that we continue executing since tree_append_json() has
* side-effects — user-provided layouts can be partly valid, partly
* invalid, leading to half of the placeholder containers being
* created. */
} else {
ysuccess(true);
}
So it checks for the type of the data it loads from the file whose path you pass to append_layout, and defaults to con if it doesn't find any "type" properties.
If the content is of type "workspace", it appends the layout to the currently focused output's workspace array. If it is a con, it searches upwards from the currently focused node until it finds a workspace, then it appends the layout to the workspace's child array.
The fact that it only appends nodes to an array and does not modify or replace existing nodes means that it is actually quite clean. I've come to appreciate the simplicity of this approach, as opposed to rewriting the existing tree.
The only part of their implementation that still seems bad to me is that placeholder container matching is only done when windows are created. We could just implement layout restoring almost exactly the same as i3, but just search the whole workspace for matching windows right after the layout is appended, instead of when new windows are mapped.
Or we could have the best of both worlds and check for matches after layout appending, and on window creation, for those use cases where placeholder windows/deferred swallowing is desirable (these cases really do exist). The caveat of this is obviously that it's less self-contained because of having to monitor window creation events.
Either way it doesn't seem like this would be that hard to implement.
Edit:
Also FWIW, layout saving is already a non-issue at this point because we can already get the entire window tree using swaymsg -t get_tree or the same thing through IPC. There's no real point in bundling an equivalent to the crappy i3-save-tree script into sway.
All that's needed right now is:
The only part of their implementation that still seems bad to me is that placeholder container matching is only done when windows are created. We could just implement layout restoring almost exactly the same as i3, but just search the whole workspace for matching windows right after the layout is appended, instead of when new windows are mapped.
lets say I have 100 windows in my layout. Right now I have a simple script that loads specific layout and then starts 100 programs in the background. Your suggestion would imply that I have to implement additional 'wait_for_all_100_windows_be_opened_before_loading_layout' and which is more important 'do_x_if_window_y_wont_appear_within_z_sec' logics. Is that what you're saying @JonnyHaystack ?
@ddevault your assertion that this could be easily done in a script is incorrect. I know because I tried it. sway doesn't have good enough commands to make it easy to be accomplished for an arbitrary layout. For example, there is no "move to this container." Instead you have to make a mark, and then move to that mark. This ends up being error prone. Another thing I tried is creating a placeholder window so that I knew its con_id and I could target it without needing to use marks as a place to drop windows on. This too is error prone as the timing required to make sure that windows transfer is not so straightforward.
Finally, there's also not a good way of modifying the layout of windows without focusing them. You may thing that criteria targetting would work, but it doesn't work reliably. And this is all from 2020 when I tried it. At the time that you made the suggestion to make a script in 2017, I am certain sway was in an even less adequate state to achieve it.
Maybe I'm not smart enough to get it done, but then again that also proves the value of having it built into sway or having an official solution.
I could try to spend more time on it, but I have already sunk 3 hours into it. It's likely not worth it to do in such a hacky way. And if you want dozens of other people to spend/waste their time simply because you are still under the misconception that this is "easy to do" then I think it would be fair to apologize for the time that others have spent and will spend time on this.
The only part of their implementation that still seems bad to me is that placeholder container matching is only done when windows are created. We could just implement layout restoring almost exactly the same as i3, but just search the whole workspace for matching windows right after the layout is appended, instead of when new windows are mapped.
lets say I have 100 windows in my layout. Right now I have a simple script that loads specific layout and then starts 100 programs in the background. Your suggestion would imply that I have to implement additional 'wait_for_all_100_windows_be_opened_before_loading_layout' and which is more important 'do_x_if_window_y_wont_appear_within_z_sec' logics. Is that what you're saying @JonnyHaystack ?
No, ideally swallowing would occur in 3 situation:
append_layout (so that you can apply a layout to existing windows)All three of these could be handled in different ways, but here's my take:
For the first and second situation, there should not be too much added complexity, but it would presumably require small modifications to event handlers in the main codebase. I could be wrong here because I haven't looked at sway's code in depth.
For the third situation, there could be a fair bit of added complexity in having to recurse over the target workspace's tree, but I think this could be kept separate from the main codebase, as part of the append_layout command's implementation. Alternatively if there's any tool for wayland/sway that lets you unmap/remap the windows in the workspace similar to what you can do with xdotool, that would be acceptable and situation 3 could be ignored, but again, that's kinda hacky.
Are there any good options for restoring layouts at this point? I'm a software developer and I use append_layout daily to start my work environments, as well as position my chat windows (I use like 8 different chat clients.) The lack of this functionality is my main blocker with Sway at the moment.
If anybody has any good workarounds using IPC I would love to see some examples. I'm not savvy enough with Sway to have any clue what ddevault is talking about when he says things like IPC_GET_TREE.
Hi,
I managed to use i3ipc-python to open a multiple windows at the same time in a certain order:
#!/usr/bin/env python3
from i3ipc import Connection
conn = Connection()
def run(cmd):
conn.command(cmd)
ilen = len(conn.get_tree().workspaces()[-1].descendants())
while len(conn.get_tree().workspaces()[-1].descendants()) == ilen:
True
con_id = max(i.id for i in conn.get_tree().workspaces()[-1].descendants())
return con_id
conn.command("workspace file")
con_id1 = run("exec wayst -e sf /")
con_id2 = run("exec wayst -e sf ~/Downloads")
con_id3 = run(f"[con_id={con_id1}] focus; splitv; exec wayst -e sf /tmp")
con_id4 = run(f"[con_id={con_id2}] focus; splitv; exec wayst -e sf /mnt")
conn.command(f"[con_id={con_id1}] focus")
Any suggestions are welcome
Most helpful comment
That's all I needed to hear. I would gladly write this feature myself, but I'm currently focusing on other projects so i3 is still my daily driver for now. I'll probably come back to it later, unless someone else is interested in doing it before me.