Skip to content

ext-workspace-v1: initial implementation#9064

Open
WhyNotHugo wants to merge 2 commits intoswaywm:masterfrom
WhyNotHugo:ext-workspace
Open

ext-workspace-v1: initial implementation#9064
WhyNotHugo wants to merge 2 commits intoswaywm:masterfrom
WhyNotHugo:ext-workspace

Conversation

@WhyNotHugo
Copy link
Contributor

Only use a single, global group. Sway does not have any concept of workspace groups, and workspaces can move freely across outputs.

ext_workspace_handle_v1::id is never emitted; sway has no concept of ids or of stable vs temporary workspaces. Everything is ephemeral to the current session.

ext_workspace_handle_v1::coordinates is never emitted; sway does not organise workspaces into any sort of grid.

Deactivating a workspace is a no-op. This functionality doesn't really align with sway, although it could potentially be implemented to "switch to previous workspace on this output" as a follow-up.

Removing a workspace is a no-op.

Implements: #8812

@WhyNotHugo WhyNotHugo force-pushed the ext-workspace branch 2 times, most recently from fcc4046 to ae319e5 Compare March 12, 2026 05:09
@WhyNotHugo
Copy link
Contributor Author

WhyNotHugo commented Mar 12, 2026

v2: use one group per output, so that clients can move workspaces to another output by moving a workspace to another group.

I haven't found a client which implements ext_workspace_handle_v1::assign, so that particular handler needs better testing.

@WhyNotHugo WhyNotHugo marked this pull request as ready for review March 12, 2026 05:11
@WhyNotHugo WhyNotHugo force-pushed the ext-workspace branch 2 times, most recently from 3959493 to 80b723d Compare March 12, 2026 15:45
@WhyNotHugo
Copy link
Contributor Author

WhyNotHugo commented Mar 12, 2026

v3: applied suggested refactor. Squashed commits; they no make much less sense separate.

@WhyNotHugo
Copy link
Contributor Author

All concerns addressed… but apparently I've regressed and now a ext_workspace_handle_v1::activate request activates the wrong workspace. Still trying to find out how I broke this.

@WhyNotHugo
Copy link
Contributor Author

There seems to now be at least one other subtle bug.

I'd mark this as draft, but the "Convert to Draft" button is broken and is a no-op.

@WhyNotHugo
Copy link
Contributor Author

I debugged this further. When I say "create workspace", I mean foot && swaymsg move window to workspace 2.

If I start waybar (my ext-workspace client) early, then workspaces get id 1,2,3,4…

If I create several workspaces, and then start waybar, workspaces get ids in inverted order: 4,3,2,1.

So the first-created workspace (with sway name "1") get ids 4, the second one (with sway name "2") gets id 3, etc…

But we don't control ids, wlroots assigns them, and it seems that it doesn't assign names until the first ext-workspace client connects?

Here's the output of starting waybar before creating other workspaces:

