# Hyprland Per-Monitor Workspace Assignment: A Deep Dive into the Source Code

> Explore Hyprland's per-monitor workspace assignment by diving into the source code. Understand how rules, placement controllers, and monitor pointers manage workspaces for a seamless workflow.

- Repository: [Hypr Development/Hyprland](https://github.com/hyprwm/Hyprland)
- Tags: deep-dive
- Published: 2026-07-27

---

**TLDR:** Hyprland implements per-monitor workspace assignment through a rule-driven placement system where `workspace = ID, monitor = NAME` configuration lines are parsed into `CWorkspaceRule` objects, stored in `WorkspaceRuleManager`, and enforced at runtime by the `CWorkspacePlacementController`, which relocates workspaces by updating their `m_monitor` pointer and reparenting windows via `moveWorkspaceToMonitor` and `swapActiveWorkspaces`.

Hyprland's per-monitor workspace assignment ensures that specific workspaces always appear on designated displays, even when monitors are hot-plugged or the layout changes. According to the Hyprwm/Hyprland source code, this behavior relies on four cooperating layers inside the compositor: configuration parsing, state storage, placement enforcement, and public API dispatch. Understanding these internals allows advanced users and plugin authors to programmatically control multi-monitor layout behavior with precision.

## Architecture Overview

The per-monitor workspace model is split into distinct layers that communicate through the compositor's state system:

- **Config** — Parses `workspace … monitor …` lines into `CWorkspaceRule` instances managed by `WorkspaceRuleManager`.
- **State** — Stores live monitor and workspace objects (`CMonitor`, `CWorkspace`) and provides query helpers.
- **Placement controller** — Enforces rules, creates persistent workspaces, and executes moves or swaps via `CWorkspacePlacementController`.
- **Public API** — Exposes commands, Lua bindings, and IPC dispatch through `CMonitor::changeWorkspace` and `Config::Actions`.

## How Workspace Rules Map Workspaces to Monitors

### The WorkspaceRuleManager Layer

Hyprland parses each `workspace` line from [`hyprland.conf`](https://github.com/hyprwm/Hyprland/blob/main/hyprland.conf) into a `Config::CWorkspaceRule`. These rules are held by the `WorkspaceRuleManager` singleton accessible via `Config::workspaceRuleMgr()`. The placement controller consumes this rule set through methods such as `add`, `replaceOrAdd`, and `getWorkspaceRuleFor`.

The manager also resolves default workspace mappings when a new monitor appears:

```cpp
// WorkspaceRuleManager.cpp – line 55‑66
std::string CWorkspaceRuleManager::getDefaultWorkspaceFor(const Monitor::IMonitorIdentifiable& monitor) {
    for (auto const& rule : m_rules) {
        if (!rule->isEnabled()) continue;
        if (!rule->m_isDefault.value_or(false)) continue;
        if (monitor.matchesStaticSelector(rule->m_monitor))
            return rule->m_workspaceString;
    }
    return "";
}

```

In this loop, the function iterates `m_rules`, skips disabled or non-default entries, and uses `matchesStaticSelector` to match the monitor identifier against the rule's `m_monitor` field.

### Static Configuration Syntax

To pin a workspace to a display in `~/.config/hypr/hyprland.conf`, use the following syntax:

```ini

# ~/.config/hypr/hyprland.conf

workspace = 1, monitor = DP-1
workspace = 2, monitor = HDMI-A-1
workspace = 3, monitor = eDP-1

```

Each line creates a `CWorkspaceRule` that instructs the placement controller to create or move the specified workspace onto the matching monitor output.

## The Placement Controller and Runtime Enforcement

The `CWorkspacePlacementController` is the engine that applies workspace-to-monitor rules. When a monitor is added, removed, or when the layout changes, the controller invokes `ensurePersistentWorkspacesPresent` and `ensureWorkspacesOnAssignedMonitors` to reconcile live state with configured rules.

The `ensureWorkspacesOnAssignedMonitors` routine demonstrates the core enforcement logic:

```cpp
// ensureWorkspacesOnAssignedMonitors – line 30‑48
void CWorkspacePlacementController::ensureWorkspacesOnAssignedMonitors(const FMoveWorkspace& moveWorkspace) const {
    for (auto const& ws : State::workspaceState()->workspacesCopy()) {
        if (!valid(ws) || ws->m_isSpecialWorkspace) continue;
        const auto RULE = Config::workspaceRuleMgr()->getWorkspaceRuleFor(ws);
        if (!RULE || RULE->m_monitor.empty()) continue;
        const auto PMONITOR = State::monitorState()->query()
                               .relativeTo(Desktop::focusState()->monitor())
                               .configString(RULE->m_monitor).run();
        if (!PMONITOR) continue;
        if (ws->m_monitor == PMONITOR) continue;
        moveWorkspace(ws, PMONITOR, true);
    }
}

```

This method iterates every workspace, skips invalid or special workspaces, fetches the corresponding rule, resolves the target monitor relative to the currently focused monitor via `relativeTo(Desktop::focusState()->monitor())`, and finally calls the `FMoveWorkspace` callback—typically `moveWorkspaceToMonitor`—if the workspace is on the wrong display.

When `moveWorkspaceToMonitor` executes, it updates the workspace's `m_monitor` pointer, emits the `monitorChanged` event so that any window belonging to the workspace can be re-parented, and adjusts window-level data such as monitor reference, floating position, and fullscreen geometry.

## The Monitor API and Workspace Activation

The `CMonitor` class provides the canonical entry points for changing which workspace is active on a given display. The primary overload sets the monitor's active workspace and emits an event:

```cpp
// Monitor.cpp – line 1439‑1451
void CMonitor::changeWorkspace(const PHLWORKSPACE& pWorkspace, bool internal, bool noMouseMove, bool noFocus) {
    if (!pWorkspace) return;
    m_activeWorkspace = pWorkspace;
    m_events.activeWorkspaceChanged.emit(pWorkspace);
    // additional housekeeping (cursor warp, focus changes, etc.)
}

```

A convenience overload accepting a `WORKSPACEID` looks up the workspace object and delegates to the implementation above. This guarantees that *each monitor has its own active workspace* while maintaining consistent window state.

## Swapping Workspaces Between Monitors

To exchange the active contents of two displays without moving individual windows one-by-one, Hyprland provides `swapActiveWorkspaces`. This function temporarily swaps the `m_monitor` fields of the two active workspaces and updates every window that belongs to them, preserving geometry and focus state across the transition.

## Runtime Control via CLI, Lua, and C++

### CLI Dispatch Commands

You can move a workspace to a different monitor at runtime using `hyprctl`:

```bash

# Switch workspace 5 to monitor HDMI-1

hyprctl dispatch workspace.move 5 monitor HDMI-1

```

Internally, this resolves through `Config::Actions::changeWorkspace` and ultimately calls `CMonitor::changeWorkspace` after the placement controller validates the target.

### Lua Scripting Interface

Hyprland exposes per-monitor workspace control through Lua bindings that forward to the same core functions:

```lua
-- Swap the active workspaces of two monitors
hl.workspace.swap_monitors({ "DP-1", "HDMI-1" })

-- Query the active workspace on a specific monitor
local ws = hl.get_active_workspace("DP-1")
print("Active workspace on DP-1:", ws and ws.name or "none")

```

The swap binding invokes `CWorkspacePlacementController::swapActiveWorkspaces`, while workspace activation goes through `CMonitor::changeWorkspace` as shown in the Lua monitor wrapper:

```cpp
// LuaMonitor.cpp – line 49‑50
(*ref)->changeWorkspace(ws->m_id);

```

### C++ Plugin API

Plugin authors can interact with the placement system directly through the state headers:

```cpp
#include <hyprland/src/state/WorkspaceState.hpp>
#include <hyprland/src/state/MonitorState.hpp>

void placeWorkspaceOnMonitor(WORKSPACEID wsid, const std::string& monitorName) {
    auto ws = State::workspaceState()->query().id(wsid).run();
    auto mon = State::monitorState()->query().name(monitorName).run();
    if (ws && mon) State::workspacePlacementController()->moveWorkspaceToMonitor(ws, mon, false);
}

```

This uses the exact same API the internal controller relies on, ensuring identical behavior between plugins and built-in logic.

## Key Source Files for Workspace-to-Monitor Assignment

- **[`src/config/shared/workspace/WorkspaceRuleManager.cpp`](https://github.com/hyprwm/Hyprland/blob/main/src/config/shared/workspace/WorkspaceRuleManager.cpp)** — Holds, adds, and resolves workspace-to-monitor rules.
- **[`src/state/WorkspacePlacementController.cpp`](https://github.com/hyprwm/Hyprland/blob/main/src/state/WorkspacePlacementController.cpp)** — Core engine that creates, moves, and swaps workspaces according to the rules.
- **[`src/output/Monitor.hpp`](https://github.com/hyprwm/Hyprland/blob/main/src/output/Monitor.hpp) / [`src/output/Monitor.cpp`](https://github.com/hyprwm/Hyprland/blob/main/src/output/Monitor.cpp)** — Public API for changing the active workspace on a monitor.
- **[`src/config/lua/objects/LuaMonitor.cpp`](https://github.com/hyprwm/Hyprland/blob/main/src/config/lua/objects/LuaMonitor.cpp)** — Lua bindings exposing `changeWorkspace` and related functions.
- **[`src/config/shared/actions/ConfigActions.cpp`](https://github.com/hyprwm/Hyprland/blob/main/src/config/shared/actions/ConfigActions.cpp)** — Dispatches CLI and IPC workspace change commands to the monitor API.
- **[`src/state/WorkspaceState.cpp`](https://github.com/hyprwm/Hyprland/blob/main/src/state/WorkspaceState.cpp)** — Tracks per-monitor workspace history and query helpers.

## Summary

- **Rule-driven config:** `workspace = ID, monitor = NAME` lines become `CWorkspaceRule` objects stored in `WorkspaceRuleManager`.
- **Active enforcement:** `CWorkspacePlacementController::ensureWorkspacesOnAssignedMonitors` evaluates rules relative to the focused monitor and relocates workspaces automatically.
- **Pointer updates:** `moveWorkspaceToMonitor` changes the workspace's `m_monitor` pointer, emits `monitorChanged`, and adjusts all attached windows.
- **Monitor activation:** `CMonitor::changeWorkspace` sets `m_activeWorkspace` and emits `activeWorkspaceChanged`.
- **Swap support:** `swapActiveWorkspaces` exchanges the active workspaces of two monitors by swapping `m_monitor` fields and re-parenting windows.
- **Scriptable surface:** CLI dispatch, Lua bindings, and C++ plugins all feed into the same internal state functions.

## Frequently Asked Questions

### How do I assign a workspace to a specific monitor in Hyprland?

Add a line such as `workspace = 1, monitor = DP-1` in your [`hyprland.conf`](https://github.com/hyprwm/Hyprland/blob/main/hyprland.conf). The compositor parses this into a `CWorkspaceRule`, stores it in `WorkspaceRuleManager`, and the `CWorkspacePlacementController` will create or move workspace `1` onto `DP-1` at startup and whenever the monitor layout changes.

### Which source files handle per-monitor workspace assignment?

The primary files are [`src/config/shared/workspace/WorkspaceRuleManager.cpp`](https://github.com/hyprwm/Hyprland/blob/main/src/config/shared/workspace/WorkspaceRuleManager.cpp) for rule storage, [`src/state/WorkspacePlacementController.cpp`](https://github.com/hyprwm/Hyprland/blob/main/src/state/WorkspacePlacementController.cpp) for enforcement, and [`src/output/Monitor.cpp`](https://github.com/hyprwm/Hyprland/blob/main/src/output/Monitor.cpp) for the activation API. Lua bindings live in [`src/config/lua/objects/LuaMonitor.cpp`](https://github.com/hyprwm/Hyprland/blob/main/src/config/lua/objects/LuaMonitor.cpp), and command dispatch is implemented in [`src/config/shared/actions/ConfigActions.cpp`](https://github.com/hyprwm/Hyprland/blob/main/src/config/shared/actions/ConfigActions.cpp).

### Are windows automatically reparented when a workspace changes monitors?

Yes. When the placement controller calls `moveWorkspaceToMonitor`, it updates the workspace's `m_monitor` pointer and emits `monitorChanged`. Every window on that workspace then receives updated monitor references, floating positions, and fullscreen geometry so it renders correctly on the new display.

### Can plugins programmatically assign workspaces to monitors?

Yes. A C++ plugin can query the workspace and monitor state objects and call `State::workspacePlacementController()->moveWorkspaceToMonitor` directly, using the same method signatures that Hyprland's internal controller uses to enforce configuration rules.