koekeishiya / yabai

A tiling window manager for macOS based on binary space partitioning

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Stacking support

pat-s opened this issue · comments

Wondering if you have thought about supporting stacking of windows (similar to i3 and sway).

I usually use this mode a lot in certain spaces in sway and it can be pretty handy.

Thanks for the great project!

A flexible implementation of this has a lot to offer. Obviously this can already be done using a combination of signals, rules and queries—similar to my monocle mode implementation from #83—but I personally think that yabai has a lot to gain from this if implemented properly.

I'm imagining a syntax like this:

# stack source window on target window
yabai -m window [<source window selector>] --stack <target window selector>

# the next window gets inserted stacked on top of the source window
# the window command --insert currently supports <DIR_SEL>
yabai -m window [<source window selector>] --insert stack

Which would make the source window occupy the same frame and node as the target window. A window could be taken out of a stack through any means that reinserts it, e.g. a warp command.

What still needs to be discussed is how window selectors interact with stacked windows: Should directional selectors (west, south, north, east) ignore stacked windows? Should next and prev cycle through stacked windows in order? Is the focused window always moved to the top of its stack whenever its focused?

How should stacked windows display? Should there be an overlap, e.g. the lowest window of the stack using the full height of the stack, the 2nd lowest has its frame start a set amount of pixels lower, the 3rd lowest has its frame start twice the set amount of pixels lower and so on? That way, mouse support would not be compromised. I would suggest using 22pt here, which is the height of the menu bar.

Some form of stacking could also be the foundation for fixing #68 (would require the internals to be changed in a similar way anyways) and would allow a more robust implementation of a monocle mode, and many more custom ways to tile windows like suggested in #98.

Thoughts @koekeishiya?

I do see the use case for this type of window management, but I am currently not interested in attempting to implement this. I would not be against a well thought out PR that integrates this nicely if anyone wants to give it a shot on their own.

Just to share some explanation on why I think this is a very VERY useful feature (for me at least):
I typically work with several (2-8) instances of IDE (Intellij). What I got used to (on i3wm) is having one instance open as fullscreen and the other ones visible only as window titles. Then whenever I need to switch I use arrows to navigate through the windows titles. What's important is that I completely don't care about any visuals or interactivity of these other windows.

I'm trying to use amethyst right now but the closest I was able to get is a huge pain in comparison to i3. Mostly because it's very hard to identify which window I want to switch to. They all look almost the same and titles are not well exposed.

To clarify, my main issue with this is not inherently the functionality by itself. It is the fact that macOS does not actually expose the functionality necessary for me to implement the various solutions that would make this a good experience, and so I'm reluctant to spend too much of my time dealing with their garbage.

I have this in the back of my mind and iterate on possible solutions that could work, but this is not as simple to solve as it would be with e.g the X windowing system.

Implemented basic logic and structure necessary for stacking windows.

Directional selectors will ignore the additional stacked windows belonging to a node (meaning that they will target the currently focused window in the stack).

Stacked windows are not cascaded; their region overlap as is. If a stacked window activates parent zoom or fullscreen zoom, that operation will simultaneously apply to all windows in the same stack.

To make a window share node/region with some managed window, use the following command:

# stack target_window_sel onto source_window_sel
yabai -m window [<source_window_sel>] --stack <target_window_sel>

# next window is inserted onto source_window_sel
yabai -m window [<source_window_sel>] --insert stack

# focus the prev window in a stack
yabai -m window --focus stack.prev

# focus the next window in a stack
yabai -m window --focus stack.next

# focus the first window in a stack
yabai -m window --focus stack.first

# focus the last window in a stack
yabai -m window --focus stack.last

# focus the most recently focused window in a stack
yabai -m window --focus stack.recent

<target_window_sel> will take on the node/region of <source_window_sel>. The reasoning behind the arguments working in this order is that I find it more intuitive due to the way that you can omit the <source_window_sel> to implicitly place an arbitrary window onto the currently focused window.

To un-stack a window, simply perform a warp operation using window --warp or using mouse integration, with the stacked window as the source operand. When a stacked window is the source operand of a mouse action it is not eligible for a swap operation, and it will always perform a warp following the regular rules, in which the end result is that the window is split into its own separate node. If a regular managed window (non-stack) is the source operand of a warp/mouse operation it will follow all the regular rules.

With the exception of mouse integration, swap operations should work as it always has.

If a stacked window is moved to a different space/display, enters fullscreen, or any operation that would make this window become unmanaged, that operation will only affect that window and it will be removed from the stack.


Remaining things that need to be implemented/solved:

  1. How to focus stacked windows? New command/selectors or make prev/next cycle through stacked windows in order? Implemented.
    • Is the focused window always moved to the top of its stack whenever its focused?
    • Properly track the focused window in a node when it has multiple windows (if it is not moved to the top of its stack). Implemented.
  2. Support stacking through window --insert. Implemented.

I would like to be able to perform operations (warp, swap, resize) on a stack, but I’m not sure how to indicate that the stack is what’s selected, not a window inside of it.

I suppose resize is no issue—just let the stack inherit sizing from the top (focused) window.

How to focus stacked windows? New command/selecotrs or should next and prev cycle through stacked windows in order?

Cycling through the stack with prev/next sounds good to me.

Is the focused window always moved to the top of its stack whenever its focused?

I believe so, yes. Would be odd to be able to focus a window underneath that’s not visible.

Moving focused window to the top is also consistent with mouse UI behavior in floated or un-managed windows that layer over one another.

But I think what you may be getting at is how to represent the behavior in the tree. Do you want to do so by index in the array of stacked windows (meaning you have to modify that array every time a different window is focused), or track it another way?

I’m not sure which way would be best, but I’m happy to discuss and think about it.

