Skip to content

Respect monitor-local workspaces in overview#111

Closed
bxn-dev wants to merge 37 commits into
raybbian:mainfrom
bxn-dev:main
Closed

Respect monitor-local workspaces in overview#111
bxn-dev wants to merge 37 commits into
raybbian:mainfrom
bxn-dev:main

Conversation

@bxn-dev

@bxn-dev bxn-dev commented Apr 27, 2026

Copy link
Copy Markdown

Build the tasking overview from Hyprland’s real workspace-to-monitor state instead of synthetic global workspace IDs. This prevents opening, rendering, closing, navigation, and drag-and-drop from moving or reassigning workspaces across monitors while keeping move targets tied to the actual displayed workspace.

Fixes #90
Fixes #96
Fixes #61

Build the tasking overview from Hyprland’s real workspace-to-monitor state instead of synthetic global workspace IDs. This prevents opening, rendering, closing, navigation, and drag-and-drop from moving or reassigning workspaces across monitors, while keeping move targets tied to the actual displayed workspace.
@bxn-dev

bxn-dev commented Apr 27, 2026

Copy link
Copy Markdown
Author

I tested it using split-monitor-workspaces on a 3-monitor setup, and it worked flawlessly.

@yerlotic

yerlotic commented Apr 28, 2026

Copy link
Copy Markdown
Contributor

Hi, @undef-user!

I'm stuck on a first workspace with other workspaces being black. I can't drag a window to a different workspace and I can't move it there with a hyprtasking:move dispatcher.
Layers also don't work.

The workspace appears only after I move a window there with hyprctl dispatch movetoworkspace +1

no_workspaces.mp4

Note that I'm not using split-monitor-workspaces (and I don't think this should be a dependency of hyprtasking)

@bxn-dev

bxn-dev commented Apr 28, 2026

Copy link
Copy Markdown
Author

Ah, I think I see the problem. My solution only looks at the currently existing workspaces; in my setup, I have 9 persistent per screen, so the grid is filled. Of course, you're right that neither this nor any other plugin should be a dependency. I'll see if I can expand the solution tomorrow. Thanks for the feedback!

Keep the grid layout visually full by adding monitor-local placeholder tiles, and create real workspaces only when users navigate, select, or drop a window onto an empty tile. This preserves monitor-local workspace state without moving workspaces during overview rendering.
@bxn-dev bxn-dev marked this pull request as draft April 29, 2026 07:55
@bxn-dev

bxn-dev commented Apr 29, 2026

Copy link
Copy Markdown
Author

Hi @yerlotic!
I think I've got it working properly, but unfortunately I've only tested it on my three-monitor split-screen setup. The linear layout hasn't been adjusted yet, but it would really help me if you could test it again.
Best regards, and thanks in advance

@yerlotic

yerlotic commented Apr 29, 2026

Copy link
Copy Markdown
Contributor

Hi, @undef-user! I tested the grid layout

hyprsomething.mp4

Here are the notes:

  • Layers don't work
  • Switching back and forth between empty workspaces produces a lot of empty workspaces (increments the ws ID)
  • Switching between workspaces where one of them is not empty does not create new ws IDs
  • Workspace rules are working on a monitor connect, so [BUG] Using Hyprtasking plugin messes up workspace rules #61 might be fixed after this is merged

@bxn-dev

bxn-dev commented Apr 29, 2026

Copy link
Copy Markdown
Author

Thanks for the feedback again @yerlotic ; I think (and hope) we’re getting closer to a solution. :D
The behavior of empty workspaces is standard Hyprland behavior: empty workspaces are cleaned up and then simply recreated. If one isn’t empty, the next highest ID is taken and, I believe, retained—though I’m not 100% sure. I can try to implement a solution for this, though.

Regarding layers, I need to take a closer look; I don’t think I’ve interpreted the behavior correctly so far. Is it the intended behavior that a layer (in a 3x3 layout) creates/is 9 new/different workspaces?

@yerlotic

Copy link
Copy Markdown
Contributor

I tested it more and found more bugs:

hyprmorebugs.mp4