[3853464.236] {Default Queue} wl_registry#2.global(28, "ext_workspace_manager_v1", 1)
[3853529.988] {Default Queue} wl_registry#34.global(28, "ext_workspace_manager_v1", 1)
[3853530.154] {Default Queue} wl_registry#35.global(28, "ext_workspace_manager_v1", 1)
[3853536.139] {Default Queue} wl_registry#41.global(28, "ext_workspace_manager_v1", 1)
[3853536.142] {Default Queue}  -> wl_registry#41.bind(28, "ext_workspace_manager_v1", 1, new id [unknown]#38)
[3853571.575] {Default Queue} ext_workspace_manager_v1#38.workspace_group(new id ext_workspace_group_handle_v1#4278190080)
[3853571.582] {Default Queue} ext_workspace_group_handle_v1#4278190080.capabilities(1)
[3853571.585] {Default Queue} ext_workspace_group_handle_v1#4278190080.output_enter(wl_output#21)
[3853571.587] {Default Queue} ext_workspace_manager_v1#38.workspace(new id ext_workspace_handle_v1#4278190081)
[3853571.756] {Default Queue} ext_workspace_handle_v1#4278190081.capabilities(9)
[3853571.762] {Default Queue} ext_workspace_handle_v1#4278190081.id("1")
[3853571.764] {Default Queue} ext_workspace_handle_v1#4278190081.state(1)
[3853571.765] {Default Queue} ext_workspace_group_handle_v1#4278190080.workspace_enter(ext_workspace_handle_v1#4278190081)
[3853571.766] {Default Queue} ext_workspace_manager_v1#38.done()
[3853571.773] {Default Queue} ext_workspace_manager_v1#38.done()
[3857757.947] {Default Queue} ext_workspace_manager_v1#38.done()
[3857823.645] {Default Queue} ext_workspace_manager_v1#38.workspace(new id ext_workspace_handle_v1#4278190082)
[3857823.872] {Default Queue} ext_workspace_handle_v1#4278190082.capabilities(9)
[3857823.886] {Default Queue} ext_workspace_handle_v1#4278190082.id("2")
[3857823.894] {Default Queue} ext_workspace_handle_v1#4278190082.state(0)
[3857823.900] {Default Queue} ext_workspace_group_handle_v1#4278190080.workspace_enter(ext_workspace_handle_v1#4278190082)
[3857823.911] {Default Queue} ext_workspace_manager_v1#38.done()
[3857869.321] {Default Queue} ext_workspace_manager_v1#38.done()
[3857929.178] {Default Queue} ext_workspace_manager_v1#38.workspace(new id ext_workspace_handle_v1#4278190083)
[3857929.330] {Default Queue} ext_workspace_handle_v1#4278190083.capabilities(9)
[3857929.338] {Default Queue} ext_workspace_handle_v1#4278190083.id("3")

Here's the output of creating workspaces before starting waybar:

[3913032.900] {Default Queue} wl_registry#2.global(28, "ext_workspace_manager_v1", 1)
[3913105.751] {Default Queue} wl_registry#34.global(28, "ext_workspace_manager_v1", 1)
[3913105.968] {Default Queue} wl_registry#35.global(28, "ext_workspace_manager_v1", 1)
[3913110.497] {Default Queue} wl_registry#41.global(28, "ext_workspace_manager_v1", 1)
[3913110.498] {Default Queue}  -> wl_registry#41.bind(28, "ext_workspace_manager_v1", 1, new id [unknown]#38)
[3913131.606] {Default Queue} ext_workspace_manager_v1#38.workspace_group(new id ext_workspace_group_handle_v1#4278190080)
[3913131.613] {Default Queue} ext_workspace_group_handle_v1#4278190080.capabilities(1)
[3913131.616] {Default Queue} ext_workspace_group_handle_v1#4278190080.output_enter(wl_output#21)
[3913131.622] {Default Queue} ext_workspace_manager_v1#38.workspace(new id ext_workspace_handle_v1#4278190081)
[3913131.822] {Default Queue} ext_workspace_handle_v1#4278190081.capabilities(9)
[3913131.828] {Default Queue} ext_workspace_handle_v1#4278190081.id("4")
[3913131.830] {Default Queue} ext_workspace_handle_v1#4278190081.state(0)
[3913131.832] {Default Queue} ext_workspace_group_handle_v1#4278190080.workspace_enter(ext_workspace_handle_v1#4278190081)
[3913131.834] {Default Queue} ext_workspace_manager_v1#38.workspace(new id ext_workspace_handle_v1#4278190082)
[3913131.863] {Default Queue} ext_workspace_handle_v1#4278190082.capabilities(9)
[3913131.865] {Default Queue} ext_workspace_handle_v1#4278190082.id("3")
[3913131.866] {Default Queue} ext_workspace_handle_v1#4278190082.state(0)
[3913131.868] {Default Queue} ext_workspace_group_handle_v1#4278190080.workspace_enter(ext_workspace_handle_v1#4278190082)
[3913131.869] {Default Queue} ext_workspace_manager_v1#38.workspace(new id ext_workspace_handle_v1#4278190083)
[3913131.884] {Default Queue} ext_workspace_handle_v1#4278190083.capabilities(9)
[3913131.886] {Default Queue} ext_workspace_handle_v1#4278190083.id("2")
[3913131.887] {Default Queue} ext_workspace_handle_v1#4278190083.state(0)
[3913131.888] {Default Queue} ext_workspace_group_handle_v1#4278190080.workspace_enter(ext_workspace_handle_v1#4278190083)
[3913131.889] {Default Queue} ext_workspace_manager_v1#38.workspace(new id ext_workspace_handle_v1#4278190084)