I would like to be able to perform operations (warp, swap, resize) on a stack, but I’m not sure how to indicate that the stack is what’s selected, not a window inside of it.

With the current implementation all operations that affect the node tree will apply to the entire stack, except for warp operations where the stacked window is the source operand. You can still warp a non-stacked window onto a stack and it will split the stack using the elected parameters. The window --insert will support stack as an option for whenever situations like this arise, where you want the warp operation to insert that window into the stack instead of splitting it.

But I think what you may be getting at is how to represent the behavior in the tree. Do you want to do so by index in the array of stacked windows (meaning you have to modify that array every time a different window is focused), or track it another way?

That's pretty much it, yeah. Visually, the window will be at the top of the stack of course.
I guess this comes down to whether we want to persist window ordering completely, or only move the focused window in a stack into index zero or something.

I think maintaining order separately from focus will allow you to have an API that is also compatible for managing a tabbed UI.

  1. How to focus stacked windows? New command/selectors or make prev/next cycle through stacked windows in order?

I would say as a new command/selector, as this would allow the user to define something like this:

yabai -m window —focus upwards || yabai-m window —focus north

Which would make selecting stacked windows similar to that of the i3 behaviour.

On the same topic, I’d say that an important aspect of making this feature easy to use, would be to give the user visual cues for what effects the navigating the stack will have. This could be achieved by introducing cascading of stacked windows, allowing the user to specify a window region offset defined in pixels. This would allow the user to see the titlebars (or as many pixels of each window as they would like), of each window in the stack, removing a lot of guesswork.

This might introduce complexities regarding zooming stacked windows, however.

This could be achieved by introducing cascading of stacked windows, allowing the user to specify a window region offset defined in pixels.

Cascading is not really something that I consider a good solution because a lot of windows on macOS have size constraints, and this kind of feature will fall apart quickly when more than a couple of windows get stacked.

I don't think upwards/downwards are intuitive names. Sure, it is a stack (maybe group would be a better term), but if you are on window 1 out of 3, and go "downwards", window 2 gets focus and is moved to the top (visually), and then intuitively "downwards" feels like it would re-focus window 1, because it is now below window 2 and above window 3.

Agreed, downwards/upwards certainly aren’t the best descriptors. Dedicated descriptors is probably the way to go though, I believe.

On the topic of cascading. My use case with yabai, is that I only tile whitelisted applications. I do this because most windows simply cannot be tiled properly. Either they’re too large or too small. If I whitelist only the applications I want to have tiled, and which I know can be resized, I can have an experience that feels less hacky.

I’m sure I’m in the minority in regards to this. But with such a use case, cascading the windows certainly makes sense.

Now I’m getting a bit off topic, but is this use case something you might be interested in supporting more fully in the future (There are some things which doesn’t align well with this idea right now)? If so, I’d be happy to work on that, including optionally adding cascading support to stacked windows.

One thing I'd like to see for this is mouse support à la #142. Maybe there should be an option to have the center drag be stack instead of swap now.

@koekeishiya
How to focus stacked windows? New command/selectors or make prev/next cycle through stacked windows in order?

I've got a few thoughts on this:

  • prev / next should be untouched
  • for custom focus commands (window query -> window id through some jq filter) to work properly, the stack depth of a window must be included in window queries
  • focusing within a stack should be explicit, and all of these I can see being useful: stack-next, stack-prev, stack-first, stack-last, stack-recent

Edit: One further thought:

What if the window --focus command could take multiple selectors as fallbacks? There's currently a lot of overhead involved when doing yabai -m window --focus next || yabai -m window --focus first, and if there was a built-in fallback this would be much easier. Something like yabai -m window --focus next:first. Not quite sure how this interacts with window labels as window selectors, but I can see this being very useful.

@jonatan-branting

Now I’m getting a bit off topic, but is this use case something you might be interested in supporting more fully in the future (There are some things which doesn’t align well with this idea right now)?

That depends on how well it can be integrated. I don't want to support niche features that don't feel appropriate in the overall experience that I am going for. I understand that it might work fine in the way you use yabai, but it doesn't really fit the model that I am currently going for based on my personal usage. At this point I have been able to do a lot more things than I had ever hoped, so I am not going to say that it is impossible to support something like that in the future; I just don't see a way right now.

@dominiklohmann

I've got a few thoughts on this:

prev / next should be untouched

I ended up with the same conclusion. I think it is important to have a clear understanding of how/when a selector/operation is affecting a stack and when it targets a single window, which I think is fairly easy to understand with the current implementation.

for custom focus commands (window query -> window id through some jq filter) to work properly, the stack depth of a window must be included in window queries

I assume this would be the index of the window in the stack, or?

What if the window --focus command could take multiple selectors as fallbacks? There's currently a lot of overhead involved when doing yabai -m window --focus next || yabai -m window --focus first, and if there was a built-in fallback this would be much easier. Something like yabai -m window --focus next:first. Not quite sure how this interacts with window labels as window selectors, but I can see this being very useful.

I have mixed thoughts on this, but we can surely move that into a separate suggestions issue for now.

One thing I'd like to see for this is mouse support à la #142. Maybe there should be an option to have the center drag be stack instead of swap now.

This would make for an interesting decision. Will give it some thought while iterating on this implementation.

for custom focus commands (window query -> window id through some jq filter) to work properly, the stack depth of a window must be included in window queries

I assume this would be the index of the window in the stack, or?

Yes, that would suffice.

What if the window --focus command could take multiple selectors as fallbacks? [...]

I have mixed thoughts on this, but we can surely move that into a separate suggestions issue for now.

I'll flesh the thought out a bit and post it with a better rationale again should this really become an issue.

Re: prev/next...I don't know exactly how you're representing the tree, but consider the following example if you wanted to iterate in alphabetical order with prev/next, where EFG is a stack:

