Hyprland Per-Monitor Workspace Assignment: A Deep Dive into the Source Code
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 intoCWorkspaceRuleinstances managed byWorkspaceRuleManager. - 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::changeWorkspaceandConfig::Actions.
How Workspace Rules Map Workspaces to Monitors
The WorkspaceRuleManager Layer
Hyprland parses each workspace line from 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:
// 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:
# ~/.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:
// 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:
// 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:
# 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:
-- 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:
// 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:
#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— Holds, adds, and resolves workspace-to-monitor rules.src/state/WorkspacePlacementController.cpp— Core engine that creates, moves, and swaps workspaces according to the rules.src/output/Monitor.hpp/src/output/Monitor.cpp— Public API for changing the active workspace on a monitor.src/config/lua/objects/LuaMonitor.cpp— Lua bindings exposingchangeWorkspaceand related functions.src/config/shared/actions/ConfigActions.cpp— Dispatches CLI and IPC workspace change commands to the monitor API.src/state/WorkspaceState.cpp— Tracks per-monitor workspace history and query helpers.
Summary
- Rule-driven config:
workspace = ID, monitor = NAMElines becomeCWorkspaceRuleobjects stored inWorkspaceRuleManager. - Active enforcement:
CWorkspacePlacementController::ensureWorkspacesOnAssignedMonitorsevaluates rules relative to the focused monitor and relocates workspaces automatically. - Pointer updates:
moveWorkspaceToMonitorchanges the workspace'sm_monitorpointer, emitsmonitorChanged, and adjusts all attached windows. - Monitor activation:
CMonitor::changeWorkspacesetsm_activeWorkspaceand emitsactiveWorkspaceChanged. - Swap support:
swapActiveWorkspacesexchanges the active workspaces of two monitors by swappingm_monitorfields 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. 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 for rule storage, src/state/WorkspacePlacementController.cpp for enforcement, and src/output/Monitor.cpp for the activation API. Lua bindings live in src/config/lua/objects/LuaMonitor.cpp, and command dispatch is implemented in 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.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →