Skip to content

Conversation

@edeustua
Copy link
Contributor

Description:
This PR fixes an issue where workspace updates in Hyprland were being handled using the workspace name, which is not guaranteed to be unique. As a result, updates could mistakenly overwrite the state of different workspaces, particularly those on other monitors that share the same name.

The fix switches the logic to use the workspace ID, which is guaranteed to be unique, ensuring that workspace updates are correctly associated with their intended targets.

Why it matters:
Using non-unique identifiers like name caused bugs where state data (e.g. layout, focus, etc.) was mismatched or overwritten across monitors. This change ensures updates behave as expected and improves overall multi-monitor workspace reliability.

@edeustua edeustua changed the title fix: Use workspace ID to handle workspace updates fix: Use workspace ID to handle hyprland/workspace updates Jul 17, 2025
@arnaud-ma
Copy link
Contributor

arnaud-ma commented Jul 19, 2025

I think this is a problem for persistent workspaces. If they are not called at least once, they do not really exists and do not have a specific id from hyprland. But we still need to show them in the waybar.

We can see in the code:

auto workspaceId = parseWorkspaceId(name);
if (!workspaceId.has_value()) {
workspaceId = 0;
}
workspaceData["id"] = *workspaceId;

That it tries to convert the name (almost the only information we have from the persistent workspace) into an id and if it fails the id is just 0. So for example workspace = name:myworkspace, gapsin:0, gapsout:0 from the wiki of hyprland will have an id of 0 in waybar.

@edeustua
Copy link
Contributor Author

I think this is a problem for persistent workspaces. If they are not called at least once, they do not really exists and do not have a specific id from hyprland. But we still need to show them in the waybar.

We can see in the code:

auto workspaceId = parseWorkspaceId(name);
if (!workspaceId.has_value()) {
workspaceId = 0;
}
workspaceData["id"] = *workspaceId;

That it tries to convert the name (almost the only information we have from the persistent workspace) into an id and if it fails the id is just 0. So for example workspace = name:myworkspace, gapsin:0, gapsout:0 from the wiki of hyprland will have an id of 0 in waybar.

OK. This is something I had not thought about. I will check it out and figure out a solution.

I'm writing this PR because in my 2 monitor setup, m_windows (in workspace.cpp) fails to update correctly. All workspaces in the second monitor are basically rewritten with data from monitor 1.

I will get a deeper understanding and get back to this. Thanks!

@arnaud-ma
Copy link
Contributor

Related discussion: hyprwm/Hyprland#3791
I think one of the easiest way would be to find a way to let hyprland "activate" the persistent workspaces before doing anything, this would simplify everything.

Otherwise, because it is also possible to have the monitor(s), maybe a tuple (name, monitor) instead of the id is possible, for example here instead of checking only the name if the id is <= 0, check the name and the monitor.

auto workspace =
std::ranges::find_if(m_workspaces, [&](std::unique_ptr<Workspace> const &w) {
if (workspaceId > 0) {
return w->id() == workspaceId;
}
return (workspaceName.starts_with("special:") && workspaceName.substr(8) == w->name()) ||
workspaceName == w->name();
});

@edeustua
Copy link
Contributor Author

edeustua commented Jul 21, 2025

Related discussion: hyprwm/Hyprland#3791 I think one of the easiest way would be to find a way to let hyprland "activate" the persistent workspaces before doing anything, this would simplify everything.

Otherwise, because it is also possible to have the monitor(s), maybe a tuple (name, monitor) instead of the id is possible, for example here instead of checking only the name if the id is <= 0, check the name and the monitor.

auto workspace =
std::ranges::find_if(m_workspaces, [&](std::unique_ptr<Workspace> const &w) {
if (workspaceId > 0) {
return w->id() == workspaceId;
}
return (workspaceName.starts_with("special:") && workspaceName.substr(8) == w->name()) ||
workspaceName == w->name();
});

Just to give context, I'm using the following hyprland workspace config:

workspace = 1, monitor:DP-2, defaultName:1, persistent:true
workspace = 2, monitor:DP-2, defaultName:2, persistent:true
workspace = 3, monitor:DP-2, defaultName:3, persistent:true
workspace = 4, monitor:DP-2, defaultName:4, persistent:true
workspace = 5, monitor:DP-2, defaultName:5, persistent:true
workspace = 6, monitor:DP-2, defaultName:6, persistent:true
workspace = 7, monitor:DP-2, defaultName:7, persistent:true
workspace = 8, monitor:DP-2, defaultName:8, persistent:true
workspace = 9, monitor:DP-2, defaultName:9, persistent:true
workspace = 10, monitor:DP-2, defaultName:10, persistent:true

workspace = 11, monitor:DP-3, defaultName:1, persistent:true
workspace = 12, monitor:DP-3, defaultName:2, persistent:true
workspace = 13, monitor:DP-3, defaultName:3, persistent:true
workspace = 14, monitor:DP-3, defaultName:4, persistent:true
workspace = 15, monitor:DP-3, defaultName:5, persistent:true
workspace = 16, monitor:DP-3, defaultName:6, persistent:true
workspace = 17, monitor:DP-3, defaultName:7, persistent:true
workspace = 18, monitor:DP-3, defaultName:8, persistent:true
workspace = 19, monitor:DP-3, defaultName:9, persistent:true
workspace = 20, monitor:DP-3, defaultName:10, persistent:true