Notes:

  1. I didn't test what happens when there is not enough space on a monitor to fit all the windows from an unplugged one
  2. When trying to loop through the edges in a workspace grid: sometimes I can, sometimes I can't
  3. Windows go to an existing monitor after unplugging a monitor
  4. After moving a window that came from a different monitor, some other windows from that monitor trigger a reflow in the workspace grid so windows switch places, which is not wanted

@yerlotic

yerlotic commented Apr 29, 2026

Copy link
Copy Markdown
Contributor

Regarding layers, I need to take a closer look; I don’t think I’ve interpreted the behavior correctly so far. Is it the intended behavior that a layer (in a 3x3 layout) creates/is 9 new/different workspaces?

Yes, it creates ROWS*COLS workspaces for every layer

For more info see #60

@bxn-dev

bxn-dev commented Apr 29, 2026

Copy link
Copy Markdown
Author

Thanks for the information I am looking into it. :D

@bxn-dev

bxn-dev commented Apr 30, 2026

Copy link
Copy Markdown
Author

Hello @yerlotic, sorry for bothering you again.
I think I’ve fixed the layer handling now:

  1. Layer switching should work consistently.
  2. Moving between workspaces should now be stable as well.

The behavior where switching back and forth between empty workspaces creates new workspace IDs is still present. As far as I understand, this is normal Hyprland behavior: empty non-persistent workspaces are cleaned up by Hyprland, so when hyprtasking enters that empty slot again, a new workspace has to be created and Hyprland assigns it a new ID. Workspaces that contain windows stay alive, which is why this does not happen there.

I would really appreciate it if you could test this again when you have time. Also, please let me know whether you think hyprtasking should try to handle the empty-workspace ID behavior differently.

@bxn-dev

bxn-dev commented Apr 30, 2026

Copy link
Copy Markdown
Author

While rereading the README, I also noticed that I accidentally added functionality allowing the setLayer dispatcher to accept relative positional arguments (+1/-1) as well as 1, 2, 3 for specific layers.

@yerlotic

yerlotic commented Apr 30, 2026

Copy link
Copy Markdown
Contributor

Hi again, @undef-user!

The behavior where switching back and forth between empty workspaces creates new workspace IDs is still present. As far as I understand, this is normal Hyprland behavior: empty non-persistent workspaces are cleaned up by Hyprland, so when hyprtasking enters that empty slot again, a new workspace has to be created and Hyprland assigns it a new ID. Workspaces that contain windows stay alive, which is why this does not happen there.

I think it will be better if we assign lower id workspaces whenever possible. It's unlikely that anyone will ever run out of workspaces, but not letting this happen is a good thing

While rereading the README, I also noticed that I accidentally added functionality allowing the setLayer dispatcher to accept relative positional arguments (+1/-1) as well as 1, 2, 3 for specific layers.

This was actually the intended behavior

Layer switching should work consistently.

Well, it doesn't

  1. I can't change the layer when I'm not on overview
  2. When I am on overview and change the layer (hyprctl dispatch hyprtasking:setlayer +1) then the workspace I would expect to be active is not actually active and the focus remains on that workspace

I was thinking about implementing something so each layer has its own focused workspace in a grid. Anyway, now the expected behavior is to run hyprtasking:move in and see that the same workspace in a grid (the same x and y) is still focused and the workspace contents should change

Comment thread src/main.cpp Outdated
return {.success = false, .error = "active workspace not in layout"};

const int original_layer = cursor_view->layout->layer;
const std::string layer_arg = arg.empty() ? "+1" : arg;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the function should just return a dispatcher error in case there is no arg

@yerlotic

Copy link
Copy Markdown
Contributor

