workspaces
Workspaces per output, special workspaces and the focused window.
panel {
id = "bar",
layer = "top",
anchor = { top = true, left = true, right = true },
height = 28,
child = function(output) -- one instance per monitor, named by connector
return text {
content = mantle.workspaces:map(function(workspaces)
for _, entry in ipairs(workspaces.outputs) do
if entry.name == output then
for _, workspace in ipairs(entry.workspaces) do
if workspace.id == entry.active_workspace then
return "workspace " .. (workspace.number or workspace.name)
end
end
end
end
return ""
end, ""),
}
end,
}
State
mantle.workspaces:get() returns WorkspacesState, nil before the first push. A field marked ? may be absent.
mantle.workspaces payload; nil without niri, Hyprland, sway or mango.
| Field | Type | Description |
|---|---|---|
active_client? | ActiveClient | The focused window, or nil when none has focus. One per session, not per output. |
compositor | string | "niri", "hyprland", "sway" or "mango". |
outputs | OutputWorkspaces[] | One entry per output, sorted by connector name. |
overview_open? | boolean | Whether niri’s overview is open; nil on Hyprland, which has none. |
special? | SpecialWorkspace[] | Hyprland special workspaces, sorted by name. nil on niri; empty means none exist. |
ActiveClient
The focused window.
| Field | Type | Description |
|---|---|---|
app_id | string | Wayland app_id, e.g. "firefox"; the key of applications.by_app_id. Empty when unset. |
floating | boolean | Whether the window floats rather than tiles. |
fullscreen? | boolean | Whether the window is fullscreen (maximized is false); nil on niri, which does not report it. |
title | string | Window title; empty when unset. |
OutputWorkspaces
One output’s workspaces.
| Field | Type | Description |
|---|---|---|
active_workspace | string | WorkspaceEntry.id shown on this output. |
focused_workspace? | string | WorkspaceEntry.id with focus, present only on the focused output. |
name | string | Connector name, e.g. "eDP-1", as in mantle.screens and a panel’s output. |
workspaces | WorkspaceEntry[] | Workspaces on this output: niri by position, Hyprland numbered ones by number, then named ones by name; mango’s tags by number. |
SpecialWorkspace
One Hyprland special workspace.
| Field | Type | Description |
|---|---|---|
app_id? | string | app_id of its representative window, chosen as WorkspaceEntry.app_id is. |
name | string | Full name, "special:scratch" or "special"; the argument of "toggle_special". |
populated | boolean | Whether at least one window sits on it. |
shown_on? | string | Connector showing it, or nil while hidden. |
window_id? | string | window_id of its representative window, chosen as WorkspaceEntry.app_id is. |
WorkspaceEntry
One workspace. Draw number or name, send id.
| Field | Type | Description |
|---|---|---|
app_id? | string | app_id of a window here: Hyprland’s most recently focused one with an app_id; on niri the focused one, else the lowest id, nil if that one has no app_id. nil when empty. |
id | string | Opaque string, only passed back to actions such as "focus". Hyprland’s workspace id in decimal, so a numbered workspace’s id is its number and focusing an unlisted number creates it; named workspaces have negative ids. niri’s id in decimal; sway’s workspace name; mango’s tag on its output, "<output>:<tag>" ("DP-1:3"). |
name? | string | Workspace name; nil when unnamed, always on mango, or on Hyprland when the name is just the number. |
number? | integer | The number a keybind targets: niri’s 1-based position on the output, renumbered on reorder; Hyprland’s workspace number; sway’s leading number; mango’s tag number. nil for a Hyprland named or non-numeric sway workspace. |
populated | boolean | Whether a window sits here. |
urgent | boolean | Whether a window here is asking for attention. Clears when the compositor clears it, on Hyprland when that window gains focus. Hyprland special workspaces carry none; their windows report it in windows. |
window_id? | string | window_id of a window here, chosen as WorkspaceEntry.app_id is. nil when empty. |
Actions
Call each as mantle.workspaces:<action>(arguments...); ? marks an argument you may omit.
| Action | Arguments | Description |
|---|---|---|
focus | id: string | Focuses a WorkspaceEntry.id. Hyprland creates a missing number and sway a missing name; niri ignores it. mango shows only that tag on its output and focuses the output. |
toggle_special | name: string | Shows or hides a special[].name on Hyprland, creating an unknown one; no-op on niri. |
Backend
The Supervisor picks the compositor once, from $HYPRLAND_INSTANCE_SIGNATURE, then $NIRI_SOCKET, then $SWAYSOCK, then $MANGO_INSTANCE_SIGNATURE
(compositor/). One reader feeds both workspaces and
windows.
| Capability | niri | Hyprland | sway | mango | None |
|---|---|---|---|---|---|
workspaces | IPC event stream | .socket2.sock events, then one re-read per burst over .socket.sock; a title change alone patches in place | i3 IPC subscription (workspace, window, input events), then GET_WORKSPACES and GET_TREE re-read per event | watch all-monitors snapshots plus one get all-clients each | nil for the run |
windows | Same event stream | Same re-read | Same re-read | Same snapshots | zwlr_foreign_toplevel_manager_v1 on its own Wayland connection; nil if the protocol is missing or setup takes over 5 s |
Hyprland’s, sway’s and mango’s refusal of a write logs at debug level only (MANTLE_LOG=debug); niri’s is not logged.
mango tags
mango has tags, not workspaces: each output has a fixed set (1 to 9 by default) and may show several at once. Every tag is one workspace with number = the tag number, no name, and id = "<output>:<tag>" (for example "DP-1:3"), listed whether empty or not. With several tags shown, only the lowest is active_workspace; focus shows just the chosen tag on its output and moves focus there. special and overview_open are nil.
How do I…
Draw workspace buttons
Draw number or name, send id (list builds one button per entry):
list {
direction = "horizontal",
spacing = 4,
items = mantle.workspaces:map(function(workspaces)
local output = workspaces.outputs[1]
return output and output.workspaces or {}
end),
key = function(workspace) return tostring(workspace.id) end,
each = function(workspace)
local active = mantle.workspaces:map(function(workspaces)
local output = workspaces.outputs[1]
return output ~= nil and output.active_workspace == workspace.id
end, false)
return rect {
padding = { left = 8, right = 8 },
radius = 6,
background = active:map(function(is_active) return is_active and "#89B4FA" or "#313244" end),
on_click = function() mantle.workspaces:focus(workspace.id) end,
children = { text { content = tostring(workspace.number or workspace.name) } },
}
end,
}
Gotchas
| Trap | Fix |
|---|---|
| Labels show large or odd numbers | Draw number or name, send id. id is an opaque string: on Hyprland the workspace id in decimal ("3", or negative like "-1337" for a named workspace), on niri its own id, on mango "<output>:<tag>", on sway the workspace name (sway focuses and moves by name). Don’t do arithmetic on it |
| The strip differs between compositors | Hyprland lists no empty workspace but the active one, and focus on an unlisted number creates it (a numbered workspace’s id is its number as a string, so focus("7") works); niri keeps its own empty workspace and ignores an unknown id. sway also creates a workspace on focus of an unlisted name. Branch on compositor |
focus does nothing on sway for some workspace names | Sway reads next, prev, current, number, output, gaps, back_and_forth (any case) and names starting -- as commands, and cannot take a name holding ", \ or $. The call is logged at debug level and dropped |
| Actions do nothing on Hyprland older than 0.56 | Writes use 0.56’s Lua dispatch syntax; older versions refuse them while reads still work. Update Hyprland; MANTLE_LOG=debug shows the refusal |
See also: Workspaces recipe.