Skip to content

Commit 5919d75

Browse files
committed
workspace: add position-based monitor selection
Add support for monitor:position:XxY syntax in workspace rules to enable deterministic monitor assignment based on monitor position coordinates. Monitor IDs are assigned based on connection order, making them unreliable for multi-monitor setups. Position-based selection allows consistent workspace placement regardless of hotplug order. Usage: workspace = 1, monitor:position:0x0, persistent:true workspace = 2, monitor:position:1920x0, persistent:true Fallback to monitor ID 0 when position doesn't match any monitor.
1 parent 43527d3 commit 5919d75

File tree

5 files changed

+155
-1
lines changed

5 files changed

+155
-1
lines changed

hyprtester/src/tests/main/persistent.cpp

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,65 @@ static bool test() {
6767
EXPECT_COUNT_STRING(str, "workspace ID ", 2);
6868
}
6969

70+
// Test position-based monitor selection
71+
NLog::log("{}Testing position-based monitor selection", Colors::YELLOW);
72+
73+
// Get the position of HEADLESS-PERSISTENT-TEST monitor
74+
std::string monitorPos;
75+
{
76+
auto str = getFromSocket("/monitors");
77+
auto pos = str.find("HEADLESS-PERSISTENT-TEST");
78+
if (pos != std::string::npos) {
79+
// Find "at: X,Y" for this monitor
80+
auto atPos = str.find("at: ", pos);
81+
if (atPos != std::string::npos) {
82+
auto lineEnd = str.find("\n", atPos);
83+
auto posStr = str.substr(atPos + 4, lineEnd - atPos - 4);
84+
// Convert "X,Y" to "XxY"
85+
auto commaPos = posStr.find(",");
86+
if (commaPos != std::string::npos) {
87+
monitorPos = posStr.substr(0, commaPos) + "x" + posStr.substr(commaPos + 1);
88+
}
89+
}
90+
}
91+
}
92+
93+
NLog::log("{}HEADLESS-PERSISTENT-TEST monitor at position {}", Colors::YELLOW, monitorPos);
94+
95+
// Test 1: Normal position (XxY format)
96+
const auto ws7Rule =
97+
"/keyword workspace 7, monitor:position:" + monitorPos + ", persistent:1";
98+
OK(getFromSocket(ws7Rule));
99+
100+
{
101+
auto str = getFromSocket("/workspaces");
102+
EXPECT_CONTAINS(str, "ID 7 (7)");
103+
EXPECT_CONTAINS(str, "on monitor HEADLESS-PERSISTENT-TEST");
104+
}
105+
106+
// Test 2: Relative position (r-Xxb-Y format)
107+
NLog::log("{}Testing relative position r-0xb-0", Colors::YELLOW);
108+
OK(getFromSocket("/keyword workspace 8, monitor:position:r-0xb-0, persistent:1"));
109+
110+
{
111+
auto str = getFromSocket("/workspaces");
112+
EXPECT_CONTAINS(str, "ID 8 (8)");
113+
EXPECT_CONTAINS(str, "on monitor HEADLESS-PERSISTENT-TEST");
114+
}
115+
116+
// Test 3: Invalid position (fallback to monitor ID 0)
117+
NLog::log("{}Testing invalid position 9999x9999 (fallback)", Colors::YELLOW);
118+
OK(getFromSocket("/keyword workspace 9, monitor:position:9999x9999, persistent:1"));
119+
120+
{
121+
auto str = getFromSocket("/workspaces");
122+
EXPECT_CONTAINS(str, "ID 9 (9)");
123+
EXPECT_NOT_CONTAINS(str, "ID 9 (9) on monitor HEADLESS-PERSISTENT-TEST");
124+
}
125+
126+
NLog::log("{}Cleaning up position-based test workspaces", Colors::YELLOW);
127+
OK(getFromSocket("/reload"));
128+
70129
OK(getFromSocket("/output remove HEADLESS-PERSISTENT-TEST"));
71130

72131
// kill all

src/Compositor.cpp

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -825,6 +825,82 @@ PHLMONITOR CCompositor::getMonitorFromDesc(const std::string& desc) {
825825
return nullptr;
826826
}
827827

