summaryrefslogtreecommitdiff
path: root/src/config/lua/objects
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/objects
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/objects')
-rw-r--r--src/config/lua/objects/LuaEventSubscription.cpp74
-rw-r--r--src/config/lua/objects/LuaEventSubscription.hpp12
-rw-r--r--src/config/lua/objects/LuaKeybind.cpp180
-rw-r--r--src/config/lua/objects/LuaKeybind.hpp12
-rw-r--r--src/config/lua/objects/LuaLayerRule.cpp80
-rw-r--r--src/config/lua/objects/LuaLayerRule.hpp12
-rw-r--r--src/config/lua/objects/LuaLayerSurface.cpp88
-rw-r--r--src/config/lua/objects/LuaLayerSurface.hpp12
-rw-r--r--src/config/lua/objects/LuaMonitor.cpp122
-rw-r--r--src/config/lua/objects/LuaMonitor.hpp12
-rw-r--r--src/config/lua/objects/LuaNotification.cpp377
-rw-r--r--src/config/lua/objects/LuaNotification.hpp12
-rw-r--r--src/config/lua/objects/LuaObjectHelpers.hpp51
-rw-r--r--src/config/lua/objects/LuaTimer.cpp109
-rw-r--r--src/config/lua/objects/LuaTimer.hpp12
-rw-r--r--src/config/lua/objects/LuaWindow.cpp277
-rw-r--r--src/config/lua/objects/LuaWindow.hpp12
-rw-r--r--src/config/lua/objects/LuaWindowRule.cpp80
-rw-r--r--src/config/lua/objects/LuaWindowRule.hpp12
-rw-r--r--src/config/lua/objects/LuaWorkspace.cpp87
-rw-r--r--src/config/lua/objects/LuaWorkspace.hpp12
21 files changed, 1645 insertions, 0 deletions
diff --git a/src/config/lua/objects/LuaEventSubscription.cpp b/src/config/lua/objects/LuaEventSubscription.cpp
new file mode 100644
index 00000000..c37491d7
--- /dev/null
+++ b/src/config/lua/objects/LuaEventSubscription.cpp
@@ -0,0 +1,74 @@
+#include "LuaEventSubscription.hpp"
+
+#include <format>
+#include <string_view>
+
+using namespace Config::Lua;
+
+static constexpr const char* MT = "HL.EventSubscription";
+
+namespace {
+ struct SEventSubscriptionRef {
+ CLuaEventHandler* handler = nullptr;
+ uint64_t handle = 0;
+ bool active = false;
+ };
+}
+
+static int eventSubscriptionEq(lua_State* L) {
+ const auto* lhs = sc<SEventSubscriptionRef*>(luaL_checkudata(L, 1, MT));
+ const auto* rhs = sc<SEventSubscriptionRef*>(luaL_checkudata(L, 2, MT));
+
+ lua_pushboolean(L, lhs->handler == rhs->handler && lhs->handle == rhs->handle);
+ return 1;
+}
+
+static int eventSubscriptionToString(lua_State* L) {
+ const auto* ref = sc<SEventSubscriptionRef*>(luaL_checkudata(L, 1, MT));
+
+ const auto str = std::format("HL.EventSubscription({},{})", ref->handle, ref->active ? "active" : "inactive");
+ lua_pushstring(L, str.c_str());
+ return 1;
+}
+
+static int eventSubscriptionRemove(lua_State* L) {
+ auto* ref = sc<SEventSubscriptionRef*>(luaL_checkudata(L, 1, MT));
+
+ if (!ref->active || !ref->handler)
+ return 0;
+
+ ref->handler->unregisterEvent(ref->handle);
+ ref->active = false;
+ return 0;
+}
+
+static int eventSubscriptionIsActive(lua_State* L) {
+ auto* ref = sc<SEventSubscriptionRef*>(luaL_checkudata(L, 1, MT));
+
+ lua_pushboolean(L, ref->active);
+ return 1;
+}
+
+static int eventSubscriptionIndex(lua_State* L) {
+ luaL_checkudata(L, 1, MT);
+ const std::string_view key = luaL_checkstring(L, 2);
+
+ if (key == "remove")
+ lua_pushcfunction(L, eventSubscriptionRemove);
+ else if (key == "is_active")
+ lua_pushcfunction(L, eventSubscriptionIsActive);
+ else
+ lua_pushnil(L);
+
+ return 1;
+}
+
+void Objects::CLuaEventSubscription::setup(lua_State* L) {
+ registerMetatable(L, MT, eventSubscriptionIndex, gcRef<SEventSubscriptionRef>, eventSubscriptionEq, eventSubscriptionToString);
+}
+
+void Objects::CLuaEventSubscription::push(lua_State* L, CLuaEventHandler* handler, uint64_t handle) {
+ new (lua_newuserdata(L, sizeof(SEventSubscriptionRef))) SEventSubscriptionRef{.handler = handler, .handle = handle, .active = true};
+ luaL_getmetatable(L, MT);
+ lua_setmetatable(L, -2);
+}
diff --git a/src/config/lua/objects/LuaEventSubscription.hpp b/src/config/lua/objects/LuaEventSubscription.hpp
new file mode 100644
index 00000000..11cdfc62
--- /dev/null
+++ b/src/config/lua/objects/LuaEventSubscription.hpp
@@ -0,0 +1,12 @@
+#pragma once
+
+#include "LuaObjectHelpers.hpp"
+#include "../LuaEventHandler.hpp"
+
+namespace Config::Lua::Objects {
+ class CLuaEventSubscription : public ILuaObjectWrapper {
+ public:
+ void setup(lua_State* L) override;
+ static void push(lua_State* L, CLuaEventHandler* handler, uint64_t handle);
+ };
+}
diff --git a/src/config/lua/objects/LuaKeybind.cpp b/src/config/lua/objects/LuaKeybind.cpp
new file mode 100644
index 00000000..aab2735d
--- /dev/null
+++ b/src/config/lua/objects/LuaKeybind.cpp
@@ -0,0 +1,180 @@
+#include "LuaKeybind.hpp"
+
+#include <optional>
+#include <string_view>
+
+using namespace Config::Lua;
+
+static constexpr const char* MT = "HL.Keybind";
+
+namespace {
+ std::optional<SP<SKeybind>> getKeybindFromUserdata(lua_State* L) {
+ auto* ref = sc<WP<SKeybind>*>(luaL_checkudata(L, 1, MT));
+ return ref->lock();
+ }
+
+ void pushDeviceList(lua_State* L, const SKeybind& keybind) {
+ lua_newtable(L);
+ int i = 1;
+ for (const auto& device : keybind.devices) {
+ lua_pushstring(L, device.c_str());
+ lua_rawseti(L, -2, i++);
+ }
+ }
+}
+
+static int keybindEq(lua_State* L) {
+ const auto* lhs = sc<WP<SKeybind>*>(luaL_checkudata(L, 1, MT));
+ const auto* rhs = sc<WP<SKeybind>*>(luaL_checkudata(L, 2, MT));
+
+ lua_pushboolean(L, lhs->lock() == rhs->lock());
+ return 1;
+}
+
+static int keybindToString(lua_State* L) {
+ const auto* ref = sc<WP<SKeybind>*>(luaL_checkudata(L, 1, MT));
+ const auto keybind = ref->lock();
+
+ if (!keybind)
+ lua_pushstring(L, "HL.Keybind(expired)");
+ else
+ lua_pushfstring(L, "HL.Keybind(%p)", keybind.get());
+
+ return 1;
+}
+
+static int keybindSetEnabled(lua_State* L) {
+ luaL_checktype(L, 2, LUA_TBOOLEAN);
+
+ const auto keybind = getKeybindFromUserdata(L);
+ if (!keybind)
+ return 0;
+
+ (*keybind)->enabled = lua_toboolean(L, 2);
+ return 0;
+}
+
+static int keybindIsEnabled(lua_State* L) {
+ const auto keybind = getKeybindFromUserdata(L);
+ if (!keybind) {
+ lua_pushnil(L);
+ return 1;
+ }
+
+ lua_pushboolean(L, (*keybind)->enabled);
+ return 1;
+}
+
+static int keybindRemove(lua_State* L) {
+ const auto keybind = getKeybindFromUserdata(L);
+ if (!keybind || !g_pKeybindManager)
+ return 0;
+
+ if ((*keybind)->handler == "__lua") {
+ try {
+ const int ref = std::stoi((*keybind)->arg);
+ if (ref > 0)
+ luaL_unref(L, LUA_REGISTRYINDEX, ref);
+ } catch (...) {
+ // invalid ref, ignore
+ }
+
+ (*keybind)->arg = std::to_string(LUA_NOREF);
+ }
+
+ g_pKeybindManager->removeKeybind((*keybind)->modmask, SParsedKey{.key = (*keybind)->key, .keycode = (*keybind)->keycode, .catchAll = (*keybind)->catchAll});
+ return 0;
+}
+
+static int keybindGetDescription(lua_State* L) {
+ const auto keybind = getKeybindFromUserdata(L);
+ if (!keybind) {
+ lua_pushnil(L);
+ return 1;
+ }
+
+ if (!(*keybind)->hasDescription)
+ lua_pushnil(L);
+ else
+ lua_pushstring(L, (*keybind)->description.c_str());
+
+ return 1;
+}
+
+static int keybindIndex(lua_State* L) {
+ const auto keybind = getKeybindFromUserdata(L);
+ const std::string_view key = luaL_checkstring(L, 2);
+
+ if (key == "set_enabled")
+ lua_pushcfunction(L, keybindSetEnabled);
+ else if (key == "is_enabled")
+ lua_pushcfunction(L, keybindIsEnabled);
+ else if (key == "remove" || key == "unbind")
+ lua_pushcfunction(L, keybindRemove);
+ else if (!keybind)
+ lua_pushnil(L);
+ else if (key == "enabled")
+ lua_pushboolean(L, (*keybind)->enabled);
+ else if (key == "has_description")
+ lua_pushboolean(L, (*keybind)->hasDescription);
+ else if (key == "description")
+ return keybindGetDescription(L);
+ else if (key == "display_key")
+ lua_pushstring(L, (*keybind)->displayKey.c_str());
+ else if (key == "submap")
+ lua_pushstring(L, (*keybind)->submap.name.c_str());
+ else if (key == "handler")
+ lua_pushstring(L, (*keybind)->handler.c_str());
+ else if (key == "arg")
+ lua_pushstring(L, (*keybind)->arg.c_str());
+ else if (key == "modmask")
+ lua_pushinteger(L, sc<lua_Integer>((*keybind)->modmask));
+ else if (key == "key")
+ lua_pushstring(L, (*keybind)->key.c_str());
+ else if (key == "keycode")
+ lua_pushinteger(L, sc<lua_Integer>((*keybind)->keycode));
+ else if (key == "catchall")
+ lua_pushboolean(L, (*keybind)->catchAll);
+ else if (key == "repeating")
+ lua_pushboolean(L, (*keybind)->repeat);
+ else if (key == "locked")
+ lua_pushboolean(L, (*keybind)->locked);
+ else if (key == "release")
+ lua_pushboolean(L, (*keybind)->release);
+ else if (key == "non_consuming")
+ lua_pushboolean(L, (*keybind)->nonConsuming);
+ else if (key == "transparent")
+ lua_pushboolean(L, (*keybind)->transparent);
+ else if (key == "ignore_mods")
+ lua_pushboolean(L, (*keybind)->ignoreMods);
+ else if (key == "long_press")
+ lua_pushboolean(L, (*keybind)->longPress);
+ else if (key == "dont_inhibit")
+ lua_pushboolean(L, (*keybind)->dontInhibit);
+ else if (key == "click")
+ lua_pushboolean(L, (*keybind)->click);
+ else if (key == "drag")
+ lua_pushboolean(L, (*keybind)->drag);
+ else if (key == "submap_universal")
+ lua_pushboolean(L, (*keybind)->submapUniversal);
+ else if (key == "mouse")
+ lua_pushboolean(L, (*keybind)->mouse);
+ else if (key == "device_inclusive")
+ lua_pushboolean(L, (*keybind)->deviceInclusive);
+ else if (key == "devices")
+ pushDeviceList(L, **keybind);
+ else
+ lua_pushnil(L);
+
+ return 1;
+}
+
+void Objects::CLuaKeybind::setup(lua_State* L) {
+ registerMetatable(L, MT, keybindIndex, gcRef<WP<SKeybind>>, keybindEq, keybindToString);
+}
+
+void Objects::CLuaKeybind::push(lua_State* L, const SP<SKeybind>& keybind) {
+ new (lua_newuserdata(L, sizeof(WP<SKeybind>))) WP<SKeybind>(keybind);
+ luaL_getmetatable(L, MT);
+ lua_setmetatable(L, -2);
+}
diff --git a/src/config/lua/objects/LuaKeybind.hpp b/src/config/lua/objects/LuaKeybind.hpp
new file mode 100644
index 00000000..c55ed998
--- /dev/null
+++ b/src/config/lua/objects/LuaKeybind.hpp
@@ -0,0 +1,12 @@
+#pragma once
+
+#include "LuaObjectHelpers.hpp"
+#include "../../../managers/KeybindManager.hpp"
+
+namespace Config::Lua::Objects {
+ class CLuaKeybind : public ILuaObjectWrapper {
+ public:
+ void setup(lua_State* L) override;
+ static void push(lua_State* L, const SP<SKeybind>& keybind);
+ };
+}
diff --git a/src/config/lua/objects/LuaLayerRule.cpp b/src/config/lua/objects/LuaLayerRule.cpp
new file mode 100644
index 00000000..22e06272
--- /dev/null
+++ b/src/config/lua/objects/LuaLayerRule.cpp
@@ -0,0 +1,80 @@
+#include "LuaLayerRule.hpp"
+
+#include "../../../desktop/rule/Engine.hpp"
+
+#include <string_view>
+
+using namespace Config::Lua;
+
+static constexpr const char* MT = "HL.LayerRule";
+
+//
+static int layerRuleEq(lua_State* L) {
+ const auto* lhs = sc<WP<Desktop::Rule::CLayerRule>*>(luaL_checkudata(L, 1, MT));
+ const auto* rhs = sc<WP<Desktop::Rule::CLayerRule>*>(luaL_checkudata(L, 2, MT));
+
+ lua_pushboolean(L, lhs->lock() == rhs->lock());
+ return 1;
+}
+
+static int layerRuleToString(lua_State* L) {
+ const auto* ref = sc<WP<Desktop::Rule::CLayerRule>*>(luaL_checkudata(L, 1, MT));
+ const auto rule = ref->lock();
+
+ if (!rule)
+ lua_pushstring(L, "HL.LayerRule(expired)");
+ else
+ lua_pushfstring(L, "HL.LayerRule(%p)", rule.get());
+
+ return 1;
+}
+
+static int layerRuleSetEnabled(lua_State* L) {
+ auto* ref = sc<WP<Desktop::Rule::CLayerRule>*>(luaL_checkudata(L, 1, MT));
+ luaL_checktype(L, 2, LUA_TBOOLEAN);
+
+ const auto rule = ref->lock();
+ if (!rule)
+ return 0;
+
+ rule->setEnabled(lua_toboolean(L, 2));
+ Desktop::Rule::ruleEngine()->updateAllRules();
+ return 0;
+}
+
+static int layerRuleIsEnabled(lua_State* L) {
+ auto* ref = sc<WP<Desktop::Rule::CLayerRule>*>(luaL_checkudata(L, 1, MT));
+
+ const auto rule = ref->lock();
+ if (!rule) {
+ lua_pushnil(L);
+ return 1;
+ }
+
+ lua_pushboolean(L, rule->isEnabled());
+ return 1;
+}
+
+static int layerRuleIndex(lua_State* L) {
+ luaL_checkudata(L, 1, MT);
+ const std::string_view key = luaL_checkstring(L, 2);
+
+ if (key == "set_enabled")
+ lua_pushcfunction(L, layerRuleSetEnabled);
+ else if (key == "is_enabled")
+ lua_pushcfunction(L, layerRuleIsEnabled);
+ else
+ lua_pushnil(L);
+
+ return 1;
+}
+
+void Objects::CLuaLayerRule::setup(lua_State* L) {
+ registerMetatable(L, MT, layerRuleIndex, gcRef<WP<Desktop::Rule::CLayerRule>>, layerRuleEq, layerRuleToString);
+}
+
+void Objects::CLuaLayerRule::push(lua_State* L, const SP<Desktop::Rule::CLayerRule>& rule) {
+ new (lua_newuserdata(L, sizeof(WP<Desktop::Rule::CLayerRule>))) WP<Desktop::Rule::CLayerRule>(rule);
+ luaL_getmetatable(L, MT);
+ lua_setmetatable(L, -2);
+}
diff --git a/src/config/lua/objects/LuaLayerRule.hpp b/src/config/lua/objects/LuaLayerRule.hpp
new file mode 100644
index 00000000..2e4d24e7
--- /dev/null
+++ b/src/config/lua/objects/LuaLayerRule.hpp
@@ -0,0 +1,12 @@
+#pragma once
+
+#include "LuaObjectHelpers.hpp"
+#include "../../../desktop/rule/layerRule/LayerRule.hpp"
+
+namespace Config::Lua::Objects {
+ class CLuaLayerRule : public ILuaObjectWrapper {
+ public:
+ void setup(lua_State* L) override;
+ static void push(lua_State* L, const SP<Desktop::Rule::CLayerRule>& rule);
+ };
+}
diff --git a/src/config/lua/objects/LuaLayerSurface.cpp b/src/config/lua/objects/LuaLayerSurface.cpp
new file mode 100644
index 00000000..99b22154
--- /dev/null
+++ b/src/config/lua/objects/LuaLayerSurface.cpp
@@ -0,0 +1,88 @@
+#include "LuaLayerSurface.hpp"
+#include "LuaMonitor.hpp"
+#include "LuaObjectHelpers.hpp"
+
+#include "../../../desktop/view/LayerSurface.hpp"
+
+#include <format>
+#include <string_view>
+
+using namespace Config::Lua;
+
+static constexpr const char* MT = "HL.LayerSurface";
+
+//
+static int layerSurfaceEq(lua_State* L) {
+ const auto* lhs = sc<PHLLSREF*>(luaL_checkudata(L, 1, MT));
+ const auto* rhs = sc<PHLLSREF*>(luaL_checkudata(L, 2, MT));
+
+ lua_pushboolean(L, lhs->lock() == rhs->lock());
+ return 1;
+}
+
+static int layerSurfaceToString(lua_State* L) {
+ const auto* ref = sc<PHLLSREF*>(luaL_checkudata(L, 1, MT));
+ const auto ls = ref->lock();
+
+ if (!ls)
+ lua_pushstring(L, "HL.LayerSurface(expired)");
+ else
+ lua_pushfstring(L, "HL.LayerSurface(%p)", ls.get());
+
+ return 1;
+}
+
+static int layerSurfaceIndex(lua_State* L) {
+ auto* ref = sc<PHLLSREF*>(luaL_checkudata(L, 1, MT));
+ const auto ls = ref->lock();
+ if (!ls) {
+ Log::logger->log(Log::DEBUG, "[lua] Tried to access an expired object");
+ lua_pushnil(L);
+ return 1;
+ }
+
+ const std::string_view key = luaL_checkstring(L, 2);
+
+ if (key == "address")
+ lua_pushstring(L, std::format("0x{:x}", reinterpret_cast<uintptr_t>(ls.get())).c_str());
+ else if (key == "x")
+ lua_pushinteger(L, ls->m_geometry.x);
+ else if (key == "y")
+ lua_pushinteger(L, ls->m_geometry.y);
+ else if (key == "w")
+ lua_pushinteger(L, ls->m_geometry.width);
+ else if (key == "h")
+ lua_pushinteger(L, ls->m_geometry.height);
+ else if (key == "namespace")
+ lua_pushstring(L, ls->m_namespace.c_str());
+ else if (key == "pid")
+ lua_pushinteger(L, sc<lua_Integer>(ls->getPID()));
+ else if (key == "monitor") {
+ const auto mon = ls->m_monitor.lock();
+ if (mon)
+ Objects::CLuaMonitor::push(L, mon);
+ else
+ lua_pushnil(L);
+ } else if (key == "mapped")
+ lua_pushboolean(L, ls->m_mapped);
+ else if (key == "layer")
+ lua_pushinteger(L, sc<lua_Integer>(ls->m_layer));
+ else if (key == "interactivity")
+ lua_pushinteger(L, sc<lua_Integer>(ls->m_interactivity));
+ else if (key == "above_fullscreen")
+ lua_pushboolean(L, ls->m_aboveFullscreen);
+ else
+ lua_pushnil(L);
+
+ return 1;
+}
+
+void Objects::CLuaLayerSurface::setup(lua_State* L) {
+ registerMetatable(L, MT, layerSurfaceIndex, gcRef<PHLLSREF>, layerSurfaceEq, layerSurfaceToString);
+}
+
+void Objects::CLuaLayerSurface::push(lua_State* L, PHLLS ls) {
+ new (lua_newuserdata(L, sizeof(PHLLSREF))) PHLLSREF(ls ? ls->m_self : nullptr);
+ luaL_getmetatable(L, MT);
+ lua_setmetatable(L, -2);
+}
diff --git a/src/config/lua/objects/LuaLayerSurface.hpp b/src/config/lua/objects/LuaLayerSurface.hpp
new file mode 100644
index 00000000..bfbd93e7
--- /dev/null
+++ b/src/config/lua/objects/LuaLayerSurface.hpp
@@ -0,0 +1,12 @@
+#pragma once
+
+#include "LuaObjectHelpers.hpp"
+#include "../../../desktop/DesktopTypes.hpp"
+
+namespace Config::Lua::Objects {
+ class CLuaLayerSurface : public ILuaObjectWrapper {
+ public:
+ void setup(lua_State* L) override;
+ static void push(lua_State* L, PHLLS ls);
+ };
+}
diff --git a/src/config/lua/objects/LuaMonitor.cpp b/src/config/lua/objects/LuaMonitor.cpp
new file mode 100644
index 00000000..9d0701c1
--- /dev/null
+++ b/src/config/lua/objects/LuaMonitor.cpp
@@ -0,0 +1,122 @@
+#include "LuaMonitor.hpp"
+#include "LuaWorkspace.hpp"
+#include "LuaObjectHelpers.hpp"
+
+#include "../../../helpers/Monitor.hpp"
+#include "../../../desktop/state/FocusState.hpp"
+
+#include <string_view>
+
+using namespace Config::Lua;
+
+static constexpr const char* MT = "HL.Monitor";
+
+//
+static int monitorEq(lua_State* L) {
+ const auto* lhs = sc<PHLMONITORREF*>(luaL_checkudata(L, 1, MT));
+ const auto* rhs = sc<PHLMONITORREF*>(luaL_checkudata(L, 2, MT));
+
+ lua_pushboolean(L, lhs->lock() == rhs->lock());
+ return 1;
+}
+
+static int monitorToString(lua_State* L) {
+ const auto* ref = sc<PHLMONITORREF*>(luaL_checkudata(L, 1, MT));
+ const auto mon = ref->lock();
+
+ if (!mon)
+ lua_pushstring(L, "HL.Monitor(expired)");
+ else
+ lua_pushfstring(L, "HL.Monitor(%d:%s)", mon->m_id, mon->m_name.c_str());
+
+ return 1;
+}
+
+static int monitorIndex(lua_State* L) {
+ auto* ref = sc<PHLMONITORREF*>(luaL_checkudata(L, 1, MT));
+ const auto mon = ref->lock();
+ if (!mon) {
+ Log::logger->log(Log::DEBUG, "[lua] Tried to access an expired object");
+ lua_pushnil(L);
+ return 1;
+ }
+
+ const std::string_view key = luaL_checkstring(L, 2);
+
+ if (key == "id")
+ lua_pushinteger(L, sc<lua_Integer>(mon->m_id));
+ else if (key == "name")
+ lua_pushstring(L, mon->m_name.c_str());
+ else if (key == "description")
+ lua_pushstring(L, mon->m_shortDescription.c_str());
+ else if (key == "width")
+ lua_pushinteger(L, sc<int>(mon->m_pixelSize.x));
+ else if (key == "height")
+ lua_pushinteger(L, sc<int>(mon->m_pixelSize.y));
+ else if (key == "refresh_rate")
+ lua_pushnumber(L, mon->m_refreshRate);
+ else if (key == "x")
+ lua_pushinteger(L, sc<int>(mon->m_position.x));
+ else if (key == "y")
+ lua_pushinteger(L, sc<int>(mon->m_position.y));
+ else if (key == "active_workspace") {
+ if (mon->m_activeWorkspace)
+ Objects::CLuaWorkspace::push(L, mon->m_activeWorkspace);
+ else
+ lua_pushnil(L);
+ } else if (key == "active_special_workspace") {
+ if (mon->m_activeSpecialWorkspace)
+ Objects::CLuaWorkspace::push(L, mon->m_activeSpecialWorkspace);
+ else
+ lua_pushnil(L);
+ } else if (key == "position") {
+ lua_newtable(L);
+ lua_pushinteger(L, sc<int>(mon->m_position.x));
+ lua_setfield(L, -2, "x");
+ lua_pushinteger(L, sc<int>(mon->m_position.y));
+ lua_setfield(L, -2, "y");
+ } else if (key == "size") {
+ lua_newtable(L);
+ lua_pushinteger(L, sc<int>(mon->m_pixelSize.x));
+ lua_setfield(L, -2, "width");
+ lua_pushinteger(L, sc<int>(mon->m_pixelSize.y));
+ lua_setfield(L, -2, "height");
+ } else if (key == "scale")
+ lua_pushnumber(L, mon->m_scale);
+ else if (key == "transform")
+ lua_pushinteger(L, sc<int>(mon->m_transform));
+ else if (key == "dpms_status")
+ lua_pushboolean(L, mon->m_dpmsStatus);
+ else if (key == "vrr_active")
+ lua_pushboolean(L, mon->m_vrrActive);
+ else if (key == "is_mirror")
+ lua_pushboolean(L, mon->isMirror());
+ else if (key == "mirrors") {
+ lua_newtable(L);
+
+ int i = 1;
+ for (const auto& mirrorRef : mon->m_mirrors) {
+ const auto mirror = mirrorRef.lock();
+ if (!mirror)
+ continue;
+
+ Objects::CLuaMonitor::push(L, mirror);
+ lua_rawseti(L, -2, i++);
+ }
+ } else if (key == "focused")
+ lua_pushboolean(L, mon == Desktop::focusState()->monitor());
+ else
+ lua_pushnil(L);
+
+ return 1;
+}
+
+void Objects::CLuaMonitor::setup(lua_State* L) {
+ registerMetatable(L, MT, monitorIndex, gcRef<PHLMONITORREF>, monitorEq, monitorToString);
+}
+
+void Objects::CLuaMonitor::push(lua_State* L, PHLMONITOR mon) {
+ new (lua_newuserdata(L, sizeof(PHLMONITORREF))) PHLMONITORREF(mon ? mon->m_self : nullptr);
+ luaL_getmetatable(L, MT);
+ lua_setmetatable(L, -2);
+}
diff --git a/src/config/lua/objects/LuaMonitor.hpp b/src/config/lua/objects/LuaMonitor.hpp
new file mode 100644
index 00000000..585d51e8
--- /dev/null
+++ b/src/config/lua/objects/LuaMonitor.hpp
@@ -0,0 +1,12 @@
+#pragma once
+
+#include "LuaObjectHelpers.hpp"
+#include "../../../desktop/DesktopTypes.hpp"
+
+namespace Config::Lua::Objects {
+ class CLuaMonitor : public ILuaObjectWrapper {
+ public:
+ void setup(lua_State* L) override;
+ static void push(lua_State* L, PHLMONITOR mon);
+ };
+}
diff --git a/src/config/lua/objects/LuaNotification.cpp b/src/config/lua/objects/LuaNotification.cpp
new file mode 100644
index 00000000..f116e2eb
--- /dev/null
+++ b/src/config/lua/objects/LuaNotification.cpp
@@ -0,0 +1,377 @@
+#include "LuaNotification.hpp"
+
+#include "../../../helpers/MiscFunctions.hpp"
+
+#include <array>
+#include <algorithm>
+#include <cctype>
+#include <optional>
+#include <string_view>
+
+using namespace Config::Lua;
+
+static constexpr const char* MT = "HL.Notification";
+
+namespace {
+ struct SNotificationRef {
+ WP<Notification::CNotification> notification;
+ bool paused = false;
+ };
+
+ std::optional<CHyprColor> parseColor(lua_State* L, int idx) {
+ if (lua_isnumber(L, idx))
+ return CHyprColor(sc<uint64_t>(lua_tonumber(L, idx)));
+
+ if (lua_isstring(L, idx)) {
+ auto parsed = configStringToInt(lua_tostring(L, idx));
+ if (!parsed)
+ return std::nullopt;
+
+ return CHyprColor(sc<uint64_t>(*parsed));
+ }
+
+ return std::nullopt;
+ }
+
+ std::optional<eIcons> iconFromString(std::string iconName) {
+ std::ranges::transform(iconName, iconName.begin(), [](const unsigned char c) { return std::tolower(c); });
+
+ static constexpr std::array<std::pair<const char*, eIcons>, 10> ICON_NAMES = {
+ std::pair{"warning", ICON_WARNING}, std::pair{"warn", ICON_WARNING}, std::pair{"info", ICON_INFO}, std::pair{"hint", ICON_HINT},
+ std::pair{"error", ICON_ERROR}, std::pair{"err", ICON_ERROR}, std::pair{"confused", ICON_CONFUSED}, std::pair{"question", ICON_CONFUSED},
+ std::pair{"ok", ICON_OK}, std::pair{"none", ICON_NONE},
+ };
+
+ for (const auto& [name, icon] : ICON_NAMES) {
+ if (name == iconName)
+ return icon;
+ }
+
+ return std::nullopt;
+ }
+
+ std::optional<eIcons> parseIcon(lua_State* L, int idx) {
+ if (lua_isnumber(L, idx)) {
+ const auto raw = sc<int>(lua_tonumber(L, idx));
+ if (raw >= ICON_WARNING && raw <= ICON_NONE)
+ return sc<eIcons>(raw);
+
+ return std::nullopt;
+ }
+
+ if (lua_isstring(L, idx))
+ return iconFromString(lua_tostring(L, idx));
+
+ return std::nullopt;
+ }
+}
+
+static int notificationEq(lua_State* L) {
+ const auto* lhs = sc<SNotificationRef*>(luaL_checkudata(L, 1, MT));
+ const auto* rhs = sc<SNotificationRef*>(luaL_checkudata(L, 2, MT));
+
+ lua_pushboolean(L, lhs->notification.lock() == rhs->notification.lock());
+ return 1;
+}
+
+static int notificationToString(lua_State* L) {
+ const auto* ref = sc<SNotificationRef*>(luaL_checkudata(L, 1, MT));
+ const auto notification = ref->notification.lock();
+
+ if (!notification)
+ lua_pushstring(L, "HL.Notification(expired)");
+ else
+ lua_pushfstring(L, "HL.Notification(%p)", notification.get());
+
+ return 1;
+}
+
+static int notificationGC(lua_State* L) {
+ auto* ref = sc<SNotificationRef*>(luaL_checkudata(L, 1, MT));
+
+ if (ref->paused) {
+ if (const auto notification = ref->notification.lock(); notification)
+ notification->unlock();
+ }
+
+ ref->~SNotificationRef();
+ return 0;
+}
+
+static int notificationPause(lua_State* L) {
+ auto* ref = sc<SNotificationRef*>(luaL_checkudata(L, 1, MT));
+
+ const auto notification = ref->notification.lock();
+ if (!notification)
+ return 0;
+
+ if (ref->paused)
+ return 0;
+
+ notification->lock();
+ ref->paused = true;
+ return 0;
+}
+
+static int notificationResume(lua_State* L) {
+ auto* ref = sc<SNotificationRef*>(luaL_checkudata(L, 1, MT));
+
+ const auto notification = ref->notification.lock();
+ if (!notification)
+ return 0;
+
+ if (!ref->paused)
+ return 0;
+
+ notification->unlock();
+ ref->paused = false;
+ return 0;
+}
+
+static int notificationSetPaused(lua_State* L) {
+ luaL_checkudata(L, 1, MT);
+ luaL_checktype(L, 2, LUA_TBOOLEAN);
+
+ if (lua_toboolean(L, 2))
+ return notificationPause(L);
+
+ return notificationResume(L);
+}
+
+static int notificationIsPaused(lua_State* L) {
+ auto* ref = sc<SNotificationRef*>(luaL_checkudata(L, 1, MT));
+
+ const auto notification = ref->notification.lock();
+ if (!notification) {
+ lua_pushnil(L);
+ return 1;
+ }
+
+ lua_pushboolean(L, notification->isLocked());
+ return 1;
+}
+
+static int notificationSetText(lua_State* L) {
+ auto* ref = sc<SNotificationRef*>(luaL_checkudata(L, 1, MT));
+ const auto* text = luaL_checkstring(L, 2);
+
+ if (const auto notification = ref->notification.lock(); notification)
+ notification->setText(text);
+
+ return 0;
+}
+
+static int notificationSetTimeout(lua_State* L) {
+ auto* ref = sc<SNotificationRef*>(luaL_checkudata(L, 1, MT));
+
+ const auto timeoutMs = sc<float>(luaL_checknumber(L, 2));
+ if (timeoutMs < 0.F)
+ return Config::Lua::Bindings::Internal::configError(L, "HL.Notification:set_timeout: timeout must be >= 0");
+
+ if (const auto notification = ref->notification.lock(); notification)
+ notification->resetTimeout(timeoutMs);
+
+ return 0;
+}
+
+static int notificationSetColor(lua_State* L) {
+ auto* ref = sc<SNotificationRef*>(luaL_checkudata(L, 1, MT));
+
+ const auto color = parseColor(L, 2);
+ if (!color)
+ return Config::Lua::Bindings::Internal::configError(L, "HL.Notification:set_color: expected a color string or number");
+
+ if (const auto notification = ref->notification.lock(); notification)
+ notification->setColor(*color);
+
+ return 0;
+}
+
+static int notificationSetIcon(lua_State* L) {
+ auto* ref = sc<SNotificationRef*>(luaL_checkudata(L, 1, MT));
+
+ const auto icon = parseIcon(L, 2);
+ if (!icon)
+ return Config::Lua::Bindings::Internal::configError(L, "HL.Notification:set_icon: expected an icon name or number");
+
+ if (const auto notification = ref->notification.lock(); notification)
+ notification->setIcon(*icon);
+
+ return 0;
+}
+
+static int notificationSetFontSize(lua_State* L) {
+ auto* ref = sc<SNotificationRef*>(luaL_checkudata(L, 1, MT));
+
+ const auto fontSize = sc<float>(luaL_checknumber(L, 2));
+ if (fontSize <= 0.F)
+ return Config::Lua::Bindings::Internal::configError(L, "HL.Notification:set_font_size: font size must be > 0");
+
+ if (const auto notification = ref->notification.lock(); notification)
+ notification->setFontSize(fontSize);
+
+ return 0;
+}
+
+static int notificationDismiss(lua_State* L) {
+ auto* ref = sc<SNotificationRef*>(luaL_checkudata(L, 1, MT));
+
+ if (const auto notification = ref->notification.lock(); notification)
+ Notification::overlay()->dismissNotification(notification);
+
+ return 0;
+}
+
+static int notificationGetText(lua_State* L) {
+ auto* ref = sc<SNotificationRef*>(luaL_checkudata(L, 1, MT));
+
+ const auto notification = ref->notification.lock();
+ if (!notification) {
+ lua_pushnil(L);
+ return 1;
+ }
+
+ lua_pushstring(L, notification->text().c_str());
+ return 1;
+}
+
+static int notificationGetTimeout(lua_State* L) {
+ auto* ref = sc<SNotificationRef*>(luaL_checkudata(L, 1, MT));
+
+ const auto notification = ref->notification.lock();
+ if (!notification) {
+ lua_pushnil(L);
+ return 1;
+ }
+
+ lua_pushnumber(L, notification->timeMs());
+ return 1;
+}
+
+static int notificationGetColor(lua_State* L) {
+ auto* ref = sc<SNotificationRef*>(luaL_checkudata(L, 1, MT));
+
+ const auto notification = ref->notification.lock();
+ if (!notification) {
+ lua_pushnil(L);
+ return 1;
+ }
+
+ lua_pushinteger(L, sc<lua_Integer>(notification->color().getAsHex()));
+ return 1;
+}
+
+static int notificationGetIcon(lua_State* L) {
+ auto* ref = sc<SNotificationRef*>(luaL_checkudata(L, 1, MT));
+
+ const auto notification = ref->notification.lock();
+ if (!notification) {
+ lua_pushnil(L);
+ return 1;
+ }
+
+ lua_pushinteger(L, sc<lua_Integer>(notification->icon()));
+ return 1;
+}
+
+static int notificationGetFontSize(lua_State* L) {
+ auto* ref = sc<SNotificationRef*>(luaL_checkudata(L, 1, MT));
+
+ const auto notification = ref->notification.lock();
+ if (!notification) {
+ lua_pushnil(L);
+ return 1;
+ }
+
+ lua_pushnumber(L, notification->fontSize());
+ return 1;
+}
+
+static int notificationGetElapsed(lua_State* L) {
+ auto* ref = sc<SNotificationRef*>(luaL_checkudata(L, 1, MT));
+
+ const auto notification = ref->notification.lock();
+ if (!notification) {
+ lua_pushnil(L);
+ return 1;
+ }
+
+ lua_pushnumber(L, notification->timeElapsedMs());
+ return 1;
+}
+
+static int notificationGetElapsedSinceCreation(lua_State* L) {
+ auto* ref = sc<SNotificationRef*>(luaL_checkudata(L, 1, MT));
+
+ const auto notification = ref->notification.lock();
+ if (!notification) {
+ lua_pushnil(L);
+ return 1;
+ }
+
+ lua_pushnumber(L, notification->timeElapsedSinceCreationMs());
+ return 1;
+}
+
+static int notificationIsAlive(lua_State* L) {
+ auto* ref = sc<SNotificationRef*>(luaL_checkudata(L, 1, MT));
+
+ lua_pushboolean(L, ref->notification.lock().get() != nullptr);
+ return 1;
+}
+
+static int notificationIndex(lua_State* L) {
+ luaL_checkudata(L, 1, MT);
+ const std::string_view key = luaL_checkstring(L, 2);
+
+ if (key == "pause")
+ lua_pushcfunction(L, notificationPause);
+ else if (key == "resume")
+ lua_pushcfunction(L, notificationResume);
+ else if (key == "set_paused")
+ lua_pushcfunction(L, notificationSetPaused);
+ else if (key == "is_paused")
+ lua_pushcfunction(L, notificationIsPaused);
+ else if (key == "set_text")
+ lua_pushcfunction(L, notificationSetText);
+ else if (key == "set_timeout")
+ lua_pushcfunction(L, notificationSetTimeout);
+ else if (key == "set_color")
+ lua_pushcfunction(L, notificationSetColor);
+ else if (key == "set_icon")
+ lua_pushcfunction(L, notificationSetIcon);
+ else if (key == "set_font_size")
+ lua_pushcfunction(L, notificationSetFontSize);
+ else if (key == "dismiss")
+ lua_pushcfunction(L, notificationDismiss);
+ else if (key == "get_text")
+ lua_pushcfunction(L, notificationGetText);
+ else if (key == "get_timeout")
+ lua_pushcfunction(L, notificationGetTimeout);
+ else if (key == "get_color")
+ lua_pushcfunction(L, notificationGetColor);
+ else if (key == "get_icon")
+ lua_pushcfunction(L, notificationGetIcon);
+ else if (key == "get_font_size")
+ lua_pushcfunction(L, notificationGetFontSize);
+ else if (key == "get_elapsed")
+ lua_pushcfunction(L, notificationGetElapsed);
+ else if (key == "get_elapsed_since_creation")
+ lua_pushcfunction(L, notificationGetElapsedSinceCreation);
+ else if (key == "is_alive")
+ lua_pushcfunction(L, notificationIsAlive);
+ else
+ lua_pushnil(L);
+
+ return 1;
+}
+
+void Objects::CLuaNotification::setup(lua_State* L) {
+ registerMetatable(L, MT, notificationIndex, notificationGC, notificationEq, notificationToString);
+}
+
+void Objects::CLuaNotification::push(lua_State* L, const SP<Notification::CNotification>& notification) {
+ new (lua_newuserdata(L, sizeof(SNotificationRef))) SNotificationRef{.notification = WP<Notification::CNotification>(notification)};
+ luaL_getmetatable(L, MT);
+ lua_setmetatable(L, -2);
+}
diff --git a/src/config/lua/objects/LuaNotification.hpp b/src/config/lua/objects/LuaNotification.hpp
new file mode 100644
index 00000000..2ac8b0eb
--- /dev/null
+++ b/src/config/lua/objects/LuaNotification.hpp
@@ -0,0 +1,12 @@
+#pragma once
+
+#include "LuaObjectHelpers.hpp"
+#include "../../../notification/NotificationOverlay.hpp"
+
+namespace Config::Lua::Objects {
+ class CLuaNotification : public ILuaObjectWrapper {
+ public:
+ void setup(lua_State* L) override;
+ static void push(lua_State* L, const SP<Notification::CNotification>& notification);
+ };
+}
diff --git a/src/config/lua/objects/LuaObjectHelpers.hpp b/src/config/lua/objects/LuaObjectHelpers.hpp
new file mode 100644
index 00000000..69c284ef
--- /dev/null
+++ b/src/config/lua/objects/LuaObjectHelpers.hpp
@@ -0,0 +1,51 @@
+#pragma once
+
+#include "../bindings/LuaBindingsInternal.hpp"
+
+#include "../../../helpers/memory/Memory.hpp"
+
+extern "C" {
+#include <lauxlib.h>
+}
+
+namespace Config::Lua::Objects {
+
+ template <typename T>
+ inline int gcRef(lua_State* L) {
+ sc<T*>(lua_touserdata(L, 1))->~T();
+ return 0;
+ }
+
+ inline int readOnlyNewIndex(lua_State* L) {
+ return Config::Lua::Bindings::Internal::configError(L, "attempt to modify read-only hl object");
+ }
+
+ inline void registerMetatable(lua_State* L, const char* name, lua_CFunction indexFn, lua_CFunction gcFn, lua_CFunction eqFn = nullptr, lua_CFunction toStringFn = nullptr) {
+ luaL_newmetatable(L, name);
+ lua_pushcfunction(L, indexFn);
+ lua_setfield(L, -2, "__index");
+ lua_pushcfunction(L, readOnlyNewIndex);
+ lua_setfield(L, -2, "__newindex");
+ lua_pushcfunction(L, gcFn);
+ lua_setfield(L, -2, "__gc");
+
+ if (eqFn) {
+ lua_pushcfunction(L, eqFn);
+ lua_setfield(L, -2, "__eq");
+ }
+
+ if (toStringFn) {
+ lua_pushcfunction(L, toStringFn);
+ lua_setfield(L, -2, "__tostring");
+ }
+
+ lua_pop(L, 1);
+ }
+
+ class ILuaObjectWrapper {
+ public:
+ virtual ~ILuaObjectWrapper() = default;
+ virtual void setup(lua_State* L) = 0;
+ };
+
+}
diff --git a/src/config/lua/objects/LuaTimer.cpp b/src/config/lua/objects/LuaTimer.cpp
new file mode 100644
index 00000000..46f9a519
--- /dev/null
+++ b/src/config/lua/objects/LuaTimer.cpp
@@ -0,0 +1,109 @@
+#include "LuaTimer.hpp"
+
+#include <algorithm>
+#include <string_view>
+
+using namespace Config::Lua;
+
+static constexpr const char* MT = "HL.Timer";
+
+namespace {
+ struct STimerRef {
+ WP<CEventLoopTimer> timer;
+ int timeoutMs = 0;
+ };
+}
+
+static int timerEq(lua_State* L) {
+ const auto* lhs = sc<STimerRef*>(luaL_checkudata(L, 1, MT));
+ const auto* rhs = sc<STimerRef*>(luaL_checkudata(L, 2, MT));
+
+ lua_pushboolean(L, lhs->timer.lock() == rhs->timer.lock());
+ return 1;
+}
+
+static int timerToString(lua_State* L) {
+ const auto* ref = sc<STimerRef*>(luaL_checkudata(L, 1, MT));
+ const auto timer = ref->timer.lock();
+
+ if (!timer)
+ lua_pushstring(L, "HL.Timer(expired)");
+ else
+ lua_pushfstring(L, "HL.Timer(%p)", timer.get());
+
+ return 1;
+}
+
+static int timerSetEnabled(lua_State* L) {
+ auto* ref = sc<STimerRef*>(luaL_checkudata(L, 1, MT));
+ luaL_checktype(L, 2, LUA_TBOOLEAN);
+
+ const auto timer = ref->timer.lock();
+ if (!timer)
+ return 0;
+
+ if (lua_toboolean(L, 2))
+ timer->updateTimeout(std::chrono::milliseconds(std::max(ref->timeoutMs, 1)));
+ else
+ timer->updateTimeout(std::nullopt);
+
+ return 0;
+}
+
+static int timerSetTimeout(lua_State* L) {
+ auto* ref = sc<STimerRef*>(luaL_checkudata(L, 1, MT));
+ luaL_checktype(L, 2, LUA_TNUMBER);
+
+ const auto timer = ref->timer.lock();
+ if (!timer)
+ return 0;
+
+ uint64_t x = lua_tointeger(L, 2);
+
+ if (x < 1)
+ return Config::Lua::Bindings::Internal::configError(L, "hl.timer:set_timeout: timeout must be greater or equal to 1ms");
+
+ timer->updateTimeout(std::chrono::milliseconds(x));
+ ref->timeoutMs = x;
+
+ return 0;
+}
+
+static int timerIsEnabled(lua_State* L) {
+ auto* ref = sc<STimerRef*>(luaL_checkudata(L, 1, MT));
+
+ const auto timer = ref->timer.lock();
+ if (!timer) {
+ lua_pushnil(L);
+ return 1;
+ }
+
+ lua_pushboolean(L, timer->armed());
+ return 1;
+}
+
+static int timerIndex(lua_State* L) {
+ luaL_checkudata(L, 1, MT);
+ const std::string_view key = luaL_checkstring(L, 2);
+
+ if (key == "set_enabled")
+ lua_pushcfunction(L, timerSetEnabled);
+ else if (key == "is_enabled")
+ lua_pushcfunction(L, timerIsEnabled);
+ else if (key == "set_timeout")
+ lua_pushcfunction(L, timerSetTimeout);
+ else
+ lua_pushnil(L);
+
+ return 1;
+}
+
+void Objects::CLuaTimer::setup(lua_State* L) {
+ registerMetatable(L, MT, timerIndex, gcRef<STimerRef>, timerEq, timerToString);
+}
+
+void Objects::CLuaTimer::push(lua_State* L, const SP<CEventLoopTimer>& timer, int timeoutMs) {
+ new (lua_newuserdata(L, sizeof(STimerRef))) STimerRef{WP<CEventLoopTimer>(timer), timeoutMs};
+ luaL_getmetatable(L, MT);
+ lua_setmetatable(L, -2);
+}
diff --git a/src/config/lua/objects/LuaTimer.hpp b/src/config/lua/objects/LuaTimer.hpp
new file mode 100644
index 00000000..0a602848
--- /dev/null
+++ b/src/config/lua/objects/LuaTimer.hpp
@@ -0,0 +1,12 @@
+#pragma once
+
+#include "LuaObjectHelpers.hpp"
+#include "../../../managers/eventLoop/EventLoopTimer.hpp"
+
+namespace Config::Lua::Objects {
+ class CLuaTimer : public ILuaObjectWrapper {
+ public:
+ void setup(lua_State* L) override;
+ static void push(lua_State* L, const SP<CEventLoopTimer>& timer, int timeoutMs);
+ };
+}
diff --git a/src/config/lua/objects/LuaWindow.cpp b/src/config/lua/objects/LuaWindow.cpp
new file mode 100644
index 00000000..bf332584
--- /dev/null
+++ b/src/config/lua/objects/LuaWindow.cpp
@@ -0,0 +1,277 @@
+#include "LuaWindow.hpp"
+#include "LuaWorkspace.hpp"
+#include "LuaMonitor.hpp"
+#include "LuaObjectHelpers.hpp"
+
+#include "../../../desktop/view/Window.hpp"
+#include "../../../desktop/view/Group.hpp"
+#include "../../../desktop/Workspace.hpp"
+#include "../../../desktop/state/FocusState.hpp"
+#include "../../../desktop/history/WindowHistoryTracker.hpp"
+#include "../../../layout/algorithm/Algorithm.hpp"
+#include "../../../layout/algorithm/tiled/master/MasterAlgorithm.hpp"
+#include "../../../layout/algorithm/tiled/scrolling/ScrollingAlgorithm.hpp"
+#include "../../../layout/algorithm/tiled/dwindle/DwindleAlgorithm.hpp"
+#include "../../../layout/space/Space.hpp"
+#include "../../../layout/supplementary/WorkspaceAlgoMatcher.hpp"
+#include "../../../managers/input/InputManager.hpp"
+
+#include <format>
+#include <string_view>
+
+using namespace Config::Lua;
+
+static constexpr const char* MT = "HL.Window";
+
+//
+static int getFocusHistoryID(PHLWINDOW wnd) {
+ const auto& history = Desktop::History::windowTracker()->fullHistory();
+ for (size_t i = 0; i < history.size(); ++i) {
+ if (history[i].lock() == wnd)
+ return sc<int>(history.size() - i - 1); // reverse order for backwards compat
+ }
+
+ return -1;
+}
+
+static int windowEq(lua_State* L) {
+ const auto* lhs = sc<PHLWINDOWREF*>(luaL_checkudata(L, 1, MT));
+ const auto* rhs = sc<PHLWINDOWREF*>(luaL_checkudata(L, 2, MT));
+
+ lua_pushboolean(L, lhs->lock() == rhs->lock());
+ return 1;
+}
+
+static int windowToString(lua_State* L) {
+ const auto* ref = sc<PHLWINDOWREF*>(luaL_checkudata(L, 1, MT));
+ const auto w = ref->lock();
+
+ if (!w)
+ lua_pushstring(L, "HL.Window(expired)");
+ else
+ lua_pushfstring(L, "HL.Window(%p)", w.get());
+
+ return 1;
+}
+
+static int windowIndex(lua_State* L) {
+ auto* ref = sc<PHLWINDOWREF*>(luaL_checkudata(L, 1, MT));
+ const auto w = ref->lock();
+ if (!w) {
+ Log::logger->log(Log::DEBUG, "[lua] Tried to access an expired object");
+ lua_pushnil(L);
+ return 1;
+ }
+
+ const std::string_view key = luaL_checkstring(L, 2);
+
+ if (key == "address")
+ lua_pushstring(L, std::format("0x{:x}", reinterpret_cast<uintptr_t>(w.get())).c_str());
+ else if (key == "mapped")
+ lua_pushboolean(L, w->m_isMapped);
+ else if (key == "hidden")
+ lua_pushboolean(L, w->isHidden());
+ else if (key == "at") {
+ lua_newtable(L);
+ lua_pushinteger(L, sc<int>(w->m_realPosition->goal().x));
+ lua_setfield(L, -2, "x");
+ lua_pushinteger(L, sc<int>(w->m_realPosition->goal().y));
+ lua_setfield(L, -2, "y");
+ } else if (key == "size") {
+ lua_newtable(L);
+ lua_pushinteger(L, sc<int>(w->m_realSize->goal().x));
+ lua_setfield(L, -2, "x");
+ lua_pushinteger(L, sc<int>(w->m_realSize->goal().y));
+ lua_setfield(L, -2, "y");
+ } else if (key == "workspace") {
+ if (w->m_workspace)
+ Objects::CLuaWorkspace::push(L, w->m_workspace);
+ else
+ lua_pushnil(L);
+ } else if (key == "floating")
+ lua_pushboolean(L, w->m_isFloating);
+ else if (key == "monitor") {
+ const auto mon = w->m_monitor.lock();
+ if (mon)
+ Objects::CLuaMonitor::push(L, mon);
+ else
+ lua_pushnil(L);
+ } else if (key == "class")
+ lua_pushstring(L, w->m_class.c_str());
+ else if (key == "title")
+ lua_pushstring(L, w->m_title.c_str());
+ else if (key == "initial_class")
+ lua_pushstring(L, w->m_initialClass.c_str());
+ else if (key == "initial_title")
+ lua_pushstring(L, w->m_initialTitle.c_str());
+ else if (key == "pid")
+ lua_pushinteger(L, sc<lua_Integer>(w->getPID()));
+ else if (key == "xwayland")
+ lua_pushboolean(L, w->m_isX11);
+ else if (key == "pinned")
+ lua_pushboolean(L, w->m_pinned);
+ else if (key == "fullscreen")
+ lua_pushinteger(L, sc<lua_Integer>(sc<uint8_t>(w->m_fullscreenState.internal)));
+ else if (key == "fullscreen_client")
+ lua_pushinteger(L, sc<lua_Integer>(sc<uint8_t>(w->m_fullscreenState.client)));
+ else if (key == "over_fullscreen")
+ lua_pushboolean(L, w->m_createdOverFullscreen);
+ else if (key == "group") {
+ if (!w->m_group) {
+ lua_pushnil(L);
+ return 1;
+ }
+
+ lua_newtable(L);
+
+ lua_pushboolean(L, w->m_group->locked());
+ lua_setfield(L, -2, "locked");
+
+ lua_pushboolean(L, w->m_group->denied());
+ lua_setfield(L, -2, "denied");
+
+ lua_pushinteger(L, sc<lua_Integer>(w->m_group->size()));
+ lua_setfield(L, -2, "size");
+
+ lua_pushinteger(L, sc<lua_Integer>(w->m_group->getCurrentIdx()) + 1);
+ lua_setfield(L, -2, "current_index");
+
+ const auto current = w->m_group->current();
+ if (current)
+ Objects::CLuaWindow::push(L, current);
+ else
+ lua_pushnil(L);
+ lua_setfield(L, -2, "current");
+
+ lua_newtable(L);
+ int i = 1;
+ for (const auto& grouped : w->m_group->windows()) {
+ const auto groupedWindow = grouped.lock();
+ if (!groupedWindow)
+ continue;
+
+ Objects::CLuaWindow::push(L, groupedWindow);
+ lua_rawseti(L, -2, i++);
+ }
+ lua_setfield(L, -2, "members");
+ } else if (key == "tags") {
+ lua_newtable(L);
+
+ int i = 1;
+ for (const auto& tag : w->m_ruleApplicator->m_tagKeeper.getTags()) {
+ lua_pushstring(L, tag.c_str());
+ lua_rawseti(L, -2, i++);
+ }
+ } else if (key == "swallowing") {
+ const auto swallowed = w->m_swallowed.lock();
+ if (swallowed)
+ Objects::CLuaWindow::push(L, swallowed);
+ else
+ lua_pushnil(L);
+ } else if (key == "focus_history_id")
+ lua_pushinteger(L, sc<lua_Integer>(getFocusHistoryID(w)));
+ else if (key == "inhibiting_idle")
+ lua_pushboolean(L, g_pInputManager && g_pInputManager->isWindowInhibiting(w, false));
+ else if (key == "xdg_tag") {
+ const auto xdgTag = w->xdgTag();
+ if (xdgTag)
+ lua_pushstring(L, xdgTag->c_str());
+ else
+ lua_pushnil(L);
+ } else if (key == "xdg_description") {
+ const auto xdgDescription = w->xdgDescription();
+ if (xdgDescription)
+ lua_pushstring(L, xdgDescription->c_str());
+ else
+ lua_pushnil(L);
+ } else if (key == "content_type")
+ lua_pushstring(L, NContentType::toString(w->getContentType()).c_str());
+ else if (key == "stable_id")
+ lua_pushinteger(L, sc<lua_Integer>(w->m_stableID));
+ else if (key == "layout") {
+ const auto target = w->layoutTarget();
+ if (!target || target->floating() || !w->m_workspace || !w->m_workspace->m_space) {
+ lua_pushnil(L);
+ return 1;
+ }
+
+ const auto& algo = w->m_workspace->m_space->algorithm();
+ if (!algo || !algo->tiledAlgo()) {
+ lua_pushnil(L);
+ return 1;
+ }
+
+ const auto& tiledAlgo = algo->tiledAlgo();
+ const std::string name = Layout::Supplementary::algoMatcher()->getNameForTiledAlgo(&typeid(*tiledAlgo.get()));
+
+ lua_newtable(L);
+ lua_pushstring(L, name.c_str());
+ lua_setfield(L, -2, "name");
+
+ if (const auto* master = dynamic_cast<Layout::Tiled::CMasterAlgorithm*>(tiledAlgo.get())) {
+ const auto node = master->getNodeFromTarget(target);
+ if (node) {
+ lua_pushboolean(L, node->isMaster);
+ lua_setfield(L, -2, "is_master");
+
+ lua_pushnumber(L, node->percMaster);
+ lua_setfield(L, -2, "perc_master");
+
+ lua_pushnumber(L, node->percSize);
+ lua_setfield(L, -2, "perc_size");
+ }
+ } else if (auto* scrolling = dynamic_cast<Layout::Tiled::CScrollingAlgorithm*>(tiledAlgo.get())) {
+ const auto data = scrolling->dataFor(target);
+ if (data) {
+ const auto col = data->column.lock();
+ if (col) {
+ const auto scrollingData = col->scrollingData.lock();
+
+ lua_newtable(L);
+
+ if (scrollingData) {
+ lua_pushinteger(L, sc<lua_Integer>(scrollingData->idx(col)));
+ lua_setfield(L, -2, "index");
+ }
+
+ lua_pushnumber(L, col->getColumnWidth());
+ lua_setfield(L, -2, "width");
+
+ lua_newtable(L);
+ int i = 1;
+ for (const auto& td : col->targetDatas) {
+ const auto t = td->target.lock();
+ if (t) {
+ const auto win = t->window();
+ if (win) {
+ Objects::CLuaWindow::push(L, win);
+ lua_rawseti(L, -2, i++);
+ }
+ }
+ }
+ lua_setfield(L, -2, "windows");
+
+ lua_setfield(L, -2, "column");
+
+ lua_pushinteger(L, sc<lua_Integer>(col->idx(target)));
+ lua_setfield(L, -2, "index_in_column");
+ }
+ }
+ }
+ } else if (key == "active") {
+ lua_pushboolean(L, w == Desktop::focusState()->window());
+ } else
+ lua_pushnil(L);
+
+ return 1;
+}
+
+void Objects::CLuaWindow::setup(lua_State* L) {
+ registerMetatable(L, MT, windowIndex, gcRef<PHLWINDOWREF>, windowEq, windowToString);
+}
+
+void Objects::CLuaWindow::push(lua_State* L, PHLWINDOW w) {
+ new (lua_newuserdata(L, sizeof(PHLWINDOWREF))) PHLWINDOWREF(w ? w->m_self : nullptr);
+ luaL_getmetatable(L, MT);
+ lua_setmetatable(L, -2);
+}
diff --git a/src/config/lua/objects/LuaWindow.hpp b/src/config/lua/objects/LuaWindow.hpp
new file mode 100644
index 00000000..49f86149
--- /dev/null
+++ b/src/config/lua/objects/LuaWindow.hpp
@@ -0,0 +1,12 @@
+#pragma once
+
+#include "LuaObjectHelpers.hpp"
+#include "../../../desktop/DesktopTypes.hpp"
+
+namespace Config::Lua::Objects {
+ class CLuaWindow : public ILuaObjectWrapper {
+ public:
+ void setup(lua_State* L) override;
+ static void push(lua_State* L, PHLWINDOW w);
+ };
+}
diff --git a/src/config/lua/objects/LuaWindowRule.cpp b/src/config/lua/objects/LuaWindowRule.cpp
new file mode 100644
index 00000000..63fd1a2c
--- /dev/null
+++ b/src/config/lua/objects/LuaWindowRule.cpp
@@ -0,0 +1,80 @@
+#include "LuaWindowRule.hpp"
+
+#include "../../../desktop/rule/Engine.hpp"
+
+#include <string_view>
+
+using namespace Config::Lua;
+
+static constexpr const char* MT = "HL.WindowRule";
+
+//
+static int windowRuleEq(lua_State* L) {
+ const auto* lhs = sc<WP<Desktop::Rule::CWindowRule>*>(luaL_checkudata(L, 1, MT));
+ const auto* rhs = sc<WP<Desktop::Rule::CWindowRule>*>(luaL_checkudata(L, 2, MT));
+
+ lua_pushboolean(L, lhs->lock() == rhs->lock());
+ return 1;
+}
+
+static int windowRuleToString(lua_State* L) {
+ const auto* ref = sc<WP<Desktop::Rule::CWindowRule>*>(luaL_checkudata(L, 1, MT));
+ const auto rule = ref->lock();
+
+ if (!rule)
+ lua_pushstring(L, "HL.WindowRule(expired)");
+ else
+ lua_pushfstring(L, "HL.WindowRule(%p)", rule.get());
+
+ return 1;
+}
+
+static int windowRuleSetEnabled(lua_State* L) {
+ auto* ref = sc<WP<Desktop::Rule::CWindowRule>*>(luaL_checkudata(L, 1, MT));
+ luaL_checktype(L, 2, LUA_TBOOLEAN);
+
+ const auto rule = ref->lock();
+ if (!rule)
+ return 0;
+
+ rule->setEnabled(lua_toboolean(L, 2));
+ Desktop::Rule::ruleEngine()->updateAllRules();
+ return 0;
+}
+
+static int windowRuleIsEnabled(lua_State* L) {
+ auto* ref = sc<WP<Desktop::Rule::CWindowRule>*>(luaL_checkudata(L, 1, MT));
+
+ const auto rule = ref->lock();
+ if (!rule) {
+ lua_pushnil(L);
+ return 1;
+ }
+
+ lua_pushboolean(L, rule->isEnabled());
+ return 1;
+}
+
+static int windowRuleIndex(lua_State* L) {
+ luaL_checkudata(L, 1, MT);
+ const std::string_view key = luaL_checkstring(L, 2);
+
+ if (key == "set_enabled")
+ lua_pushcfunction(L, windowRuleSetEnabled);
+ else if (key == "is_enabled")
+ lua_pushcfunction(L, windowRuleIsEnabled);
+ else
+ lua_pushnil(L);
+
+ return 1;
+}
+
+void Objects::CLuaWindowRule::setup(lua_State* L) {
+ registerMetatable(L, MT, windowRuleIndex, gcRef<WP<Desktop::Rule::CWindowRule>>, windowRuleEq, windowRuleToString);
+}
+
+void Objects::CLuaWindowRule::push(lua_State* L, const SP<Desktop::Rule::CWindowRule>& rule) {
+ new (lua_newuserdata(L, sizeof(WP<Desktop::Rule::CWindowRule>))) WP<Desktop::Rule::CWindowRule>(rule);
+ luaL_getmetatable(L, MT);
+ lua_setmetatable(L, -2);
+}
diff --git a/src/config/lua/objects/LuaWindowRule.hpp b/src/config/lua/objects/LuaWindowRule.hpp
new file mode 100644
index 00000000..92d0651d
--- /dev/null
+++ b/src/config/lua/objects/LuaWindowRule.hpp
@@ -0,0 +1,12 @@
+#pragma once
+
+#include "LuaObjectHelpers.hpp"
+#include "../../../desktop/rule/windowRule/WindowRule.hpp"
+
+namespace Config::Lua::Objects {
+ class CLuaWindowRule : public ILuaObjectWrapper {
+ public:
+ void setup(lua_State* L) override;
+ static void push(lua_State* L, const SP<Desktop::Rule::CWindowRule>& rule);
+ };
+}
diff --git a/src/config/lua/objects/LuaWorkspace.cpp b/src/config/lua/objects/LuaWorkspace.cpp
new file mode 100644
index 00000000..84e4df0b
--- /dev/null
+++ b/src/config/lua/objects/LuaWorkspace.cpp
@@ -0,0 +1,87 @@
+#include "LuaWorkspace.hpp"
+#include "LuaMonitor.hpp"
+#include "LuaObjectHelpers.hpp"
+
+#include "../../../desktop/Workspace.hpp"
+#include "../../../helpers/Monitor.hpp"
+
+#include <string_view>
+
+using namespace Config::Lua;
+
+static constexpr const char* MT = "HL.Workspace";
+
+//
+static int workspaceEq(lua_State* L) {
+ const auto* lhs = sc<PHLWORKSPACEREF*>(luaL_checkudata(L, 1, MT));
+ const auto* rhs = sc<PHLWORKSPACEREF*>(luaL_checkudata(L, 2, MT));
+
+ lua_pushboolean(L, lhs->lock() == rhs->lock());
+ return 1;
+}
+
+static int workspaceToString(lua_State* L) {
+ const auto* ref = sc<PHLWORKSPACEREF*>(luaL_checkudata(L, 1, MT));
+ const auto ws = ref->lock();
+
+ if (!ws || ws->inert())
+ lua_pushstring(L, "HL.Workspace(expired)");
+ else
+ lua_pushfstring(L, "HL.Workspace(%d:%s)", ws->m_id, ws->m_name.c_str());
+
+ return 1;
+}
+
+static int workspaceIndex(lua_State* L) {
+ auto* ref = sc<PHLWORKSPACEREF*>(luaL_checkudata(L, 1, MT));
+ const auto ws = ref->lock();
+ if (!ws || ws->inert()) {
+ Log::logger->log(Log::DEBUG, "[lua] Tried to access an expired object");
+ lua_pushnil(L);
+ return 1;
+ }
+
+ const std::string_view key = luaL_checkstring(L, 2);
+
+ if (key == "id")
+ lua_pushinteger(L, sc<lua_Integer>(ws->m_id));
+ else if (key == "name")
+ lua_pushstring(L, ws->m_name.c_str());
+ else if (key == "monitor") {
+ const auto mon = ws->m_monitor.lock();
+ if (mon)
+ Objects::CLuaMonitor::push(L, mon);
+ else
+ lua_pushnil(L);
+ } else if (key == "windows")
+ lua_pushinteger(L, sc<lua_Integer>(ws->getWindows()));
+ else if (key == "visible")
+ lua_pushboolean(L, ws->isVisible());
+ else if (key == "special")
+ lua_pushboolean(L, ws->m_isSpecialWorkspace);
+ else if (key == "active") {
+ const auto mon = ws->m_monitor.lock();
+ lua_pushboolean(L, mon && (mon->m_activeWorkspace == ws || mon->m_activeSpecialWorkspace == ws));
+ } else if (key == "has_urgent")
+ lua_pushboolean(L, ws->hasUrgentWindow());
+ else if (key == "fullscreen_mode")
+ lua_pushinteger(L, sc<lua_Integer>(ws->m_fullscreenMode));
+ else if (key == "has_fullscreen")
+ lua_pushboolean(L, ws->m_hasFullscreenWindow);
+ else if (key == "is_persistent")
+ lua_pushboolean(L, ws->isPersistent());
+ else
+ lua_pushnil(L);
+
+ return 1;
+}
+
+void Objects::CLuaWorkspace::setup(lua_State* L) {
+ registerMetatable(L, MT, workspaceIndex, gcRef<PHLWORKSPACEREF>, workspaceEq, workspaceToString);
+}
+
+void Objects::CLuaWorkspace::push(lua_State* L, PHLWORKSPACE ws) {
+ new (lua_newuserdata(L, sizeof(PHLWORKSPACEREF))) PHLWORKSPACEREF(ws ? ws->m_self : nullptr);
+ luaL_getmetatable(L, MT);
+ lua_setmetatable(L, -2);
+}
diff --git a/src/config/lua/objects/LuaWorkspace.hpp b/src/config/lua/objects/LuaWorkspace.hpp
new file mode 100644
index 00000000..db07feba
--- /dev/null
+++ b/src/config/lua/objects/LuaWorkspace.hpp
@@ -0,0 +1,12 @@
+#pragma once
+
+#include "LuaObjectHelpers.hpp"
+#include "../../../desktop/DesktopTypes.hpp"
+
+namespace Config::Lua::Objects {
+ class CLuaWorkspace : public ILuaObjectWrapper {
+ public:
+ void setup(lua_State* L) override;
+ static void push(lua_State* L, PHLWORKSPACE ws);
+ };
+}