Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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.

FieldTypeDescription
active_client?ActiveClientThe focused window, or nil when none has focus. One per session, not per output.
compositorstring"niri", "hyprland", "sway" or "mango".
outputsOutputWorkspaces[]One entry per output, sorted by connector name.
overview_open?booleanWhether 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.

FieldTypeDescription
app_idstringWayland app_id, e.g. "firefox"; the key of applications.by_app_id. Empty when unset.
floatingbooleanWhether the window floats rather than tiles.
fullscreen?booleanWhether the window is fullscreen (maximized is false); nil on niri, which does not report it.
titlestringWindow title; empty when unset.

OutputWorkspaces

One output’s workspaces.

FieldTypeDescription
active_workspacestringWorkspaceEntry.id shown on this output.
focused_workspace?stringWorkspaceEntry.id with focus, present only on the focused output.
namestringConnector name, e.g. "eDP-1", as in mantle.screens and a panel’s output.
workspacesWorkspaceEntry[]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.

FieldTypeDescription
app_id?stringapp_id of its representative window, chosen as WorkspaceEntry.app_id is.
namestringFull name, "special:scratch" or "special"; the argument of "toggle_special".
populatedbooleanWhether at least one window sits on it.
shown_on?stringConnector showing it, or nil while hidden.
window_id?stringwindow_id of its representative window, chosen as WorkspaceEntry.app_id is.

WorkspaceEntry

One workspace. Draw number or name, send id.

FieldTypeDescription
app_id?stringapp_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.
idstringOpaque 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?stringWorkspace name; nil when unnamed, always on mango, or on Hyprland when the name is just the number.
number?integerThe 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.
populatedbooleanWhether a window sits here.
urgentbooleanWhether 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?stringwindow_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.

ActionArgumentsDescription
focusid: stringFocuses 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_specialname: stringShows 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.

CapabilityniriHyprlandswaymangoNone
workspacesIPC event stream.socket2.sock events, then one re-read per burst over .socket.sock; a title change alone patches in placei3 IPC subscription (workspace, window, input events), then GET_WORKSPACES and GET_TREE re-read per eventwatch all-monitors snapshots plus one get all-clients eachnil for the run
windowsSame event streamSame re-readSame re-readSame snapshotszwlr_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

TrapFix
Labels show large or odd numbersDraw 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 compositorsHyprland 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 namesSway 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.56Writes 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.

Source: supervisor/src/capabilities/workspaces/