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
This commit is contained in:
Virt 2026-03-06 21:11:42 +01:00 committed by GitHub
parent 42f0a6005b
commit e0c5710059
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 230 additions and 14 deletions

View file

@ -10,7 +10,9 @@
#include <src/managers/PointerManager.hpp>
#include <src/managers/input/trackpad/TrackpadGestures.hpp>
#include <src/desktop/rule/windowRule/WindowRuleEffectContainer.hpp>
#include <src/desktop/rule/layerRule/LayerRuleEffectContainer.hpp>
#include <src/desktop/rule/windowRule/WindowRuleApplicator.hpp>
#include <src/desktop/view/LayerSurface.hpp>
#include <src/Compositor.hpp>
#include <src/desktop/state/FocusState.hpp>
#include <src/layout/LayoutManager.hpp>
@ -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

View file

@ -0,0 +1,53 @@
#include "../../Log.hpp"
#include "../shared.hpp"
#include "tests.hpp"
#include "../../shared.hpp"
#include "../../hyprctlCompat.hpp"
#include <hyprutils/os/Process.hpp>
#include <hyprutils/memory/WeakPtr.hpp>
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)

View file

@ -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();

View file

@ -3,6 +3,7 @@
#include <cerrno>
#include <thread>
#include <print>
#include <fstream>
#include "../shared.hpp"
#include "../hyprctlCompat.hpp"
@ -39,6 +40,38 @@ CUniquePointer<CProcess> Tests::spawnKitty(const std::string& class_, const std:
return kitty;
}
CUniquePointer<CProcess> Tests::spawnLayerKitty(const std::string& namespace_, const std::vector<std::string> args) {
std::vector<std::string> 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<CProcess> kitty = makeUnique<CProcess>("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;
}

View file

@ -9,10 +9,14 @@
//NOLINTNEXTLINE
namespace Tests {
Hyprutils::Memory::CUniquePointer<Hyprutils::OS::CProcess> spawnKitty(const std::string& class_ = "", const std::vector<std::string> args = {});
Hyprutils::Memory::CUniquePointer<Hyprutils::OS::CProcess> spawnLayerKitty(const std::string& namespace_ = "", const std::vector<std::string> 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);
};

View file

@ -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<eRuleProperty> 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<CLayerRule>& 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<std::underlying_type_t<eLayerRuleEffect>>(key));
break;
}
// custom type, add to our vec
if (!m_otherProps.props.contains(key)) {
m_otherProps.props.emplace(key,
makeUnique<SCustomPropContainer>(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_t<eRulePropert
applyDynamicRule(wr);
}
// for plugins
Event::bus()->m_events.layer.updateRules.emit(m_ls.lock());
}

View file

@ -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<eRuleProperty> props);
void resetProps(std::underlying_type_t<eRuleProperty> props, Types::eOverridePriority prio = Types::PRIORITY_WINDOW_RULE);
struct SCustomPropContainer {
CLayerRuleEffectContainer::storageType idx = LAYER_RULE_EFFECT_NONE;
std::underlying_type_t<eRuleProperty> propMask = RULE_PROP_NONE;
std::string effect;
};
// This struct holds props that were dynamically registered. Plugins may read this.
struct {
std::unordered_map<CLayerRuleEffectContainer::storageType, UP<SCustomPropContainer>> props;
} m_otherProps;
#define COMMA ,
#define DEFINE_PROP(type, name, def) \
private: \

View file

@ -55,6 +55,7 @@ namespace Event {
struct {
Event<PHLLS> opened;
Event<PHLLS> closed;
Event<PHLLS> updateRules;
} layer;
struct {
@ -140,4 +141,4 @@ namespace Event {
};
UP<CEventBus>& bus();
};
};