Given this config, hyprland does create all workspaces, with their respective ids, at start time. I'm using hyprland 0.50.

@arnaud-ma
Copy link
Contributor

It is a bit weird, after some tests i think a workspace is directly created when:

  • it is not named (e.g. workspace=1, and not workspace=name:x1)
  • it is the first named in the config file
  • it is added after startup, i.e. adding it to the config file and save the file.

So for example this:

workspace = name:x1, persistent:true
workspace = name:x2, persistent:true
workspace = 1, persistent:true

Will have x1 and 1 to be created directly, but not x2.

@arnaud-ma
Copy link
Contributor

Created an issue in hyprland hyprwm/Hyprland#11146

@arnaud-ma
Copy link
Contributor

In the end it was a bug on Hyprland's side, after that hyprwm/Hyprland#11161 will be merged, each workspace will have its own ID and everything will be fine!

@edeustua
Copy link
Contributor Author

In the end it was a bug on Hyprland's side, after that hyprwm/Hyprland#11161 will be merged, each workspace will have its own ID and everything will be fine!

Thanks a lot! I will then cleanup this PR and get it ready for hyprlands fix.

@edeustua edeustua force-pushed the fix_hyprland_window_count branch from 06602d6 to fa3ab26 Compare July 22, 2025 16:48
@arnaud-ma
Copy link
Contributor

I think it should be ok now with the new release of hyprland

@edeustua edeustua force-pushed the fix_hyprland_window_count branch from fa3ab26 to 06c3411 Compare October 15, 2025 01:18
@edeustua
Copy link
Contributor Author

edeustua commented Oct 15, 2025

Hi everyone 👋

This issue still persists even with the latest Waybar commit — Waybar is currently unable to distinguish between two workspaces that share the same name.

There’s also a related problem stemming from how loadPersistentWorkspacesFromWorkspace handles workspace identifiers. This function treats the workspace name as if it were unique and ends up fabricating an ID based on it.

As a result, a configuration like this:

workspace = 1, monitor:DP-2, defaultName:1, persistent:true
workspace = 2, monitor:DP-2, defaultName:2, persistent:true
...
workspace = 10, monitor:DP-2, defaultName:10, persistent:true

workspace = 11, monitor:DP-3, defaultName:1, persistent:true
workspace = 12, monitor:DP-3, defaultName:2, persistent:true
...
workspace = 20, monitor:DP-3, defaultName:10, persistent:true

leads to Waybar creating 20 workspaces on the second monitor (DP-3).
This happens because loadPersistentWorkspacesFromWorkspace generates new IDs for each duplicate defaultName, rather than correctly recognizing them as separate but validly named workspaces on different monitors.

@arnaud-ma
Copy link
Contributor

arnaud-ma commented Oct 15, 2025

I could be wrong, but how i see the thing now with this part:

// get all current workspaces
auto const workspacesJson = m_ipc.getSocket1JsonReply("workspaces");
auto const clientsJson = m_ipc.getSocket1JsonReply("clients");
for (Json::Value workspaceJson : workspacesJson) {
std::string workspaceName = workspaceJson["name"].asString();
if ((allOutputs() || m_bar.output->name == workspaceJson["monitor"].asString()) &&
(!workspaceName.starts_with("special") || showSpecial()) &&
!isWorkspaceIgnored(workspaceName)) {
m_workspacesToCreate.emplace_back(workspaceJson, clientsJson);
} else {
extendOrphans(workspaceJson["id"].asInt(), clientsJson);
}
}
spdlog::debug("Initializing persistent workspaces");
if (m_persistentWorkspaceConfig.isObject()) {
// a persistent workspace config is defined, so use that instead of workspace rules
loadPersistentWorkspacesFromConfig(clientsJson);
}
// load Hyprland's workspace rules
loadPersistentWorkspacesFromWorkspaceRules(clientsJson);
}

Every workspace (even the persistent ones) must be directly obtained in the first line. If not it is a bug from Hyprland. The "persistent" in the config of waybar (loadPersistentWorkspacesFromConfig) is deprecated in favor of the same option in Hyprland. And the last line was a "fix" for a bug in the Hyprland's side, which is now fixed. So it can be removed.

@edeustua edeustua force-pushed the fix_hyprland_window_count branch from 06c3411 to dc735f3 Compare October 15, 2025 02:19
@arnaud-ma
Copy link
Contributor

So in the end the goal of this PR is just to completely remove every "workspace uniqueness" management by completely relying on Hyprland's workspace id. Do you have anything else to modify @edeustua?

@edeustua
Copy link
Contributor Author

Yes, you are correct. The end goal of this PR is to rely on hyprland to set workspace ids, which in turn waybar should use as the ground truth. I don't think there is anything else to modify. But let me know if you want me to test anything further.

Again, thanks a lot for reviewing this and helping out @arnaud-ma.

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.

2 participants