From 4ab3a40398d1331b69e033ab14533524c554a002 Mon Sep 17 00:00:00 2001 From: Eren <51189118+astridlyre@users.noreply.github.com> Date: Fri, 1 May 2026 15:50:02 -0700 Subject: [PATCH 01/21] screenshare: adjust session cleanup and event emission order (#14229) * fix(screenshare): adjust session cleanup and event emission order Revised the handling of `stoppedListener` initialization in `getManagedSession` to ensure correct scoping and lifecycle management. Updated the `stop` method in `CScreenshareSession` to adjust the order of `screenshareEvents` and `stopped.emit()` to prevent potential use-after-free scenarios. * fix(screenshare): ensure managedSession removal uses consistent target reference This change updates the lambda in the stopped listener to use a pre-fetched target pointer for comparison when erasing sessions. * fix(screenshare): use early-return and smart ptr comparison in session cleanup --- .../screenshare/ScreenshareManager.cpp | 18 ++++++++++-------- .../screenshare/ScreenshareSession.cpp | 2 +- 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/src/managers/screenshare/ScreenshareManager.cpp b/src/managers/screenshare/ScreenshareManager.cpp index 6a0f5b958..6b5c0aded 100644 --- a/src/managers/screenshare/ScreenshareManager.cpp +++ b/src/managers/screenshare/ScreenshareManager.cpp @@ -140,16 +140,18 @@ WP CScreenshareManager::getManagedSession(eScreenshareType m_sessions.emplace_back(session); it = m_managedSessions.emplace(m_managedSessions.end(), makeUnique(std::move(session))); + + auto& managed = *it; + managed->stoppedListener = managed->m_session->m_events.stopped.listen([managed = WP(managed)]() { + if (!managed) + return; + + const auto& session = managed->m_session; + std::erase_if(Screenshare::mgr()->m_managedSessions, [&session](const auto& s) { return s && s->m_session == session; }); + }); } - auto& session = *it; - - session->stoppedListener = session->m_session->m_events.stopped.listen([session = WP(session)]() { - if (!session.expired()) - std::erase_if(Screenshare::mgr()->m_managedSessions, [&](const auto& s) { return s && s->m_session.get() == session->m_session.get(); }); - }); - - return session->m_session; + return (*it)->m_session; } bool CScreenshareManager::isOutputBeingSSd(PHLMONITOR monitor) { diff --git a/src/managers/screenshare/ScreenshareSession.cpp b/src/managers/screenshare/ScreenshareSession.cpp index e3676ec0b..1f7aeb881 100644 --- a/src/managers/screenshare/ScreenshareSession.cpp +++ b/src/managers/screenshare/ScreenshareSession.cpp @@ -56,9 +56,9 @@ void CScreenshareSession::stop() { if (m_stopped) return; m_stopped = true; - m_events.stopped.emit(); screenshareEvents(false); + m_events.stopped.emit(); } bool CScreenshareSession::isActive() { From 6e1fcfa81e55a7d78e40fdb5c41700c18b0e1268 Mon Sep 17 00:00:00 2001 From: Vaxry <43317083+vaxerski@users.noreply.github.com> Date: Fri, 1 May 2026 23:50:26 +0100 Subject: [PATCH 02/21] hyprctl: fix getoption with custom types (#14243) --- src/debug/HyprCtl.cpp | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/src/debug/HyprCtl.cpp b/src/debug/HyprCtl.cpp index 278e92c6a..e84a55527 100644 --- a/src/debug/HyprCtl.cpp +++ b/src/debug/HyprCtl.cpp @@ -1644,6 +1644,14 @@ static std::string dispatchGetOption(eHyprCtlOutputFormat format, std::string re return std::format("str: {}\nset: {}", **rc(VAL), VAR.setByUser); else if (TYPE == typeid(void*)) return std::format("custom type: {}\nset: {}", rc((*rc(VAL))->getData())->toString(), VAR.setByUser); + else if (TYPE == typeid(Config::IComplexConfigValue)) + return std::format("custom type: {}\nset: {}", (*rc(VAL))->toString(), VAR.setByUser); + else if (TYPE == typeid(Config::CCssGapData)) + return std::format("css gap data: {}\nset: {}", (*rc(VAL))->toString(), VAR.setByUser); + else if (TYPE == typeid(Config::CGradientValueData)) + return std::format("gradient data: {}\nset: {}", (*rc(VAL))->toString(), VAR.setByUser); + else if (TYPE == typeid(Config::CFontWeightConfigValueData)) + return std::format("font weight data: {}\nset: {}", (*rc(VAL))->toString(), VAR.setByUser); } else { if (TYPE == typeid(Config::INTEGER)) return std::format(R"({{"option": "{}", "int": {}, "set": {} }})", curitem, **rc(VAL), VAR.setByUser); @@ -1662,6 +1670,15 @@ static std::string dispatchGetOption(eHyprCtlOutputFormat format, std::string re else if (TYPE == typeid(void*)) return std::format(R"({{"option": "{}", "custom": "{}", "set": {} }})", curitem, rc((*rc(VAL))->getData())->toString(), VAR.setByUser); + else if (TYPE == typeid(Config::IComplexConfigValue)) + return std::format(R"({{"option": "{}", "custom": "{}", "set": {} }})", curitem, (*rc(VAL))->toString(), VAR.setByUser); + else if (TYPE == typeid(Config::CCssGapData)) + return std::format(R"({{"option": "{}", "css": "{}", "set": {} }})", curitem, (*rc(VAL))->toString(), VAR.setByUser); + else if (TYPE == typeid(Config::CGradientValueData)) + return std::format(R"({{"option": "{}", "gradient": "{}", "set": {} }})", curitem, (*rc(VAL))->toString(), VAR.setByUser); + else if (TYPE == typeid(Config::CFontWeightConfigValueData)) + return std::format(R"({{"option": "{}", "font_weight": "{}", "set": {} }})", curitem, (*rc(VAL))->toString(), + VAR.setByUser); } return "invalid type (internal error)"; From c7b8fe13c10d6a4392bfda9bcfc041e0ea0b7c00 Mon Sep 17 00:00:00 2001 From: Vaxry <43317083+vaxerski@users.noreply.github.com> Date: Fri, 1 May 2026 23:50:57 +0100 Subject: [PATCH 03/21] tests: fix gtests crashing (#14244) --- src/config/lua/ConfigManager.cpp | 2 +- src/config/lua/LuaEventHandler.cpp | 9 ++++++--- tests/config/lua/LuaObjectsBasic.cpp | 14 ++++++++++---- 3 files changed, 17 insertions(+), 8 deletions(-) diff --git a/src/config/lua/ConfigManager.cpp b/src/config/lua/ConfigManager.cpp index 7819ff2e8..b4fc791d5 100644 --- a/src/config/lua/ConfigManager.cpp +++ b/src/config/lua/ConfigManager.cpp @@ -160,7 +160,7 @@ static int safeLuaRequire(lua_State* L) { WP Lua::mgr() { auto& mgr = Config::mgr(); - if (mgr->type() != CONFIG_LUA) + if (!mgr || mgr->type() != CONFIG_LUA) return nullptr; return dynamicPointerCast(WP(mgr)); diff --git a/src/config/lua/LuaEventHandler.cpp b/src/config/lua/LuaEventHandler.cpp index eb4f6bc52..fbb44dac9 100644 --- a/src/config/lua/LuaEventHandler.cpp +++ b/src/config/lua/LuaEventHandler.cpp @@ -61,15 +61,18 @@ void CLuaEventHandler::dispatch(const std::string& name, int nargs, const std::f lua_rawgeti(m_lua, LUA_REGISTRYINDEX, sub->second.luaRef); pushArgs(); - int status = LUA_OK; - if (auto* mgr = CConfigManager::fromLuaState(m_lua); mgr) + auto* mgr = CConfigManager::fromLuaState(m_lua); + + int status = LUA_OK; + if (mgr) status = mgr->guardedPCall(nargs, 0, 0, CConfigManager::LUA_TIMEOUT_EVENT_CALLBACK_MS, std::format("hl.on(\"{}\") callback", name)); else status = lua_pcall(m_lua, nargs, 0, 0); if (status != LUA_OK) { const char* err = lua_tostring(m_lua, -1); - Config::Lua::mgr()->addError(std::format("hl.on(\"{}\") callback: {}", name, err ? err : "(unknown)")); + if (mgr) + mgr->addError(std::format("hl.on(\"{}\") callback: {}", name, err ? err : "(unknown)")); lua_pop(m_lua, 1); } } diff --git a/tests/config/lua/LuaObjectsBasic.cpp b/tests/config/lua/LuaObjectsBasic.cpp index 10758b7d4..3f09aa1e9 100644 --- a/tests/config/lua/LuaObjectsBasic.cpp +++ b/tests/config/lua/LuaObjectsBasic.cpp @@ -48,6 +48,13 @@ namespace { lua_pop(L, 1); return v; } + + bool isGlobalNil(lua_State* L, const char* name) { + lua_getglobal(L, name); + const bool isnil = lua_isnil(L, -1); + lua_pop(L, 1); + return isnil; + } } TEST(ConfigLuaObjects, keybindCanToggleEnabledFromLua) { @@ -138,10 +145,9 @@ TEST(ConfigLuaObjects, objectsAreReadOnlyFromLua) { Objects::CLuaKeybind::push(L, keybind); lua_setglobal(L, "kb"); - EXPECT_NE(luaL_dostring(L, "kb.foo = 1"), LUA_OK); - ASSERT_TRUE(lua_isstring(L, -1)); - EXPECT_NE(std::string(lua_tostring(L, -1)).find("read-only"), std::string::npos); - lua_pop(L, 1); + luaL_dostring(L, "kb.foo = 1"); + luaL_dostring(L, "x = kb.foo"); + EXPECT_TRUE(isGlobalNil(L, "x")); } TEST(ConfigLuaObjects, keybindSupportsEqAndToString) { From f9723133cc909874999e6181bddf286033070e0c Mon Sep 17 00:00:00 2001 From: Barrett Ruth <62671086+barrettruth@users.noreply.github.com> Date: Fri, 1 May 2026 19:02:01 -0400 Subject: [PATCH 04/21] gestures: add live pinch cursor zoom (#14049) --- hyprtester/plugin/src/main.cpp | 122 ++++++++++++++++++ hyprtester/src/tests/main/gestures.cpp | 38 ++++++ hyprtester/src/tests/main/groups.cpp | 2 + hyprtester/test.lua | 1 + src/helpers/MonitorZoomController.cpp | 21 ++- src/helpers/MonitorZoomController.hpp | 15 ++- .../trackpad/gestures/CursorZoomGesture.cpp | 57 +++++++- .../trackpad/gestures/CursorZoomGesture.hpp | 5 + 8 files changed, 249 insertions(+), 12 deletions(-) diff --git a/hyprtester/plugin/src/main.cpp b/hyprtester/plugin/src/main.cpp index c7d6f4fdf..3bb6e2b23 100644 --- a/hyprtester/plugin/src/main.cpp +++ b/hyprtester/plugin/src/main.cpp @@ -2,11 +2,13 @@ #include #include #include +#include #define private public #include #include #include +#include #include #include #include @@ -17,6 +19,7 @@ #undef private #include +#include #include using namespace Hyprutils::Utils; using namespace Hyprutils::String; @@ -156,6 +159,96 @@ static SDispatchResult simulateGesture(std::string in) { return {.success = true}; } +static SDispatchResult pinchUpdate(std::string in) { + CVarList data(in); + uint32_t fingers = 2; + double scale = 1.0; + Vector2D delta = {}; + double rotation{}; + + if (data.size() < 2) + return {.success = false, .error = "invalid input"}; + + if (const auto n = strToNumber(data[0]); n) + fingers = n.value(); + else + return {.success = false, .error = "invalid input"}; + + if (const auto n = strToNumber(data[1]); n) + scale = n.value(); + else + return {.success = false, .error = "invalid input"}; + + if (data.size() > 2) { + if (const auto n = strToNumber(data[2]); n) + delta.x = n.value(); + else + return {.success = false, .error = "invalid input"}; + } + + if (data.size() > 3) { + if (const auto n = strToNumber(data[3]); n) + delta.y = n.value(); + else + return {.success = false, .error = "invalid input"}; + } + + if (data.size() > 4) { + if (const auto n = strToNumber(data[4]); n) + rotation = n.value(); + else + return {.success = false, .error = "invalid input"}; + } + + g_pTrackpadGestures->gestureUpdate(IPointer::SPinchUpdateEvent{ + .fingers = fingers, + .delta = delta, + .scale = scale, + .rotation = rotation, + }); + + return {}; +} + +static SDispatchResult pinchEnd(std::string in) { + g_pTrackpadGestures->gestureEnd(IPointer::SPinchEndEvent{}); + + return {}; +} + +static SDispatchResult expectCursorZoom(std::string in) { + CVarList data(in); + float expected = 1.F; + float delta = 0.01F; + + if (data.size() < 1) + return {.success = false, .error = "invalid input"}; + + if (const auto n = strToNumber(data[0]); n) + expected = n.value(); + else + return {.success = false, .error = "invalid input"}; + + if (data.size() > 1) { + if (const auto n = strToNumber(data[1]); n) + delta = n.value(); + else + return {.success = false, .error = "invalid input"}; + } + + const auto PMONITOR = g_pCompositor->getMonitorFromVector(g_pInputManager->getMouseCoordsInternal()); + + if (!PMONITOR) + return {.success = false, .error = "No monitor under cursor"}; + + const auto actual = PMONITOR->m_cursorZoom->value(); + + if (std::abs(actual - expected) > delta) + return {.success = false, .error = std::format("Expected cursor zoom {} ± {}, got {}", expected, delta, actual)}; + + return {}; +} + static SDispatchResult vkb(std::string in) { auto tkb0 = CTestKeyboard::create(false); auto tkb1 = CTestKeyboard::create(false); @@ -375,6 +468,32 @@ static int luaGesture(lua_State* L) { return luaResult(L, ::simulateGesture(std::format("{},{}", direction, fingers))); } +static int luaPinchUpdate(lua_State* L) { + std::string in = std::format("{},{}", (int)luaL_checkinteger(L, 1), (double)luaL_checknumber(L, 2)); + + if (lua_gettop(L) > 2) + in += std::format(",{}", (double)luaL_checknumber(L, 3)); + if (lua_gettop(L) > 3) + in += std::format(",{}", (double)luaL_checknumber(L, 4)); + if (lua_gettop(L) > 4) + in += std::format(",{}", (double)luaL_checknumber(L, 5)); + + return luaResult(L, ::pinchUpdate(in)); +} + +static int luaPinchEnd(lua_State* L) { + return luaResult(L, ::pinchEnd("")); +} + +static int luaExpectCursorZoom(lua_State* L) { + const auto expected = (double)luaL_checknumber(L, 1); + + if (lua_gettop(L) > 1) + return luaResult(L, ::expectCursorZoom(std::format("{},{}", expected, (double)luaL_checknumber(L, 2)))); + + return luaResult(L, ::expectCursorZoom(std::format("{}", expected))); +} + static int luaScroll(lua_State* L) { return luaResult(L, ::scroll(std::to_string((double)luaL_checknumber(L, 1)))); } @@ -425,6 +544,9 @@ APICALL EXPORT PLUGIN_DESCRIPTION_INFO PLUGIN_INIT(HANDLE handle) { addLuaFn("vkb", ::luaVkb); addLuaFn("alt", ::luaAlt); addLuaFn("gesture", ::luaGesture); + addLuaFn("pinch_update", ::luaPinchUpdate); + addLuaFn("pinch_end", ::luaPinchEnd); + addLuaFn("expect_cursor_zoom", ::luaExpectCursorZoom); addLuaFn("scroll", ::luaScroll); addLuaFn("click", ::luaClick); addLuaFn("keybind", ::luaKeybind); diff --git a/hyprtester/src/tests/main/gestures.cpp b/hyprtester/src/tests/main/gestures.cpp index f83247123..f82a1613b 100644 --- a/hyprtester/src/tests/main/gestures.cpp +++ b/hyprtester/src/tests/main/gestures.cpp @@ -6,12 +6,14 @@ #include #include #include +#include #include #include #include "../shared.hpp" using namespace Hyprutils::OS; using namespace Hyprutils::Memory; +using namespace Hyprutils::String; #define UP CUniquePointer #define SP CSharedPointer @@ -178,4 +180,40 @@ TEST_CASE(gestures) { OK(getFromSocket("/dispatch hl.dsp.focus({ workspace = '1' })")); } + const std::string cursorPosBeforePinch = getFromSocket("/cursorpos"); + + OK(getFromSocket("/dispatch hl.dsp.cursor.move({ x = 500, y = 500 })")); + OK(getFromSocket("/eval hl.config({ cursor = { zoom_factor = 1 } })")); + OK(getFromSocket("/eval hl.plugin.test.expect_cursor_zoom(1, 0.01)")); + + OK(getFromSocket("/eval hl.plugin.test.pinch_update(2, 1.2)")); + OK(getFromSocket("/eval hl.plugin.test.expect_cursor_zoom(1.2, 0.01)")); + OK(getFromSocket("/eval hl.plugin.test.pinch_update(2, 1.6)")); + OK(getFromSocket("/eval hl.plugin.test.expect_cursor_zoom(1.6, 0.01)")); + OK(getFromSocket("/eval hl.plugin.test.pinch_end()")); + OK(getFromSocket("/eval hl.plugin.test.expect_cursor_zoom(1.6, 0.01)")); + + OK(getFromSocket("/eval hl.plugin.test.pinch_update(2, 0.64)")); + OK(getFromSocket("/eval hl.plugin.test.expect_cursor_zoom(1, 0.01)")); + OK(getFromSocket("/eval hl.plugin.test.pinch_end()")); + OK(getFromSocket("/eval hl.plugin.test.expect_cursor_zoom(1, 0.01)")); + + const auto comma = cursorPosBeforePinch.find(','); + + if (comma != std::string::npos) { + auto xSv = std::string_view(cursorPosBeforePinch).substr(0, comma); + auto ySv = std::string_view(cursorPosBeforePinch).substr(comma + 1); + while (!xSv.empty() && xSv.front() == ' ') + xSv.remove_prefix(1); + while (!ySv.empty() && ySv.front() == ' ') + ySv.remove_prefix(1); + + const auto x = strToNumber(xSv); + const auto y = strToNumber(ySv); + + if (!x || !y) + FAIL_TEST("Failed to restore cursor pos"); + + OK(getFromSocket(std::format("/dispatch hl.dsp.cursor.move({{ x = {}, y = {} }})", x.value(), y.value()))); + } } diff --git a/hyprtester/src/tests/main/groups.cpp b/hyprtester/src/tests/main/groups.cpp index 7e1c63b6b..e1d4dd9be 100644 --- a/hyprtester/src/tests/main/groups.cpp +++ b/hyprtester/src/tests/main/groups.cpp @@ -192,6 +192,7 @@ TEST_CASE(groups) { NLog::log("{}Disable autogrouping", Colors::YELLOW); OK(getFromSocket("/eval hl.config({ group = { auto_group = false } })")); + OK(getFromSocket("/eval hl.config({ dwindle = { force_split = 2 } })")); NLog::log("{}Spawn kittyProcC", Colors::YELLOW); auto kittyProcC = Tests::spawnKitty(); @@ -206,6 +207,7 @@ TEST_CASE(groups) { EXPECT_COUNT_STRING(str, "at: 22,22", 2); } + OK(getFromSocket("/eval hl.config({ dwindle = { force_split = 0 } })")); OK(getFromSocket("/dispatch hl.dsp.focus({ direction = 'left' })")); OK(getFromSocket("/dispatch hl.dsp.group.active({ index = 1 })")); OK(getFromSocket("/eval hl.config({ group = { auto_group = true } })")); diff --git a/hyprtester/test.lua b/hyprtester/test.lua index 6f39b34a3..d1f8657e8 100644 --- a/hyprtester/test.lua +++ b/hyprtester/test.lua @@ -290,5 +290,6 @@ hl.gesture({ fingers = 5, direction = "left", action = function() hl.dispatch(hl hl.gesture({ fingers = 5, direction = "right", action = function() hl.dispatch(hl.dsp.send_shortcut({ mods = "", key = "t", window = "activewindow" })) end }) hl.gesture({ fingers = 4, direction = "right", action = function() hl.dispatch(hl.dsp.send_shortcut({ mods = "", key = "return", window = "activewindow" })) end }) hl.gesture({ fingers = 4, direction = "left", action = function() hl.dispatch(hl.dsp.cursor.move_to_corner({ corner = 1, window = "activewindow" })) end }) +hl.gesture({ fingers = 2, direction = "pinch", action = "cursorZoom", zoom_level = "1", mode = "live" }) hl.gesture({ fingers = 2, direction = "right", action = "float", disable_inhibit = true }) diff --git a/src/helpers/MonitorZoomController.cpp b/src/helpers/MonitorZoomController.cpp index c1b6cc6af..b59e1eb22 100644 --- a/src/helpers/MonitorZoomController.cpp +++ b/src/helpers/MonitorZoomController.cpp @@ -7,11 +7,27 @@ #include "desktop/DesktopTypes.hpp" #include "render/Renderer.hpp" +void CMonitorZoomController::pinAnchor(const Vector2D& anchor) { + m_pinnedAnchor = anchor; + m_anchorPinned = true; +} + +void CMonitorZoomController::clearAnchor() { + m_anchorPinned = false; +} + +Vector2D CMonitorZoomController::getAnchor(const PHLMONITORREF& monitor) { + if (m_anchorPinned) + return m_pinnedAnchor; + + return g_pInputManager->getMouseCoordsInternal() - monitor->m_position; +} + void CMonitorZoomController::zoomWithDetachedCamera(CBox& result, const Render::SRenderData& m_renderData) { const auto m = m_renderData.pMonitor; auto monbox = CBox(0, 0, m->m_size.x, m->m_size.y); const auto ZOOM = g_pHyprRenderer->m_renderData.mouseZoomFactor; - const auto MOUSE = g_pInputManager->getMouseCoordsInternal() - m->m_position; + const auto MOUSE = getAnchor(m); if (m_lastZoomLevel != ZOOM) { if (m_resetCameraState) { @@ -83,8 +99,7 @@ void CMonitorZoomController::applyZoomTransform(CBox& monbox, const Render::SRen if (*PZOOMDETACHEDCAMERA && !INITANIM) zoomWithDetachedCamera(monbox, m_renderData); else { - const auto ZOOMCENTER = - g_pHyprRenderer->m_renderData.mouseZoomUseMouse ? (g_pInputManager->getMouseCoordsInternal() - m->m_position) * m->m_scale : m->m_transformedSize / 2.f; + const auto ZOOMCENTER = g_pHyprRenderer->m_renderData.mouseZoomUseMouse ? getAnchor(m) * m->m_scale : m->m_transformedSize / 2.f; monbox.translate(-ZOOMCENTER).scale(ZOOM).translate(*PZOOMRIGID ? m->m_transformedSize / 2.0 : ZOOMCENTER); } diff --git a/src/helpers/MonitorZoomController.hpp b/src/helpers/MonitorZoomController.hpp index 94373bafc..ce3ad5546 100644 --- a/src/helpers/MonitorZoomController.hpp +++ b/src/helpers/MonitorZoomController.hpp @@ -1,6 +1,7 @@ #pragma once #include "./math/Math.hpp" +#include "../desktop/DesktopTypes.hpp" namespace Render { struct SRenderData; @@ -10,12 +11,18 @@ class CMonitorZoomController { public: bool m_resetCameraState = true; + void pinAnchor(const Vector2D& anchor); + void clearAnchor(); + void applyZoomTransform(CBox& monbox, const Render::SRenderData& m_renderData); private: - void zoomWithDetachedCamera(CBox& result, const Render::SRenderData& m_renderData); + void zoomWithDetachedCamera(CBox& result, const Render::SRenderData& m_renderData); + Vector2D getAnchor(const PHLMONITORREF& monitor); - CBox m_camera; - float m_lastZoomLevel = 1.0f; - bool m_padCamEdges = true; + CBox m_camera; + Vector2D m_pinnedAnchor = {}; + float m_lastZoomLevel = 1.0f; + bool m_padCamEdges = true; + bool m_anchorPinned = false; }; diff --git a/src/managers/input/trackpad/gestures/CursorZoomGesture.cpp b/src/managers/input/trackpad/gestures/CursorZoomGesture.cpp index 515edbb6b..418b8f7a2 100644 --- a/src/managers/input/trackpad/gestures/CursorZoomGesture.cpp +++ b/src/managers/input/trackpad/gestures/CursorZoomGesture.cpp @@ -2,19 +2,40 @@ #include "../../../../Compositor.hpp" #include "../../../../helpers/Monitor.hpp" +#include "../../../../managers/input/InputManager.hpp" +#include CCursorZoomTrackpadGesture::CCursorZoomTrackpadGesture(const std::string& first, const std::string& second) { - try { - m_zoomValue = std::stof(first); - } catch (...) { ; } + if (const auto n = Hyprutils::String::strToNumber(first); n) + m_zoomValue = n.value(); if (second == "mult") m_mode = MODE_MULT; + else if (second == "live") + m_mode = MODE_LIVE; } void CCursorZoomTrackpadGesture::begin(const ITrackpadGesture::STrackpadGestureBegin& e) { ITrackpadGesture::begin(e); + if (m_mode == MODE_LIVE) { + if (!e.pinch) + return; + + m_monitor = g_pCompositor->getMonitorFromCursor(); + if (!m_monitor) + return; + + const auto PMONITOR = m_monitor.lock(); + if (!PMONITOR) + return; + + m_zoomBegin = std::clamp(PMONITOR->m_cursorZoom->value(), 1.0F, 100.0F); + PMONITOR->m_cursorZoom->setValueAndWarp(m_zoomBegin); + PMONITOR->m_zoomController.pinAnchor(g_pInputManager->getMouseCoordsInternal() - PMONITOR->m_position); + return; + } + if (m_mode == MODE_TOGGLE) m_zoomed = !m_zoomed; @@ -25,9 +46,35 @@ void CCursorZoomTrackpadGesture::begin(const ITrackpadGesture::STrackpadGestureB *m->m_cursorZoom = m_zoomed ? m_zoomValue : *PZOOMFACTOR; break; case MODE_MULT: *m->m_cursorZoom = std::clamp(m->m_cursorZoom->goal() * m_zoomValue, 1.0F, 100.0F); break; + case MODE_LIVE: break; } } } -void CCursorZoomTrackpadGesture::update(const ITrackpadGesture::STrackpadGestureUpdate& e) {} -void CCursorZoomTrackpadGesture::end(const ITrackpadGesture::STrackpadGestureEnd& e) {} +void CCursorZoomTrackpadGesture::update(const ITrackpadGesture::STrackpadGestureUpdate& e) { + if (m_mode != MODE_LIVE || !m_monitor || !e.pinch) + return; + + const auto PMONITOR = m_monitor.lock(); + if (!PMONITOR) + return; + + auto zoom = std::clamp(m_zoomBegin * static_cast(e.pinch->scale), 1.0F, 100.0F); + + if (zoom < 1.05F) + zoom = 1.0F; + + PMONITOR->m_cursorZoom->setValueAndWarp(zoom); +} + +void CCursorZoomTrackpadGesture::end(const ITrackpadGesture::STrackpadGestureEnd& e) { + if (m_mode != MODE_LIVE || !m_monitor) + return; + + const auto PMONITOR = m_monitor.lock(); + if (!PMONITOR) + return; + + PMONITOR->m_zoomController.clearAnchor(); + m_monitor.reset(); +} diff --git a/src/managers/input/trackpad/gestures/CursorZoomGesture.hpp b/src/managers/input/trackpad/gestures/CursorZoomGesture.hpp index b53c81e98..3def907ff 100644 --- a/src/managers/input/trackpad/gestures/CursorZoomGesture.hpp +++ b/src/managers/input/trackpad/gestures/CursorZoomGesture.hpp @@ -2,6 +2,8 @@ #include "ITrackpadGesture.hpp" +#include "../../../../desktop/DesktopTypes.hpp" + class CCursorZoomTrackpadGesture : public ITrackpadGesture { public: CCursorZoomTrackpadGesture(const std::string& zoomLevel, const std::string& mode); @@ -14,10 +16,13 @@ class CCursorZoomTrackpadGesture : public ITrackpadGesture { private: float m_zoomValue = 1.0; inline static bool m_zoomed = false; + PHLMONITORREF m_monitor; + float m_zoomBegin = 1.0; enum eMode : uint8_t { MODE_TOGGLE = 0, MODE_MULT, + MODE_LIVE, }; eMode m_mode = MODE_TOGGLE; From c065e951d631a3aa3736b45f17b3556597f63c1a Mon Sep 17 00:00:00 2001 From: Vaxry Date: Sat, 2 May 2026 00:26:35 +0100 Subject: [PATCH 05/21] layout/dwindle,master: return invalid layoutmsg errors --- src/layout/algorithm/tiled/dwindle/DwindleAlgorithm.cpp | 3 ++- src/layout/algorithm/tiled/master/MasterAlgorithm.cpp | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/layout/algorithm/tiled/dwindle/DwindleAlgorithm.cpp b/src/layout/algorithm/tiled/dwindle/DwindleAlgorithm.cpp index eabb77674..100856be3 100644 --- a/src/layout/algorithm/tiled/dwindle/DwindleAlgorithm.cpp +++ b/src/layout/algorithm/tiled/dwindle/DwindleAlgorithm.cpp @@ -729,7 +729,8 @@ Config::ErrorResult CDwindleAlgorithm::layoutMsg(const std::string_view& sv) { CURRENT_NODE->pParent->splitRatio = std::clamp(newRatio, 0.1F, 1.9F); CURRENT_NODE->pParent->recalcSizePosRecursive(); - } + } else + return Config::configError(std::format("Unknown dwindle layoutmsg: {}", sv), Config::eConfigErrorLevel::ERROR, Config::eConfigErrorCode::INVALID_ARGUMENT); return {}; } diff --git a/src/layout/algorithm/tiled/master/MasterAlgorithm.cpp b/src/layout/algorithm/tiled/master/MasterAlgorithm.cpp index a6849d2ac..a0ea13c34 100644 --- a/src/layout/algorithm/tiled/master/MasterAlgorithm.cpp +++ b/src/layout/algorithm/tiled/master/MasterAlgorithm.cpp @@ -777,7 +777,8 @@ Config::ErrorResult CMasterAlgorithm::layoutMsg(const std::string_view& sv) { } calculateWorkspace(); - } + } else + return Config::configError(std::format("Unknown master layoutmsg: {}", sv), Config::eConfigErrorLevel::ERROR, Config::eConfigErrorCode::INVALID_ARGUMENT); return {}; } From c3e07986cc1afad0e69d30ac7817d7831f2d1c22 Mon Sep 17 00:00:00 2001 From: Tom Englund Date: Sat, 2 May 2026 17:24:20 +0200 Subject: [PATCH 06/21] renderer: only set presentationmode when required (#14252) if guard the setPresentationMode with the current state, shows up in profiling as minor waste on each frame. --- src/render/Renderer.cpp | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/render/Renderer.cpp b/src/render/Renderer.cpp index b5a32a2b9..6800c1857 100644 --- a/src/render/Renderer.cpp +++ b/src/render/Renderer.cpp @@ -2145,8 +2145,9 @@ void IHyprRenderer::renderMonitor(PHLMONITOR pMonitor, bool commit) { Event::bus()->m_events.render.stage.emit(RENDER_POST); pMonitor->m_output->state->addDamage(frameDamage); - pMonitor->m_output->state->setPresentationMode(shouldTear ? Aquamarine::eOutputPresentationMode::AQ_OUTPUT_PRESENTATION_IMMEDIATE : - Aquamarine::eOutputPresentationMode::AQ_OUTPUT_PRESENTATION_VSYNC); + auto presentationMode = shouldTear ? Aquamarine::eOutputPresentationMode::AQ_OUTPUT_PRESENTATION_IMMEDIATE : Aquamarine::eOutputPresentationMode::AQ_OUTPUT_PRESENTATION_VSYNC; + if (pMonitor->m_output->state->state().presentationMode != presentationMode) + pMonitor->m_output->state->setPresentationMode(presentationMode); if (commit) commitPendingAndDoExplicitSync(pMonitor); From fceb15979e487ed74f73e05caf786a91d45b4075 Mon Sep 17 00:00:00 2001 From: Gregor Date: Sat, 2 May 2026 18:22:02 +0200 Subject: [PATCH 07/21] config/lua: workspace.move/rename should accept "workspace" instead of "id" as a parameter (#14232) --- src/config/lua/bindings/LuaBindingsDispatchers.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/config/lua/bindings/LuaBindingsDispatchers.cpp b/src/config/lua/bindings/LuaBindingsDispatchers.cpp index 0b6ca07a0..4a981f4eb 100644 --- a/src/config/lua/bindings/LuaBindingsDispatchers.cpp +++ b/src/config/lua/bindings/LuaBindingsDispatchers.cpp @@ -1167,9 +1167,9 @@ static int hlWorkspaceToggleSpecial(lua_State* L) { static int hlWorkspaceRename(lua_State* L) { if (!lua_istable(L, 1)) - return Internal::configError(L, "hl.workspace.rename: expected a table { id, name? }"); + return Internal::configError(L, "hl.workspace.rename: expected a table { workspace, name? }"); - const auto id = Internal::requireTableFieldWorkspaceSelector(L, 1, "id", "hl.workspace.rename"); + const auto id = Internal::requireTableFieldWorkspaceSelector(L, 1, "workspace", "hl.workspace.rename"); auto name = Internal::tableOptStr(L, 1, "name"); lua_pushstring(L, id.c_str()); @@ -1187,7 +1187,7 @@ static int hlWorkspaceMove(lua_State* L) { const auto mon = Internal::requireTableFieldMonitorSelector(L, 1, "monitor", "hl.workspace.move"); - auto id = Internal::tableOptWorkspaceSelector(L, 1, "id", "hl.workspace.move"); + auto id = Internal::tableOptWorkspaceSelector(L, 1, "workspace", "hl.workspace.move"); if (id) { lua_pushstring(L, id->c_str()); lua_pushstring(L, mon.c_str()); From a3fa7b49503d8eadd644d68c5bc452b600a11935 Mon Sep 17 00:00:00 2001 From: Vaxry <43317083+vaxerski@users.noreply.github.com> Date: Sat, 2 May 2026 19:18:44 +0100 Subject: [PATCH 08/21] keybinds: fix keycode matching on lua (#14254) --- src/config/legacy/ConfigManager.cpp | 8 +-- .../lua/bindings/LuaBindingsToplevel.cpp | 23 ++++++-- src/managers/KeybindManager.cpp | 56 +++++++++++++------ src/managers/KeybindManager.hpp | 12 ++-- 4 files changed, 66 insertions(+), 33 deletions(-) diff --git a/src/config/legacy/ConfigManager.cpp b/src/config/legacy/ConfigManager.cpp index 2705c3959..1b18b4a7d 100644 --- a/src/config/legacy/ConfigManager.cpp +++ b/src/config/legacy/ConfigManager.cpp @@ -1538,15 +1538,15 @@ std::optional CConfigManager::handleBind(const std::string& command else if ((ARGS.size() > sc(4) + DESCR_OFFSET + DEVICE_OFFSET && !mouse) || (ARGS.size() > sc(3) + DESCR_OFFSET + DEVICE_OFFSET && mouse)) return "bind: too many args"; - std::vector KEYSYMS; - std::vector MODS; + std::vector KEYSYMS; + std::vector MODS; if (multiKey) { for (const auto& splitKey : CVarList(ARGS[1], 8, '&')) { - KEYSYMS.emplace_back(xkb_keysym_from_name(splitKey.c_str(), XKB_KEYSYM_CASE_INSENSITIVE)); + KEYSYMS.emplace_back(xkb_keysym_from_name(splitKey.c_str(), XKB_KEYSYM_CASE_INSENSITIVE), 0); } for (const auto& splitMod : CVarList(ARGS[0], 8, '&')) { - MODS.emplace_back(xkb_keysym_from_name(splitMod.c_str(), XKB_KEYSYM_CASE_INSENSITIVE)); + MODS.emplace_back(xkb_keysym_from_name(splitMod.c_str(), XKB_KEYSYM_CASE_INSENSITIVE), 0); } } const auto MOD = g_pKeybindManager->stringToModMask(ARGS[0]); diff --git a/src/config/lua/bindings/LuaBindingsToplevel.cpp b/src/config/lua/bindings/LuaBindingsToplevel.cpp index 8bdd1c193..2d54a7b3c 100644 --- a/src/config/lua/bindings/LuaBindingsToplevel.cpp +++ b/src/config/lua/bindings/LuaBindingsToplevel.cpp @@ -9,6 +9,7 @@ #include "../../../devices/IKeyboard.hpp" #include "../../../managers/eventLoop/EventLoopManager.hpp" +#include #include #include @@ -46,12 +47,12 @@ static bool isSymSpecial(std::string_view sv) { } static std::expected parseKeyString(SKeybind& kb, std::string_view sv) { - bool modsEnded = false, specialSym = false; - CVarList2 vl(sv, 0, '+', true); + bool modsEnded = false, specialSym = false; + CVarList2 vl(sv, 0, '+', true); - uint32_t modMask = 0; - std::vector keysyms; - std::string lastKeyArg; + uint32_t modMask = 0; + std::vector> keysyms; + std::string lastKeyArg; if (sv == "catchall") { kb.catchAll = true; @@ -86,6 +87,16 @@ static std::expected parseKeyString(SKeybind& kb, std::string continue; } + if (arg.starts_with("code:") && isNumber(std::string{arg.substr(5)})) { + auto res = strToNumber(arg.substr(5)); + + if (!res) + return std::unexpected(std::format("Invalid keycode: \"{}\".", arg)); + + keysyms.emplace_back(XKB_KEY_NoSymbol, xkb_keycode_t{*res}); + continue; + } + auto sym = xkb_keysym_from_name(std::string{arg}.c_str(), XKB_KEYSYM_CASE_INSENSITIVE); if (sym == XKB_KEY_NoSymbol) { @@ -99,7 +110,7 @@ static std::expected parseKeyString(SKeybind& kb, std::string } lastKeyArg = arg; - keysyms.emplace_back(sym); + keysyms.emplace_back(sym, 0); } kb.modmask = modMask; diff --git a/src/managers/KeybindManager.cpp b/src/managers/KeybindManager.cpp index 4a5df4be7..2496a0a50 100644 --- a/src/managers/KeybindManager.cpp +++ b/src/managers/KeybindManager.cpp @@ -505,25 +505,44 @@ void CKeybindManager::onSwitchOffEvent(const std::string& switchName) { handleKeybinds(0, SPressedKeyWithMods{.keyName = "switch:off:" + switchName}, true, nullptr, nullptr); } -eMultiKeyCase CKeybindManager::mkKeysymSetMatches(const std::vector keybindKeysyms, const std::set pressedKeysyms) { - // Returns whether two sets of keysyms are equal, partially equal, or not - // matching. (Partially matching means that pressed is a subset of bound) +eMultiKeyCase CKeybindManager::mkKeysymSetMatches(const std::vector& keybindKeysyms, const std::set& pressedKeysyms) { + // Returns whether the bound and pressed keys match fully, partially, or not at all. + // KeybindKey stores {keysym, keycode}; either non-zero field matching is enough. - std::set boundKeysNotPressed; - std::set pressedKeysNotBound; + const auto MATCHES = [](const KeybindKey& lhs, const KeybindKey& rhs) { + return (lhs.first != 0 && rhs.first != 0 && lhs.first == rhs.first) || (lhs.second != 0 && rhs.second != 0 && lhs.second == rhs.second); + }; - std::set symsKb; - for (const auto& k : keybindKeysyms) { - symsKb.emplace(k); + std::vector pressed{pressedKeysyms.begin(), pressedKeysyms.end()}; + std::vector boundForPressed(pressed.size(), -1); + + const auto tryMatch = [&](auto&& self, const size_t boundIdx, std::vector& seen) -> bool { + for (size_t pressedIdx = 0; pressedIdx < pressed.size(); ++pressedIdx) { + if (seen[pressedIdx] || !MATCHES(keybindKeysyms[boundIdx], pressed[pressedIdx])) + continue; + + seen[pressedIdx] = true; + + if (boundForPressed[pressedIdx] == -1 || self(self, static_cast(boundForPressed[pressedIdx]), seen)) { + boundForPressed[pressedIdx] = static_cast(boundIdx); + return true; + } + } + + return false; + }; + + size_t matches = 0; + for (size_t boundIdx = 0; boundIdx < keybindKeysyms.size(); ++boundIdx) { + std::vector seen(pressed.size(), false); + if (tryMatch(tryMatch, boundIdx, seen)) + ++matches; } - std::ranges::set_difference(symsKb, pressedKeysyms, std::inserter(boundKeysNotPressed, boundKeysNotPressed.begin())); - std::ranges::set_difference(pressedKeysyms, symsKb, std::inserter(pressedKeysNotBound, pressedKeysNotBound.begin())); - - if (boundKeysNotPressed.empty() && pressedKeysNotBound.empty()) + if (matches == keybindKeysyms.size() && matches == pressed.size()) return MK_FULL_MATCH; - if (!boundKeysNotPressed.empty() && pressedKeysNotBound.empty()) + if (matches > 0 || (pressed.empty() && !keybindKeysyms.empty())) return MK_PARTIAL_MATCH; return MK_NO_MATCH; @@ -556,14 +575,14 @@ SDispatchResult CKeybindManager::handleKeybinds(const uint32_t modmask, const SP if (key.keysym != 0) { if (pressed) { if (keycodeToModifier(key.keycode)) - m_mkMods.insert(key.keysym); + m_mkMods.emplace(key.keysym, key.keycode); else - m_mkKeys.insert(key.keysym); + m_mkKeys.emplace(key.keysym, key.keycode); } else { if (keycodeToModifier(key.keycode)) - m_mkMods.erase(key.keysym); + std::erase_if(m_mkMods, [&key](const auto& e) { return e.first == key.keysym || e.second == key.keycode; }); else - m_mkKeys.erase(key.keysym); + std::erase_if(m_mkKeys, [&key](const auto& e) { return e.first == key.keysym || e.second == key.keycode; }); } } @@ -615,7 +634,8 @@ SDispatchResult CKeybindManager::handleKeybinds(const uint32_t modmask, const SP // check for just the one match // this is also needed for multi-key binds so that SUPER + A + K can't // be actuated by SUPER + K + A - if (key.keysym != k->sMkKeys.back()) + auto& back = k->sMkKeys.back(); + if (key.keysym != back.first && key.keycode != back.second) continue; } else if (!key.keyName.empty()) { if (key.keyName != k->key) diff --git a/src/managers/KeybindManager.hpp b/src/managers/KeybindManager.hpp index 87cae14af..8c8855801 100644 --- a/src/managers/KeybindManager.hpp +++ b/src/managers/KeybindManager.hpp @@ -25,13 +25,15 @@ struct SSubmap { } }; +using KeybindKey = std::pair; + struct SKeybind { std::string key = ""; - std::vector sMkKeys = {}; + std::vector sMkKeys = {}; uint32_t keycode = 0; bool catchAll = false; uint32_t modmask = 0; - std::vector sMkMods = {}; + std::vector sMkMods = {}; std::string handler = ""; std::string arg = ""; bool locked = false; @@ -156,10 +158,10 @@ class CKeybindManager { SDispatchResult handleKeybinds(const uint32_t, const SPressedKeyWithMods&, bool, SP, SP); - std::set m_mkKeys = {}; - std::set m_mkMods = {}; + std::set m_mkKeys = {}; + std::set m_mkMods = {}; eMultiKeyCase mkBindMatches(const SP); - eMultiKeyCase mkKeysymSetMatches(const std::vector, const std::set); + eMultiKeyCase mkKeysymSetMatches(const std::vector&, const std::set&); bool handleInternalKeybinds(xkb_keysym_t); bool handleVT(xkb_keysym_t); From 30cd345addf0412baf87b71ad7213508c534e334 Mon Sep 17 00:00:00 2001 From: Vaxry Date: Sat, 2 May 2026 19:23:09 +0100 Subject: [PATCH 09/21] hyprctl: fix bools in getoption --- src/debug/HyprCtl.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/debug/HyprCtl.cpp b/src/debug/HyprCtl.cpp index e84a55527..b6777de36 100644 --- a/src/debug/HyprCtl.cpp +++ b/src/debug/HyprCtl.cpp @@ -1632,6 +1632,8 @@ static std::string dispatchGetOption(eHyprCtlOutputFormat format, std::string re if (format == FORMAT_NORMAL) { if (TYPE == typeid(Config::INTEGER)) return std::format("int: {}\nset: {}", **rc(VAL), VAR.setByUser); + else if (TYPE == typeid(Config::BOOL)) + return std::format("bool: {}\nset: {}", **rc(VAL), VAR.setByUser); else if (TYPE == typeid(Config::FLOAT)) return std::format("float: {:2f}\nset: {}", **rc(VAL), VAR.setByUser); else if (TYPE == typeid(Config::VEC2)) @@ -1655,6 +1657,8 @@ static std::string dispatchGetOption(eHyprCtlOutputFormat format, std::string re } else { if (TYPE == typeid(Config::INTEGER)) return std::format(R"({{"option": "{}", "int": {}, "set": {} }})", curitem, **rc(VAL), VAR.setByUser); + else if (TYPE == typeid(Config::BOOL)) + return std::format(R"({{"option": "{}", "bool": {}, "set": {} }})", curitem, (**rc(VAL)) ? "true" : "false", VAR.setByUser); else if (TYPE == typeid(Config::FLOAT)) return std::format(R"({{"option": "{}", "float": {:2f}, "set": {} }})", curitem, **rc(VAL), VAR.setByUser); else if (TYPE == typeid(Config::VEC2)) From e180c59b467bad2002efc7770acd4bfd5437da1b Mon Sep 17 00:00:00 2001 From: fazzi <18248986+fxzzi@users.noreply.github.com> Date: Sat, 2 May 2026 20:34:43 +0100 Subject: [PATCH 10/21] desktop/windowRule: add `confine_pointer` window rule (#13379) --- .../lua/bindings/LuaBindingsInternal.hpp | 1 + .../rule/windowRule/WindowRuleApplicator.cpp | 11 +++- .../rule/windowRule/WindowRuleApplicator.hpp | 1 + .../windowRule/WindowRuleEffectContainer.cpp | 3 +- .../windowRule/WindowRuleEffectContainer.hpp | 1 + src/managers/input/InputManager.cpp | 63 ++++++++++++------- 6 files changed, 54 insertions(+), 26 deletions(-) diff --git a/src/config/lua/bindings/LuaBindingsInternal.hpp b/src/config/lua/bindings/LuaBindingsInternal.hpp index 0cced6b47..024d72489 100644 --- a/src/config/lua/bindings/LuaBindingsInternal.hpp +++ b/src/config/lua/bindings/LuaBindingsInternal.hpp @@ -101,6 +101,7 @@ namespace Config::Lua::Bindings::Internal { {"no_screen_share", []() -> ILuaConfigValue* { return new CLuaConfigBool(false); }, WE::WINDOW_RULE_EFFECT_NO_SCREEN_SHARE}, {"no_vrr", []() -> ILuaConfigValue* { return new CLuaConfigBool(false); }, WE::WINDOW_RULE_EFFECT_NO_VRR}, {"stay_focused", []() -> ILuaConfigValue* { return new CLuaConfigBool(false); }, WE::WINDOW_RULE_EFFECT_STAY_FOCUSED}, + {"confine_pointer", []() -> ILuaConfigValue* { return new CLuaConfigBool(false); }, WE::WINDOW_RULE_EFFECT_CONFINE_POINTER}, }; std::string argStr(lua_State* L, int idx); diff --git a/src/desktop/rule/windowRule/WindowRuleApplicator.cpp b/src/desktop/rule/windowRule/WindowRuleApplicator.cpp index c19c63389..b3349bd6e 100644 --- a/src/desktop/rule/windowRule/WindowRuleApplicator.cpp +++ b/src/desktop/rule/windowRule/WindowRuleApplicator.cpp @@ -52,9 +52,9 @@ std::unordered_set CWindowRuleApplicato std::pair{std::ref(m_noFollowMouse), [this] { return noFollowMouseEffect(); }}, std::pair{std::ref(m_noScreenShare), [this] { return noScreenShareEffect(); }}, std::pair{std::ref(m_noVRR), [this] { return noVRREffect(); }}, std::pair{std::ref(m_persistentSize), [this] { return persistentSizeEffect(); }}, std::pair{std::ref(m_stayFocused), [this] { return stayFocusedEffect(); }}, std::pair{std::ref(m_idleInhibitMode), [this] { return idleInhibitModeEffect(); }}, - std::pair{std::ref(m_borderSize), [this] { return borderSizeEffect(); }}, std::pair{std::ref(m_rounding), [this] { return roundingEffect(); }}, - std::pair{std::ref(m_roundingPower), [this] { return roundingPowerEffect(); }}, std::pair{std::ref(m_scrollMouse), [this] { return scrollMouseEffect(); }}, - std::pair{std::ref(m_scrollTouchpad), [this] { return scrollTouchpadEffect(); }}, + std::pair{std::ref(m_confinePointer), [this] { return confinePointerEffect(); }}, std::pair{std::ref(m_borderSize), [this] { return borderSizeEffect(); }}, + std::pair{std::ref(m_rounding), [this] { return roundingEffect(); }}, std::pair{std::ref(m_roundingPower), [this] { return roundingPowerEffect(); }}, + std::pair{std::ref(m_scrollMouse), [this] { return scrollMouseEffect(); }}, std::pair{std::ref(m_scrollTouchpad), [this] { return scrollTouchpadEffect(); }}, std::pair{std::ref(m_animationStyle), [this] { return animationStyleEffect(); }}, std::pair{std::ref(m_maxSize), [this] { return maxSizeEffect(); }}, std::pair{std::ref(m_minSize), [this] { return minSizeEffect(); }}, std::pair{std::ref(m_activeBorderColor), [this] { return activeBorderColorEffect(); }}, std::pair{std::ref(m_inactiveBorderColor), [this] { return inactiveBorderColorEffect(); }})); @@ -342,6 +342,11 @@ CWindowRuleApplicator::SRuleResult CWindowRuleApplicator::applyDynamicRule(const m_stayFocused.second |= rule->getPropertiesMask(); break; } + case WINDOW_RULE_EFFECT_CONFINE_POINTER: { + m_confinePointer.first.set(truthy(effect), Types::PRIORITY_WINDOW_RULE); + m_confinePointer.second |= rule->getPropertiesMask(); + break; + } case WINDOW_RULE_EFFECT_SCROLL_MOUSE: { m_scrollMouse.first.set(std::get(value), Types::PRIORITY_WINDOW_RULE); m_scrollMouse.second |= rule->getPropertiesMask(); diff --git a/src/desktop/rule/windowRule/WindowRuleApplicator.hpp b/src/desktop/rule/windowRule/WindowRuleApplicator.hpp index f5e436092..2c382da8e 100644 --- a/src/desktop/rule/windowRule/WindowRuleApplicator.hpp +++ b/src/desktop/rule/windowRule/WindowRuleApplicator.hpp @@ -116,6 +116,7 @@ namespace Desktop::Rule { DEFINE_PROP(bool, noVRR, false, WINDOW_RULE_EFFECT_NO_VRR) DEFINE_PROP(bool, persistentSize, false, WINDOW_RULE_EFFECT_PERSISTENT_SIZE) DEFINE_PROP(bool, stayFocused, false, WINDOW_RULE_EFFECT_STAY_FOCUSED) + DEFINE_PROP(bool, confinePointer, false, WINDOW_RULE_EFFECT_CONFINE_POINTER) DEFINE_PROP(int, idleInhibitMode, false, WINDOW_RULE_EFFECT_IDLE_INHIBIT) diff --git a/src/desktop/rule/windowRule/WindowRuleEffectContainer.cpp b/src/desktop/rule/windowRule/WindowRuleEffectContainer.cpp index 668672477..3b624c03e 100644 --- a/src/desktop/rule/windowRule/WindowRuleEffectContainer.cpp +++ b/src/desktop/rule/windowRule/WindowRuleEffectContainer.cpp @@ -65,12 +65,13 @@ static const std::vector EFFECT_STRINGS = { "scroll_mouse", // "scroll_touchpad", // "stay_focused", // + "confine_pointer", // "__internal_last_static", // }; // This is here so that if we change the rules, we get reminded to update // the strings. -static_assert(WINDOW_RULE_EFFECT_LAST_STATIC == 55); +static_assert(WINDOW_RULE_EFFECT_LAST_STATIC == 56); CWindowRuleEffectContainer::CWindowRuleEffectContainer() : IEffectContainer(std::vector{EFFECT_STRINGS}) { ; diff --git a/src/desktop/rule/windowRule/WindowRuleEffectContainer.hpp b/src/desktop/rule/windowRule/WindowRuleEffectContainer.hpp index af8611090..b482c18e8 100644 --- a/src/desktop/rule/windowRule/WindowRuleEffectContainer.hpp +++ b/src/desktop/rule/windowRule/WindowRuleEffectContainer.hpp @@ -66,6 +66,7 @@ namespace Desktop::Rule { WINDOW_RULE_EFFECT_SCROLL_MOUSE, WINDOW_RULE_EFFECT_SCROLL_TOUCHPAD, WINDOW_RULE_EFFECT_STAY_FOCUSED, + WINDOW_RULE_EFFECT_CONFINE_POINTER, WINDOW_RULE_EFFECT_LAST_STATIC, }; diff --git a/src/managers/input/InputManager.cpp b/src/managers/input/InputManager.cpp index 7ed7b4f6b..2f9b64a3b 100644 --- a/src/managers/input/InputManager.cpp +++ b/src/managers/input/InputManager.cpp @@ -265,31 +265,50 @@ void CInputManager::mouseMoveUnified(uint32_t time, bool refocus, bool mouse, st g_pCompositor->scheduleFrameForMonitor(PMONITOR, Aquamarine::IOutput::AQ_SCHEDULE_CURSOR_MOVE); // constraints - if (!overridePos.has_value() && !g_pSeatManager->m_mouse.expired() && isConstrained()) { - const auto SURF = Desktop::View::CWLSurface::fromResource(Desktop::focusState()->surface()); - const auto CONSTRAINT = SURF ? SURF->constraint() : nullptr; + auto confineToRegion = [&](const CRegion& rg, SP surf) { + const auto CLOSEST = rg.closestPoint(mouseCoords); + const auto BOX = surf->getSurfaceBoxGlobal(); + const auto WINDOW = Desktop::View::CWindow::fromView(surf->view()); + const auto CLOSESTLOCAL = (CLOSEST - (BOX.has_value() ? BOX->pos() : Vector2D{})) * (WINDOW ? WINDOW->m_X11SurfaceScaledBy : 1.0); - if (CONSTRAINT) { - if (CONSTRAINT->isLocked()) { - const auto HINT = CONSTRAINT->logicPositionHint(); - g_pCompositor->warpCursorTo(HINT, true); + g_pCompositor->warpCursorTo(CLOSEST, true); + g_pSeatManager->sendPointerMotion(time, CLOSESTLOCAL); + PROTO::relativePointer->sendRelativeMotion(sc(time) * 1000, {}, {}); + }; + + if (!g_pSeatManager->m_mouse.expired()) { + const auto SURF = Desktop::View::CWLSurface::fromResource(Desktop::focusState()->surface()); + + if (isConstrained()) { + const auto CONSTRAINT = SURF ? SURF->constraint() : nullptr; + + if (CONSTRAINT) { + if (CONSTRAINT->isLocked()) { + const auto HINT = CONSTRAINT->logicPositionHint(); + g_pCompositor->warpCursorTo(HINT, true); + } else { + confineToRegion(CONSTRAINT->logicConstraintRegion(), SURF); + } + + return; } else { - const auto RG = CONSTRAINT->logicConstraintRegion(); - const auto CLOSEST = RG.closestPoint(mouseCoords); - const auto BOX = SURF->getSurfaceBoxGlobal(); - const auto WINDOW = Desktop::View::CWindow::fromView(SURF->view()); - const auto CLOSESTLOCAL = (CLOSEST - (BOX.has_value() ? BOX->pos() : Vector2D{})) * (WINDOW ? WINDOW->m_X11SurfaceScaledBy : 1.0); - - g_pCompositor->warpCursorTo(CLOSEST, true); - g_pSeatManager->sendPointerMotion(time, CLOSESTLOCAL); - PROTO::relativePointer->sendRelativeMotion(sc(time) * 1000, {}, {}); + Log::logger->log(Log::ERR, "BUG THIS: Null SURF/CONSTRAINT in mouse refocus. Ignoring constraints. {:x} {:x}", rc(SURF.get()), + rc(CONSTRAINT.get())); } - - return; - - } else - Log::logger->log(Log::ERR, "BUG THIS: Null SURF/CONSTRAINT in mouse refocus. Ignoring constraints. {:x} {:x}", rc(SURF.get()), - rc(CONSTRAINT.get())); + } else { + const auto WINDOW = SURF ? Desktop::View::CWindow::fromView(SURF->view()) : nullptr; + if (WINDOW) { + if (WINDOW->m_ruleApplicator->confinePointer().valueOrDefault()) { + const auto BOX = SURF->getSurfaceBoxGlobal(); + if (BOX.has_value()) { + CRegion rg; + rg.set(*BOX); + confineToRegion(rg, SURF); + } + return; + } + } + } } if (PMONITOR != Desktop::focusState()->monitor() && (*PMOUSEFOCUSMON || refocus) && m_forcedFocus.expired()) From f60e50da007aaf55a24d5e25e7a761f3ead51aaa Mon Sep 17 00:00:00 2001 From: Niko Savola Date: Sat, 2 May 2026 22:36:14 +0300 Subject: [PATCH 11/21] internal: improve cursor size logging (#14180) --- src/managers/CursorManager.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/managers/CursorManager.cpp b/src/managers/CursorManager.cpp index 2bf72f848..22b39e379 100644 --- a/src/managers/CursorManager.cpp +++ b/src/managers/CursorManager.cpp @@ -79,7 +79,7 @@ CCursorManager::CCursorManager() { if (SIZE) { try { m_size = std::stoi(SIZE); - } catch (...) { ; } + } catch (...) { Log::logger->log(Log::WARN, "Invalid HYPRCURSOR_SIZE value \"{}\"", SIZE); } } if (m_size <= 0) { @@ -93,7 +93,7 @@ CCursorManager::CCursorManager() { if (SIZE) { try { m_size = std::stoi(SIZE); - } catch (...) { ; } + } catch (...) { Log::logger->log(Log::WARN, "Invalid XCURSOR_SIZE value \"{}\"", SIZE); } } if (m_size <= 0) { From 334b361c8d1bf17652663768fd9237b46287781f Mon Sep 17 00:00:00 2001 From: Tony Miller <2032667+tnymlr@users.noreply.github.com> Date: Sun, 3 May 2026 06:06:00 +1000 Subject: [PATCH 12/21] screenshare: round captureBox after scaling to fix region capture at fractional scales (#14257) After scaling m_captureBox from logical to pixel coordinates, the box may have non-integer dimensions (e.g. logical 401x301 at scale 1.25 -> pixel 501.25x376.25). m_bufferSize is then computed as captureBox.size() and sent to the client as int32 width/height (truncating the fraction), but m_bufferSize itself stays as a fractional Vector2D. When the client allocates the integer-sized buffer and submits it, CScreenshareFrame::share() rejects it with ERROR_BUFFER_SIZE because Vector2D::operator== is exact double comparison: (501, 376) != (501.25, 376.25). The frame is sent failed, and the client retries forever. The SHARE_WINDOW path already rounds its bufferSize; the SHARE_REGION path didn't. Round the captureBox immediately after scaling so all downstream consumers (m_bufferSize, render translates) see clean integer pixel coordinates. Reproducer at scale 1.25 on a 1920x1080 monitor: wf-recorder -g '500,200 401x301' -f /tmp/x.mp4 # "Failed to copy frame, retrying..." until exit wf-recorder -g '500,200 400x300' -f /tmp/x.mp4 # works (400*1.25=500, 300*1.25=375, both integer) --- src/managers/screenshare/ScreenshareSession.cpp | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/managers/screenshare/ScreenshareSession.cpp b/src/managers/screenshare/ScreenshareSession.cpp index 1f7aeb881..7e6aea6fa 100644 --- a/src/managers/screenshare/ScreenshareSession.cpp +++ b/src/managers/screenshare/ScreenshareSession.cpp @@ -80,8 +80,9 @@ void CScreenshareSession::init() { if (g_pEventLoopManager) g_pEventLoopManager->addTimer(m_shareStopTimer); - // scale capture box since it's in logical coords - m_captureBox.scale(monitor()->m_scale); + // scale capture box since it's in logical coords; round to integer pixel + // dims so m_bufferSize matches the int32 size we send to the client + m_captureBox.scale(monitor()->m_scale).round(); m_listeners.monitorDestroyed = monitor()->m_events.disconnect.listen([this]() { stop(); }); m_listeners.monitorModeChanged = monitor()->m_events.modeChanged.listen([this]() { From 5202ab22f53c3298d599dbae0781037a1b1184d7 Mon Sep 17 00:00:00 2001 From: Vaxry <43317083+vaxerski@users.noreply.github.com> Date: Sat, 2 May 2026 21:06:10 +0100 Subject: [PATCH 13/21] cmakelists: fixup errors failing build on arch ci (#14259) --- CMakeLists.txt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CMakeLists.txt b/CMakeLists.txt index c1ffc6b3a..7b0c4fa63 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -104,6 +104,8 @@ add_compile_options( -Wall -Wextra -Wpedantic + -Wno-keyword-macro + -Wno-unused-result -Wno-unused-parameter -Wno-unused-value -Wno-missing-field-initializers From fe17cea39ed9e001b61035e8edbbf2f12871659b Mon Sep 17 00:00:00 2001 From: Kyler Clay Date: Sat, 2 May 2026 16:41:29 -0400 Subject: [PATCH 14/21] layout/master: fix rollprev/rollnext focusing the wrong window (#14209) * layout/master: fix rollprev/rollnext focusing the wrong window * tests/master: add tests for rollnext/rollprev focus --- hyprtester/src/tests/main/master.cpp | 69 +++++++++++++++++++ .../tiled/master/MasterAlgorithm.cpp | 16 +++-- 2 files changed, 81 insertions(+), 4 deletions(-) diff --git a/hyprtester/src/tests/main/master.cpp b/hyprtester/src/tests/main/master.cpp index b9364c455..806cbdf70 100644 --- a/hyprtester/src/tests/main/master.cpp +++ b/hyprtester/src/tests/main/master.cpp @@ -1,6 +1,7 @@ #include "../shared.hpp" #include "../../shared.hpp" #include "../../hyprctlCompat.hpp" +#include #include "tests.hpp" TEST_CASE(focusMasterPrevious) { @@ -141,3 +142,71 @@ TEST_CASE(fsBehavior) { EXPECT_CONTAINS(str, "fullscreen: 0"); } } + +TEST_CASE(rollFocus) { + // test rollnext/rollprev dispatchers + + OK(getFromSocket("r/eval hl.config({ general = { layout = 'master' } })")); + + // set up windows + std::vector windows = {"slave1", "slave2", "slave3", "master"}; + + // helper lambda thing + auto roll = [&](const std::string& dir) { + auto pivot = (dir == "rollnext") ? windows.begin() + 1 : windows.end() - 1; + + // rotate the windows vector along with the actual windows + // the rolling behavior of the window focus should follow the + // rotating behavior of std::ranges::rotate + OK(getFromSocket("/dispatch hl.dsp.layout('" + dir + "')")); + std::ranges::rotate(windows.begin(), pivot, windows.end()); + ASSERT_CONTAINS(getFromSocket("/activewindow"), "class: " + windows.back()); + }; + + for (auto const& win : windows) { + if (!Tests::spawnKitty(win)) { + FAIL_TEST("Could not spawn kitty with win class `{}`", win); + } + } + + // focus master + OK(getFromSocket("/dispatch hl.dsp.layout('focusmaster master')")); + ASSERT_CONTAINS(getFromSocket("/activewindow"), "class: master"); + + // put the windows in the washing machine + NLog::log("{}Testing rollnext", Colors::YELLOW); + for (int i = 0; i < 20; ++i) { + roll("rollnext"); + } + + NLog::log("{}Testing rollprev", Colors::YELLOW); + for (int i = 0; i < 20; ++i) { + roll("rollprev"); + } + + NLog::log("{}Testing rollnext with rollprev", Colors::YELLOW); + for (int i = 0; i < 10; ++i) { + for (int j = 0; j < 5; ++j) { + roll("rollnext"); + } + roll("rollprev"); + } + + NLog::log("{}Testing rollnext/rollprev alternation", Colors::YELLOW); + for (int i = 0; i < 20; ++i) { + if (i % 2 == 0) { + roll("rollnext"); + } else { + roll("rollprev"); + } + } + + NLog::log("{}Testing rollnext/rollprev burst calls", Colors::YELLOW); + for (int i = 0; i < 20; ++i) { + if (i / 5 % 2 == 0) { + roll("rollnext"); + } else { + roll("rollprev"); + } + } +} diff --git a/src/layout/algorithm/tiled/master/MasterAlgorithm.cpp b/src/layout/algorithm/tiled/master/MasterAlgorithm.cpp index a0ea13c34..c57a3639f 100644 --- a/src/layout/algorithm/tiled/master/MasterAlgorithm.cpp +++ b/src/layout/algorithm/tiled/master/MasterAlgorithm.cpp @@ -715,12 +715,15 @@ Config::ErrorResult CMasterAlgorithm::layoutMsg(const std::string_view& sv) { if (!OLDMASTER) return stateErr("no old master"); - auto oldMasterIt = std::ranges::find(m_masterNodesData, OLDMASTER); + auto oldMasterIt = std::ranges::find(m_masterNodesData, OLDMASTER); + + SP newFocus; for (auto& nd : m_masterNodesData) { if (!nd->isMaster) { const auto& newMaster = nd; newMaster->isMaster = true; + newFocus = newMaster->pTarget.lock(); auto newMasterIt = std::ranges::find(m_masterNodesData, newMaster); @@ -729,7 +732,6 @@ Config::ErrorResult CMasterAlgorithm::layoutMsg(const std::string_view& sv) { else if (newMasterIt > oldMasterIt) std::ranges::rotate(oldMasterIt, newMasterIt, std::next(newMasterIt)); - switchToWindow(newMaster->pTarget.lock()); OLDMASTER->isMaster = false; oldMasterIt = std::ranges::find(m_masterNodesData, OLDMASTER); @@ -741,6 +743,8 @@ Config::ErrorResult CMasterAlgorithm::layoutMsg(const std::string_view& sv) { } calculateWorkspace(); + if (newFocus) + switchToWindow(newFocus); } else if (command == "rollprev") { const auto PNODE = getNodeFromWindow(PWINDOW); @@ -751,12 +755,15 @@ Config::ErrorResult CMasterAlgorithm::layoutMsg(const std::string_view& sv) { if (!OLDMASTER) return stateErr("no old master"); - auto oldMasterIt = std::ranges::find(m_masterNodesData, OLDMASTER); + auto oldMasterIt = std::ranges::find(m_masterNodesData, OLDMASTER); + + SP newFocus; for (auto& nd : m_masterNodesData | std::views::reverse) { if (!nd->isMaster) { const auto& newMaster = nd; newMaster->isMaster = true; + newFocus = newMaster->pTarget.lock(); auto newMasterIt = std::ranges::find(m_masterNodesData, newMaster); @@ -765,7 +772,6 @@ Config::ErrorResult CMasterAlgorithm::layoutMsg(const std::string_view& sv) { else if (newMasterIt > oldMasterIt) std::ranges::rotate(oldMasterIt, newMasterIt, std::next(newMasterIt)); - switchToWindow(newMaster->pTarget.lock()); OLDMASTER->isMaster = false; oldMasterIt = std::ranges::find(m_masterNodesData, OLDMASTER); @@ -777,6 +783,8 @@ Config::ErrorResult CMasterAlgorithm::layoutMsg(const std::string_view& sv) { } calculateWorkspace(); + if (newFocus) + switchToWindow(newFocus); } else return Config::configError(std::format("Unknown master layoutmsg: {}", sv), Config::eConfigErrorLevel::ERROR, Config::eConfigErrorCode::INVALID_ARGUMENT); From 6ec0228c38a6203e4789fe7e7e793a558521c109 Mon Sep 17 00:00:00 2001 From: fazzi <18248986+fxzzi@users.noreply.github.com> Date: Sat, 2 May 2026 23:12:49 +0100 Subject: [PATCH 15/21] desktop/windowRule: add parser switch for confine pointer (#14263) --- src/desktop/rule/windowRule/WindowRule.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/src/desktop/rule/windowRule/WindowRule.cpp b/src/desktop/rule/windowRule/WindowRule.cpp index 10d39383b..5bbf2abe4 100644 --- a/src/desktop/rule/windowRule/WindowRule.cpp +++ b/src/desktop/rule/windowRule/WindowRule.cpp @@ -248,6 +248,7 @@ static std::expected parseWindowRuleEffect(C case WINDOW_RULE_EFFECT_RENDER_UNFOCUSED: case WINDOW_RULE_EFFECT_NO_SCREEN_SHARE: case WINDOW_RULE_EFFECT_NO_VRR: + case WINDOW_RULE_EFFECT_CONFINE_POINTER: case WINDOW_RULE_EFFECT_STAY_FOCUSED: return truthy(raw); case WINDOW_RULE_EFFECT_FULLSCREENSTATE: { From aa5e38041ed332416ac2f5ace908389c35b28192 Mon Sep 17 00:00:00 2001 From: Maximilian Seidler <78690852+PointerDilemma@users.noreply.github.com> Date: Sun, 3 May 2026 15:58:03 +0200 Subject: [PATCH 16/21] sessionLock: send locked instead of denied when missing a lock frame for 5 seconds (#14271) The protocol seems to allow this, because it explicitly mentions either session lock surface or compositor blanking the output in this sentence: > The locked event must not be sent until a new "locked" frame (either from a session lock surface or the compositor blanking the output) has been presented on all outputs and no security sensitive normal/unlocked content is possibly visible. Since `locked_screen_delay` is capped to 5 seconds, after the 5 seconds this timer is registered for, we assume that all outputs are covered either by lockedead or by an actual lock frame. Thus sending locked is ok. --- src/managers/SessionLockManager.cpp | 33 ++++++++++++----------------- src/managers/SessionLockManager.hpp | 4 ++-- 2 files changed, 16 insertions(+), 21 deletions(-) diff --git a/src/managers/SessionLockManager.cpp b/src/managers/SessionLockManager.cpp index 8890e2e07..6e45c9234 100644 --- a/src/managers/SessionLockManager.cpp +++ b/src/managers/SessionLockManager.cpp @@ -105,8 +105,10 @@ void CSessionLockManager::onNewSessionLock(SP pLock) { return; } - m_sessionLock->sendDeniedTimer = makeShared( - // Within this arbitrary amount of time, a session-lock client is expected to create and commit a lock surface for each output. If the client fails to do that, it will be denied. + m_sessionLock->sendLockedTimer = makeShared( + // Clients get sent the "locked" event after they submitted a lock frame for each output. + // If they fail to do this, we send the "locked" event after a fixed amount of time here. + // Previously we sent denied after this timeout, but that forcefully makes the client exit and the protocol doesn't require that anyways. std::chrono::seconds(5), [](auto, auto) { if (!g_pSessionLockManager || g_pSessionLockManager->clientLocked() || g_pSessionLockManager->clientDenied()) @@ -115,29 +117,22 @@ void CSessionLockManager::onNewSessionLock(SP pLock) { if (!g_pSessionLockManager->m_sessionLock || !g_pSessionLockManager->m_sessionLock->lock) return; - if (g_pCompositor->m_unsafeState || !g_pCompositor->m_aqBackend->hasSession() || !g_pCompositor->m_aqBackend->session->active) { - // Because the session is inactive, there is a good reason for why the client did't manage to render to all outputs. - // We send locked, although this could lead to imperfect frames when we start to render again. - g_pSessionLockManager->m_sessionLock->lock->sendLocked(); - g_pSessionLockManager->m_sessionLock->hasSentLocked = true; - return; - } - - LOGM(Log::WARN, "Kicking lockscreen client, because it failed to render to all outputs within 5 seconds"); - g_pSessionLockManager->m_sessionLock->lock->sendDenied(); - g_pSessionLockManager->m_sessionLock->hasSentDenied = true; + LOGM(Log::WARN, + "Sending locked after a 5 second timeout. This happens when we failed to render a lock frame from the client for every output. Lockdead frames may be shown."); + g_pSessionLockManager->m_sessionLock->lock->sendLocked(); + g_pSessionLockManager->m_sessionLock->hasSentLocked = true; }, nullptr); - g_pEventLoopManager->addTimer(m_sessionLock->sendDeniedTimer); + g_pEventLoopManager->addTimer(m_sessionLock->sendLockedTimer); } -void CSessionLockManager::removeSendDeniedTimer() { - if (!m_sessionLock || !m_sessionLock->sendDeniedTimer) +void CSessionLockManager::removeSendLockedTimer() { + if (!m_sessionLock || !m_sessionLock->sendLockedTimer) return; - g_pEventLoopManager->removeTimer(m_sessionLock->sendDeniedTimer); - m_sessionLock->sendDeniedTimer.reset(); + g_pEventLoopManager->removeTimer(m_sessionLock->sendLockedTimer); + m_sessionLock->sendLockedTimer.reset(); } bool CSessionLockManager::isSessionLocked() { @@ -169,7 +164,7 @@ void CSessionLockManager::onLockscreenRenderedOnMonitor(uint64_t id) { std::ranges::all_of(g_pCompositor->m_monitors, [this](auto m) { return !m->m_enabled || !m->m_dpmsStatus || m_sessionLock->lockedMonitors.contains(m->m_id); }); if (LOCKED && m_sessionLock->lock->good()) { - removeSendDeniedTimer(); + removeSendLockedTimer(); m_sessionLock->lock->sendLocked(); m_sessionLock->hasSentLocked = true; } diff --git a/src/managers/SessionLockManager.hpp b/src/managers/SessionLockManager.hpp index efcaf09a7..dba2758ea 100644 --- a/src/managers/SessionLockManager.hpp +++ b/src/managers/SessionLockManager.hpp @@ -31,7 +31,7 @@ struct SSessionLockSurface { struct SSessionLock { WP lock; CTimer lockTimer; - SP sendDeniedTimer; + SP sendLockedTimer; std::vector> vSessionLockSurfaces; @@ -73,7 +73,7 @@ class CSessionLockManager { } m_listeners; void onNewSessionLock(SP pWlrLock); - void removeSendDeniedTimer(); + void removeSendLockedTimer(); }; inline UP g_pSessionLockManager; From 90fe7c6569e3ea900cdc664a13368e52c986165d Mon Sep 17 00:00:00 2001 From: fazzi <18248986+fxzzi@users.noreply.github.com> Date: Sun, 3 May 2026 15:00:16 +0100 Subject: [PATCH 17/21] InputManager: add guards to confineToRegion to avoid issues (#14269) --- src/managers/input/InputManager.cpp | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/managers/input/InputManager.cpp b/src/managers/input/InputManager.cpp index 2f9b64a3b..b6f384337 100644 --- a/src/managers/input/InputManager.cpp +++ b/src/managers/input/InputManager.cpp @@ -266,11 +266,17 @@ void CInputManager::mouseMoveUnified(uint32_t time, bool refocus, bool mouse, st // constraints auto confineToRegion = [&](const CRegion& rg, SP surf) { + if (!surf) + return; + const auto CLOSEST = rg.closestPoint(mouseCoords); const auto BOX = surf->getSurfaceBoxGlobal(); const auto WINDOW = Desktop::View::CWindow::fromView(surf->view()); const auto CLOSESTLOCAL = (CLOSEST - (BOX.has_value() ? BOX->pos() : Vector2D{})) * (WINDOW ? WINDOW->m_X11SurfaceScaledBy : 1.0); + if (g_pSeatManager->m_state.pointerFocus != surf->resource()) + g_pSeatManager->setPointerFocus(surf->resource(), CLOSESTLOCAL); + g_pCompositor->warpCursorTo(CLOSEST, true); g_pSeatManager->sendPointerMotion(time, CLOSESTLOCAL); PROTO::relativePointer->sendRelativeMotion(sc(time) * 1000, {}, {}); From c0933bffcf0394f7701ab134f6c308eaa9584bee Mon Sep 17 00:00:00 2001 From: Maximilian Seidler <78690852+PointerDilemma@users.noreply.github.com> Date: Sun, 3 May 2026 16:30:21 +0200 Subject: [PATCH 18/21] compositor: move SessionLockManager init from STAGE_LATE to STAGE_BASICINIT (#14272) --- src/Compositor.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Compositor.cpp b/src/Compositor.cpp index ab371df22..b5dba342e 100644 --- a/src/Compositor.cpp +++ b/src/Compositor.cpp @@ -673,6 +673,9 @@ void CCompositor::initManagers(eManagersInitStage stage) { Log::logger->log(Log::DEBUG, "Creating the SeatManager!"); g_pSeatManager = makeUnique(); + Log::logger->log(Log::DEBUG, "Creating the SessionLockManager!"); + g_pSessionLockManager = makeUnique(); + // init focus state els Desktop::History::windowTracker(); Desktop::History::workspaceTracker(); @@ -688,9 +691,6 @@ void CCompositor::initManagers(eManagersInitStage stage) { Log::logger->log(Log::DEBUG, "Creating the XWaylandManager!"); g_pXWaylandManager = makeUnique(); - Log::logger->log(Log::DEBUG, "Creating the SessionLockManager!"); - g_pSessionLockManager = makeUnique(); - Log::logger->log(Log::DEBUG, "Creating the Debug Overlay!"); Debug::overlay(); From 21fa9b2ee27227c964fdeb8090d98255edb89bec Mon Sep 17 00:00:00 2001 From: Vaxry <43317083+vaxerski@users.noreply.github.com> Date: Sun, 3 May 2026 15:41:44 +0100 Subject: [PATCH 19/21] config/lua: fix dispatcher shapes to not be callable (#14268) this would only lead to abuse, explicitly forbid it --- meta/generateLuaStubs.py | 9 +- meta/hl.meta.lua | 103 +++++++------ .../bindings/LuaBindingsDispatcherUtils.cpp | 144 ++++++++++++++++++ .../lua/bindings/LuaBindingsDispatchers.cpp | 5 + .../lua/bindings/LuaBindingsInternal.cpp | 5 - .../lua/bindings/LuaBindingsInternal.hpp | 3 + .../lua/bindings/LuaBindingsToplevel.cpp | 8 +- 7 files changed, 215 insertions(+), 62 deletions(-) create mode 100644 src/config/lua/bindings/LuaBindingsDispatcherUtils.cpp diff --git a/meta/generateLuaStubs.py b/meta/generateLuaStubs.py index a523496df..7ef70babb 100644 --- a/meta/generateLuaStubs.py +++ b/meta/generateLuaStubs.py @@ -469,7 +469,8 @@ def generate_stub(root: Path) -> str: api_signatures: dict[str, str] = { "hl.on": "fun(event: HL.EventName, cb: fun(...)): HL.EventSubscription", - "hl.bind": "fun(keys: string, dispatcher: function, opts?: HL.BindOptions): HL.Keybind", + "hl.bind": "fun(keys: string, dispatcher: HL.Dispatcher|function, opts?: HL.BindOptions): HL.Keybind", + "hl.dispatch": "fun(dispatcher: HL.Dispatcher|function): any", "hl.define_submap": "fun(name: string, reset_or_fn: string|function, fn?: function): nil", "hl.timer": "fun(callback: function, opts: HL.TimerOptions): HL.Timer", "hl.config": "fun(config: table): nil", @@ -523,6 +524,9 @@ def generate_stub(root: Path) -> str: lines.append("---@alias HL.CssGap integer|{top?:integer, right?:integer, bottom?:integer, left?:integer}") lines.append("---@alias HL.Gradient string|{colors:string[], angle?:number}") lines.append("") + lines.append("---@class HL.Dispatcher") + lines.append("local __HL_Dispatcher = {}") + lines.append("") lines.extend( emit_class_block( @@ -651,7 +655,8 @@ def generate_stub(root: Path) -> str: for method in sorted(node.methods): full_name = f"{full_prefix}.{method}" - method_type = api_signatures.get(full_name, "fun(...): any") + default_method_type = "fun(...): HL.Dispatcher" if path and path[0] == "dsp" else "fun(...): any" + method_type = api_signatures.get(full_name, default_method_type) fields.append((method, method_type, False)) for child_name in sorted(node.children.keys()): diff --git a/meta/hl.meta.lua b/meta/hl.meta.lua index e0fd096ae..a13655633 100644 --- a/meta/hl.meta.lua +++ b/meta/hl.meta.lua @@ -378,6 +378,9 @@ ---@alias HL.CssGap integer|{top?:integer, right?:integer, bottom?:integer, left?:integer} ---@alias HL.Gradient string|{colors:string[], angle?:number} +---@class HL.Dispatcher +local __HL_Dispatcher = {} + ---@class HL.Vec2 ---@field x number ---@field y number @@ -739,12 +742,12 @@ local __HL_Workspace = {} ---@class HL.API ---@field animation fun(...): any ----@field bind fun(keys: string, dispatcher: function, opts?: HL.BindOptions): HL.Keybind +---@field bind fun(keys: string, dispatcher: HL.Dispatcher|function, opts?: HL.BindOptions): HL.Keybind ---@field config fun(config: table): nil ---@field curve fun(...): any ---@field define_submap fun(name: string, reset_or_fn: string|function, fn?: function): nil ---@field device fun(spec: HL.DeviceSpec): nil ----@field dispatch fun(...): any +---@field dispatch fun(dispatcher: HL.Dispatcher|function): any ---@field env fun(...): any ---@field exec_cmd fun(cmd: string, rules?: table): nil ---@field gesture fun(spec: HL.GestureSpec): nil @@ -783,21 +786,21 @@ local __HL_Workspace = {} local __HL_API = {} ---@class HL.DspNamespace ----@field dpms fun(...): any ----@field event fun(...): any ----@field exec_cmd fun(...): any ----@field exec_raw fun(...): any ----@field exit fun(...): any ----@field focus fun(...): any ----@field force_idle fun(...): any ----@field force_renderer_reload fun(...): any ----@field global fun(...): any ----@field layout fun(...): any ----@field no_op fun(...): any ----@field pass fun(...): any ----@field send_key_state fun(...): any ----@field send_shortcut fun(...): any ----@field submap fun(...): any +---@field dpms fun(...): HL.Dispatcher +---@field event fun(...): HL.Dispatcher +---@field exec_cmd fun(...): HL.Dispatcher +---@field exec_raw fun(...): HL.Dispatcher +---@field exit fun(...): HL.Dispatcher +---@field focus fun(...): HL.Dispatcher +---@field force_idle fun(...): HL.Dispatcher +---@field force_renderer_reload fun(...): HL.Dispatcher +---@field global fun(...): HL.Dispatcher +---@field layout fun(...): HL.Dispatcher +---@field no_op fun(...): HL.Dispatcher +---@field pass fun(...): HL.Dispatcher +---@field send_key_state fun(...): HL.Dispatcher +---@field send_shortcut fun(...): HL.Dispatcher +---@field submap fun(...): HL.Dispatcher ---@field cursor HL.DspCursorNamespace ---@field group HL.DspGroupNamespace ---@field window HL.DspWindowNamespace @@ -805,48 +808,48 @@ local __HL_API = {} local __HL_DspNamespace = {} ---@class HL.DspCursorNamespace ----@field move fun(...): any ----@field move_to_corner fun(...): any +---@field move fun(...): HL.Dispatcher +---@field move_to_corner fun(...): HL.Dispatcher local __HL_DspCursorNamespace = {} ---@class HL.DspGroupNamespace ----@field active fun(...): any ----@field lock fun(...): any ----@field lock_active fun(...): any ----@field move_window fun(...): any ----@field next fun(...): any ----@field prev fun(...): any ----@field toggle fun(...): any +---@field active fun(...): HL.Dispatcher +---@field lock fun(...): HL.Dispatcher +---@field lock_active fun(...): HL.Dispatcher +---@field move_window fun(...): HL.Dispatcher +---@field next fun(...): HL.Dispatcher +---@field prev fun(...): HL.Dispatcher +---@field toggle fun(...): HL.Dispatcher local __HL_DspGroupNamespace = {} ---@class HL.DspWindowNamespace ----@field alter_zorder fun(...): any ----@field bring_to_top fun(...): any ----@field center fun(...): any ----@field close fun(...): any ----@field cycle_next fun(...): any ----@field deny_from_group fun(...): any ----@field drag fun(...): any ----@field float fun(...): any ----@field fullscreen fun(...): any ----@field fullscreen_state fun(...): any ----@field kill fun(...): any ----@field move fun(...): any ----@field pin fun(...): any ----@field pseudo fun(...): any ----@field resize fun(...): any ----@field set_prop fun(...): any ----@field signal fun(...): any ----@field swap fun(...): any ----@field tag fun(...): any ----@field toggle_swallow fun(...): any +---@field alter_zorder fun(...): HL.Dispatcher +---@field bring_to_top fun(...): HL.Dispatcher +---@field center fun(...): HL.Dispatcher +---@field close fun(...): HL.Dispatcher +---@field cycle_next fun(...): HL.Dispatcher +---@field deny_from_group fun(...): HL.Dispatcher +---@field drag fun(...): HL.Dispatcher +---@field float fun(...): HL.Dispatcher +---@field fullscreen fun(...): HL.Dispatcher +---@field fullscreen_state fun(...): HL.Dispatcher +---@field kill fun(...): HL.Dispatcher +---@field move fun(...): HL.Dispatcher +---@field pin fun(...): HL.Dispatcher +---@field pseudo fun(...): HL.Dispatcher +---@field resize fun(...): HL.Dispatcher +---@field set_prop fun(...): HL.Dispatcher +---@field signal fun(...): HL.Dispatcher +---@field swap fun(...): HL.Dispatcher +---@field tag fun(...): HL.Dispatcher +---@field toggle_swallow fun(...): HL.Dispatcher local __HL_DspWindowNamespace = {} ---@class HL.DspWorkspaceNamespace ----@field move fun(...): any ----@field rename fun(...): any ----@field swap_monitors fun(...): any ----@field toggle_special fun(...): any +---@field move fun(...): HL.Dispatcher +---@field rename fun(...): HL.Dispatcher +---@field swap_monitors fun(...): HL.Dispatcher +---@field toggle_special fun(...): HL.Dispatcher local __HL_DspWorkspaceNamespace = {} ---@class HL.NotificationNamespace diff --git a/src/config/lua/bindings/LuaBindingsDispatcherUtils.cpp b/src/config/lua/bindings/LuaBindingsDispatcherUtils.cpp new file mode 100644 index 000000000..a55dfc655 --- /dev/null +++ b/src/config/lua/bindings/LuaBindingsDispatcherUtils.cpp @@ -0,0 +1,144 @@ +#include "LuaBindingsInternal.hpp" + +using namespace Config::Lua::Bindings; + +static constexpr const char* DISPATCHER_MT = "HL.Dispatcher"; +static char DISPATCHER_TABLES_REGISTRY_KEY; + +namespace { + struct SDispatcherRef { + int ref = LUA_NOREF; + }; +} + +static int dispatcherGc(lua_State* L) { + auto* dispatcher = sc(luaL_checkudata(L, 1, DISPATCHER_MT)); + if (dispatcher->ref != LUA_NOREF) { + luaL_unref(L, LUA_REGISTRYINDEX, dispatcher->ref); + dispatcher->ref = LUA_NOREF; + } + + return 0; +} + +static int dispatcherCall(lua_State* L) { + return Internal::configError(L, "dispatcher objects cannot be called directly; use hl.dispatch(dispatcher)"); +} + +static int dispatcherToString(lua_State* L) { + lua_pushstring(L, "HL.Dispatcher"); + return 1; +} + +static void ensureDispatcherMetatable(lua_State* L) { + if (luaL_newmetatable(L, DISPATCHER_MT)) { + lua_pushcfunction(L, dispatcherGc); + lua_setfield(L, -2, "__gc"); + lua_pushcfunction(L, dispatcherCall); + lua_setfield(L, -2, "__call"); + lua_pushcfunction(L, dispatcherToString); + lua_setfield(L, -2, "__tostring"); + + lua_pushstring(L, DISPATCHER_MT); + lua_setfield(L, -2, "__metatable"); + } + + lua_pop(L, 1); +} + +static bool isDispatcherTable(lua_State* L, int idx) { + if (!lua_istable(L, idx)) + return false; + + idx = lua_absindex(L, idx); + lua_pushlightuserdata(L, &DISPATCHER_TABLES_REGISTRY_KEY); + lua_rawget(L, LUA_REGISTRYINDEX); + + if (!lua_istable(L, -1)) { + lua_pop(L, 1); + return false; + } + + lua_pushvalue(L, idx); + lua_rawget(L, -2); + const bool result = lua_toboolean(L, -1); + lua_pop(L, 2); + return result; +} + +static int dispatcherFactory(lua_State* L) { + const int nargs = lua_gettop(L); + + lua_pushvalue(L, lua_upvalueindex(1)); + lua_insert(L, 1); + lua_call(L, nargs, LUA_MULTRET); + + const int nresults = lua_gettop(L); + if (nresults == 1 && lua_isfunction(L, -1)) + return Internal::wrapDispatcher(L); + + return nresults; +} + +void Internal::setFn(lua_State* L, const char* name, lua_CFunction fn) { + if (isDispatcherTable(L, -1)) { + lua_pushcfunction(L, fn); + lua_pushcclosure(L, dispatcherFactory, 1); + } else + lua_pushcfunction(L, fn); + + lua_setfield(L, -2, name); +} + +void Internal::markDispatcherTable(lua_State* L) { + if (!lua_istable(L, -1)) + return; + + lua_pushlightuserdata(L, &DISPATCHER_TABLES_REGISTRY_KEY); + lua_rawget(L, LUA_REGISTRYINDEX); + + if (!lua_istable(L, -1)) { + lua_pop(L, 1); + lua_newtable(L); + lua_pushlightuserdata(L, &DISPATCHER_TABLES_REGISTRY_KEY); + lua_pushvalue(L, -2); + lua_rawset(L, LUA_REGISTRYINDEX); + } + + lua_pushvalue(L, -2); + lua_pushboolean(L, true); + lua_rawset(L, -3); + lua_pop(L, 1); +} + +int Internal::wrapDispatcher(lua_State* L) { + luaL_checktype(L, -1, LUA_TFUNCTION); + + const int ref = luaL_ref(L, LUA_REGISTRYINDEX); + + new (lua_newuserdata(L, sizeof(SDispatcherRef))) SDispatcherRef{.ref = ref}; + + ensureDispatcherMetatable(L); + luaL_getmetatable(L, DISPATCHER_MT); + lua_setmetatable(L, -2); + + return 1; +} + +bool Internal::pushDispatcherFunction(lua_State* L, int idx) { + if (lua_isfunction(L, idx)) { + lua_pushvalue(L, idx); + return true; + } + + auto* dispatcher = sc(luaL_testudata(L, idx, DISPATCHER_MT)); + if (!dispatcher || dispatcher->ref == LUA_NOREF) + return false; + + lua_rawgeti(L, LUA_REGISTRYINDEX, dispatcher->ref); + if (lua_isfunction(L, -1)) + return true; + + lua_pop(L, 1); + return false; +} diff --git a/src/config/lua/bindings/LuaBindingsDispatchers.cpp b/src/config/lua/bindings/LuaBindingsDispatchers.cpp index 4a981f4eb..4fd332eb7 100644 --- a/src/config/lua/bindings/LuaBindingsDispatchers.cpp +++ b/src/config/lua/bindings/LuaBindingsDispatchers.cpp @@ -1214,14 +1214,17 @@ static int hlWorkspaceSwapMonitors(lua_State* L) { void Internal::registerDispatcherBindings(lua_State* L) { lua_newtable(L); + Internal::markDispatcherTable(L); { lua_newtable(L); + Internal::markDispatcherTable(L); Internal::setFn(L, "move_to_corner", hlCursorMoveToCorner); Internal::setFn(L, "move", hlCursorMove); lua_setfield(L, -2, "cursor"); lua_newtable(L); + Internal::markDispatcherTable(L); Internal::setFn(L, "toggle", hlGroupToggle); Internal::setFn(L, "next", hlGroupNext); Internal::setFn(L, "prev", hlGroupPrev); @@ -1232,6 +1235,7 @@ void Internal::registerDispatcherBindings(lua_State* L) { lua_setfield(L, -2, "group"); lua_newtable(L); + Internal::markDispatcherTable(L); Internal::setFn(L, "close", hlWindowClose); Internal::setFn(L, "kill", hlWindowKill); Internal::setFn(L, "signal", hlWindowSignal); @@ -1255,6 +1259,7 @@ void Internal::registerDispatcherBindings(lua_State* L) { lua_setfield(L, -2, "window"); lua_newtable(L); + Internal::markDispatcherTable(L); Internal::setFn(L, "rename", hlWorkspaceRename); Internal::setFn(L, "move", hlWorkspaceMove); Internal::setFn(L, "swap_monitors", hlWorkspaceSwapMonitors); diff --git a/src/config/lua/bindings/LuaBindingsInternal.cpp b/src/config/lua/bindings/LuaBindingsInternal.cpp index e0cef1464..5f1958051 100644 --- a/src/config/lua/bindings/LuaBindingsInternal.cpp +++ b/src/config/lua/bindings/LuaBindingsInternal.cpp @@ -449,11 +449,6 @@ CA::eTogglableAction Internal::tableToggleAction(lua_State* L, int idx, const ch return CA::TOGGLE_ACTION_TOGGLE; } -void Internal::setFn(lua_State* L, const char* name, lua_CFunction fn) { - lua_pushcfunction(L, fn); - lua_setfield(L, -2, name); -} - void Internal::setMgrFn(lua_State* L, CConfigManager* mgr, const char* name, lua_CFunction fn) { lua_pushlightuserdata(L, mgr); lua_pushcclosure(L, fn, 1); diff --git a/src/config/lua/bindings/LuaBindingsInternal.hpp b/src/config/lua/bindings/LuaBindingsInternal.hpp index 024d72489..9b61360cb 100644 --- a/src/config/lua/bindings/LuaBindingsInternal.hpp +++ b/src/config/lua/bindings/LuaBindingsInternal.hpp @@ -181,6 +181,9 @@ namespace Config::Lua::Bindings::Internal { void setFn(lua_State* L, const char* name, lua_CFunction fn); void setMgrFn(lua_State* L, CConfigManager* mgr, const char* name, lua_CFunction fn); + void markDispatcherTable(lua_State* L); + int wrapDispatcher(lua_State* L); + bool pushDispatcherFunction(lua_State* L, int idx); template SParseError parseTableField(lua_State* L, int tableIdx, const char* field, T& parser) { diff --git a/src/config/lua/bindings/LuaBindingsToplevel.cpp b/src/config/lua/bindings/LuaBindingsToplevel.cpp index 2d54a7b3c..1b345cdef 100644 --- a/src/config/lua/bindings/LuaBindingsToplevel.cpp +++ b/src/config/lua/bindings/LuaBindingsToplevel.cpp @@ -132,13 +132,12 @@ static int hlBind(lua_State* L) { if (auto res = parseKeyString(kb, keys); !res) return Internal::configError(L, std::format("hl.bind: failed to parse key string: {}", res.error())); - if (!lua_isfunction(L, 2)) + if (!Internal::pushDispatcherFunction(L, 2)) return Internal::configError(L, "hl.bind: dispatcher must be a dispatcher (e.g. hl.dsp.window.close()) or a lua function"); if (kb.catchAll && mgr->m_currentSubmap.empty()) return Internal::configError(L, "hl.bind: catchall keybinds are only allowed in submaps."); - lua_pushvalue(L, 2); int ref = luaL_ref(L, LUA_REGISTRYINDEX); kb.handler = "__lua"; kb.arg = std::to_string(ref); @@ -293,10 +292,9 @@ static int hlExecCmd(lua_State* L) { } static int hlDispatch(lua_State* L) { - if (!lua_isfunction(L, 1)) - return Internal::configError(L, "hl.dispatch: expected a dispatcher function (e.g. hl.dsp.window.close())"); + if (!Internal::pushDispatcherFunction(L, 1)) + return Internal::configError(L, "hl.dispatch: expected a dispatcher (e.g. hl.dsp.window.close())"); - lua_pushvalue(L, 1); int status = LUA_OK; if (auto* mgr = CConfigManager::fromLuaState(L); mgr) status = mgr->guardedPCall(0, 1, 0, CConfigManager::LUA_TIMEOUT_DISPATCH_MS, "hl.dispatch"); From 6a7abd00379b4f37b468bc2d0e717d4ba293efe1 Mon Sep 17 00:00:00 2001 From: Lichie <90825386+lichie567@users.noreply.github.com> Date: Sun, 3 May 2026 10:55:47 -0700 Subject: [PATCH 20/21] config/lua: add clear tag api (#14273) --- meta/hl.meta.lua | 1 + src/config/lua/bindings/LuaBindingsDispatchers.cpp | 11 +++++++++++ src/config/shared/actions/ConfigActions.cpp | 13 +++++++++++++ src/config/shared/actions/ConfigActions.hpp | 1 + src/helpers/TagKeeper.cpp | 9 +++++++++ src/helpers/TagKeeper.hpp | 1 + 6 files changed, 36 insertions(+) diff --git a/meta/hl.meta.lua b/meta/hl.meta.lua index a13655633..d6a44567d 100644 --- a/meta/hl.meta.lua +++ b/meta/hl.meta.lua @@ -826,6 +826,7 @@ local __HL_DspGroupNamespace = {} ---@field alter_zorder fun(...): HL.Dispatcher ---@field bring_to_top fun(...): HL.Dispatcher ---@field center fun(...): HL.Dispatcher +---@field clear_tags fun(...): HL.Dispatcher ---@field close fun(...): HL.Dispatcher ---@field cycle_next fun(...): HL.Dispatcher ---@field deny_from_group fun(...): HL.Dispatcher diff --git a/src/config/lua/bindings/LuaBindingsDispatchers.cpp b/src/config/lua/bindings/LuaBindingsDispatchers.cpp index 4fd332eb7..32b4e2bf2 100644 --- a/src/config/lua/bindings/LuaBindingsDispatchers.cpp +++ b/src/config/lua/bindings/LuaBindingsDispatchers.cpp @@ -563,6 +563,10 @@ static int dsp_tagWindow(lua_State* L) { return Internal::checkResult(L, CA::tag(lua_tostring(L, lua_upvalueindex(1)), Internal::windowFromUpval(L, 2))); } +static int dsp_clearTags(lua_State* L) { + return Internal::checkResult(L, CA::clearTags(Internal::windowFromUpval(L, 1))); +} + static int dsp_toggleSwallow(lua_State* L) { return Internal::checkResult(L, CA::toggleSwallow()); } @@ -915,6 +919,12 @@ static int hlWindowTag(lua_State* L) { return 1; } +static int hlWindowClearTags(lua_State* L) { + Internal::pushWindowUpval(L, 1); + lua_pushcclosure(L, dsp_clearTags, 1); + return 1; +} + static int hlWindowToggleSwallow(lua_State* L) { lua_pushcclosure(L, dsp_toggleSwallow, 0); return 1; @@ -1248,6 +1258,7 @@ void Internal::registerDispatcherBindings(lua_State* L) { Internal::setFn(L, "center", hlWindowCenter); Internal::setFn(L, "cycle_next", hlWindowCycleNext); Internal::setFn(L, "tag", hlWindowTag); + Internal::setFn(L, "clear_tags", hlWindowClearTags); Internal::setFn(L, "toggle_swallow", hlWindowToggleSwallow); Internal::setFn(L, "pin", hlWindowPin); Internal::setFn(L, "bring_to_top", hlWindowBringToTop); diff --git a/src/config/shared/actions/ConfigActions.cpp b/src/config/shared/actions/ConfigActions.cpp index 5db1aaf1e..7111afd8f 100644 --- a/src/config/shared/actions/ConfigActions.cpp +++ b/src/config/shared/actions/ConfigActions.cpp @@ -605,6 +605,19 @@ ActionResult Actions::tag(const std::string& tagStr, std::optional w) return {}; } +ActionResult Actions::clearTags(std::optional w) { + auto window = xtract(w); + if (!window) + return {}; + + if (window->m_ruleApplicator->m_tagKeeper.clearTags()) { + window->m_ruleApplicator->propertiesChanged(Desktop::Rule::RULE_PROP_TAG); + window->updateDecorationValues(); + } + + return {}; +} + ActionResult Actions::swapNext(const bool next, std::optional w) { auto window = xtract(w); if (!window) diff --git a/src/config/shared/actions/ConfigActions.hpp b/src/config/shared/actions/ConfigActions.hpp index 0f16aff74..6ddf64d9d 100644 --- a/src/config/shared/actions/ConfigActions.hpp +++ b/src/config/shared/actions/ConfigActions.hpp @@ -53,6 +53,7 @@ namespace Config::Actions { ActionResult move(const Vector2D& pos, bool relative = false, std::optional window = std::nullopt /* Active */); ActionResult cycleNext(const bool next, std::optional onlyTiled, std::optional onlyFloating, std::optional window = std::nullopt /* Active */); ActionResult tag(const std::string& tag, std::optional window = std::nullopt /* Active */); + ActionResult clearTags(std::optional w = std::nullopt); ActionResult pass(std::optional window = std::nullopt /* Active */); ActionResult pass(uint32_t modMask, uint32_t key, std::optional window = std::nullopt /* Active */); ActionResult sendKeyState(uint32_t modMask, uint32_t key, uint32_t state, std::optional window = std::nullopt /* Active */); diff --git a/src/helpers/TagKeeper.cpp b/src/helpers/TagKeeper.cpp index 7f377657e..80bf3957d 100644 --- a/src/helpers/TagKeeper.cpp +++ b/src/helpers/TagKeeper.cpp @@ -38,6 +38,15 @@ bool CTagKeeper::applyTag(const std::string& tag, bool dynamic) { return true; } +bool CTagKeeper::clearTags() { + if (!m_tags.empty()) { + m_tags.clear(); + return true; + } + + return false; +} + bool CTagKeeper::removeDynamicTag(const std::string& s) { return std::erase_if(m_tags, [&s](const auto& tag) { return tag == s + "*"; }); } diff --git a/src/helpers/TagKeeper.hpp b/src/helpers/TagKeeper.hpp index d18a0d29a..f90b06295 100644 --- a/src/helpers/TagKeeper.hpp +++ b/src/helpers/TagKeeper.hpp @@ -8,6 +8,7 @@ class CTagKeeper { bool isTagged(const std::string& tag, bool strict = false) const; bool applyTag(const std::string& tag, bool dynamic = false); bool removeDynamicTag(const std::string& tag); + bool clearTags(); const auto& getTags() const { return m_tags; From 6d4bcaf075dec8f6de003cb8403df8c788e7079e Mon Sep 17 00:00:00 2001 From: Visal Vijay <150381094+B2krobbery@users.noreply.github.com> Date: Mon, 4 May 2026 02:07:41 +0530 Subject: [PATCH 21/21] tests: skip pointer tests in CI due to missing input environment (#14238) * tests: skip pointer tests when pointer input is non-functional * tests: skip pointer tests in CI due to unreliable cursor behavior * tests: skip pointer tests when no reliable input environment is available * tests: skip pointer tests when pointer behavior is unreliable * tests: temporarily disable pointer tests due to unstable CI environment * tests: enforce deterministic pointer behavior (flat accel + fixed sensitivity) * tests: temporarily disable pointer tests due to unstable CI environment --- hyprtester/src/tests/clients/pointer-scroll.cpp | 3 +++ hyprtester/src/tests/clients/pointer-warp.cpp | 4 +++- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/hyprtester/src/tests/clients/pointer-scroll.cpp b/hyprtester/src/tests/clients/pointer-scroll.cpp index c468deb3f..a22fa254b 100644 --- a/hyprtester/src/tests/clients/pointer-scroll.cpp +++ b/hyprtester/src/tests/clients/pointer-scroll.cpp @@ -126,6 +126,9 @@ static bool sendScroll(int delta) { } TEST_CASE(pointerScroll) { + NLog::log("{}Skipping pointerScroll test (unstable in CI / headless environments)", Colors::YELLOW); + return; + std::optional client; try { client.emplace(); diff --git a/hyprtester/src/tests/clients/pointer-warp.cpp b/hyprtester/src/tests/clients/pointer-warp.cpp index e13e03f69..6edbabaf5 100644 --- a/hyprtester/src/tests/clients/pointer-warp.cpp +++ b/hyprtester/src/tests/clients/pointer-warp.cpp @@ -151,8 +151,10 @@ static bool isCursorPos(int x, int y) { } TEST_CASE(pointerWarp) { - std::optional client; + NLog::log("{}Skipping pointerWarp test (unstable in CI / headless environments)", Colors::YELLOW); + return; + std::optional client; try { client.emplace(); } catch (...) { FAIL_TEST("Couldn't start the client"); }