summaryrefslogtreecommitdiff
path: root/src/config/lua/bindings/LuaBindingsConfigRules.cpp
diff options
context:
space:
mode:
authorVaxry <43317083+vaxerski@users.noreply.github.com>2026-04-26 15:16:36 +0100
committerGitHub <noreply@github.com>2026-04-26 15:16:36 +0100
commit5ba33f846129f3e6c0505b19f94634b24505e828 (patch)
treeaaacabe678006040c342935481a86607c66607fd /src/config/lua/bindings/LuaBindingsConfigRules.cpp
parent329b2b356dd92ac07f4e2aa69cbce82ee9ba9d19 (diff)
config/lua: init lua config manager, use lua if available (#13817)
Adds a new lua config manager, which will automatically be used if there is a lua config present.
Diffstat (limited to 'src/config/lua/bindings/LuaBindingsConfigRules.cpp')
-rw-r--r--src/config/lua/bindings/LuaBindingsConfigRules.cpp1224
1 files changed, 1224 insertions, 0 deletions
diff --git a/src/config/lua/bindings/LuaBindingsConfigRules.cpp b/src/config/lua/bindings/LuaBindingsConfigRules.cpp
new file mode 100644
index 00000000..2faab8c6
--- /dev/null
+++ b/src/config/lua/bindings/LuaBindingsConfigRules.cpp
@@ -0,0 +1,1224 @@
+#include "LuaBindingsInternal.hpp"
+
+#include "../objects/LuaLayerRule.hpp"
+#include "../objects/LuaWindowRule.hpp"
+
+#include "../types/LuaConfigBool.hpp"
+#include "../types/LuaConfigCssGap.hpp"
+#include "../types/LuaConfigFloat.hpp"
+#include "../types/LuaConfigGradient.hpp"
+#include "../types/LuaConfigInt.hpp"
+#include "../types/LuaConfigString.hpp"
+#include "../types/LuaConfigVec2.hpp"
+
+#include "../../supplementary/executor/Executor.hpp"
+#include "../../supplementary/propRefresher/PropRefresher.hpp"
+#include "../../shared/animation/AnimationTree.hpp"
+#include "../../shared/monitor/MonitorRuleManager.hpp"
+#include "../../shared/monitor/Parser.hpp"
+#include "../../shared/workspace/WorkspaceRuleManager.hpp"
+
+#include "../../../desktop/rule/Engine.hpp"
+#include "../../../desktop/rule/layerRule/LayerRule.hpp"
+#include "../../../desktop/rule/layerRule/LayerRuleEffectContainer.hpp"
+#include "../../../desktop/rule/windowRule/WindowRule.hpp"
+#include "../../../desktop/rule/windowRule/WindowRuleEffectContainer.hpp"
+#include "../../../layout/LayoutManager.hpp"
+#include "../../../layout/supplementary/WorkspaceAlgoMatcher.hpp"
+#include "../../../managers/animation/AnimationManager.hpp"
+#include "../../../managers/input/InputManager.hpp"
+#include "../../../managers/input/trackpad/TrackpadGestures.hpp"
+#include "../../../managers/input/trackpad/gestures/CloseGesture.hpp"
+#include "../../../managers/input/trackpad/gestures/CursorZoomGesture.hpp"
+#include "../../../managers/input/trackpad/gestures/DispatcherGesture.hpp"
+#include "../../../managers/input/trackpad/gestures/FloatGesture.hpp"
+#include "../../../managers/input/trackpad/gestures/FullscreenGesture.hpp"
+#include "../../../managers/input/trackpad/gestures/LuaFunctionGesture.hpp"
+#include "../../../managers/input/trackpad/gestures/MoveGesture.hpp"
+#include "../../../managers/input/trackpad/gestures/ResizeGesture.hpp"
+#include "../../../managers/input/trackpad/gestures/SpecialWorkspaceGesture.hpp"
+#include "../../../managers/input/trackpad/gestures/WorkspaceSwipeGesture.hpp"
+#include "../../../managers/permissions/DynamicPermissionManager.hpp"
+
+using namespace Config;
+using namespace Config::Lua;
+using namespace Config::Lua::Bindings;
+
+namespace {
+ struct SFieldDesc {
+ const char* name;
+ std::function<ILuaConfigValue*()> factory;
+ };
+
+ struct SMonitorFieldDesc {
+ const char* name;
+ std::function<ILuaConfigValue*()> factory;
+ std::function<bool(ILuaConfigValue*, CMonitorRuleParser&)> apply;
+ };
+
+ struct SLayerRuleEffectDesc {
+ const char* name;
+ std::function<ILuaConfigValue*()> factory;
+ uint16_t effect;
+ };
+
+ struct SWorkspaceRuleFieldDesc {
+ const char* name;
+ std::function<ILuaConfigValue*()> factory;
+ std::function<void(ILuaConfigValue*, Config::CWorkspaceRule&)> apply;
+ };
+
+ using LE = Desktop::Rule::eLayerRuleEffect;
+
+ inline const SMonitorFieldDesc MONITOR_FIELDS[] = {
+ {"mode", []() -> ILuaConfigValue* { return new CLuaConfigString("preferred"); },
+ [](ILuaConfigValue* v, CMonitorRuleParser& p) { return p.parseMode(*sc<const Config::STRING*>(v->data())); }},
+ {"position", []() -> ILuaConfigValue* { return new CLuaConfigString("auto"); },
+ [](ILuaConfigValue* v, CMonitorRuleParser& p) { return p.parsePosition(*sc<const Config::STRING*>(v->data())); }},
+ {"scale", []() -> ILuaConfigValue* { return new CLuaConfigString("auto"); },
+ [](ILuaConfigValue* v, CMonitorRuleParser& p) { return p.parseScale(*sc<const Config::STRING*>(v->data())); }},
+ {"reserved", []() -> ILuaConfigValue* { return new CLuaConfigCssGap(0); },
+ [](ILuaConfigValue* v, CMonitorRuleParser& p) {
+ const auto& gap = *sc<const Config::CCssGapData*>(v->data());
+ return p.setReserved(Desktop::CReservedArea(gap.m_top, gap.m_right, gap.m_bottom, gap.m_left));
+ }},
+ {"reserved_area", []() -> ILuaConfigValue* { return new CLuaConfigCssGap(0); },
+ [](ILuaConfigValue* v, CMonitorRuleParser& p) {
+ const auto& gap = *sc<const Config::CCssGapData*>(v->data());
+ return p.setReserved(Desktop::CReservedArea(gap.m_top, gap.m_right, gap.m_bottom, gap.m_left));
+ }},
+ {"disabled", []() -> ILuaConfigValue* { return new CLuaConfigBool(false); },
+ [](ILuaConfigValue* v, CMonitorRuleParser& p) {
+ if (*sc<const Config::BOOL*>(v->data()))
+ p.setDisabled();
+ return true;
+ }},
+ {"transform", []() -> ILuaConfigValue* { return new CLuaConfigInt(0, 0, 7); },
+ [](ILuaConfigValue* v, CMonitorRuleParser& p) {
+ p.rule().m_transform = sc<wl_output_transform>(*sc<const Config::INTEGER*>(v->data()));
+ return true;
+ }},
+ {"mirror", []() -> ILuaConfigValue* { return new CLuaConfigString(STRVAL_EMPTY); },
+ [](ILuaConfigValue* v, CMonitorRuleParser& p) {
+ p.setMirror(*sc<const Config::STRING*>(v->data()));
+ return true;
+ }},
+ {"bitdepth", []() -> ILuaConfigValue* { return new CLuaConfigInt(8); },
+ [](ILuaConfigValue* v, CMonitorRuleParser& p) {
+ p.rule().m_enable10bit = *sc<const Config::INTEGER*>(v->data()) == 10;
+ return true;
+ }},
+ {"cm", []() -> ILuaConfigValue* { return new CLuaConfigString("srgb"); },
+ [](ILuaConfigValue* v, CMonitorRuleParser& p) { return p.parseCM(*sc<const Config::STRING*>(v->data())); }},
+ {"sdr_eotf", []() -> ILuaConfigValue* { return new CLuaConfigString("default"); },
+ [](ILuaConfigValue* v, CMonitorRuleParser& p) {
+ p.rule().m_sdrEotf = NTransferFunction::fromString(*sc<const Config::STRING*>(v->data()));
+ return true;
+ }},
+ {"sdrbrightness", []() -> ILuaConfigValue* { return new CLuaConfigFloat(1.F); },
+ [](ILuaConfigValue* v, CMonitorRuleParser& p) {
+ p.rule().m_sdrBrightness = *sc<const Config::FLOAT*>(v->data());
+ return true;
+ }},
+ {"sdrsaturation", []() -> ILuaConfigValue* { return new CLuaConfigFloat(1.F); },
+ [](ILuaConfigValue* v, CMonitorRuleParser& p) {
+ p.rule().m_sdrSaturation = *sc<const Config::FLOAT*>(v->data());
+ return true;
+ }},
+ {"vrr", []() -> ILuaConfigValue* { return new CLuaConfigInt(0, 0, 3); },
+ [](ILuaConfigValue* v, CMonitorRuleParser& p) {
+ p.rule().m_vrr = sc<int>(*sc<const Config::INTEGER*>(v->data()));
+ return true;
+ }},
+ {"icc", []() -> ILuaConfigValue* { return new CLuaConfigString(STRVAL_EMPTY); },
+ [](ILuaConfigValue* v, CMonitorRuleParser& p) { return p.parseICC(*sc<const Config::STRING*>(v->data())); }},
+ {"supports_wide_color", []() -> ILuaConfigValue* { return new CLuaConfigInt(0, -1, 1); },
+ [](ILuaConfigValue* v, CMonitorRuleParser& p) {
+ p.rule().m_supportsWideColor = sc<int>(*sc<const Config::INTEGER*>(v->data()));
+ return true;
+ }},
+ {"supports_hdr", []() -> ILuaConfigValue* { return new CLuaConfigInt(0, -1, 1); },
+ [](ILuaConfigValue* v, CMonitorRuleParser& p) {
+ p.rule().m_supportsHDR = sc<int>(*sc<const Config::INTEGER*>(v->data()));
+ return true;
+ }},
+ {"sdr_min_luminance", []() -> ILuaConfigValue* { return new CLuaConfigFloat(0.2F); },
+ [](ILuaConfigValue* v, CMonitorRuleParser& p) {
+ p.rule().m_sdrMinLuminance = *sc<const Config::FLOAT*>(v->data());
+ return true;
+ }},
+ {"sdr_max_luminance", []() -> ILuaConfigValue* { return new CLuaConfigInt(80); },
+ [](ILuaConfigValue* v, CMonitorRuleParser& p) {
+ p.rule().m_sdrMaxLuminance = sc<int>(*sc<const Config::INTEGER*>(v->data()));
+ return true;
+ }},
+ {"min_luminance", []() -> ILuaConfigValue* { return new CLuaConfigFloat(-1.F); },
+ [](ILuaConfigValue* v, CMonitorRuleParser& p) {
+ p.rule().m_minLuminance = *sc<const Config::FLOAT*>(v->data());
+ return true;
+ }},
+ {"max_luminance", []() -> ILuaConfigValue* { return new CLuaConfigInt(-1); },
+ [](ILuaConfigValue* v, CMonitorRuleParser& p) {
+ p.rule().m_maxLuminance = sc<int>(*sc<const Config::INTEGER*>(v->data()));
+ return true;
+ }},
+ {"max_avg_luminance", []() -> ILuaConfigValue* { return new CLuaConfigInt(-1); },
+ [](ILuaConfigValue* v, CMonitorRuleParser& p) {
+ p.rule().m_maxAvgLuminance = sc<int>(*sc<const Config::INTEGER*>(v->data()));
+ return true;
+ }},
+ };
+
+ static_assert(sizeof(Internal::WINDOW_RULE_EFFECT_DESCS) / sizeof(Internal::SWindowRuleEffectDesc) == Internal::WE::WINDOW_RULE_EFFECT_LAST_STATIC - 1);
+
+ inline const SLayerRuleEffectDesc LAYER_RULE_EFFECT_DESCS[] = {
+ {"no_anim", []() -> ILuaConfigValue* { return new CLuaConfigBool(false); }, LE::LAYER_RULE_EFFECT_NO_ANIM},
+ {"blur", []() -> ILuaConfigValue* { return new CLuaConfigBool(false); }, LE::LAYER_RULE_EFFECT_BLUR},
+ {"blur_popups", []() -> ILuaConfigValue* { return new CLuaConfigBool(false); }, LE::LAYER_RULE_EFFECT_BLUR_POPUPS},
+ {"ignore_alpha", []() -> ILuaConfigValue* { return new CLuaConfigFloat(0.F, 0.F, 1.F); }, LE::LAYER_RULE_EFFECT_IGNORE_ALPHA},
+ {"dim_around", []() -> ILuaConfigValue* { return new CLuaConfigBool(false); }, LE::LAYER_RULE_EFFECT_DIM_AROUND},
+ {"xray", []() -> ILuaConfigValue* { return new CLuaConfigBool(false); }, LE::LAYER_RULE_EFFECT_XRAY},
+ {"animation", []() -> ILuaConfigValue* { return new CLuaConfigString(STRVAL_EMPTY); }, LE::LAYER_RULE_EFFECT_ANIMATION},
+ {"order", []() -> ILuaConfigValue* { return new CLuaConfigInt(0); }, LE::LAYER_RULE_EFFECT_ORDER},
+ {"above_lock", []() -> ILuaConfigValue* { return new CLuaConfigInt(0, 0, 2); }, LE::LAYER_RULE_EFFECT_ABOVE_LOCK},
+ {"no_screen_share", []() -> ILuaConfigValue* { return new CLuaConfigBool(false); }, LE::LAYER_RULE_EFFECT_NO_SCREEN_SHARE},
+ };
+
+ static_assert(sizeof(LAYER_RULE_EFFECT_DESCS) / sizeof(SLayerRuleEffectDesc) == LE::LAYER_RULE_EFFECT_LAST_STATIC - 1);
+
+ inline const SWorkspaceRuleFieldDesc WORKSPACE_RULE_FIELDS[] = {
+ {"monitor", []() -> ILuaConfigValue* { return new CLuaConfigString(STRVAL_EMPTY); },
+ [](ILuaConfigValue* v, Config::CWorkspaceRule& r) { r.m_monitor = *sc<const Config::STRING*>(v->data()); }},
+ {"default", []() -> ILuaConfigValue* { return new CLuaConfigBool(false); },
+ [](ILuaConfigValue* v, Config::CWorkspaceRule& r) { r.m_isDefault = *sc<const Config::BOOL*>(v->data()); }},
+ {"persistent", []() -> ILuaConfigValue* { return new CLuaConfigBool(false); },
+ [](ILuaConfigValue* v, Config::CWorkspaceRule& r) { r.m_isPersistent = *sc<const Config::BOOL*>(v->data()); }},
+ {"gaps_in", []() -> ILuaConfigValue* { return new CLuaConfigCssGap(5); },
+ [](ILuaConfigValue* v, Config::CWorkspaceRule& r) { r.m_gapsIn = *sc<const Config::CCssGapData*>(v->data()); }},
+ {"gaps_out", []() -> ILuaConfigValue* { return new CLuaConfigCssGap(20); },
+ [](ILuaConfigValue* v, Config::CWorkspaceRule& r) { r.m_gapsOut = *sc<const Config::CCssGapData*>(v->data()); }},
+ {"float_gaps", []() -> ILuaConfigValue* { return new CLuaConfigCssGap(0); },
+ [](ILuaConfigValue* v, Config::CWorkspaceRule& r) { r.m_floatGaps = *sc<const Config::CCssGapData*>(v->data()); }},
+ {"border_size", []() -> ILuaConfigValue* { return new CLuaConfigInt(-1); },
+ [](ILuaConfigValue* v, Config::CWorkspaceRule& r) { r.m_borderSize = *sc<const Config::INTEGER*>(v->data()); }},
+ {"no_border", []() -> ILuaConfigValue* { return new CLuaConfigBool(false); },
+ [](ILuaConfigValue* v, Config::CWorkspaceRule& r) { r.m_noBorder = *sc<const Config::BOOL*>(v->data()); }},
+ {"no_rounding", []() -> ILuaConfigValue* { return new CLuaConfigBool(false); },
+ [](ILuaConfigValue* v, Config::CWorkspaceRule& r) { r.m_noRounding = *sc<const Config::BOOL*>(v->data()); }},
+ {"decorate", []() -> ILuaConfigValue* { return new CLuaConfigBool(true); },
+ [](ILuaConfigValue* v, Config::CWorkspaceRule& r) { r.m_decorate = *sc<const Config::BOOL*>(v->data()); }},
+ {"no_shadow", []() -> ILuaConfigValue* { return new CLuaConfigBool(false); },
+ [](ILuaConfigValue* v, Config::CWorkspaceRule& r) { r.m_noShadow = *sc<const Config::BOOL*>(v->data()); }},
+ {"on_created_empty", []() -> ILuaConfigValue* { return new CLuaConfigString(STRVAL_EMPTY); },
+ [](ILuaConfigValue* v, Config::CWorkspaceRule& r) { r.m_onCreatedEmptyRunCmd = *sc<const Config::STRING*>(v->data()); }},
+ {"default_name", []() -> ILuaConfigValue* { return new CLuaConfigString(STRVAL_EMPTY); },
+ [](ILuaConfigValue* v, Config::CWorkspaceRule& r) { r.m_defaultName = *sc<const Config::STRING*>(v->data()); }},
+ {"layout", []() -> ILuaConfigValue* { return new CLuaConfigString(STRVAL_EMPTY); },
+ [](ILuaConfigValue* v, Config::CWorkspaceRule& r) { r.m_layout = *sc<const Config::STRING*>(v->data()); }},
+ {"animation", []() -> ILuaConfigValue* { return new CLuaConfigString(STRVAL_EMPTY); },
+ [](ILuaConfigValue* v, Config::CWorkspaceRule& r) { r.m_animationStyle = *sc<const Config::STRING*>(v->data()); }},
+ };
+
+ inline const SFieldDesc DEVICE_FIELDS[] = {
+ {"sensitivity", []() -> ILuaConfigValue* { return new CLuaConfigFloat(0.F, -1.F, 1.F); }},
+ {"accel_profile", []() -> ILuaConfigValue* { return new CLuaConfigString(STRVAL_EMPTY); }},
+ {"rotation", []() -> ILuaConfigValue* { return new CLuaConfigInt(0, 0, 359); }},
+ {"kb_file", []() -> ILuaConfigValue* { return new CLuaConfigString(STRVAL_EMPTY); }},
+ {"kb_layout", []() -> ILuaConfigValue* { return new CLuaConfigString("us"); }},
+ {"kb_variant", []() -> ILuaConfigValue* { return new CLuaConfigString(STRVAL_EMPTY); }},
+ {"kb_options", []() -> ILuaConfigValue* { return new CLuaConfigString(STRVAL_EMPTY); }},
+ {"kb_rules", []() -> ILuaConfigValue* { return new CLuaConfigString(STRVAL_EMPTY); }},
+ {"kb_model", []() -> ILuaConfigValue* { return new CLuaConfigString(STRVAL_EMPTY); }},
+ {"repeat_rate", []() -> ILuaConfigValue* { return new CLuaConfigInt(25, 0, 200); }},
+ {"repeat_delay", []() -> ILuaConfigValue* { return new CLuaConfigInt(600, 0, 2000); }},
+ {"natural_scroll", []() -> ILuaConfigValue* { return new CLuaConfigBool(false); }},
+ {"tap_button_map", []() -> ILuaConfigValue* { return new CLuaConfigString(STRVAL_EMPTY); }},
+ {"numlock_by_default", []() -> ILuaConfigValue* { return new CLuaConfigBool(false); }},
+ {"resolve_binds_by_sym", []() -> ILuaConfigValue* { return new CLuaConfigBool(false); }},
+ {"disable_while_typing", []() -> ILuaConfigValue* { return new CLuaConfigBool(true); }},
+ {"clickfinger_behavior", []() -> ILuaConfigValue* { return new CLuaConfigBool(false); }},
+ {"middle_button_emulation", []() -> ILuaConfigValue* { return new CLuaConfigBool(false); }},
+ {"tap_to_click", []() -> ILuaConfigValue* { return new CLuaConfigBool(true); }},
+ {"tap_and_drag", []() -> ILuaConfigValue* { return new CLuaConfigBool(true); }},
+ {"drag_lock", []() -> ILuaConfigValue* { return new CLuaConfigInt(0, 0, 2); }},
+ {"left_handed", []() -> ILuaConfigValue* { return new CLuaConfigBool(false); }},
+ {"scroll_method", []() -> ILuaConfigValue* { return new CLuaConfigString(STRVAL_EMPTY); }},
+ {"scroll_button", []() -> ILuaConfigValue* { return new CLuaConfigInt(0, 0, 300); }},
+ {"scroll_button_lock", []() -> ILuaConfigValue* { return new CLuaConfigBool(false); }},
+ {"scroll_points", []() -> ILuaConfigValue* { return new CLuaConfigString(STRVAL_EMPTY); }},
+ {"scroll_factor", []() -> ILuaConfigValue* { return new CLuaConfigFloat(1.F, 0.F, 100.F); }},
+ {"transform", []() -> ILuaConfigValue* { return new CLuaConfigInt(-1); }},
+ {"output", []() -> ILuaConfigValue* { return new CLuaConfigString(STRVAL_EMPTY); }},
+ {"enabled", []() -> ILuaConfigValue* { return new CLuaConfigBool(true); }},
+ {"region_position", []() -> ILuaConfigValue* { return new CLuaConfigVec2({0, 0}); }},
+ {"absolute_region_position", []() -> ILuaConfigValue* { return new CLuaConfigBool(false); }},
+ {"region_size", []() -> ILuaConfigValue* { return new CLuaConfigVec2({0, 0}); }},
+ {"relative_input", []() -> ILuaConfigValue* { return new CLuaConfigBool(false); }},
+ {"active_area_position", []() -> ILuaConfigValue* { return new CLuaConfigVec2({0, 0}); }},
+ {"active_area_size", []() -> ILuaConfigValue* { return new CLuaConfigVec2({0, 0}); }},
+ {"flip_x", []() -> ILuaConfigValue* { return new CLuaConfigBool(false); }},
+ {"flip_y", []() -> ILuaConfigValue* { return new CLuaConfigBool(false); }},
+ {"drag_3fg", []() -> ILuaConfigValue* { return new CLuaConfigInt(0, 0, 2); }},
+ {"keybinds", []() -> ILuaConfigValue* { return new CLuaConfigBool(true); }},
+ {"share_states", []() -> ILuaConfigValue* { return new CLuaConfigInt(0, 0, 2); }},
+ {"release_pressed_on_close", []() -> ILuaConfigValue* { return new CLuaConfigBool(false); }},
+ };
+
+}
+
+static int hlCurve(lua_State* L) {
+ CLuaConfigString nameParser("");
+ lua_pushvalue(L, 1);
+ auto nameErr = nameParser.parse(L);
+ lua_pop(L, 1);
+ if (nameErr.errorCode != PARSE_ERROR_OK)
+ return Internal::configError(L, std::format("hl.curve: first argument (name) must be a string: {}", nameErr.message));
+
+ const auto& name = nameParser.parsed();
+
+ if (!lua_istable(L, 2))
+ return Internal::configError(L, "hl.curve: second argument must be a table, e.g. { type = \"bezier\", points = { {0, 0}, {1, 1} } }");
+
+ CLuaConfigString typeParser("");
+ auto typeErr = Internal::parseTableField(L, 2, "type", typeParser);
+ if (typeErr.errorCode != PARSE_ERROR_OK)
+ return Internal::configError(L, std::format("hl.curve(\"{}\"): {}", name, typeErr.message));
+
+ const auto& curveType = typeParser.parsed();
+
+ if (curveType != "bezier")
+ return Internal::configError(L, std::format("hl.curve(\"{}\"): unknown curve type \"{}\", expected \"bezier\"", name, curveType));
+
+ lua_getfield(L, 2, "points");
+ if (!lua_istable(L, -1)) {
+ lua_pop(L, 1);
+ return Internal::configError(L, std::format("hl.curve(\"{}\"): missing or invalid \"points\" field, expected a table of two points", name));
+ }
+ int pointsIdx = lua_gettop(L);
+
+ if (luaL_len(L, pointsIdx) != 2) {
+ lua_pop(L, 1);
+ return Internal::configError(L, std::format("hl.curve(\"{}\"): \"points\" must contain exactly 2 points, e.g. {{ {{0, 0}}, {{1, 1}} }}", name));
+ }
+
+ float coords[4] = {};
+ for (int pt = 1; pt <= 2; pt++) {
+ lua_rawgeti(L, pointsIdx, pt);
+ if (!lua_istable(L, -1) || luaL_len(L, -1) != 2) {
+ lua_pop(L, 2);
+ return Internal::configError(L, std::format("hl.curve(\"{}\"): point {} must be a table of 2 numbers, e.g. {{0.25, 0.1}}", name, pt));
+ }
+ int ptIdx = lua_gettop(L);
+
+ for (int comp = 0; comp < 2; comp++) {
+ lua_rawgeti(L, ptIdx, comp + 1);
+ CLuaConfigFloat coordParser(0.F, -1.F, 2.F);
+ auto coordErr = coordParser.parse(L);
+ lua_pop(L, 1);
+ if (coordErr.errorCode != PARSE_ERROR_OK) {
+ lua_pop(L, 2);
+ return Internal::configError(L, std::format("hl.curve(\"{}\"): point {}[{}]: {}", name, pt, comp + 1, coordErr.message));
+ }
+ coords[((pt - 1) * 2) + comp] = coordParser.parsed();
+ }
+
+ lua_pop(L, 1);
+ }
+ lua_pop(L, 1);
+
+ g_pAnimationManager->addBezierWithName(name, Vector2D(coords[0], coords[1]), Vector2D(coords[2], coords[3]));
+ return 0;
+}
+
+static int hlAnimation(lua_State* L) {
+ if (!lua_istable(L, 1))
+ return Internal::configError(L, "hl.animation: expected a table, e.g. { leaf = \"global\", enabled = true, speed = 5, bezier = \"default\" }");
+
+ CLuaConfigString leafParser("");
+ auto leafErr = Internal::parseTableField(L, 1, "leaf", leafParser);
+ if (leafErr.errorCode != PARSE_ERROR_OK)
+ return Internal::configError(L, std::format("hl.animation: {}", leafErr.message));
+
+ const auto leaf = leafParser.parsed();
+
+ if (!Config::animationTree()->nodeExists(leaf))
+ return Internal::configError(L, std::format("hl.animation: no such animation leaf \"{}\"", leaf));
+
+ CLuaConfigBool enabledParser(true);
+ auto enabledErr = Internal::parseTableField(L, 1, "enabled", enabledParser);
+ if (enabledErr.errorCode != PARSE_ERROR_OK)
+ return Internal::configError(L, std::format("hl.animation(\"{}\"): {}", leaf, enabledErr.message));
+
+ bool enabled = enabledParser.parsed();
+
+ if (!enabled) {
+ Config::animationTree()->setConfigForNode(leaf, false, 1, "default");
+ return 0;
+ }
+
+ CLuaConfigFloat speedParser(0.F, 0.F, 100.F);
+ auto speedErr = Internal::parseTableField(L, 1, "speed", speedParser);
+ if (speedErr.errorCode != PARSE_ERROR_OK)
+ return Internal::configError(L, std::format("hl.animation(\"{}\"): {}", leaf, speedErr.message));
+
+ float speed = speedParser.parsed();
+
+ if (speed <= 0)
+ return Internal::configError(L, std::format("hl.animation(\"{}\"): speed must be greater than 0", leaf));
+
+ CLuaConfigString bezierParser("");
+ auto bezierErr = Internal::parseTableField(L, 1, "bezier", bezierParser);
+ if (bezierErr.errorCode != PARSE_ERROR_OK)
+ return Internal::configError(L, std::format("hl.animation(\"{}\"): {}", leaf, bezierErr.message));
+
+ const auto& bezierName = bezierParser.parsed();
+
+ if (!g_pAnimationManager->bezierExists(bezierName))
+ return Internal::configError(L, std::format("hl.animation(\"{}\"): no such bezier \"{}\"", leaf, bezierName));
+
+ std::string style;
+ lua_getfield(L, 1, "style");
+ if (!lua_isnil(L, -1)) {
+ CLuaConfigString styleParser("");
+ auto styleErr = styleParser.parse(L);
+ if (styleErr.errorCode != PARSE_ERROR_OK) {
+ lua_pop(L, 1);
+ return Internal::configError(L, std::format("hl.animation(\"{}\"): field \"style\": {}", leaf, styleErr.message));
+ }
+ style = styleParser.parsed();
+ }
+ lua_pop(L, 1);
+
+ if (!style.empty()) {
+ auto err = g_pAnimationManager->styleValidInConfigVar(leaf, style);
+ if (!err.empty())
+ return Internal::configError(L, std::format("hl.animation(\"{}\"): {}", leaf, err));
+ }
+
+ Config::animationTree()->setConfigForNode(leaf, true, speed, bezierName, style);
+ return 0;
+}
+
+static int hlEnv(lua_State* L) {
+ auto* mgr = sc<CConfigManager*>(lua_touserdata(L, lua_upvalueindex(1)));
+
+ CLuaConfigString nameParser("");
+ lua_pushvalue(L, 1);
+ auto nameErr = nameParser.parse(L);
+ lua_pop(L, 1);
+ if (nameErr.errorCode != PARSE_ERROR_OK)
+ return Internal::configError(L, std::format("hl.env: first argument (name) must be a string: {}", nameErr.message));
+
+ const auto& name = nameParser.parsed();
+
+ if (name.empty())
+ return Internal::configError(L, "hl.env: name must not be empty");
+
+ CLuaConfigString valueParser("");
+ lua_pushvalue(L, 2);
+ auto valueErr = valueParser.parse(L);
+ lua_pop(L, 1);
+ if (valueErr.errorCode != PARSE_ERROR_OK)
+ return Internal::configError(L, std::format("hl.env: second argument (value) must be a string: {}", valueErr.message));
+
+ const auto& value = valueParser.parsed();
+
+ if (!mgr->isFirstLaunch()) {
+ const auto* ENV = getenv(name.c_str());
+ if (ENV && ENV == value)
+ return 0;
+ }
+
+ setenv(name.c_str(), value.c_str(), 1);
+
+ bool dbus = false;
+ if (!lua_isnoneornil(L, 3)) {
+ CLuaConfigBool dbusParser(false);
+ lua_pushvalue(L, 3);
+ auto dbusErr = dbusParser.parse(L);
+ lua_pop(L, 1);
+ if (dbusErr.errorCode != PARSE_ERROR_OK)
+ return Internal::configError(L, std::format("hl.env: third argument (dbus) must be a boolean: {}", dbusErr.message));
+
+ dbus = dbusParser.parsed();
+ }
+
+ if (dbus) {
+ std::string CMD;
+#ifdef USES_SYSTEMD
+ CMD = "systemctl --user import-environment '" + name + "' && hash dbus-update-activation-environment 2>/dev/null && ";
+#endif
+ CMD += "dbus-update-activation-environment --systemd '" + name + "'";
+ if (mgr->isFirstLaunch())
+ Config::Supplementary::executor()->addExecOnce({CMD, false});
+ else
+ Config::Supplementary::executor()->spawnRaw(CMD);
+ }
+
+ return 0;
+}
+
+static int hlPluginLoad(lua_State* L) {
+ auto* mgr = sc<CConfigManager*>(lua_touserdata(L, lua_upvalueindex(1)));
+
+ CLuaConfigString pathParser("");
+ lua_pushvalue(L, 1);
+ auto pathErr = pathParser.parse(L);
+ lua_pop(L, 1);
+ if (pathErr.errorCode != PARSE_ERROR_OK)
+ return Internal::configError(L, std::format("hl.plugin.load: first argument (path) must be a string: {}", pathErr.message));
+
+ const auto& path = pathParser.parsed();
+
+ if (path.empty())
+ return Internal::configError(L, "hl.plugin.load: path must not be empty");
+
+ mgr->m_registeredPlugins.emplace_back(path);
+ return 0;
+}
+
+static int hlPermission(lua_State* L) {
+ auto* mgr = sc<CConfigManager*>(lua_touserdata(L, lua_upvalueindex(1)));
+
+ std::string binary;
+ std::string typeStr;
+ std::string modeStr;
+
+ if (lua_istable(L, 1)) {
+ auto b = Internal::tableOptStr(L, 1, "binary");
+ if (!b)
+ b = Internal::tableOptStr(L, 1, "target");
+ auto t = Internal::tableOptStr(L, 1, "type");
+ auto m = Internal::tableOptStr(L, 1, "mode");
+
+ if (!b || !t || !m)
+ return Internal::configError(L, "hl.permission: expected { binary, type, mode }");
+
+ binary = *b;
+ typeStr = *t;
+ modeStr = *m;
+ } else {
+ binary = luaL_checkstring(L, 1);
+ typeStr = luaL_checkstring(L, 2);
+ modeStr = luaL_checkstring(L, 3);
+ }
+
+ if (binary.empty())
+ return Internal::configError(L, "hl.permission: binary must not be empty");
+
+ eDynamicPermissionType type = PERMISSION_TYPE_UNKNOWN;
+ eDynamicPermissionAllowMode mode = PERMISSION_RULE_ALLOW_MODE_UNKNOWN;
+
+ if (typeStr == "screencopy")
+ type = PERMISSION_TYPE_SCREENCOPY;
+ else if (typeStr == "cursorpos")
+ type = PERMISSION_TYPE_CURSOR_POS;
+ else if (typeStr == "plugin")
+ type = PERMISSION_TYPE_PLUGIN;
+ else if (typeStr == "keyboard" || typeStr == "keeb")
+ type = PERMISSION_TYPE_KEYBOARD;
+
+ if (modeStr == "ask")
+ mode = PERMISSION_RULE_ALLOW_MODE_ASK;
+ else if (modeStr == "allow")
+ mode = PERMISSION_RULE_ALLOW_MODE_ALLOW;
+ else if (modeStr == "deny")
+ mode = PERMISSION_RULE_ALLOW_MODE_DENY;
+
+ if (type == PERMISSION_TYPE_UNKNOWN)
+ return Internal::configError(L, "hl.permission: unknown permission type");
+ if (mode == PERMISSION_RULE_ALLOW_MODE_UNKNOWN)
+ return Internal::configError(L, "hl.permission: unknown permission allow mode");
+
+ if (mgr->isFirstLaunch() && g_pDynamicPermissionManager)
+ g_pDynamicPermissionManager->addConfigPermissionRule(binary, type, mode);
+
+ return 0;
+}
+
+static int hlWorkspaceRule(lua_State* L) {
+ auto* self = sc<CConfigManager*>(lua_touserdata(L, lua_upvalueindex(1)));
+
+ if (!lua_istable(L, 1)) {
+ self->addError("hl.workspace_rule: argument must be a table");
+ return 0;
+ }
+
+ const std::string sourceInfo = Internal::getSourceInfo(L);
+
+ lua_getfield(L, 1, "workspace");
+ if (!lua_isstring(L, -1)) {
+ self->addError(std::format("{}: hl.workspace_rule: 'workspace' field is required and must be a string", sourceInfo));
+ lua_pop(L, 1);
+ return 0;
+ }
+ const std::string wsStr = lua_tostring(L, -1);
+ lua_pop(L, 1);
+
+ bool enabled = true;
+ lua_getfield(L, 1, "enabled");
+ if (lua_isboolean(L, -1))
+ enabled = lua_toboolean(L, -1) != 0;
+ lua_pop(L, 1);
+
+ const auto& [wsId, wsName, isAutoID] = getWorkspaceIDNameFromString(wsStr);
+
+ Config::CWorkspaceRule wsRule;
+ wsRule.m_workspaceString = wsStr;
+ wsRule.m_workspaceName = wsName;
+ wsRule.m_workspaceId = isAutoID ? WORKSPACE_INVALID : wsId;
+ wsRule.m_enabled = enabled;
+
+ lua_pushnil(L);
+ while (lua_next(L, 1) != 0) {
+ if (lua_type(L, -2) != LUA_TSTRING) {
+ lua_pop(L, 1);
+ continue;
+ }
+
+ std::string_view key = lua_tostring(L, -2);
+ if (key == "workspace" || key == "enabled") {
+ lua_pop(L, 1);
+ continue;
+ }
+
+ if (key == "layout_opts") {
+ if (!lua_istable(L, -1)) {
+ self->addError(std::format("{}: hl.workspace_rule: field 'layout_opts' must be a table", sourceInfo));
+ lua_pop(L, 1);
+ continue;
+ }
+
+ const int optsIdx = lua_gettop(L);
+ lua_pushnil(L);
+ while (lua_next(L, optsIdx) != 0) {
+ if (lua_type(L, -2) != LUA_TSTRING) {
+ self->addError(std::format("{}: hl.workspace_rule: field 'layout_opts' keys must be strings", sourceInfo));
+ lua_pop(L, 1);
+ continue;
+ }
+
+ std::string optKey = lua_tostring(L, -2);
+ std::string optVal;
+
+ if (lua_type(L, -1) == LUA_TBOOLEAN)
+ optVal = lua_toboolean(L, -1) ? "true" : "false";
+ else if (lua_type(L, -1) == LUA_TNUMBER) {
+ if (lua_isinteger(L, -1))
+ optVal = std::to_string(lua_tointeger(L, -1));
+ else
+ optVal = std::to_string(lua_tonumber(L, -1));
+ } else if (lua_isstring(L, -1))
+ optVal = lua_tostring(L, -1);
+ else {
+ self->addError(std::format("{}: hl.workspace_rule: field 'layout_opts.{}' must be string, bool, or number", sourceInfo, optKey));
+ lua_pop(L, 1);
+ continue;
+ }
+
+ wsRule.m_layoutopts[std::move(optKey)] = std::move(optVal);
+ lua_pop(L, 1);
+ }
+
+ lua_pop(L, 1);
+ continue;
+ }
+
+ const auto* desc = Internal::findDescByName(WORKSPACE_RULE_FIELDS, key);
+ if (!desc) {
+ self->addError(std::format("{}: hl.workspace_rule: unknown field '{}'", sourceInfo, key));
+ lua_pop(L, 1);
+ continue;
+ }
+
+ auto val = UP<ILuaConfigValue>(desc->factory());
+ auto err = val->parse(L);
+ if (err.errorCode != PARSE_ERROR_OK)
+ self->addError(std::format("{}: hl.workspace_rule: field '{}': {}", sourceInfo, key, err.message));
+ else
+ desc->apply(val.get(), wsRule);
+
+ lua_pop(L, 1);
+ }
+
+ Config::workspaceRuleMgr()->replaceOrAdd(std::move(wsRule));
+
+ Supplementary::refresher()->scheduleRefresh(Supplementary::REFRESH_MONITOR_STATES | Config::Supplementary::REFRESH_WINDOW_STATES);
+
+ return 0;
+}
+
+static int hlGesture(lua_State* L) {
+ if (!lua_istable(L, 1))
+ return Internal::configError(L, "hl.gesture: expected a table, e.g. { fingers = 3, direction = \"horizontal\", action = \"workspace\" }");
+
+ CLuaConfigInt fingersParser(0, 2, 9);
+ auto fingersErr = Internal::parseTableField(L, 1, "fingers", fingersParser);
+ if (fingersErr.errorCode != PARSE_ERROR_OK)
+ return Internal::configError(L, std::format("hl.gesture: {}", fingersErr.message));
+
+ size_t fingerCount = fingersParser.parsed();
+
+ CLuaConfigString dirParser("");
+ auto dirErr = Internal::parseTableField(L, 1, "direction", dirParser);
+ if (dirErr.errorCode != PARSE_ERROR_OK)
+ return Internal::configError(L, std::format("hl.gesture: {}", dirErr.message));
+
+ const auto direction = g_pTrackpadGestures->dirForString(dirParser.parsed());
+ if (direction == TRACKPAD_GESTURE_DIR_NONE)
+ return Internal::configError(L, std::format("hl.gesture: invalid direction \"{}\"", dirParser.parsed()));
+
+ int functionRef = LUA_NOREF;
+
+ {
+ // check if the action arg is a lua fn, that's fine
+ // we can ref that fucker and call him later
+ lua_getfield(L, 1, "action");
+
+ if (lua_isfunction(L, -1)) {
+ lua_pushvalue(L, -1);
+ functionRef = luaL_ref(L, LUA_REGISTRYINDEX);
+ Lua::mgr()->registerLuaRef(functionRef);
+ }
+
+ lua_pop(L, 1);
+ }
+
+ // bitch ass macro because it's kinda long to get these things and it's ugly
+#define GET_ACTION_STRING(var, name) \
+ std::string var; \
+ lua_getfield(L, 1, name); \
+ if (!lua_isnil(L, -1)) { \
+ CLuaConfigString argParser(""); \
+ auto argErr = argParser.parse(L); \
+ if (argErr.errorCode != PARSE_ERROR_OK) { \
+ lua_pop(L, 1); \
+ return Internal::configError(L, std::format("hl.gesture: field \"" name "\": {}", argErr.message)); \
+ } \
+ var = argParser.parsed(); \
+ } \
+ lua_pop(L, 1);
+
+ GET_ACTION_STRING(zoomLevel, "zoom_level");
+ GET_ACTION_STRING(workspaceName, "workspace_name");
+ GET_ACTION_STRING(mode, "mode");
+
+#undef GET_ACTION_STRING
+
+ uint32_t modMask = 0;
+ lua_getfield(L, 1, "mods");
+ if (!lua_isnil(L, -1)) {
+ CLuaConfigString modsParser("");
+ auto modsErr = modsParser.parse(L);
+ if (modsErr.errorCode != PARSE_ERROR_OK) {
+ lua_pop(L, 1);
+ return Internal::configError(L, std::format("hl.gesture: field \"mods\": {}", modsErr.message));
+ }
+ modMask = g_pKeybindManager->stringToModMask(modsParser.parsed());
+ }
+ lua_pop(L, 1);
+
+ float deltaScale = 1.F;
+ lua_getfield(L, 1, "scale");
+ if (!lua_isnil(L, -1)) {
+ CLuaConfigFloat scaleParser(1.F, 0.1F, 10.F);
+ auto scaleErr = scaleParser.parse(L);
+ if (scaleErr.errorCode != PARSE_ERROR_OK) {
+ lua_pop(L, 1);
+ return Internal::configError(L, std::format("hl.gesture: field \"scale\": {}", scaleErr.message));
+ }
+ deltaScale = scaleParser.parsed();
+ }
+ lua_pop(L, 1);
+
+ bool disableInhibit = false;
+ lua_getfield(L, 1, "disable_inhibit");
+ if (!lua_isnil(L, -1)) {
+ CLuaConfigBool disableInhibitParser(false);
+ auto disableInhibitErr = disableInhibitParser.parse(L);
+ if (disableInhibitErr.errorCode != PARSE_ERROR_OK) {
+ lua_pop(L, 1);
+ return Internal::configError(L, std::format("hl.gesture: field \"disable_inhibit\": {}", disableInhibitErr.message));
+ }
+ disableInhibit = disableInhibitParser.parsed();
+ }
+ lua_pop(L, 1);
+
+ std::expected<void, std::string> result;
+
+ if (functionRef != LUA_NOREF) {
+ // this is a lua fn gesture
+ result = g_pTrackpadGestures->addGesture(makeUnique<CLuaFunctionGesture>(functionRef), fingerCount, direction, modMask, deltaScale, disableInhibit);
+ } else {
+ CLuaConfigString actionParser("");
+ auto actionErr = Internal::parseTableField(L, 1, "action", actionParser);
+ if (actionErr.errorCode != PARSE_ERROR_OK)
+ return Internal::configError(L, std::format("hl.gesture: {}", actionErr.message));
+
+ const auto& action = actionParser.parsed();
+
+ if (action == "workspace")
+ result = g_pTrackpadGestures->addGesture(makeUnique<CWorkspaceSwipeGesture>(), fingerCount, direction, modMask, deltaScale, disableInhibit);
+ else if (action == "resize")
+ result = g_pTrackpadGestures->addGesture(makeUnique<CResizeTrackpadGesture>(), fingerCount, direction, modMask, deltaScale, disableInhibit);
+ else if (action == "move")
+ result = g_pTrackpadGestures->addGesture(makeUnique<CMoveTrackpadGesture>(), fingerCount, direction, modMask, deltaScale, disableInhibit);
+ else if (action == "special")
+ result = g_pTrackpadGestures->addGesture(makeUnique<CSpecialWorkspaceGesture>(workspaceName), fingerCount, direction, modMask, deltaScale, disableInhibit);
+ else if (action == "close")
+ result = g_pTrackpadGestures->addGesture(makeUnique<CCloseTrackpadGesture>(), fingerCount, direction, modMask, deltaScale, disableInhibit);
+ else if (action == "float")
+ result = g_pTrackpadGestures->addGesture(makeUnique<CFloatTrackpadGesture>(mode), fingerCount, direction, modMask, deltaScale, disableInhibit);
+ else if (action == "fullscreen")
+ result = g_pTrackpadGestures->addGesture(makeUnique<CFullscreenTrackpadGesture>(mode), fingerCount, direction, modMask, deltaScale, disableInhibit);
+ else if (action == "cursorZoom")
+ result = g_pTrackpadGestures->addGesture(makeUnique<CCursorZoomTrackpadGesture>(zoomLevel, mode), fingerCount, direction, modMask, deltaScale, disableInhibit);
+ else if (action == "unset")
+ result = g_pTrackpadGestures->removeGesture(fingerCount, direction, modMask, deltaScale, disableInhibit);
+ else
+ return Internal::configError(L, std::format("hl.gesture: unknown action \"{}\"", action));
+ }
+
+ if (!result)
+ return Internal::configError(L, std::format("hl.gesture: {}", result.error()));
+
+ return 0;
+}
+
+static int hlConfig(lua_State* L) {
+ auto* self = sc<CConfigManager*>(lua_touserdata(L, lua_upvalueindex(1)));
+
+ if (!lua_istable(L, 1)) {
+ self->addError("hl.config: argument must be a table");
+ return 0;
+ }
+
+ const std::string sourceInfo = Internal::getSourceInfo(L);
+
+ std::function<void(const std::string&, int)> walk = [&](const std::string& prefix, int tableIdx) {
+ lua_pushnil(L);
+ while (lua_next(L, tableIdx) != 0) {
+ if (lua_type(L, -2) != LUA_TSTRING) {
+ lua_pop(L, 1);
+ continue;
+ }
+
+ const std::string key = lua_tostring(L, -2);
+ std::string fullKey;
+ if (!prefix.empty()) {
+ fullKey.reserve(prefix.size() + 1 + key.size());
+ fullKey = prefix;
+ fullKey += '.';
+ }
+ fullKey += key;
+
+ auto it = self->m_configValues.find(fullKey);
+
+ if (it == self->m_configValues.end() && lua_istable(L, -1))
+ walk(fullKey, lua_gettop(L));
+ else {
+ if (it == self->m_configValues.end())
+ self->addError(std::format("{}: unknown config key '{}'", sourceInfo, fullKey));
+ else {
+ const auto err = it->second->parse(L);
+ if (err.errorCode != PARSE_ERROR_OK)
+ self->addError(std::format("{}: error setting '{}': {}", sourceInfo, it->first, err.message));
+ else if (self->isDynamicParse())
+ Supplementary::refresher()->scheduleRefresh(it->second->refreshBits());
+ }
+ }
+
+ lua_pop(L, 1);
+ }
+ };
+
+ walk("", 1);
+ return 0;
+}
+
+static int hlGetConfig(lua_State* L) {
+ auto* self = sc<CConfigManager*>(lua_touserdata(L, lua_upvalueindex(1)));
+
+ std::string key = luaL_checkstring(L, 1);
+
+ auto it = self->m_configValues.find(key);
+ if (it == self->m_configValues.end()) {
+ std::ranges::replace(key, ':', '.');
+ it = self->m_configValues.find(key);
+ }
+
+ if (it == self->m_configValues.end()) {
+ lua_pushnil(L);
+ const auto msg = std::format("unknown config key '{}'", key);
+ lua_pushstring(L, msg.c_str());
+ return 2;
+ }
+
+ it->second->push(L);
+ return 1;
+}
+
+static int hlDevice(lua_State* L) {
+ auto* self = sc<CConfigManager*>(lua_touserdata(L, lua_upvalueindex(1)));
+
+ if (!lua_istable(L, 1)) {
+ self->addError("hl.device: argument must be a table");
+ return 0;
+ }
+
+ const std::string sourceInfo = Internal::getSourceInfo(L);
+
+ lua_getfield(L, 1, "name");
+ if (!lua_isstring(L, -1)) {
+ self->addError(std::format("{}: hl.device: 'name' field is required and must be a string", sourceInfo));
+ lua_pop(L, 1);
+ return 0;
+ }
+ std::string devName = lua_tostring(L, -1);
+ lua_pop(L, 1);
+ std::ranges::replace(devName, ' ', '-');
+
+ lua_pushnil(L);
+ while (lua_next(L, 1) != 0) {
+ if (lua_type(L, -2) != LUA_TSTRING) {
+ lua_pop(L, 1);
+ continue;
+ }
+
+ const char* key = lua_tostring(L, -2);
+
+ if (std::string_view{key} == "name") {
+ lua_pop(L, 1);
+ continue;
+ }
+
+ const auto* desc = Internal::findDescByName(DEVICE_FIELDS, key);
+
+ if (!desc) {
+ self->addError(std::format("{}: hl.device: unknown field '{}'", sourceInfo, key));
+ lua_pop(L, 1);
+ continue;
+ }
+
+ auto val = UP<ILuaConfigValue>(desc->factory());
+ auto err = val->parse(L);
+ if (err.errorCode != PARSE_ERROR_OK)
+ self->addError(std::format("{}: hl.device: field '{}': {}", sourceInfo, key, err.message));
+ else
+ self->m_deviceConfigs[devName].values.insert_or_assign(key, std::move(val));
+
+ lua_pop(L, 1);
+ }
+
+ Supplementary::refresher()->scheduleRefresh(Supplementary::REFRESH_INPUT_DEVICES);
+
+ return 0;
+}
+
+static int hlMonitor(lua_State* L) {
+ auto* self = sc<CConfigManager*>(lua_touserdata(L, lua_upvalueindex(1)));
+
+ if (!lua_istable(L, 1)) {
+ self->addError("hl.monitor: argument must be a table");
+ return 0;
+ }
+
+ const std::string sourceInfo = Internal::getSourceInfo(L);
+
+ lua_getfield(L, 1, "output");
+ if (!lua_isstring(L, -1)) {
+ self->addError(std::format("{}: hl.monitor: 'output' field is required and must be a string", sourceInfo));
+ lua_pop(L, 1);
+ return 0;
+ }
+ const std::string output = lua_tostring(L, -1);
+ lua_pop(L, 1);
+
+ CMonitorRuleParser parser(output);
+
+ const auto existing = std::ranges::find_if(Config::monitorRuleMgr()->all(), [&output](const auto& rule) { return rule.m_name == output; });
+ if (existing != Config::monitorRuleMgr()->all().end())
+ parser.rule() = *existing;
+
+ lua_pushnil(L);
+ while (lua_next(L, 1) != 0) {
+ if (lua_type(L, -2) != LUA_TSTRING) {
+ lua_pop(L, 1);
+ continue;
+ }
+
+ const char* key = lua_tostring(L, -2);
+
+ if (std::string_view{key} == "output") {
+ lua_pop(L, 1);
+ continue;
+ }
+
+ const auto* desc = Internal::findDescByName(MONITOR_FIELDS, key);
+
+ if (!desc) {
+ self->addError(std::format("{}: hl.monitor: unknown field '{}'", sourceInfo, key));
+ lua_pop(L, 1);
+ continue;
+ }
+
+ auto val = UP<ILuaConfigValue>(desc->factory());
+ auto err = val->parse(L);
+ if (err.errorCode != PARSE_ERROR_OK)
+ self->addError(std::format("{}: hl.monitor: field '{}': {}", sourceInfo, key, err.message));
+ else if (!desc->apply(val.get(), parser))
+ self->addError(std::format("{}: hl.monitor: error applying field '{}'", sourceInfo, key));
+
+ lua_pop(L, 1);
+ }
+
+ Config::monitorRuleMgr()->add(std::move(parser.rule()));
+
+ Supplementary::refresher()->scheduleRefresh(Supplementary::REFRESH_MONITOR_STATES);
+
+ return 0;
+}
+
+static int hlWindowRule(lua_State* L) {
+ auto* self = sc<CConfigManager*>(lua_touserdata(L, lua_upvalueindex(1)));
+
+ if (!lua_istable(L, 1)) {
+ self->addError("hl.window_rule: argument must be a table");
+ return 0;
+ }
+
+ const std::string sourceInfo = Internal::getSourceInfo(L);
+
+ std::string name;
+ lua_getfield(L, 1, "name");
+ if (lua_isstring(L, -1))
+ name = lua_tostring(L, -1);
+ lua_pop(L, 1);
+
+ bool enabled = true;
+ lua_getfield(L, 1, "enabled");
+ if (lua_isboolean(L, -1))
+ enabled = lua_toboolean(L, -1) != 0;
+ lua_pop(L, 1);
+
+ SP<Desktop::Rule::CWindowRule> rule;
+ if (!name.empty() && self->m_luaWindowRules.contains(name)) {
+ rule = self->m_luaWindowRules[name];
+ } else {
+ rule = makeShared<Desktop::Rule::CWindowRule>(name);
+ if (!name.empty())
+ self->m_luaWindowRules[name] = rule;
+ Desktop::Rule::ruleEngine()->registerRule(SP<Desktop::Rule::IRule>(rule));
+ }
+ rule->setEnabled(enabled);
+
+ lua_getfield(L, 1, "match");
+ if (lua_istable(L, -1)) {
+ int matchIdx = lua_gettop(L);
+ lua_pushnil(L);
+ while (lua_next(L, matchIdx) != 0) {
+ if (lua_type(L, -2) == LUA_TSTRING) {
+ std::string matchKey = lua_tostring(L, -2);
+ std::string matchVal;
+ if (lua_type(L, -1) == LUA_TBOOLEAN)
+ matchVal = lua_toboolean(L, -1) ? "true" : "false";
+ else if (lua_type(L, -1) == LUA_TNUMBER)
+ matchVal = std::to_string(lua_tointeger(L, -1));
+ else if (lua_isstring(L, -1))
+ matchVal = lua_tostring(L, -1);
+ else {
+ self->addError(std::format("{}: hl.window_rule: match value for '{}' must be string, bool, or number", sourceInfo, matchKey));
+ lua_pop(L, 1);
+ continue;
+ }
+ auto prop = Desktop::Rule::matchPropFromString(matchKey);
+ if (prop.has_value())
+ rule->registerMatch(*prop, matchVal);
+ else
+ self->addError(std::format("{}: hl.window_rule: unknown match property '{}'", sourceInfo, matchKey));
+ }
+ lua_pop(L, 1);
+ }
+ }
+ lua_pop(L, 1);
+
+ lua_pushnil(L);
+ while (lua_next(L, 1) != 0) {
+ if (lua_type(L, -2) != LUA_TSTRING) {
+ lua_pop(L, 1);
+ continue;
+ }
+ std::string_view key = lua_tostring(L, -2);
+ if (key == "name" || key == "enabled" || key == "match") {
+ lua_pop(L, 1);
+ continue;
+ }
+
+ const auto* desc = Internal::findDescByName(Internal::WINDOW_RULE_EFFECT_DESCS, key);
+ if (!desc) {
+ const auto dynamicEffect = Desktop::Rule::windowEffects()->get(key);
+ if (!dynamicEffect.has_value()) {
+ self->addError(std::format("{}: hl.window_rule: unknown field '{}'", sourceInfo, key));
+ lua_pop(L, 1);
+ continue;
+ }
+
+ auto val = Internal::ruleValueToString(L);
+ if (!val)
+ self->addError(std::format("{}: hl.window_rule: field '{}': {}", sourceInfo, key, val.error()));
+ else
+ rule->addEffect(*dynamicEffect, *val);
+
+ lua_pop(L, 1);
+ continue;
+ }
+
+ auto val = UP<ILuaConfigValue>(desc->factory());
+ auto err = val->parse(L);
+ if (err.errorCode != PARSE_ERROR_OK) {
+ const bool allowLegacyString = (key == "max_size" || key == "min_size" || key == "border_color") && lua_isstring(L, -1);
+ if (allowLegacyString)
+ rule->addEffect(desc->effect, lua_tostring(L, -1));
+ else
+ self->addError(std::format("{}: hl.window_rule: field '{}': {}", sourceInfo, key, err.message));
+ } else
+ rule->addEffect(desc->effect, val->toString());
+
+ lua_pop(L, 1);
+ }
+
+ Supplementary::refresher()->scheduleRefresh(Supplementary::REFRESH_WINDOW_STATES);
+
+ Objects::CLuaWindowRule::push(L, rule);
+ return 1;
+}
+
+static int hlLayerRule(lua_State* L) {
+ auto* self = sc<CConfigManager*>(lua_touserdata(L, lua_upvalueindex(1)));
+
+ if (!lua_istable(L, 1)) {
+ self->addError("hl.layer_rule: argument must be a table");
+ return 0;
+ }
+
+ const std::string sourceInfo = Internal::getSourceInfo(L);
+
+ std::string name;
+ lua_getfield(L, 1, "name");
+ if (lua_isstring(L, -1))
+ name = lua_tostring(L, -1);
+ lua_pop(L, 1);
+
+ bool enabled = true;
+ lua_getfield(L, 1, "enabled");
+ if (lua_isboolean(L, -1))
+ enabled = lua_toboolean(L, -1) != 0;
+ lua_pop(L, 1);
+
+ SP<Desktop::Rule::CLayerRule> rule;
+ if (!name.empty() && self->m_luaLayerRules.contains(name)) {
+ rule = self->m_luaLayerRules[name];
+ } else {
+ rule = makeShared<Desktop::Rule::CLayerRule>(name);
+ if (!name.empty())
+ self->m_luaLayerRules[name] = rule;
+ Desktop::Rule::ruleEngine()->registerRule(SP<Desktop::Rule::IRule>(rule));
+ }
+ rule->setEnabled(enabled);
+
+ lua_getfield(L, 1, "match");
+ if (lua_istable(L, -1)) {
+ int matchIdx = lua_gettop(L);
+ lua_pushnil(L);
+ while (lua_next(L, matchIdx) != 0) {
+ if (lua_type(L, -2) == LUA_TSTRING) {
+ std::string matchKey = lua_tostring(L, -2);
+ std::string matchVal;
+ if (lua_type(L, -1) == LUA_TBOOLEAN)
+ matchVal = lua_toboolean(L, -1) ? "true" : "false";
+ else if (lua_isstring(L, -1))
+ matchVal = lua_tostring(L, -1);
+ else {
+ self->addError(std::format("{}: hl.layer_rule: match value for '{}' must be string or bool", sourceInfo, matchKey));
+ lua_pop(L, 1);
+ continue;
+ }
+ auto prop = Desktop::Rule::matchPropFromString(matchKey);
+ if (prop.has_value())
+ rule->registerMatch(*prop, matchVal);
+ else
+ self->addError(std::format("{}: hl.layer_rule: unknown match property '{}'", sourceInfo, matchKey));
+ }
+ lua_pop(L, 1);
+ }
+ }
+ lua_pop(L, 1);
+
+ lua_pushnil(L);
+ while (lua_next(L, 1) != 0) {
+ if (lua_type(L, -2) != LUA_TSTRING) {
+ lua_pop(L, 1);
+ continue;
+ }
+ std::string_view key = lua_tostring(L, -2);
+ if (key == "name" || key == "enabled" || key == "match") {
+ lua_pop(L, 1);
+ continue;
+ }
+
+ const auto* desc = Internal::findDescByName(LAYER_RULE_EFFECT_DESCS, key);
+ if (!desc) {
+ const auto dynamicEffect = Desktop::Rule::layerEffects()->get(key);
+ if (!dynamicEffect.has_value()) {
+ self->addError(std::format("{}: hl.layer_rule: unknown field '{}'", sourceInfo, key));
+ lua_pop(L, 1);
+ continue;
+ }
+
+ auto val = Internal::ruleValueToString(L);
+ if (!val)
+ self->addError(std::format("{}: hl.layer_rule: field '{}': {}", sourceInfo, key, val.error()));
+ else
+ rule->addEffect(*dynamicEffect, *val);
+
+ lua_pop(L, 1);
+ continue;
+ }
+
+ auto val = UP<ILuaConfigValue>(desc->factory());
+ auto err = val->parse(L);
+ if (err.errorCode != PARSE_ERROR_OK)
+ self->addError(std::format("{}: hl.layer_rule: field '{}': {}", sourceInfo, key, err.message));
+ else {
+ auto str = val->toString();
+ rule->addEffect(desc->effect, str);
+ }
+
+ lua_pop(L, 1);
+ }
+
+ Supplementary::refresher()->scheduleRefresh(Supplementary::REFRESH_RULES);
+
+ Objects::CLuaLayerRule::push(L, rule);
+ return 1;
+}
+
+void Internal::registerConfigRuleBindings(lua_State* L, CConfigManager* mgr) {
+ Internal::setMgrFn(L, mgr, "config", hlConfig);
+ Internal::setMgrFn(L, mgr, "get_config", hlGetConfig);
+ Internal::setMgrFn(L, mgr, "device", hlDevice);
+ Internal::setMgrFn(L, mgr, "monitor", hlMonitor);
+ Internal::setMgrFn(L, mgr, "window_rule", hlWindowRule);
+ Internal::setMgrFn(L, mgr, "layer_rule", hlLayerRule);
+ Internal::setMgrFn(L, mgr, "workspace_rule", hlWorkspaceRule);
+ Internal::setMgrFn(L, mgr, "env", hlEnv);
+ Internal::setMgrFn(L, mgr, "permission", hlPermission);
+
+ lua_newtable(L);
+ Internal::setMgrFn(L, mgr, "load", hlPluginLoad);
+ lua_setfield(L, -2, "plugin");
+
+ Internal::setFn(L, "gesture", hlGesture);
+ Internal::setFn(L, "curve", hlCurve);
+ Internal::setFn(L, "animation", hlAnimation);
+}