828+
Vector2D CCompositor::getMaxMonitorPosition() {
829+
Vector2D max = {0, 0};
830+
for (auto const& m : m_monitors) {
831+
if (m->m_position.x > max.x)
832+
max.x = m->m_position.x;
833+
if (m->m_position.y > max.y)
834+
max.y = m->m_position.y;
835+
}
836+
return max;
837+
}
838+
839+
std::optional<Vector2D> CCompositor::parseMonitorPosition(const std::string& position,
840+
const Vector2D& maxPos) {
841+
// Parse position in format "XxY", "r-XxY", "Xxb-Y", or "r-Xxb-Y"
842+
const auto XPOS = position.find('x');
843+
if (XPOS == std::string::npos)
844+
return std::nullopt;
845+
846+
try {
847+
std::string xPart = position.substr(0, XPOS);
848+
std::string yPart = position.substr(XPOS + 1);
849+
850+
bool xRelative = xPart.starts_with("r-");
851+
bool yRelative = yPart.starts_with("b-");
852+
853+
// Parse X coordinate
854+
if (xRelative) {
855+
xPart = xPart.substr(2); // Remove "r-" prefix
856+
if (xPart.empty() || !std::isdigit(xPart[0]))
857+
return std::nullopt; // Strict validation
858+
}
859+
860+
// Parse Y coordinate
861+
if (yRelative) {
862+
yPart = yPart.substr(2); // Remove "b-" prefix
863+
if (yPart.empty() || !std::isdigit(yPart[0]))
864+
return std::nullopt; // Strict validation
865+
}
866+
867+
int xValue = std::stoi(xPart);
868+
int yValue = std::stoi(yPart);
869+
870+
// Calculate absolute position
871+
int X, Y;
872+
if (xRelative || yRelative) {
873+
X = xRelative ? (maxPos.x - xValue) : xValue;
874+
Y = yRelative ? (maxPos.y - yValue) : yValue;
875+
} else {
876+
X = xValue;
877+
Y = yValue;
878+
}
879+
880+
return Vector2D{static_cast<double>(X), static_cast<double>(Y)};
881+
} catch (...) {
882+
return std::nullopt;
883+
}
884+
}
885+
886+
PHLMONITOR CCompositor::getMonitorFromPosition(const std::string& position) {
887+
auto parsedPos = parseMonitorPosition(position, getMaxMonitorPosition());
888+
if (!parsedPos.has_value()) {
889+
Debug::log(WARN, "Invalid monitor position string: {}", position);
890+
return nullptr;
891+
}
892+
893+
const int X = parsedPos->x;
894+
const int Y = parsedPos->y;
895+
896+
for (auto const& m : m_monitors) {
897+
if (m->m_position.x == X && m->m_position.y == Y)
898+
return m;
899+
}
900+
901+
return nullptr;
902+
}
903+
828904
PHLMONITOR CCompositor::getMonitorFromCursor() {
829905
return getMonitorFromVector(g_pPointerManager->position());
830906
}

src/Compositor.hpp

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,9 @@ class CCompositor {
9696
PHLMONITOR getMonitorFromID(const MONITORID&);
9797
PHLMONITOR getMonitorFromName(const std::string&);
9898
PHLMONITOR getMonitorFromDesc(const std::string&);
99+
PHLMONITOR getMonitorFromPosition(const std::string&);
100+
Vector2D getMaxMonitorPosition();
101+
std::optional<Vector2D> parseMonitorPosition(const std::string&, const Vector2D&);
99102
PHLMONITOR getMonitorFromCursor();
100103
PHLMONITOR getMonitorFromVector(const Vector2D&);
101104
void removeWindowFromVectorSafe(PHLWINDOW);

src/config/ConfigManager.cpp

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1944,7 +1944,13 @@ PHLMONITOR CConfigManager::getBoundMonitorForWS(const std::string& wsname) {
19441944
auto monitor = getBoundMonitorStringForWS(wsname);
19451945
if (monitor.starts_with("desc:"))
19461946
return g_pCompositor->getMonitorFromDesc(trim(monitor.substr(5)));
1947-
else
1947+
else if (monitor.starts_with("position:")) {
1948+
auto mon = g_pCompositor->getMonitorFromPosition(trim(monitor.substr(9)));
1949+
// Fallback to monitor ID 0 if position not found
1950+
if (!mon)
1951+
mon = g_pCompositor->getMonitorFromID(0);
1952+
return mon;
1953+
} else
19481954
return g_pCompositor->getMonitorFromName(monitor);
19491955
}
19501956

src/helpers/Monitor.cpp

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1045,6 +1045,16 @@ bool CMonitor::matchesStaticSelector(const std::string& selector) const {
10451045
const auto DESCRIPTIONSELECTOR = trim(selector.substr(5));
10461046

10471047
return m_description.starts_with(DESCRIPTIONSELECTOR) || m_shortDescription.starts_with(DESCRIPTIONSELECTOR);
1048+
} else if (selector.starts_with("position:")) {
1049+
// match by position (supports "XxY", "r-XxY", "Xxb-Y", or "r-Xxb-Y")
1050+
const auto POSITIONSELECTOR = selector.substr(9);
1051+
auto parsedPos = g_pCompositor->parseMonitorPosition(
1052+
POSITIONSELECTOR, g_pCompositor->getMaxMonitorPosition());
1053+
1054+
if (!parsedPos.has_value())
1055+
return false;
1056+
1057+
return m_position.x == parsedPos->x && m_position.y == parsedPos->y;
10481058
} else {
10491059
// match by selector
10501060
return m_name == selector;

0 commit comments

Comments
 (0)