+---+-----+---+
| A |     |   |
+---+  D  |   |
|B|C|     |   |
+---------+ H |
|         |   |
|   EFG   |   |
|         |   |
+---------+---+

Which could be represented either with

node: { children: node[] }
window extends node { type: 'window' }
container extends node { type: 'container' }
          *
         / \
        *   H
       / \  
      *   \     
     / \   \ 
    /   D   *
   /       /|\   
  *       E F G
 / \     
A   *   
   / \
  B   C

or

node: { windows: window[], children: node[] }
stack extends node { focusedIndex: number }
        *
       / \
      *   H
     / \  
    *  [EFG]  
   / \     
  *   D
 / \     
A   *   
   / \
  B   C

In either case, does it not make sense for prev/next to simply iterate depth-first?

Additionally, after thinking through this, the less I like the idea of focusing a window by manipulating its index among its siblings in the tree.

Focusing normally split windows (like B and C) doesn't manipulate their indices. I think consistency here would be good, as well as be more accommodating for other features like tabbed or cascading windows.

Thanks for your work on this feature @koekeishiya! I started playing around with implementing a "monocle mode" script using stack functionality but there's one thing I'm missing (probably a gap in my understanding).

My use case would be to move all the managed windows on a space into a stack. That is simple enough:

current_windows=($(yabai -m query --spaces --space | jq -re .windows[]))

for window in ${current_windows[@]}; do
  yabai -m window --stack $window
done

But when I want to "toggle back" to a tiled view I get stuck. I try warping as suggested but there are not any other managed and un-stacked windows to warp.

> yabai -m window --warp east
could not locate a eastward managed window.

Either I'm missing something or perhaps I'm jumping the gun?

Monocle will be implemented as a native layout using stacking as its backing implementation, #337

stack for the center mouse drop action feels really good to me. Just have to figure out some better shortcuts for cycling windows now.

I'm fairly satisfied with this implementation as of now. The only thing that really bothers me is that it is not actually possible to properly set the initial focused window in the stack when a window is added, because it is possible to add a unfocused window to an unfocused stack and in these scenarios there is no way to actually identify whether the window is ordered above the other window or not. This isn't a major issue by any means, but it's annoying knowing that it's there..

The only thing I'd still like to see is some visual indicator for stacks. I still think ever so slightly cascading windows would work best. I know this reduces the region available to every window in the stack for the cascading step size multiplied by the size of the stack, but I think this would increase the usability a fair bit.

I have to disagree. I don't think the complexity of having cascaded windows is worth the minor benefit.
Say you have a stack of 3 windows, initially it would look something like:
Screenshot 2020-07-01 at 11 32 17

Which is fine. Then you switch to the first window in the stack, and it covers up all the cascaded windows anyway:
Screenshot 2020-07-01 at 11 32 35

I would rather draw a vertical bar at the left of the stack or something that is split into x sections, representing the number of windows in the stack, and have the section corresponding to the active window be colored differently or something.

Then you switch to the first window in the stack, and it covers up all the cascaded windows anyway:

For this to work properly, the windows would need to be rotated within the stack. E.g., the currently focused is always at the lowest y-coordinate in the stack, and the window that's stack.next from there is the next window after that.

That makes more sense, but I'm still not sold on the idea. I think it won't work all that well in practice because of the issue that I mentioned previously regarding adding newly stacked windows, combined with the fact that the user can now click on a window to make it become the focused window in its stack or even just attempt to drag the window to do a warp, and while doing so we would move & resize it to reposition it within the stack. It would also require us to do at lest 4x (increases linearly as the stack-size grows) more AX API calls every time a window in a stack is focused, which I think will be fairly noticable depending on which applications we interact with, and system specs/current load. Not saying that some of these issues aren't solvable though.

You can reduce the calls to the AX API a bit by having the background windows in the stack have the target size as well, just moved up. That way, no resize is needed, just a move. Since resize is usually the most expensive operation, this might help a lot.

stack for the center mouse drop action feels really good to me. Just have to figure out some better shortcuts for cycling windows now.

Sorry if this is a really obvious question - but how does one activate center mouse drop ? What’s the gesture - particularly with a trackpad?

FWIW I hope that a cascading feature would be optional if implemented. Definitely not a fan of it for my use case, but I can see the appeal.

commented

@koekeishiya As I tested using three stacked windows, all their stack-index values are 1. You mentioned this value reflects the stack depth of a window, so aren't they 1, 2, 3 respectively? Is this an issue?

Another thought: after supporting the stack layout feature, are you planning to bring in the tabbed layout (it is the same as stack but having a UI to list the titles of all the stacked windows so that we could recognize the windows easier)?

Thank you for your hardworking to make yabai shinier. 👍

If it's possible... I too would prefer a "tabbed" UI vs cascading.

Just to put my above proposal into pictures.. this is of course just something i put together to showcase what I would prefer going for instead of cascading windows, and it would obviously look way better and more polished if I actually sit down and implement this:

Screenshot 2020-07-01 at 21 25 34

Now you can imagine there maybe being x amount of chars of the windows title, or whatever, instead of just a color indicator. This could easily be changed to drawing a vertical bar for each window that could potentially keep its full title (but would occupy more vertical space).

commented

It looks nice. For that blue part on the left, it has two windows there. I think it is better to add an indicator/delimiter (like a |) to indicate different windows.

Personal opinion—gotta say I'm really not a fan of having a feature that's not 100% usable without drawing on the screen. I really enjoy the clean look without borders, because the borders really don't feel native to macOS at all. If something needs to be drawn, I'd prefer an overlay window that's actually using native UI elements. Hence the cascading suggestion.

commented