You removed some useful comments (and I don't see why), so here is a patch to bring them back: return_comments.diff

Also now hyprtasking:move without arguments is doing something weird

…verview, fixing layers wont switch outside of overview
@bxn-dev

bxn-dev commented May 1, 2026

Copy link
Copy Markdown
Author

Hi again @yerlotic,

The following now works as expected (hopefully 😅 ):

  • Switching layers outside of the overview
  • Switching layers within the overview (the workspace with the same X/Y coordinates is now correctly focused)
  • calling hyprtasking:move without arguments now returns an "invalid argument" error.

Regarding the empty workspace IDs continuing to increase: I have kept the current max + 1 allocation strategy for now, since the primary goal of this PR is to add multi-monitor support.

Reusing the lowest available workspace ID globally could interfere with monitor-specific workspace ranges or rules. Because of that, I would prefer not to change this behavior within this PR unless we introduce a monitor-aware allocation strategy.

If you’d like me to address this as well, I can look into it.

@yerlotic

yerlotic commented May 1, 2026

Copy link
Copy Markdown
Contributor

The following now works as expected (hopefully 😅 )

@undef-user, well, now there are more ways to crash hyprland:

  • hyprctl dispatch hyprtasking:toggle cursor - spawns the overview, the current workspace is not rendered. Instead all tiles are just the wallpaper. Then hyprland crashes
  • hyprctl dispatch hyprtasking:move in - crashes

Could you please test your code without split-monitor-workspaces plugin as well?

Regarding the empty workspace IDs continuing to increase: I have kept the current max + 1 allocation strategy for now, since the primary goal of this PR is to add multi-monitor support.

If it is too much of a hustle to implement smart allocation, then ig we can stick to it. Also we could keep track of the last empty workspace and just not allocate a new one if not needed.

@bxn-dev bxn-dev marked this pull request as ready for review May 6, 2026 14:53
Comment thread src/layout/grid.cpp
Comment on lines +150 to +163
int HTLayoutGrid::get_max_occupied_layer() {
int max_occupied = 0;
for (int l = 0; l < (int)grid_cells.size(); l++) {
for (int y = 0; y < (int)grid_cells[l].size(); y++) {
for (int x = 0; x < (int)grid_cells[l][y].size(); x++) {
if (grid_cells[l][y][x].ws_id != WORKSPACE_INVALID) {
max_occupied = std::max(max_occupied, l);
break;
}
}
}
}
return max_occupied;
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This might introduce even more performance problems

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll look into it, as of now, I am not noticing any issues with that, but it's maybe over the top again.

@yerlotic

yerlotic commented May 6, 2026

Copy link
Copy Markdown
Contributor

I’ve been using it as my daily driver for the last two days to test it properly, and after around 4–5 hours of regular computer work I’m running into significant performance issues.

Did you figure out what was causing them?
Was it the high workspace number or that hyprtasking was constantly checking all workspaces from 0 to current (does it still do this?)?

I’m currently refactoring it based on the approach you mentioned above.

So you are trying to reimplement the grid, right? I think the benefits of this approach should be at least in a reduced amount of iteration to find if some workspace is occupied or for any similar reason

@bxn-dev

bxn-dev commented May 7, 2026

Copy link
Copy Markdown
Author

The issue was, as far as I think the reinitializing of monitors after they went idle. I am trying to reimplement the grid as a 3 dimensional vector, right now I'm testing this approach as my driver so I can verify no problems occur.

@raybbian

raybbian commented May 9, 2026

Copy link
Copy Markdown
Owner

Let me know if f9f4ec1 works for you all

@yerlotic

yerlotic commented May 10, 2026

Copy link
Copy Markdown
Contributor

Let me know if f9f4ec1 works for you all

Hi @raybbian, this looks cool.

I noticed that the layers are broken after adding a new monitor and moving around a bit. Instead of moving to the same cell on a different layer, it moves rather strangely
Now the workspaces are arranged like this (somehow):

1 2 3
4 6 5
7 8 9

Then when I do nextlayer from the workspace 6 it moves me to the rightmost workspace on the next layer (ws 15).

After unplugging and plugging a monitor back in, some workspaces from that monitor appear on the main one and the layout now looks like this:

1 2 20
4 6 5
7 8 9

As a result, jumping from ws 6 and 20 breaks the behavior of layers.

The same buggy behavior happens with workspace rules:

workspace = 1, monitor:VIRTUAL
workspace = 3, monitor:VIRTUAL
workspace = 5, monitor:VIRTUAL
workspace = 7, monitor:VIRTUAL

After loading hyprtasking, all the workspaces are on their monitors, but when I try to hyprtasking:nextlayer, it is broken.

After adding workspace = 4, monitor:VIRTUAL and running hyprctl reload I would expect that workspace to jump to a different monitor but it doesn't go anywhere. Maybe hyprland doesn't do this on the fly as well, I don't really know.

Other than that, everything seems fine

@raybbian

Copy link
Copy Markdown
Owner

Try 5078467?

@yerlotic

yerlotic commented May 11, 2026

Copy link
Copy Markdown
Contributor

Layer switching now works perfectly. But dynamic workspace rules don't really work.
Let's say I'm on a workspace 3 and there are some windows on monitor HDMI-1, then I add a window rule to move workspace 3 to a monitor VIRTUAL:

workspace=3, monitor:VIRTUAL

Then I save the config, but workspace 3 is still on monitor HDMI-1 (workspace ID 3 (3) on monitor HDMI-1)

Then I move all windows from that wokspace to somewhere else and switch back, it is still in the HDMI-1 monitor grid, but hyprland thinks that it's on VIRTUAL (workspace ID 3 (3) on monitor VIRTUAL). As a result, we have a strange workspace in-between monitors.

If I do a hyprctl reload after all this, the workspaces go back to normal (the grid is reevaluated)

Edit:

I've noticed that removing a monitor moves its workspaces to the first in the grid, not in the first available one.

@yerlotic

yerlotic commented May 12, 2026

Copy link
Copy Markdown
Contributor

Hi, @raybbian!
I tested 3ca9c8f and it is almost perfect. The only strange thing I encountered was that when a monitor is plugged back in and I am staying on a workspace that used to be on that monitor, I get teleported to the first workspace on the first layer (even if layer was not the first). This leads to layer desync between overview and the active workspace.
I think it should stay on the same workspace in a grid when plugging a monitor in this weird way.

Edit:
To reproduce:

  1. fill all workspaces with windows on the first layer
  2. go to the next layer
  3. move a window to a different monitor
  4. unplug that monitor
  5. switch to a newly created workspace on the main monitor
  6. plug the monitor back in

Here is the desync

Edit 2:
Given that the coordinates in the grid are now limited by a 20 bit uint (max value 1048575), should we document it in the wiki?

long long HTLayoutGrid::pack_slot(int layer, int x, int y) {
return ((long long)layer << 40) | ((long long)(uint32_t)y << 20) | (long long)(uint32_t)x;
}

Unrelated, but wanted to ask anyway. Why is Touch and gesture support is not shown as implemented in the README even though gestures actually work?

@raybbian

Copy link
Copy Markdown
Owner

Try latest?

@bxn-dev

bxn-dev commented May 13, 2026

Copy link
Copy Markdown
Author

@raybbian Latest works fine for me, tested it with and wirhout split-monitor-workspaces and im not facing any issues

@yerlotic

Copy link
Copy Markdown
Contributor

@raybbian Tried latest and found a single issue. If you fill up all the workspaces on main monitor with windows, make a window on a second monitor then unplug the second monitor, you will not have access to windows that were on that monitor.

I think that the algorithm should just put all windows in the last workspace of the last available monitor when all workspaces on all monitors are occupied.

So there were 3 workspaces on an unplugged monitor and 1 available workspace on monitor ID=0 and on monitor ID=1. I expect the algorithm to place the first 2 workspaces in the available slots and then place the rest of windows in the last workspace of the last monitor (or the last filled workspace if all become occupied, doesn't really matter)

I didn't test the latest with 3 or more monitors so idk how it tries to fit windows if there is no place for them on one monitor and the slots are available on the second.

@bxn-dev

bxn-dev commented May 19, 2026

Copy link
Copy Markdown
Author

@raybbian @yerlotic
So, after further testing, I have to say that it doesn't work after all. It sort of works if I manually set all workspaces to “persistent,” but the Waybar workspace module doesn't work at all. When monitors come out of eco mode, either no new workspaces are created on them, or there are either 20 on Monitor 1 or sometimes 15 on Monitor 1 and 13 on Monitor 2. Sometimes it works, but after eco mode, the overlay lags and the split-monitor workspace binds don’t work properly either—only sometimes. Finally, at startup, the first workspace you land on is the second one on the grid. I think I’ll just keep using my fork and maintain it as needed. We should probably close the PR, because I think it’s become obsolete with all the changes—I’ll take the blame for that; I changed too much all at once.

bxn-dev added 14 commits May 19, 2026 10:22
- Remove #define private public hack in layout_base.cpp
- Replace concept-SFINAE config access with simple functions
- Extract shared render_workspace/render_border helpers to layout_base
- Dedup grid+linear render paths (~400 lines -> shared helpers)
- Split change_layer() monster into calculate/find/apply helpers
- Consolidate dispatch_move/dispatch_move_window into single impl
- Cache dynamicPointerCast and get_view_from_id results
- Extract create_workspace_at_grid_cell helper from input.cpp
- Replace manual loops with std::ranges::find_if/any_of
- Cache config lookups instead of repeated HTConfig::value() calls
- Remove dead code, verbose comments, empty statement bodies
- Add grid cell helpers: find_cell(), cell_at() to reduce nesting

11 files changed, 560 insertions(+), 634 deletions(-)
Build passes, LSP clean (0 errors, 0 warnings)
… safety

- Lazy-init par_view in get_monitor() instead of constructor where
  ht_manager doesn't yet have the view registered
- Add ht_manager null guard before par_view lookup
- Remove explicit monitor->m_activeWorkspace = start_workspace before
  CScopeGuard destructor fires (double restore caused focus issues)
- Fix linear.cpp similarly for big_ws restore
- Layout 0 LSP errors
…ect monitors

- Add monitor_name to HTView (stable across DPMS cycles)
- In register_monitors(), try to reconnect orphaned views by m_name
  before creating new ones, preventing workspace loss on reconnect
- remove explicit monitor->m_activeWorkspace restore before CScopeGuard
  destructor (double-restore caused focus issues)
- fix get_monitor() to lazy-init par_view with ht_manager null guard

Build passes, LSP 0 errors
- Don't erase views just because get_monitor() returns null
- Keep views with a known monitor_name so they can be reconnected
  when the same monitor wakes up with a new m_id
- In grid/linear render(), determine hovered workspace from cursor position
- Render hovered workspace as 'active' (blue border, is_active=true)
- Skip hovered in main loop, render it last as the active one
- Remove 'big_ws' logic from linear layout; use same hover approach
- render_workspace no longer sets m_activeWorkspace as side-effect;
  grid stores active_ws pointer, linear uses CScopeGuard
- add pre_overview_workspace to HTView for exit_on_hovered=false fallback
- do_exit_behavior: prefer hovered workspace if available

Build passes, LSP 0 errors
- restore m_activeWorkspace setting in render_workspace() but with
  CScopeGuard so it is reset after each workspace render. This avoids
  the global 'active' state being set to the hovered workspace, which
  caused the mouse cursor to constantly change workspaces.
- remove explicit m_activeWorkspace = pre_overview_ws from grid/linear
  after the loop (redundant with CScopeGuard).
- pre_overview_workspace is now correctly restored from the CScopeGuard
  in both grid and linear, even when a different ws was hovered.
- render_border is now called AFTER render_workspace, so the correct
  border color (active vs inactive) is used for each workspace.

Build passes, LSP clean
- render_workspace() no longer sets m_activeWorkspace as side-effect.
- The only thing that changes during hover is the border color
  (render_border uses  flag explicitly).
- This prevents the workspace from actually switching when hovering,
  while still showing a blue border on the hovered workspace.

Build passes, LSP clean
- Revert grid.cpp render() to original behavior (active workspace rendering)
- Restore linear.cpp to untouched original
- render_workspace() uses  for correct border color
- Add  variable for raw render_workspace_hook calls
- Build passes, LSP clean
… logic

- After clicking a workspace, the overview closes immediately
- hyprtasking:move without move_window also closes overview now
- Removed hover-active tracking; border color is purely based on
   again, like the original
- Restored original render() behavior for grid and linear
- Verified build passes and waybar shows correct active workspace
@bxn-dev bxn-dev closed this May 21, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Hyprtasking not showing windows in workspaces 1-9 [BUG] Using Hyprtasking plugin messes up workspace rules

3 participants