swaywm / sway

i3-compatible Wayland compositor
https://swaywm.org
MIT License
14.68k stars 1.11k forks source link

Layout save/restore #1005

Closed ddevault closed 7 years ago

ddevault commented 7 years ago

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.

cyphar commented 7 years ago

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:

So from what I can see there are a few things we need to implement and/or consider:

  1. Is there a nicer way of implementing "swallowing" in Wayland than in X? The above seems quite hacky to me.
  2. What is the analogy for WM_CLASS, _NET_WM_NAME and WM_WINDOW_ROLE in the Wayland (and wlc) protocols?
  3. We need to implement the concept of a "swallowing" window, which is effectively an empty window which is "busy".
  4. Need to create protocol endpoints for communicating about generating the JSON blob.
  5. Need to augment the configuration to allow you to append_layout (source a JSON blob and add it to the current layout).
ddevault commented 7 years ago

I've decided that we are just not going to support this i3 feature, ever.

mahmoudhossam commented 7 years ago

@SirCmpwn Any specific reason why?

ddevault commented 7 years ago

It's too complicated and hacky for too little benefit. I don't like the design constraints it imposes upon sway.

hyiltiz commented 6 years ago

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.

ddevault commented 6 years ago

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.

hyiltiz commented 6 years ago

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!

flux242 commented 6 years ago

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.

ddevault commented 6 years ago

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.

flux242 commented 6 years ago

give an IPC command example please of moving a program window into a specific layout window on a workspaceX

ddevault commented 6 years ago

To arrange three windows like this on an empty workspace:

xz
yz
  1. Execute the 3 processes and wait for their windows to appear (you'll be notified via IPC_EVENT_WINDOW)
  2. Use IPC_GET_TREE and find the container IDs which map to those pids
  3. If they're not already all on the desired workspace, issue [con_id=$id] move to workspace $ws commands until they are
  4. [con_id=x] splitv
  5. [con_id=y] move left
flux242 commented 6 years ago

right, 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?

ddevault commented 6 years ago

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

flux242 commented 6 years ago

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

9ary commented 6 years ago

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:

ddevault commented 6 years ago

I would consider a patch, but I'm not interested in doing this myself and I'm worried about the complexity/value tradeoff.

9ary commented 6 years ago

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.

JonnyHaystack commented 5 years ago

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.

JonnyHaystack commented 5 years ago

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:

flux242 commented 4 years ago

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 ?

norcalli commented 4 years ago

@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.

emersion commented 4 years ago

Ref https://github.com/swaywm/sway/pull/3022

JonnyHaystack commented 4 years ago

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:

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.

Hubro commented 3 years ago

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.

yellowhat commented 3 years ago

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

Hubro commented 3 years ago

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

Aha, I see... Your example would probably work in some simple cases, but I can already see it falling apart with applications like Discord, where it displays a loading splash screen for a little while before starting, and often also starts an updater. Although this could be worked around by waiting for a new window that matches certain filters.

Also it has to open applications one at a time, which could be potentially very slow, but getting around that would be pretty complicated :thinking: I'm imagining a script that would launch a bunch of applications simultaneously, then loop on get_tree and resolve each new window ID based on a filter and set each new window to floating, then a different thread would execute an ordered list of movement commands to place the windows in the right position after "unfloating" them. The script could be made reusable by a simple DSL, something like this:

from sway_layout import Row, Column, Application, launch

launch(
    Column(
        Application("chromium --app=https://messenger.com", cls="Chromium", instance="^Messenger$"),
        Application("slack", cls="Slack", instance="^Messenger$"),
    ),
    Column(
        Application("viber", cls="^Viber$"),
        Row(
            Application("discord", cls="^discord$", instance="^discord$", window_type="normal", name="- Discord$"),
            Application("teamspeak3", cls="^TeamSpeak3$"),
        )
    )
)

I don't see any reason this wouldn't be possible to implement. I will probably take a swing at it when I get some free time, I am really missing this feature.

9ary commented 3 years ago

This is my take on the problem, it's definitely not bulletproof but it works most of the time for me. https://github.com/9ary/dotfiles/blob/master/i3/ws-1.py Again, the ability to pass a tree into sway and have it arrange windows accordingly would help a lot for writing external tools to do this. It doesn't even need to be smart and match criteria, just identify windows by their ID and leave the rest up to the application.

tgunnoe commented 3 years ago

@9ary can you explain a bit to me how this works? I couldn't get your example working after a day or so, but it could be due to my lack of experience with the sway IPC.

in particular, https://github.com/9ary/dotfiles/blob/master/i3/ws-1.py#L83

event_loop = asyncio.create_task(sway.main())

It seems to never respond, or report back anything after it hits this point. What am I doing wrong?

9ary commented 3 years ago

Yeah, it blocks until every leaf node has a match, so it won't do anything until all members of your layout exist (I would have to implement placeholders otherwise, which is significantly more work, and my workflow doesn't require them).

