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 and workspaces.outputs or {}) do
if entry.name == output then
for _, workspace in ipairs(entry.workspaces) do
if workspace.id == entry.active_workspace then
return "workspace " .. workspace.idx
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 or Hyprland.
| Field | Type | Description |
|---|---|---|
active_client? | ActiveClient | The focused window, or nil when none has focus. One per session, not per output. |
compositor | string | "niri" or "hyprland". |
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 |
|---|---|---|
class | string | Wayland app_id, e.g. "firefox"; the key of applications.by_app_id. Empty when unset. |
is_floating | boolean | Whether the window floats rather than tiles. |
is_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 | integer | WorkspaceEntry::id shown on this output. |
focused_workspace? | integer | WorkspaceEntry::id with focus, present only on the focused output. |
name | string | Connector name, e.g. "eDP-1", as in mantle.screens and a surface’s monitor. |
workspaces | WorkspaceEntry[] | Workspaces on this output, sorted by WorkspaceEntry::idx. |
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. |
WorkspaceEntry
One workspace. Draw idx, 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 | integer | Stable id, the argument of "focus". Hyprland’s workspace number; opaque on niri. |
idx | integer | Label number: niri’s 1-based position on the output, renumbered on reorder; Hyprland’s workspace number, equal to id up to 255, where it saturates. |
name? | string | Workspace name; nil when unnamed, or on Hyprland when the name is just the number. |
populated | boolean | Whether a window sits here. |
Actions
Call each as mantle.workspaces:<action>(arguments...); ? marks an argument you may omit.
| Action | Arguments | Description |
|---|---|---|
focus | id: integer | Focuses a WorkspaceEntry.id. Hyprland creates a missing number; niri ignores it. |
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
(compositor.rs). One reader feeds both workspaces and
windows.
| Capability | niri | Hyprland | Neither |
|---|---|---|---|
workspaces | IPC event stream | .socket2.sock events, then one re-read per burst over .socket.sock; a title change alone patches in place | nil for the run |
windows | Same event stream | Same re-read | zwlr_foreign_toplevel_manager_v1 on its own Wayland connection; nil if the protocol is missing or setup takes over 5 s |
Hyprland’s refusal of a write logs at debug level only (MANTLE_LOG=debug); niri’s is not logged.
How do I…
Draw workspace buttons
Draw idx, send id (list builds one button per entry):
list {
direction = "Horizontal",
spacing = 4,
source = mantle.workspaces:map(function(workspaces)
local output = workspaces and workspaces.outputs[1]
return output and output.workspaces or {}
end),
key = function(workspace) return tostring(workspace.id) end,
itemfn = function(workspace)
local active = mantle.workspaces:map(function(workspaces)
local output = workspaces and workspaces.outputs[1]
return output ~= nil and output.active_workspace == workspace.id
end)
return button {
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.idx) } },
}
end,
}
Gotchas
| Trap | Fix |
|---|---|
| Labels show large or odd numbers on niri | Draw idx, send id. niri’s id is opaque |
| The strip differs between compositors | Hyprland lists no empty workspace but the active one, and focus on a missing number creates it; niri keeps its own empty workspace and ignores an unknown id. Branch on compositor |
| 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.