I don't like the border either because it performs not that native to macOS as Dominik mentioned (like the border still stays there for a while after the window is closed, or it delays when I move or resize the window, or some application has a big round corner and the border cannot fit, and etc). Assuming the bar is implemented using the same mechanism as the window border, I am worrying that the bar would have similar troubles. For example, when I close or open a window, the bar would have a delayed reflection.

As you can see this is why I have not implemented this feature before, because frankly there is no good solution here.

It looks nice. For that blue part on the left, it has two windows there. I think it is better to add an indicator/delimiter (like a |) to indicate different windows.

That is one amongst many other things that would be improved upon if I actually where to do it that way, yes. The picture above is simply to pitch the idea. The actual graphics is not limited to drawing simple colors; you can draw whatever you want.

Just my 2¢, but I think the cascading windows looks great.

Assuming there are 3 windows in the stack, perhaps when the first window is selected, it could be brought to the front and the other 2 windows could be shown cascading behind it. This way, no matter which window you select, you could still see the other windows behind it.

commented

For the cascading, I think it is hard to indicate how many windows are in the stack currently, especially when the stack has a large amount of windows. I remember someone said he will open 2-8 IDEs (somewhere a comment above), and if so, the cascading will make a mess (see the attached screenshot below) and be not that pretty, and it will be very hard to tell which window you want to switch to. Also, it is difficult to integrate the tabbed layout if we use cascading. This is just my personal opinion. Thanks.

image

To clarify; the main reason why I don't want to do cascading windows is that it will be a very sluggish experience, for reasons mentioned above (e.g: more calls to the AX API). It is also actually not possible for us to say that window A should be ordered in front of window B, without also making window A the front-most window (aka it steals input focus and ruin focus history). This becomes a problem when you e.g focus a window in the middle of the stack and we are supposed to re-order the windows so that the stack is visually correct, both in terms of where the focused window is placed, and where the prev/next (recursive operation) window is placed.

I also agree that unless the offset between each window is fairly large it is not exactly easy to spot which application is actually there, which again makes this feature less valuable from my pov, and as usual, I find the net gain in this tradeoff to be fairly low compared to the cost.

I see. What about a compromise? We can draw bars (in a different color than the selected border color) on the top and bottom of the stack to show the position within the stack and also show the number of windows above and below the selected window.

Sorry about the poor mock up, btw :)
Untitled_Artwork

