From e0c571005912c342d8d812b368bc490cb6f28796 Mon Sep 17 00:00:00 2001 From: Virt <41426325+VirtCode@users.noreply.github.com> Date: Fri, 6 Mar 2026 21:11:42 +0100 Subject: [PATCH] layerrules: add dynamically registered rules for plugins (#13331) * layerrules: add dynamically registered rules for plugins * be gone * layerrules: add layer tests with waybar * fix: use kitty layers instead of waybar --- hyprtester/plugin/src/main.cpp | 57 +++++++++++--- hyprtester/src/tests/main/layer.cpp | 53 +++++++++++++ hyprtester/src/tests/main/window.cpp | 8 +- hyprtester/src/tests/shared.cpp | 76 +++++++++++++++++++ hyprtester/src/tests/shared.hpp | 4 + .../rule/layerRule/LayerRuleApplicator.cpp | 31 ++++++++ .../rule/layerRule/LayerRuleApplicator.hpp | 12 +++ src/event/EventBus.hpp | 3 +- 8 files changed, 230 insertions(+), 14 deletions(-) create mode 100644 hyprtester/src/tests/main/layer.cpp diff --git a/hyprtester/plugin/src/main.cpp b/hyprtester/plugin/src/main.cpp index 6db352fc..ce83c5b4 100644 --- a/hyprtester/plugin/src/main.cpp +++ b/hyprtester/plugin/src/main.cpp @@ -10,7 +10,9 @@ #include #include #include +#include #include +#include #include #include #include @@ -270,32 +272,67 @@ static SDispatchResult keybind(std::string in) { return {}; } -static Desktop::Rule::CWindowRuleEffectContainer::storageType ruleIDX = 0; +static Desktop::Rule::CWindowRuleEffectContainer::storageType windowRuleIDX = 0; // -static SDispatchResult addRule(std::string in) { - ruleIDX = Desktop::Rule::windowEffects()->registerEffect("plugin_rule"); +static SDispatchResult addWindowRule(std::string in) { + windowRuleIDX = Desktop::Rule::windowEffects()->registerEffect("plugin_rule"); - if (Desktop::Rule::windowEffects()->registerEffect("plugin_rule") != ruleIDX) + if (Desktop::Rule::windowEffects()->registerEffect("plugin_rule") != windowRuleIDX) return {.success = false, .error = "re-registering returned a different id?"}; return {}; } -static SDispatchResult checkRule(std::string in) { +static SDispatchResult checkWindowRule(std::string in) { const auto PLASTWINDOW = Desktop::focusState()->window(); if (!PLASTWINDOW) return {.success = false, .error = "No window"}; - if (!PLASTWINDOW->m_ruleApplicator->m_otherProps.props.contains(ruleIDX)) + if (!PLASTWINDOW->m_ruleApplicator->m_otherProps.props.contains(windowRuleIDX)) return {.success = false, .error = "No rule"}; - if (PLASTWINDOW->m_ruleApplicator->m_otherProps.props[ruleIDX]->effect != "effect") + if (PLASTWINDOW->m_ruleApplicator->m_otherProps.props[windowRuleIDX]->effect != "effect") return {.success = false, .error = "Effect isn't \"effect\""}; return {}; } +static Desktop::Rule::CLayerRuleEffectContainer::storageType layerRuleIDX = 0; + +static SDispatchResult addLayerRule(std::string in) { + layerRuleIDX = Desktop::Rule::layerEffects()->registerEffect("plugin_rule"); + + if (Desktop::Rule::layerEffects()->registerEffect("plugin_rule") != layerRuleIDX) + return {.success = false, .error = "re-registering returned a different id?"}; + return {}; +} + +static SDispatchResult checkLayerRule(std::string in) { + if (g_pCompositor->m_layers.size() != 3) + return {.success = false, .error = "Layers under test not here"}; + + for (const auto& layer : g_pCompositor->m_layers) { + if (layer->m_namespace == "rule-layer") { + + if (!layer->m_ruleApplicator->m_otherProps.props.contains(layerRuleIDX)) + return {.success = false, .error = "No rule"}; + + if (layer->m_ruleApplicator->m_otherProps.props[layerRuleIDX]->effect != "effect") + return {.success = false, .error = "Effect isn't \"effect\""}; + + } else if (layer->m_namespace == "norule-layer") { + + if (layer->m_ruleApplicator->m_otherProps.props.contains(layerRuleIDX)) + return {.success = false, .error = "Rule even though it shouldn't"}; + + } else + return {.success = false, .error = "Unrecognized layer"}; + } + + return {}; +} + static SDispatchResult floatingFocusOnFullscreen(std::string in) { const auto PLASTWINDOW = Desktop::focusState()->window(); @@ -325,8 +362,10 @@ APICALL EXPORT PLUGIN_DESCRIPTION_INFO PLUGIN_INIT(HANDLE handle) { HyprlandAPI::addDispatcherV2(PHANDLE, "plugin:test:scroll", ::scroll); HyprlandAPI::addDispatcherV2(PHANDLE, "plugin:test:click", ::click); HyprlandAPI::addDispatcherV2(PHANDLE, "plugin:test:keybind", ::keybind); - HyprlandAPI::addDispatcherV2(PHANDLE, "plugin:test:add_rule", ::addRule); - HyprlandAPI::addDispatcherV2(PHANDLE, "plugin:test:check_rule", ::checkRule); + HyprlandAPI::addDispatcherV2(PHANDLE, "plugin:test:add_window_rule", ::addWindowRule); + HyprlandAPI::addDispatcherV2(PHANDLE, "plugin:test:check_window_rule", ::checkWindowRule); + HyprlandAPI::addDispatcherV2(PHANDLE, "plugin:test:add_layer_rule", ::addLayerRule); + HyprlandAPI::addDispatcherV2(PHANDLE, "plugin:test:check_layer_rule", ::checkLayerRule); HyprlandAPI::addDispatcherV2(PHANDLE, "plugin:test:floating_focus_on_fullscreen", ::floatingFocusOnFullscreen); // init mouse diff --git a/hyprtester/src/tests/main/layer.cpp b/hyprtester/src/tests/main/layer.cpp new file mode 100644 index 00000000..73e30ba6 --- /dev/null +++ b/hyprtester/src/tests/main/layer.cpp @@ -0,0 +1,53 @@ +#include "../../Log.hpp" +#include "../shared.hpp" +#include "tests.hpp" +#include "../../shared.hpp" +#include "../../hyprctlCompat.hpp" +#include +#include + +static int ret = 0; + +using namespace Hyprutils::OS; +using namespace Hyprutils::Memory; + +static bool spawnLayer(const std::string& namespace_) { + NLog::log("{}Spawning kitty layer {}", Colors::YELLOW, namespace_); + if (!Tests::spawnLayerKitty(namespace_)) { + NLog::log("{}Error: {} layer did not spawn", Colors::RED, namespace_); + return false; + } + return true; +} + +static bool test() { + NLog::log("{}Testing plugin layerrules", Colors::GREEN); + + if (!spawnLayer("rule-layer")) + return false; + + OK(getFromSocket("/dispatch plugin:test:add_layer_rule")); + OK(getFromSocket("/reload")); + + OK(getFromSocket("/keyword layerrule match:namespace rule-layer, plugin_rule effect")); + + if (!spawnLayer("rule-layer")) + return false; + + if (!spawnLayer("norule-layer")) + return false; + + OK(getFromSocket("/dispatch plugin:test:check_layer_rule")); + + OK(getFromSocket("/reload")); + + NLog::log("{}Killing all layers", Colors::YELLOW); + Tests::killAllLayers(); + + NLog::log("{}Expecting 0 layers", Colors::YELLOW); + EXPECT(Tests::layerCount(), 0); + + return !ret; +} + +REGISTER_TEST_FN(test) diff --git a/hyprtester/src/tests/main/window.cpp b/hyprtester/src/tests/main/window.cpp index 2bded63a..beac0298 100644 --- a/hyprtester/src/tests/main/window.cpp +++ b/hyprtester/src/tests/main/window.cpp @@ -1086,7 +1086,7 @@ static bool test() { OK(getFromSocket("/reload")); Tests::killAllWindows(); - OK(getFromSocket("/dispatch plugin:test:add_rule")); + OK(getFromSocket("/dispatch plugin:test:add_window_rule")); OK(getFromSocket("/reload")); OK(getFromSocket("/keyword windowrule match:class plugin_kitty, plugin_rule effect")); @@ -1094,12 +1094,12 @@ static bool test() { if (!spawnKitty("plugin_kitty")) return false; - OK(getFromSocket("/dispatch plugin:test:check_rule")); + OK(getFromSocket("/dispatch plugin:test:check_window_rule")); OK(getFromSocket("/reload")); Tests::killAllWindows(); - OK(getFromSocket("/dispatch plugin:test:add_rule")); + OK(getFromSocket("/dispatch plugin:test:add_window_rule")); OK(getFromSocket("/reload")); OK(getFromSocket("/keyword windowrule[test-plugin-rule]:match:class plugin_kitty")); @@ -1108,7 +1108,7 @@ static bool test() { if (!spawnKitty("plugin_kitty")) return false; - OK(getFromSocket("/dispatch plugin:test:check_rule")); + OK(getFromSocket("/dispatch plugin:test:check_window_rule")); OK(getFromSocket("/reload")); Tests::killAllWindows(); diff --git a/hyprtester/src/tests/shared.cpp b/hyprtester/src/tests/shared.cpp index 8cdd648e..f6e2fce9 100644 --- a/hyprtester/src/tests/shared.cpp +++ b/hyprtester/src/tests/shared.cpp @@ -3,6 +3,7 @@ #include #include #include +#include #include "../shared.hpp" #include "../hyprctlCompat.hpp" @@ -39,6 +40,38 @@ CUniquePointer Tests::spawnKitty(const std::string& class_, const std: return kitty; } +CUniquePointer Tests::spawnLayerKitty(const std::string& namespace_, const std::vector args) { + std::vector programArgs = args; + if (!namespace_.empty()) { + programArgs.insert(programArgs.begin(), "--class"); + programArgs.insert(programArgs.begin() + 1, namespace_); + } + + programArgs.insert(programArgs.begin(), "+kitten"); + programArgs.insert(programArgs.begin() + 1, "panel"); + + CUniquePointer kitty = makeUnique("kitty", programArgs); + kitty->addEnv("WAYLAND_DISPLAY", WLDISPLAY); + kitty->runAsync(); + + std::this_thread::sleep_for(std::chrono::milliseconds(10)); + + // wait while the layer spawns + int counter = 0; + while (processAlive(kitty->pid()) && countOccurrences(getFromSocket("/layers"), std::format("pid: {}", kitty->pid())) == 0) { + counter++; + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + + if (counter > 50) + return nullptr; + } + + if (!processAlive(kitty->pid())) + return nullptr; + + return kitty; +} + bool Tests::processAlive(pid_t pid) { errno = 0; int ret = kill(pid, 0); @@ -96,6 +129,38 @@ void Tests::waitUntilWindowsN(int n) { } } +int Tests::layerCount() { + return countOccurrences(getFromSocket("/layers"), "namespace: "); +} + +bool Tests::killAllLayers() { + auto str = getFromSocket("/layers"); + auto pos = str.find("pid: "); + while (pos != std::string::npos) { + auto pid = stoi(str.substr(pos + 5, str.find('\n', pos))); + kill(pid, 15); + + // we need to wait for a bit because for some reason otherwise we'll end up + // with layers with pid -1 if they are all removed at the same time + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + + pos = str.find("pid: ", pos + 5); + } + + int counter = 0; + while (Tests::layerCount() != 0) { + counter++; + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + + if (counter > 50) { + std::println("{}Timed out waiting for layers to close", Colors::RED); + return false; + } + } + + return true; +} + std::string Tests::execAndGet(const std::string& cmd) { CProcess proc("/bin/sh", {"-c", cmd}); @@ -105,3 +170,14 @@ std::string Tests::execAndGet(const std::string& cmd) { return proc.stdOut(); } + +bool Tests::writeFile(const std::string& name, const std::string& contents) { + std::ofstream of(name, std::ios::trunc); + if (!of.good()) + return false; + + of << contents; + of.close(); + + return true; +} diff --git a/hyprtester/src/tests/shared.hpp b/hyprtester/src/tests/shared.hpp index fe28a69d..bf875f8b 100644 --- a/hyprtester/src/tests/shared.hpp +++ b/hyprtester/src/tests/shared.hpp @@ -9,10 +9,14 @@ //NOLINTNEXTLINE namespace Tests { Hyprutils::Memory::CUniquePointer spawnKitty(const std::string& class_ = "", const std::vector args = {}); + Hyprutils::Memory::CUniquePointer spawnLayerKitty(const std::string& namespace_ = "", const std::vector args = {}); bool processAlive(pid_t pid); int windowCount(); int countOccurrences(const std::string& in, const std::string& what); bool killAllWindows(); void waitUntilWindowsN(int n); + int layerCount(); + bool killAllLayers(); std::string execAndGet(const std::string& cmd); + bool writeFile(const std::string& name, const std::string& contents); }; diff --git a/src/desktop/rule/layerRule/LayerRuleApplicator.cpp b/src/desktop/rule/layerRule/LayerRuleApplicator.cpp index 4237e4f7..fec3a5b2 100644 --- a/src/desktop/rule/layerRule/LayerRuleApplicator.cpp +++ b/src/desktop/rule/layerRule/LayerRuleApplicator.cpp @@ -4,6 +4,7 @@ #include "../../view/LayerSurface.hpp" #include "../../types/OverridableVar.hpp" #include "../../../helpers/MiscFunctions.hpp" +#include "../../../event/EventBus.hpp" using namespace Desktop; using namespace Desktop::Rule; @@ -32,11 +33,38 @@ void CLayerRuleApplicator::resetProps(std::underlying_type_t prop UNSET(aboveLock) UNSET(ignoreAlpha) UNSET(animationStyle) + +#undef UNSET + + if (prio == Types::PRIORITY_WINDOW_RULE) + std::erase_if(m_otherProps.props, [props](const auto& el) { return !el.second || el.second->propMask & props; }); } void CLayerRuleApplicator::applyDynamicRule(const SP& rule) { for (const auto& [key, effect] : rule->effects()) { switch (key) { + default: { + if (key <= LAYER_RULE_EFFECT_LAST_STATIC) { + Log::logger->log(Log::TRACE, "CLayerRuleApplicator::applyDynamicRule: Skipping effect {}, not dynamic", sc>(key)); + break; + } + + // custom type, add to our vec + if (!m_otherProps.props.contains(key)) { + m_otherProps.props.emplace(key, + makeUnique(SCustomPropContainer{ + .idx = key, + .propMask = rule->getPropertiesMask(), + .effect = effect, + })); + } else { + auto& e = m_otherProps.props[key]; + e->propMask |= rule->getPropertiesMask(); + e->effect = effect; + } + + break; + } case LAYER_RULE_EFFECT_NONE: { Log::logger->log(Log::ERR, "CLayerRuleApplicator::applyDynamicRule: BUG THIS: LAYER_RULE_EFFECT_NONE??"); break; @@ -125,4 +153,7 @@ void CLayerRuleApplicator::propertiesChanged(std::underlying_type_tm_events.layer.updateRules.emit(m_ls.lock()); } diff --git a/src/desktop/rule/layerRule/LayerRuleApplicator.hpp b/src/desktop/rule/layerRule/LayerRuleApplicator.hpp index 97f15b04..35aa18c5 100644 --- a/src/desktop/rule/layerRule/LayerRuleApplicator.hpp +++ b/src/desktop/rule/layerRule/LayerRuleApplicator.hpp @@ -1,5 +1,6 @@ #pragma once +#include "LayerRuleEffectContainer.hpp" #include "../../DesktopTypes.hpp" #include "../Rule.hpp" #include "../../types/OverridableVar.hpp" @@ -21,6 +22,17 @@ namespace Desktop::Rule { void propertiesChanged(std::underlying_type_t props); void resetProps(std::underlying_type_t props, Types::eOverridePriority prio = Types::PRIORITY_WINDOW_RULE); + struct SCustomPropContainer { + CLayerRuleEffectContainer::storageType idx = LAYER_RULE_EFFECT_NONE; + std::underlying_type_t propMask = RULE_PROP_NONE; + std::string effect; + }; + + // This struct holds props that were dynamically registered. Plugins may read this. + struct { + std::unordered_map> props; + } m_otherProps; + #define COMMA , #define DEFINE_PROP(type, name, def) \ private: \ diff --git a/src/event/EventBus.hpp b/src/event/EventBus.hpp index 60bd1511..a30288f0 100644 --- a/src/event/EventBus.hpp +++ b/src/event/EventBus.hpp @@ -55,6 +55,7 @@ namespace Event { struct { Event opened; Event closed; + Event updateRules; } layer; struct { @@ -140,4 +141,4 @@ namespace Event { }; UP& bus(); -}; \ No newline at end of file +};