tgunnoe commented 3 years ago

Thanks for the explanation. Got it working after learning more about the script and get_tree

JonnyHaystack commented 3 years ago

This is my take on the problem, it's definitely not bulletproof but it works most of the time for me. https://github.com/9ary/dotfiles/blob/master/i3/ws-1.py Again, the ability to pass a tree into sway and have it arrange windows accordingly would help a lot for writing external tools to do this. It doesn't even need to be smart and match criteria, just identify windows by their ID and leave the rest up to the application.

@9ary How would you match the window by ID considering the ID the next time you launch the program will be different from when you saved the layout?

9ary commented 3 years ago

The point is that an external program could query the tree to identify the desired windows, and even spawn placeholder windows, effectively taking the complexity out of sway, and enabling more complex use cases.

JonnyHaystack commented 3 years ago

Right, I get you

Nama commented 2 years ago

I expanded some example script to make it work. The requirements are in the file. Only issue is that multiple windows of an application aren't handled. https://gist.github.com/Nama/55786d0b2a8349d11f9013fa1a86e6b1

MNolan147 commented 2 years ago

@Nama This script doesn't seem to work for me. It is able to save the tree, but can't restore it from the file.

Nama commented 2 years ago

Do you get any error? If yes, pls post. Do you have more than one of the same monitor? If yes, tell me the names of them from swaymsg -t get_outputs.

MNolan147 commented 2 years ago

@Nama No error, no duplicate monitors. When it finishes is says that the session was successfully loaded, but no programs were started and no already open programs were moved to the workspace they should be moved to.

Nama commented 2 years ago

The script doesn't start any programs.

Can you send the tree json the script saved?

Nama commented 2 years ago

Please send such long text as file. You should edit and delete your json tree now. My script makes use of the window_properties, which your tree doesn't have once.

              "window_properties": {
                "class": "firefox",
                "instance": "Navigator",
                "title": "Variable refresh rate - ArchWiki — Mozilla Firefox",
                "transient_for": null,
                "window_role": "browser",
                "window_type": "normal"
              }

I don't know why. The only differences seem to be that window_properties are missing and (probably for the same reason) "window" is always null. My applications have all an ID as the value. Even tho, "visible" says true, these values are missing for you. That you (somehow) hid the windows would have been my first guess.

Do you have any workspaces which aren't vertically aligned? I have only one vertical workspace, but the window_properties aren't missing there. For me, only Sublime Text is missing these values.

My version of sway is 1:1.7.

MNolan147 commented 2 years ago

Apologies for the long text. I've attached the file to this message. I've run swaymsg -t get_tree and the window_properties don't appear there either

Workspace Tree: workspace_startup_tree.txt

Nama commented 2 years ago

Figured it out. Only applications using xwayland get the "window_properties" data. And somehow, ever of my apps do this except Sublime Text. (Firefox needs even a env var)

My script is useless.

MNolan147 commented 2 years ago

You could change the script to also check for the app_id property, which seems to perform the same function as window_propeties["class"].

MNolan147 commented 2 years ago

And to move multiple windows of the same type, you could use the id property to distinguish between the different windows i.e., if the layout has 2 terminals that it needs to move to specific workstations, it loops through open terminals and moves them based on their id.

Nama commented 2 years ago

Oh yeah, app_id seems useful, thx.

Isn't the id of the node always another one, so we can't compare them? Or do you mean like, keeping track of, which id was moved already and skipping to never moved ones?

Nama commented 2 years ago

Wayland apps are now working. You need i3ipc from Github, newest commit. Last release is lacking that feature -.- Also made dunstify and sh optional, without the need to edit the script.

I'll look into handling multiple windows now. https://gist.github.com/Nama/55786d0b2a8349d11f9013fa1a86e6b1

roselandgoose commented 2 years ago

I've modified @Nama's script above to support multiple windows of the same program - for my many firefox windows... I've also made the layout file argument optional.

https://git.sr.ht/~roselandgoose/Sway_Layout_Restore/blob/main/sway_workspaces.py

Nama commented 2 years ago

Oh that's nice! Thank you, @roselandgoose. I created a repo without your changes, so you can PR your changes. Would be more convenient to work on it in a repo (and move the discussion from this issue XD). https://github.com/Nama/sway-workspaces

JoshElias commented 10 months ago

As a TLDR; for people looking for this feature, the sway dev doesn't want to add it but Nama has created a plugin that somewhat recreates i3-save-tree.

mishurov commented 5 months ago

@Nama I can traverse recursively a saved json from GET_TREE and launch apps using app_id, or map app_id to an executable, instead of moving existing apps. I wrote a script for my personal use with my own unbloated sway ipc but If you're interested, you might take a look how I do it. https://github.com/mishurov/applets/blob/master/sway_restore_workspace/srws.py

python3 ./srws.py --workspace myworkspacename --save ./test.json
python3 ./srws.py --load ./test.json