I usually only stack windows that belong to the same application (browser, IDE, etc.) and cycle through them with cmd+`, or yabai's focus commands. The current implementation with windows stacked perfectly over one another (hiding the lower layers) is perfect for my use-case:

  • It looks clean,
  • has never impeded my mental model of where my windows are,
  • and when it's available, noop is always the most performant solution ;)

Personal opinion—gotta say I'm really not a fan of having a feature that's not 100% usable without drawing on the screen

I totally understand this is usually a first-line requirement for building software UIs. If this was commercial software, I would fully agree. But consider Yabai's audience—I don't think this would cause deeply technical users any heartache. Just ignore it if you don't want to use it.

In any case, I would propose that it's fine to ship as currently specified, and tabs or window cascades could be broken out into different tickets for further discussion.

commented

I should say SxC97's compromised method is not a good choice. The reason is the same as my comment above. If a large number of windows e.g., 8-10 windows in a stack, there will be many bars on the top and bottom of the stack. Imagine that there are 9 bars on the top of the stack. Oh man, that's not pretty. :P

Also, again I think the tabbed layout is very necessary, especially when the stack has a large number of windows in it because we can easily tell the title of each window and the position of the window that we want to switch to. However, I know it is really hard to come up with a feasible solution to implement it.

This might sound complicated but how about a vertical dock-like appearance?

Icons could be displayed with title under them based on a setting yabai -m config [--space #] stack_icon_title_visible on/off

Positioning could be handled with a stack_dock_top_offset which would be a percentage offset starting from the top of the stack. This would be useful for spaces where you have little to no spacing between the window and the screen border, so the dock would be drawn on top of the stack windows. To minimise hidden window content, the user could adjust the offset so that it overlaps less useful content.

Maybe a stack_dock_side left/right would also be useful because on some windows the right side is emptier and allows for less annoying overlap.

Icons without title

stack_dock_top_offset 0

This could be useful for spaces where you have multiple different apps stacked, like social/video-conferencing apps.

yabai-stack-no-title-fs8

Icons with title

stack_dock_top_offset 10%

This could be useful for multiple stacked IDEs/editors where you need the most screen real estate.

yabai-stack-fs8

Multiple stacks

stack_dock_top_offset 5%

Useful for working/developing and previewing results side by side.

yabai-stack-multiple-fs8

I think I agree with @kvcrawford. This will probably be released without a visual indicator for now, and that can be discussed further. Suggestions by @alin23 look fairly good imo, but as mentioned require quite some work to actually implement.

I wonder if using Ubersicht is an option for the visual indication?

@zweck Hammerspoon might be slightly better for this job, since it is capable of drawing grpahics above all other windows (there's an open issue on Ubersicht regarding this).

Here's a teaser of @alin23 's idea implemented in hammerspoon (👏 for the design thinking!). I think the vertical orientation matches the mental model of stacks existing along the "z" dimension better than horizontal tabs.

The gif shows two different styles of vertical side tabs: one with app icons, and another with thin vertical tabs. I prefer the latter, as it requires less screen real estate, is less visually distracting, and does not cover any of the active window's contents. Also, in-use, I've not had difficulty locating windows in a stack using only the little pill indicator. Conversely, I'm unable to use stacks at all when there's no indication that additional windows are hiding below.

stack-indicators

If there's any interest, I'll clean up the lua and share on Github.

@AdamWagner there absolutely is interest from me! Please if you have time share on gh 😀

@AdamWagner that looks awesome! I'm already using Hammerspoon but I never used its advanced features, I had no idea you can do this with it. Please share this, maybe we can contribute to make it the de facto way to use stacks ^_^

@alin23 @zweck – thanks for the motivation. I'll share out a repo this weekend. I simply need to extract the stack module from the messy web of my Hammerspoon config.

Standard disclaimer: This is my first time working with lua (really, I was searching for how to "concatenate two lists" … err, "tables"), so I expect lots of room to contribute.

In addition to improving the basic approach/structure, there is a lot of weak/missing functionality: indicators are not properly redrawn when window is moved/resized, indicator position is not configurable, indicator bg color does not auto-adjust based on wallpaper to ensure legibility, etc. I'll create issues for these and other changes I've been considering.

@AdamWagner I don't think anybody is expecting code quality here, I certainly don't.
We just want this functionality and we want it now 😅

Definitely, there would be room for improvement, but I think it would be a good service for all yabai users to get a head start with your repo.

Also I think Hammerspoon is a better choice for this than doing it in yabai since it already has APIs for creating arbitrary windows, which would have to be custom implemented in yabai (similar to the initial statusbar).

And yeah, tables.. weird choice to have a single data structure for both lists and dictionaries

@alin23 @zweck – Ok, here's a repo with the code to reproduce the gif demo I shared above:

https://github.com/AdamWagner/stackline

My plan for the rest of the day:

  • Add a lot more comments throughout
  • Create issues for the major broken / incomplete pieces

@AdamWagner Thanks for all the work in publishing this! It already looks so polished even if it's just a POC. I'll make sure to help with some PRs as I continue to use it in the following days.

Also, I love the Thanks to contributors! section, very much appreciated and also helpful in finding people to follow/support if users like the software.

@alin23 The whole process has been pretty fun – thanks for the design!

I just pushed "the big refator" to this branch. If you find a moment in the next week or so, it'd be great to get your critique: https://github.com/AdamWagner/stackline/tree/feature__query-windows-via-hammerspoon

Updated docs and changelog to reflect the changes made for this issue. I have yet to be able to fully test this feature; did anyone encounter any bugs as a result of these changes, or does it seem fine?

is it possible to have side by side or 2 multiple stack column? right now i'm running full-screen stack without any issues

every node in the bsp layout can be turned into a stack, so you can have as many stacks side by side, vertically, horizontally, as deeply nested as you want.

Updated docs and changelog to reflect the changes made for this issue. I have yet to be able to fully test this feature; did anyone encounter any bugs as a result of these changes, or does it seem fine?

For me it works perfectly fine. It's become a feature that I use daily. I especially like that I can have a stack of terminals on the left and a stack of text editors on the right, I love that this is supported natively.

And @AdamWagner's Stackline makes this even more useful as it is easy to visualise where in the stack you are and move through the stack using actions like:

yabai -m window --focus $(yabai -m query --windows --space | jq -r 'map(select(."stack-index" == 2)) | .[0].id')

The stack index wouldn't make much sense if I wouldn't be able to visualise the window order in the stack.

I spent some time adding basic stack support to my config last night and so far it's been completely solid for me. I don't have a use case for inserting or other more complicated stacking workflows, but as far as toggling a space layout, managing a bsp node, and cycling through the stack, it's been rock solid.

@AdamWagner's Stackline looks invaluable for stacks. Thanks for creating that!

@koekeishiya stacking has also been super solid for me for the last 27 days (running yabai-v3.2.1 currently). 👏 thanks so much for yabai in general, and the stacking functionality in particular.

Stacking specfically has changed how I use macOS much more than I anticipated:

  • I'm using stacking to address the use-case for "Browser tab groups". When I want to isolate a set of tabs (or maybe am just accumulating too many tabs in a window), I'll select a range of tabs, drag then out of the window, and stack the new window with its parent.
  • I use far fewer spaces now. I've found stacking to be superior to spaces for tasks with one primary window and a set of secondary windows. E.g., Kitty | [Hammerspoon console, Qutebrowser]
  • Stacking has completey replaced zoom-parent and zoom-fullscreen for me. I always struggled with forgetting about the windows occluded by zoom-*, and sometimes opened a duplicate window instead of switching to the one that was already open. Stacking (plus stackline ;-) is strictly better than zooming, I think.

The only downside I've noticed is that actions that resize a stack (resize, change gaps/padding, rotate space, etc) is noticably slower than the same action executed on an unstacked window or space with only unstacked windows. I think it's noticable because stacked windows resize at slightly different times, often revealing portions of the unfocused windows "below" the top window in fitful flashes. I only mention this for two reasons:

  1. It's intuitive to me that stacks would slow down these ops (gotta move more stuff!), so it'd be helpful to know if this is unexpected or you suspect an issue with my environment or setup.
  2. I have an idea that could help mask the visual artifacts of slower resize ops on stacks: perform resize ops on the lowest stacked window first, and the topmost/focused stacked window last. If there was a way to ensure that the focused stacked window always resized after windows below, it would mask the jankiness. This idea is probably wack, but thought I'd share.

Thanks again for all your work on yabai, @koekeishiya & @dominiklohmann !

The only downside I've noticed is that actions that resize a stack (resize, change gaps/padding, rotate space, etc) is noticably slower than the same action executed on an unstacked window or space with only unstacked windows.

So this is unfortunately a macOS limitation. The AX API that we have to use to move and resize windows is blocking, meaning that we can only interact with a single instance at any time (even if they belong to separate applications). I did implement a work queue to parallelize this operation and verify that this limitation does in fact exist.

Masking visual artifacts by resizing/moving the lowest stacked windows first might be doable without much work.

So this is unfortunately a macOS limitation.

Thanks @koekeishiya. I thought as much.

Masking visual artifacts by resizing/moving the lowest stacked windows first might be doable without much work.

Very cool!

guys, how to cycle through all windows no matter where they are in either bsp node or stack. i just want to have a single shortcut such as shift-tab to quickly jump to a certain window without thinking too much.

I use the following hotkeys to cycle forwards and backwards in the windows on the current space:

# Cycle windows forwards
ralt - 0x1E : yabai -m query --spaces --space \
  | jq -re ".index" \
  | xargs -I{} yabai -m query --windows --space {} \
  | jq -sre "add | sort_by(.display, .frame.x, .frame.y, .id) | reverse | nth(index(map(select(.focused == 1))) - 1).id" \
  | xargs -I{} yabai -m window --focus {}

# Cycle windows backwards
ralt - 0x21 : yabai -m query --spaces --space \
  | jq -re ".index" \
  | xargs -I{} yabai -m query --windows --space {} \
  | jq -sre "add | sort_by(.display, .frame.x, .frame.y, .id) | nth(index(map(select(.focused == 1))) - 1).id" \
  | xargs -I{} yabai -m window --focus {}

They are mapped to Right Option + [/].
I can't remember where I found them, it's possible Dominik Lohmann posted them in another issue.

@alin23 that's exactly what i'm using atm. before stack support, this work really well coz i just need map either one of those to cycle windows. now if i use both bsp and stack (i don't mean fullscreen stack layout which works as expected), just like you, i have to use two hotkeys to cycle windows forwards and backwards.

@babygau, you can use the exit codes of the commands to combine stack traversal + normal BSP nodes. For example, I have these two bindings in my skhdrc:

alt - p : yabai -m window --focus stack.prev || yabai -m window --focus prev || yabai -m window --focus last
alt - n : yabai -m window --focus stack.next || yabai -m window --focus next || yabai -m window --focus first

How do you write a yabai -m command for reordering the windows in a stack? I tried doing this, analogous to the swap keybindings I have for bsp windows, but it returns "the acting window is not within a bsp space."

# swap stacked window
rctrl + shift - n : yabai -m window --swap stack.next || yabai -m window --swap stack.first
rctrl + shift - p : yabai -m window --swap stack.prev || yabai -m window --swap stack.last

stacked windows cannot be reordered currently.

Sorry, its a long thread and didn't have time to quite go through all of it.

Is it now possible to have two stacks on a screen and use hotkeys to navigate each?

yes, you can do it 👍

Thanks for this new mode!

Is there a way to make this work? I can't seem to find the right combination or ordering (allow cycling through both BSP and stack mode):

cmd - tab : yabai -m window --focus next || yabai -m window --focus stack.next || yabai -m window --focus first || yabai -m window --focus stack.first

@peppy Is this what you are trying to do?

# focus the next stacked window if possible; otherwise focus the next window or go back to the first window
cmd - tab : yabai -m window --focus stack.next || yabai -m window --focus next || yabai -m window --focus first

It's not possible to have a single bind that can both navigate between windows and also cycle within a stack (that's what your sample above looks like it wants to do).

I'm trying to do the thing you say is not possible, yeah. Since I was trialling having some layouts BSP and some stacking.

Thanks for confirming, I guess I'll need to maintain two binds for now.

every node in the bsp layout can be turned into a stack, so you can have as many stacks side by side, vertically, horizontally, as deeply nested as you want.

What's the command to do this?

@danielfalbo

yabai -m window --stack north|east|south|west

Thank you!

@koekeishiya, do you mind explaining to me? I don't know but cycling through windows in bsp and stack suddenly works for me with just one keybinding.

bf66d7b0d8a5a41fb2adc0bf2d319206

In the short clip above, I have:

  • 4 windows in a BSP space
  • 3 stack on the left (2 Firefox windows, 1 Finder window)
  • 1 Firefox window on the right.
  • hyper-; bound to @dominiklohmann snippet.

Sorry for not being aware of this thread before the mention, this really should've gone into a new issue asking how to have a single keybind for cycling instead of repurposing this thread.

Here's how I do it:

# forward
yabai -m query --spaces --space \
  | jq -re ".index" \
  | xargs -I{} yabai -m query --windows --space {} \
  | jq -sre "add | map(select(.minimized != 1)) | sort_by(.display, .frame.y, .frame.x, .id) | reverse | nth(index(map(select(.focused == 1))) - 1).id" \
  | xargs -I{} yabai -m window --focus {}

# backward
yabai -m query --spaces --space \
  | jq -re ".index" \
  | xargs -I{} yabai -m query --windows --space {} \
  | jq -sre "add | map(select(.minimized != 1)) | sort_by(.display, .frame.y, .frame.y, .id) | nth(index(map(select(.focused == 1))) - 1).id" \
  | xargs -I{} yabai -m window --focus {}

This is a slight deviation from my snippet someone posted earlier with a fix for minimized windows. I've been using this myself and it works as expected.

The reason why this works is because it doesn't try to focus the visible window on a stack, but rather performs a stable sort on all windows of the space and just focuses the next entry in the sorted list (with wraparound).

@dominiklohmann, I just tried it, and it worked perfectly. This snippet deserve to be listed on Wiki Page Tips and Tricks.

Shout-out! - Very nice contribution.

Is there a way to unstack only a single window?
wrap works if there already is another peer node already, but often I end up with a single stack node and want to pop-up one window?
Right now my work-around is to set yabai -m space --layout bsp, but then the entire tree gets unstacked.
Thanks for any help!

Shout-out! - Very nice contribution.

Is there a way to unstack only a single window?
wrap works if there already is another peer node already, but often I end up with a single stack node and want to pop-up one window?
Right now my work-around is to set yabai -m space --layout bsp, but then the entire tree gets unstacked.
Thanks for any help!

@stefandeml what about yabai -m window --toggle float ? If your space layout is bsp, doing it two times should do the job

Shout-out! - Very nice contribution.
Is there a way to unstack only a single window?
wrap works if there already is another peer node already, but often I end up with a single stack node and want to pop-up one window?
Right now my work-around is to set yabai -m space --layout bsp, but then the entire tree gets unstacked.
Thanks for any help!

@stefandeml what about yabai -m window --toggle float ? If your space layout is bsp, doing it two times should do the job

sweet - indeed, that does the job. Thanks

Apply to yabai yabai-v4.0.0

# alt-k
alt + shift - k : yabai -m query --spaces --space \
  | jq -re ".index" \
  | xargs -I{} yabai -m query --windows --space {} \
  | jq -sre 'add | map(select(."is-minimized"==false)) | . as $array | length as $array_length | index(map(select(."has-focus"==true))) as $has_index | if $has_index > 0 then nth($has_index - 1).id else nth($array_length - 1).id end'  \
  | xargs -I{} yabai -m window --swap {}

# alt-j
alt + shift - j : yabai -m query --spaces --space \
  | jq -re ".index" \
  | xargs -I{} yabai -m query --windows --space {} \
  | jq -sre 'add | map(select(."is-minimized"==false)) | . as $array | length as $array_length | index(map(select(."has-focus"==true))) as $has_index | if $array_length - 1 > $has_index then nth($has_index + 1).id else nth(0).id end' \
  | xargs -I{} yabai -m window --swap {}

# alt-k
alt - k : yabai -m query --spaces --space \
  | jq -re ".index" \
  | xargs -I{} yabai -m query --windows --space {} \
  | jq -sre 'add | map(select(."is-minimized"==false)) | . as $array | length as $array_length | index(map(select(."has-focus"==true))) as $has_index | if $has_index > 0 then nth($has_index - 1).id else nth($array_length - 1).id end'  \
  | xargs -I{} yabai -m window --focus {}

# alt-j
alt - j : yabai -m query --spaces --space \
  | jq -re ".index" \
  | xargs -I{} yabai -m query --windows --space {} \
  | jq -sre 'add | map(select(."is-minimized"==false)) | . as $array | length as $array_length | index(map(select(."has-focus"==true))) as $has_index | if $array_length - 1 > $has_index then nth($has_index + 1).id else nth(0).id end' \
  | xargs -I{} yabai -m window --focus {}
commented

Apply to yabai yabai-v4.0.0

# alt-k
alt + shift - k : yabai -m query --spaces --space \
  | jq -re ".index" \
  | xargs -I{} yabai -m query --windows --space {} \
  | jq -sre 'add | map(select(."is-minimized"==false)) | . as $array | length as $array_length | index(map(select(."has-focus"==true))) as $has_index | if $has_index > 0 then nth($has_index - 1).id else nth($array_length - 1).id end'  \
  | xargs -I{} yabai -m window --swap {}

# alt-j
alt + shift - j : yabai -m query --spaces --space \
  | jq -re ".index" \
  | xargs -I{} yabai -m query --windows --space {} \
  | jq -sre 'add | map(select(."is-minimized"==false)) | . as $array | length as $array_length | index(map(select(."has-focus"==true))) as $has_index | if $array_length - 1 > $has_index then nth($has_index + 1).id else nth(0).id end' \
  | xargs -I{} yabai -m window --swap {}

# alt-k
alt - k : yabai -m query --spaces --space \
  | jq -re ".index" \
  | xargs -I{} yabai -m query --windows --space {} \
  | jq -sre 'add | map(select(."is-minimized"==false)) | . as $array | length as $array_length | index(map(select(."has-focus"==true))) as $has_index | if $has_index > 0 then nth($has_index - 1).id else nth($array_length - 1).id end'  \
  | xargs -I{} yabai -m window --focus {}

# alt-j
alt - j : yabai -m query --spaces --space \
  | jq -re ".index" \
  | xargs -I{} yabai -m query --windows --space {} \
  | jq -sre 'add | map(select(."is-minimized"==false)) | . as $array | length as $array_length | index(map(select(."has-focus"==true))) as $has_index | if $array_length - 1 > $has_index then nth($has_index + 1).id else nth(0).id end' \
  | xargs -I{} yabai -m window --focus {}

Hi @kola-web
This is working perfectly thanks. I am wondering the reason you create this is because in Yabai 4

alt - h : yabai -m window --focus west

Not working?

适用于yabai yabai-v4.0.0

# alt-k
alt + shift - k : yabai -m query --spaces --space \
  | jq -re ".index" \
  | xargs -I{} yabai -m query --windows --space {} \
  | jq -sre 'add | map(select(."is-minimized"==false)) | . as $array | length as $array_length | index(map(select(."has-focus"==true))) as $has_index | if $has_index > 0 then nth($has_index - 1).id else nth($array_length - 1).id end'  \
  | xargs -I{} yabai -m window --swap {}

# alt-j
alt + shift - j : yabai -m query --spaces --space \
  | jq -re ".index" \
  | xargs -I{} yabai -m query --windows --space {} \
  | jq -sre 'add | map(select(."is-minimized"==false)) | . as $array | length as $array_length | index(map(select(."has-focus"==true))) as $has_index | if $array_length - 1 > $has_index then nth($has_index + 1).id else nth(0).id end' \
  | xargs -I{} yabai -m window --swap {}

# alt-k
alt - k : yabai -m query --spaces --space \
  | jq -re ".index" \
  | xargs -I{} yabai -m query --windows --space {} \
  | jq -sre 'add | map(select(."is-minimized"==false)) | . as $array | length as $array_length | index(map(select(."has-focus"==true))) as $has_index | if $has_index > 0 then nth($has_index - 1).id else nth($array_length - 1).id end'  \
  | xargs -I{} yabai -m window --focus {}

# alt-j
alt - j : yabai -m query --spaces --space \
  | jq -re ".index" \
  | xargs -I{} yabai -m query --windows --space {} \
  | jq -sre 'add | map(select(."is-minimized"==false)) | . as $array | length as $array_length | index(map(select(."has-focus"==true))) as $has_index | if $array_length - 1 > $has_index then nth($has_index + 1).id else nth(0).id end' \
  | xargs -I{} yabai -m window --focus {}

@kola-web 这工作得很好,谢谢。我想知道你创建这个的原因是因为在 Yabai 4

alt - h : yabai -m window --focus west

不工作?
It should be because I think it is more convenient for me to use the second phase that takes up less shortcut keys.

I ended up going with this alternative (yabai 4.0):

cmd + shift - k : if [ "$(yabai -m query --spaces --space | jq -r '.type')" = "stack" ]; then (yabai -m window --focus stack.next || yabai -m window --focus stack.first); else yabai -m window --focus next || yabai -m window --focus first; fi
cmd + shift - j : if [ "$(yabai -m query --spaces --space | jq -r '.type')" = "stack" ]; then (yabai -m window --focus stack.prev || yabai -m window --focus stack.last); else yabai -m window --focus prev || yabai -m window --focus last; fi

The alternative suggested above didn't have the behavior I expected. With alt-j it would only toggle between the last two windows instead of circulating back through the windows.

@nilsolofsson I found that yours is not working with windows hidden in stack, @kola-web solution is working seamlessly:

alt + k = yabai -m query --spaces --space |
  jq -re ".index" |
  xargs -I{} yabai -m query --windows --space {} |
  jq -sre 'add | map(select(."is-minimized"==false)) | sort_by(.display, .frame.y, .frame.x, .id) | . as $array | length as $array_length | index(map(select(."has-focus"==true))) as $has_index | if $has_index > 0 then nth($has_index - 1).id else nth($array_length - 1).id end' |
  xargs -I{} yabai -m window --focus {}

alt + j = yabai -m query --spaces --space |
  jq -re ".index" |
  xargs -I{} yabai -m query --windows --space {} |
  jq -sre 'add | map(select(."is-minimized"==false)) | sort_by(.display, .frame.y, .frame.x, .id) | . as $array | length as $array_length | index(map(select(."has-focus"==true))) as $has_index | if $array_length - 1 > $has_index then nth($has_index + 1).id else nth(0).id end' |
  xargs -I{} yabai -m window --focus {}

@bangedorrunt thx this works better
here's it properly formatted for .skhrc

alt - k : yabai -m query --spaces --space \
  | jq -re ".index" \
  | xargs -I{} yabai -m query --windows --space {} \
  | jq -sre 'add | map(select(."is-minimized"==false)) | sort_by(.display, .frame.y, .frame.x, .id) | . as $array | length as $array_length | index(map(select(."has-focus"==true))) as $has_index | if $has_index > 0 then nth($has_index - 1).id else nth($array_length - 1).id end' \
  | xargs -I{} yabai -m window --focus {}

alt - j : yabai -m query --spaces --space \
  | jq -re ".index" \
  | xargs -I{} yabai -m query --windows --space {} \
  | jq -sre 'add | map(select(."is-minimized"==false)) | sort_by(.display, .frame.y, .frame.x, .id) | . as $array | length as $array_length | index(map(select(."has-focus"==true))) as $has_index | if $array_length - 1 > $has_index then nth($has_index + 1).id else nth(0).id end' \
  | xargs -I{} yabai -m window --focus {}

Is it currently possible to stack windows with the mouse (using a modifier key)?

commented

I was also looking to do this with the mouse, was hoping for something like

yabai -m config mouse_action1 stack

in my yabairc file

yabai -m config mouse_drop_action stack

Everything is described here: https://github.com/koekeishiya/yabai/blob/master/doc/yabai.asciidoc although it may be a bit terse.

The following solution doesn't work for me. Do I need to have SIP disabled to have it work?

alt - k : yabai -m query --spaces --space \
  | jq -re ".index" \
  | xargs -I{} yabai -m query --windows --space {} \
  | jq -sre 'add | map(select(."is-minimized"==false)) | sort_by(.display, .frame.y, .frame.x, .id) | . as $array | length as $array_length | index(map(select(."has-focus"==true))) as $has_index | if $has_index > 0 then nth($has_index - 1).id else nth($array_length - 1).id end' \
  | xargs -I{} yabai -m window --focus {}

alt - j : yabai -m query --spaces --space \
  | jq -re ".index" \
  | xargs -I{} yabai -m query --windows --space {} \
  | jq -sre 'add | map(select(."is-minimized"==false)) | sort_by(.display, .frame.y, .frame.x, .id) | . as $array | length as $array_length | index(map(select(."has-focus"==true))) as $has_index | if $array_length - 1 > $has_index then nth($has_index + 1).id else nth(0).id end' \
  | xargs -I{} yabai -m window --focus {}

I'm disabled

@kola-web thx for your quick reply. I had a mistake in my config.

The solution above from @bangedorrunt stays always on the same display. How could I switch the display when the last window of the stack or the furthermost window of the split is selected? E.g. focus display east if the if there is no window more east or the last window of the stack is selected.

@JustSaX Sorry, my English is not good and I don’t quite understand what you mean. You can try my yabai configuration to see if it can solve your problem.

Is there support for focusing the nth window in a stack? For example, if you have 7 windows in a stack, then you could focus the 4th window in that stack with one keystroke instead of having to cycle through multiple windows. This would allow for faster navigation, especially if you're constantly navigating between windows in a stack. I was thinking of something like this:

yabai -m window --focus stack.1
yabai -m window --focus stack.2
.
.
.
yabai -m window --focus stack.9

None of my windows has a property focused is that only available when you have them stacked?

None of my windows has a property focused is that only available when you have them stacked?

I created a PR to address the issue I was talking about: #2295

Implemented on master 84a41c9 #2342