Waybar shows me widgets for both sway IPC and ext-workspace, and the mismatch is also visually obvious.

@WhyNotHugo
Copy link
Contributor Author

My refactor accidentally removed the initial ext_workspace_handle_v1::name event, so sway wasn't emitting any names for workspaces.

I restored this and waybar works fine: it only uses ids if no names are supplied.

I'll report the wlroots issue properly, but it's no longer a problem for us. This is working and ready for review.

CI failures are due to API changes in wlroots.

@WhyNotHugo WhyNotHugo requested a review from emersion March 13, 2026 23:24
@WhyNotHugo
Copy link
Contributor Author

I'm using sway_workspace->name as id, but this property can change, and later be re-used by another workspace.

We don't have any stable id for workspaces. Maybe we can use the memory positions of the object itself?

@emersion
Copy link
Member

Maybe we can use the memory positions of the object itself?

That would be unique, but may be re-used by a completely unrelated workspace later on and is not persistent across sessions.

We can leave the ID NULL instead.

@WhyNotHugo
Copy link
Contributor Author

We can leave the ID NULL instead.

Done

Move workspace_move_to_output out of the command handler, so it can be
re-used for ext_workspace_handle_v1::assign.
Copy link
Member

@emersion emersion left a comment

Choose a reason for hiding this comment

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

Looks good apart from these last comments!

}
name = req->create_workspace.name;
} else {
name = workspace_next_name(output->wlr_output->name);
Copy link
Member

Choose a reason for hiding this comment

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

We need to free() this after the workspace has been created.

Easiest would be to unconditionally free name, and strdup(req->create_workspace.name).

Copy link
Contributor Author

Choose a reason for hiding this comment

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

We need to strdup() in the first branch to avoid a double-free, right?

Done.


// Must be called whenever an output is enabled.
void sway_ext_workspace_output_enable(struct sway_output *output) {
if (!server.workspace_manager_v1 || !output->wlr_output) {
Copy link
Member

Choose a reason for hiding this comment

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

Nit: I think the !server.workspace_manager_v1 check can be removed

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It can be NULL if wlr_ext_workspace_manager_v1_create fails. We simply ignore that failure at start-up (rather than bailing).

I ignored the error during initialisation because that's what similar calls do (e.g.: wlr_ext_foreign_toplevel_list_v1_create)

Maintain a 1:1 relationship between workspace groups and outputs, so
that moving a workspace across groups effectively moves it across
outputs.

ext_workspace_handle_v1::id is never emitted; sway has no concept of ids
or of stable vs temporary workspaces. Everything is ephemeral to the
current session.

ext_workspace_handle_v1::coordinates is never emitted; sway does not
organise workspaces into any sort of grid.

ext_workspace_handle_v1::assign is mostly untested, because no client
current implements this. Perhaps it's best to not-advertise the feature
for now?

Deactivating a workspace is a no-op. This functionality doesn't really
align with sway, although it could potentially be implemented to "switch
to previous workspace on this output" as a follow-up.

Removing a workspace is a no-op.

Implements: swaywm#8812
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

2 participants