Handle native tabs properly
dominiklohmann opened this issue · 51 comments
Issue
NSDocument-based applications send window created notification on tab creation.
This is a re-iteration of the same issue with chunkwm (https://github.com/koekeishiya/chunkwm/issues/235).
I have created another radar for this issue now that the Catalina beta is a thing. This time around, I got an answer: Works as expected.
iPad apps brought to the Mac using Catalyst have the same behaviour, so this should probably be fixed before the release of macOS 10.15 Catalina.
Proposed fix
Investigate how Amethyst works around this and replicate the behaviour in yabai.
Here's what they're doing from my understanding: When a kAXWindowCreated
notification triggers, check whether the window has the same position and same size as another window of the same application. For applications like Terminal, some tolerance is needed because the window size may change ever so slightly when changing tabs.
I don't think the implementation in Amethyst is complete, or fully robust. How does Amethyst handle minimization and deminization of such grouped windows? What about hiding and restoring an application that has a grouped window, and a "normal" window?
I'm also unsure if the same heuristic applied by Amethyst can be used here, because I believe that Amethyst behaves differently in terms of how windows are stored / tracked. IIRC they have some caching mechanism that is flushed and regenerated upon a space activation? I may be mistaken as the last time I looked at Amethyst was maybe 4years ago.
So I just tried Amethyst to see how it behaves with tabbed windows.
- Creating a new tab seems to work fine.
- Closing a tab is fine.
- Switching between tabs are ok.
- Minimize and deminimize seem to work fine.
Hiding an application did not work for me at all, regardless of the application having tabbed windows or not, so I could not verify my assumption there.
I did take a quick glance at the code that is run upon a space change, and it does indeed drop and regenerate some sort of cache of windows that it consider to be current. This is not something that can be replicated in yabai, as we don't have this form of caching. In yabai, a window is tracked from the moment we learn about its existence until it is destroyed, regardless of which space or display or whatever we transition from/to, or consider active.
I'll probably spend some time to observe the behaviour in detail myself, and if I can come up with some solution that can integrate properly with yabai, I will support this. If I cannot do so, I'd rather state that tab integration is not supported. Implementing a partially working, but flawed solution, would be worse than ignoring it completely in my opinion.
There is an additional case here that Amethyst also does not seem to tackle at this moment in time.
When a window is de-tabbed we somehow need to detect this and tile the window.
I spent a bit of time looking into this, and we can actually query information about the tabs using the accessibility API. That seems a lot better to me than having to try and differentiate based on which order we receive notifications in.
Tested some more. The accessibility API is not reliable. All it really gives us is the fact that a tabgroup has been created, and the number of tabs. However, if you go from a non-tabbed window to a window with 3 or more tabs (spamming new tab hotkey). The creation of all but the last tab will not report as part of a tabgroup, as the tabgroup have yet to be created.
Looks like we may have to do some weird combination, or rely on notifications only, which I'm not sure will be good enough.
When a window is de-tabbed we only receive moved and resized notifications for said window.
There is no consistent way to know when these events correspond to a window being de-tabbed, or the window group itself being moved around.
When a window is de-tabbed we only receive moved and resized notifications for said window.
So when a tab bar is present in an NSDocument
-based app, there's an element with a role of AXTabGroup
, which has tab count + 1
children (the tabs themselves and the create tab button). The actual tab buttons have the role AXRadioButton
.
When a window is de-tabbed, this count changes. To associate this with the corresponding window id, there'd need to be a mapping of tab button UI element identifier (does such a thing exist?) to tab window id.
When a window is de-tabbed, this count changes. To associate this with the corresponding window id, there'd need to be a mapping of tab button UI element identifier (does such a thing exist?) to tab window id.
The problem here is that the detabbed window no longer has that tab-group as an accessibility element, so we are not able to know which group it came from. Tracking windows that belong to a group is not possible, at least I have not managed to do it reliably:
Tested some more. The accessibility API is not reliable. All it really gives us is the fact that a tabgroup has been created, and the number of tabs. However, if you go from a non-tabbed window to a window with 3 or more tabs (spamming new tab hotkey). The creation of all but the last tab will not report as part of a tabgroup, as the tabgroup have yet to be created.
Couldn't you created a cache that maps window id -> tab group?
Now when a window is moved/resized, you check if the windows id belonged to a different tab group than it does now.
Couldn't you created a cache that maps window id -> tab group?
I have not managed to do this no, as no API return the id of windows that are the inactive windows in a tab group. When trying to get the elements in a tab group, they all return the window id of the active window as well. I also tried to cache the AXUIElementRef itself, and use the appropriate function to compare them, but that also did not work.
This starts to become a problem when multiple tabs are opened in a short amount of time, as per my comment above.
The only way to access the tabs programmatically seems to be by using [NSWindow tabbedWindows]
, which can only be accessed by its window id from [NSApplication windowWithWindowNumber:]
, which is unavailable from other applications.
It really seems like a position based workaround is the only option: By default, macOS will never spawn a new window with the same coordinates as an existing window of the same application, except for when it's a tab.
Please handle this somehow, many people use tabs and this is a big inconvenience. How does yabai restore proper layout for tabs when switching windows but not when the tab is created in the first place? There's gotta be some way to fix it.
@koekeishiya @dominiklohmann Sorry for the noise but is there any improvement in the area?
I agree that this is a big inconvenience, but as it is yabai would need to be restructured internally for some kind of stacking support (#203, so a tree node can hold multiple windows), and with that in place this change would be easier to make, although still based on workarounds (frame x/y stay the same, w/h may change slightly for some apps like Terminal). Some kind of caching may also need to be introduced, because macOS does not include window ids from background tab windows in query results.
It's just a tough issue to solve correctly and likely to cause the need for further workarounds in the future. ¯\_(ツ)_/¯
In general I think this should still be approached, especially with 10.15 bringing iPadOS apps to macOS with Catalyst, where all document-based apps are gonna have this problem.
@dominiklohmann Thanks for your kindly explanation!
Sent with GitHawk
@dominiklohmann One thing I've seen: open a random app and finder. Add a new tab in finder, now yabai thinks there are three window and splits finder. Close one tab and it restores to normal. Now try again, add a new tab, but instead of closing, switch to another desktop and switch back. Magically, Yabai recognizes that there are only two windows, despite multiple tabs being open. It seems like Yabai just dropped all but the last tab from the tree. If you close the last tab, finder becomes a floating window because the first tab is not part of the tree. I'm sure it's just accessibility API fucking up, but how does the tree just drop a node out of nowhere?
P.s. can you point me to the part where window tree is being managed? Thanks.
I'm sure it's just accessibility API fucking up, but how does the tree just drop a node out of nowhere?
Every single API that macOS exposes (both public and private) stop reporting the window id of windows that are not the active tab, which is why yabai currently assumes that the window no longer exists, and releases said region.
P.s. can you point me to the part where window tree is being managed? Thanks.
Mostly from functions called from src/window_manager.c
and src/space_manager.c
. Handling of events (e.g: window created, window destroyed etc) can be found in src/event.c
I'm sure most people will probably be displeased with this answer, but I don't consider this issue worth more of my time. I have spent probably more than 40+ hours messing around experimenting and looking for robust ways to fully support this throughout the features that yabai make available, and my conclusion is that it is simply not possible (at the level of quality I'd expect it to have).
I also never use this feature, and macOS is not my primary OS anymore. I'll happily remain a part of the discussion If other people are interested in trying to tackle this problem themselves.
I also never use this feature, and macOS is not my primary OS anymore.
I am curious that which OS is now your primary OS? MacOS's WM is a bit frustrating
I think the best course of action would be to expose necessary functionality through rules and then let people write these per application. How to implement this would still be a big problem, maybe just adding a is-tab
option to the rule with some kind of key path to where the tab group would be located in the accessibility hierarchy?
I have spent probably more than 40+ hours messing around experimenting and looking for robust ways to fully support this throughout the features that yabai make available, and my conclusion is that it is simply not possible
I feel your pain! I have invested I think roughly 20 hours into this rabbit hole, and there just ain't no clear approach to deal with these damn tabs. Also this applies only to AppKit/native tabs. Apps that draw their own UI such as web browsers have completely custom behavior that can't be generalized. they are one the biggest use-case for tabs as well.
Figured I would mention that this specific kind of Window tabbing can be disabled globally using the below command:
defaults write NSGlobalDomain NSWindowTabbingEnabled -bool false
Logout and back in after changing this value.
Edit: Only appears to affect some applications, e.g Finder
Now that stacking is a thing I wonder if this can be worked around by having new windows that exactly match the frame of another window from the same application on the same space stack by default (instead of split). At least until the windows are moved or the tree is modified.
I've noticed that Amethyst also fails to tile tabbed windows when in the binary space partition layout, but does not fail in any other layout.
I saw that people created issues which connected to this issue from time to time.
It might make sense to maintain a list of known issues to prevent people from reporting this issue repetitively.
A list of known issues would be the issues here at github.
I mean a minimal list of general issues, which is known exist but lack of elegant solution for this moment.
Here github issues could be e.g., a feature request or documentation request.
I work in bsp mode.
When I create a new terminal tab (I use macOS default terminal app btw) the tab is added to the terminal and the terminal is halved leaving a "hole" to the desktop. Any idea why this is happens and if related to this issue? I'm on Monterey.
@pberto What you mention is exactly what this issue is about. It has not been solved yet
I moved to a new implementation on AltTab (see lwouis/alt-tab-macos#1540 (comment)).
I'm thinking that for yabai, it doesn't solve the situation since it would be best to have a notification-driven approach, since yabai is always on, unlike AltTab.
That being said, it may be acceptable to add a manual task to detect tabs like in AltTab. Users could run it manually or on a schedule, or even with complex scripting. At least they would have a command to run that detects tabs.
Maybe it's already available though, I'm not sure.
Also, another downside of this method is that it doesn't help figure out with tabs are in which tab group, which would be useful for yabai.
As I've not seen anyone mentioning quick workarounds to live with the issue, here is a quick workaround I did for this particular problem. Force a space layout refresh using skhd when creating a new tab.
ctrl + alt + cmd - o : yabai -m space --layout float && yabai -m space --layout bsp
Put this in skhdrc and when creating a tab just press ctrl+alt+cmd+o and it will fix the layout.
Hope this helps someone!
@leonardopennino with your workaround I have my windows rearranged randomly after creating a new tab in the terminal :)
I ended up disabling managing terminal at all:
yabai -m rule --add app="^Terminal$" manage=off
And before that I used to have terminal tabs inside vim :)
Oh I see, I've never tested with more than 2 tabs as I use tmux, and it did not rearrange randomly. Happy that you found a workaround :)
Hey, guys! I have also found that Hyper terminal tabs work fine on OSX. So I now switched to it. Tell if you know other terminal emulators that properly handle tabs with Yabai.
Works:
- hyper;
- zoc;
- tabby;
- wezterm;
- iTerm2;
Doesn't work:
- kitty;
- iTerm;
- alacritty;
to @Liverm0r strange, but tabs in iTerm2 (3.4.17+) works just fine.
However three years later tabs in Finder and Terminal (by Apple) still broken.
I updated my list above, and personally I am using Wezterm now, as it was faster than hyper and the easiest to set up to follow OSX system dark mode (with this script).
@koekeishiya first of all, I am so freaking thankful for creating yabai. You don't know how much UX you are improving for MANY macOS users. It's one of the greatest things ever done. Sorry for the above sentence in this thread, but I just wanted to express my big gratitude toward you.
Regarding this issue, do you see any possible solution for the future development of yabai? Or all options that you tested/considered are just not working?
@leonardopennino I found a similar workaround, which doesn't rearange the windows. The problem with setting the layout to float and back to bsp is that it seams to always put the active window in the left top corner. But if you go to another space and come back, the layout is recalucated, as already mentioned above. What we need is a way to refresh the layout.
My temportary "solution" is this. It works pretty ok but of course there is a flicker, as one needs to focus on anoter space and back on the previous one to work. That's also the reason fo the delay. It's not pretty but it get's the job done:
# Refresh yabai if a tab in finder or terminal is created or moved to new window
yabai -m signal --add event=window_created app="^Terminal$|^Finder$" \
action="yabai -m space --focus next && sleep 0.01 && yabai -m space --focus recent"
yabai -m signal --add event=window_destroyed app="^Terminal$|^Finder$" \
action="yabai -m space --focus next && sleep 0.01 && yabai -m space --focus recent"
yabai -m signal --add event=window_moved app="^Terminal$|^Finder$" \
action="yabai -m space --focus next && sleep 0.01 && yabai -m space --focus recent"
yabai -m signal --add event=window_resized app="^Terminal$|^Finder$" \
action="yabai -m space --focus next && sleep 0.01 && yabai -m space --focus recent"
In Amethyst there is an option "Force windows to be reevaluated". Would be nice if one has a similar option here as well. Like yabai -m refresh
or something. Then one could use this signals to force a reload.
@koekeishiya Do you think it is simple to implement such a comand? You already gave a startingpoint where this is handeled.
I'm sure it's just accessibility API fucking up, but how does the tree just drop a node out of nowhere?
Every single API that macOS exposes (both public and private) stop reporting the window id of windows that are not the active tab, which is why yabai currently assumes that the window no longer exists, and releases said region.
P.s. can you point me to the part where window tree is being managed? Thanks.
Mostly from functions called from
src/window_manager.c
andsrc/space_manager.c
. Handling of events (e.g: window created, window destroyed etc) can be found insrc/event.c
I found a workaround which works for me by accident. I wanted to have unresizable windows like some settings dialogs to float.
Found this issue: #1317
The last comment with the signal fixed the Finder tab behavior as a side effect.
EDIT: Just realized the signal lets the Finder window itself float.
@leonardopennino I found a similar workaround, which doesn't rearange the windows. The problem with setting the layout to float and back to bsp is that it seams to always put the active window in the left top corner. But if you go to another space and come back, the layout is recalucated, as already mentioned above. What we need is a way to refresh the layout.
My temportary "solution" is this. It works pretty ok but of course there is a flicker, as one needs to focus on anoter space and back on the previous one to work. That's also the reason fo the delay. It's not pretty but it get's the job done:
# Refresh yabai if a tab in finder or terminal is created or moved to new window yabai -m signal --add event=window_created app="^Terminal$|^Finder$" \ action="yabai -m space --focus next && sleep 0.01 && yabai -m space --focus recent" yabai -m signal --add event=window_destroyed app="^Terminal$|^Finder$" \ action="yabai -m space --focus next && sleep 0.01 && yabai -m space --focus recent" yabai -m signal --add event=window_moved app="^Terminal$|^Finder$" \ action="yabai -m space --focus next && sleep 0.01 && yabai -m space --focus recent" yabai -m signal --add event=window_resized app="^Terminal$|^Finder$" \ action="yabai -m space --focus next && sleep 0.01 && yabai -m space --focus recent"In Amethyst there is an option "Force windows to be reevaluated". Would be nice if one has a similar option here as well. Like
yabai -m refresh
or something. Then one could use this signals to force a reload.@koekeishiya Do you think it is simple to implement such a comand? You already gave a startingpoint where this is handeled.
I'm sure it's just accessibility API fucking up, but how does the tree just drop a node out of nowhere?
Every single API that macOS exposes (both public and private) stop reporting the window id of windows that are not the active tab, which is why yabai currently assumes that the window no longer exists, and releases said region.
P.s. can you point me to the part where window tree is being managed? Thanks.
Mostly from functions called from
src/window_manager.c
andsrc/space_manager.c
. Handling of events (e.g: window created, window destroyed etc) can be found insrc/event.c
This helped me with PhpStorm tabs. Thanks!
Unfortunately, yabai -m space --focus next
only works if you have the Scripting Additions enabled, which I cannot do (company laptop).
Did anyone discover a similar workaround for layout recalculation without this?
Unfortunately,
yabai -m space --focus next
only works if you have the Scripting Additions enabled, which I cannot do (company laptop). Did anyone discover a similar workaround for layout recalculation without this?
You can achieve the same thing using display
instead of space
without the Scripting Additions.
# Refresh yabai if a tab is created or moved to new window
yabai -m signal --add event=window_created app="^Terminal$" action="yabai -m display --focus prev && yabai -m display --focus next"
yabai -m signal --add event=window_destroyed app="^Terminal$" action="yabai -m display --focus prev && yabai -m display --focus next"
yabai -m signal --add event=window_moved app="^Terminal$" action="yabai -m display --focus prev && yabai -m display --focus next"
yabai -m signal --add event=window_resized app="^Terminal$" action="yabai -m display --focus prev && yabai -m display --focus next"
This works on my setup without SIP disabled.
You probably need a second display for this workaround, right? I only have a single display.
❯ yabai -m display --focus prev && yabai -m display --focus next
could not locate the previous display.
Looks like Amethyst somehow works around this issue since windows with native tabs stay balanced most of the time. In worst case scenario if I spam tabs too much it swaps places with other window. Other than that it works.
is it possible to ignore the tabs for particular program like alacritty?
@aayushsapkota9 correct, basically.
current workaround for me is after i have created a new tab, switch to another space and back, that seems to fix it
I just started using ⌘ N
for this 😉.
is it possible to fix now