#include #include #include #include #include #include #include #include extern "C" { #include #include } using namespace Config::Lua; namespace { Keybinds::CBind makeBind(std::vector keys = {"Q"}, Keybinds::BindFlags flags = 0, Keybinds::SExtraBindArgs extra = {}) { auto bind = Keybinds::CBind::make(std::move(keys), flags, [] { return Keybinds::SBindResult{}; }, std::move(extra)); if (!bind) throw std::runtime_error(bind.error()); return std::move(*bind); } Keybinds::PBind makeBindPtr(std::vector keys = {"Q"}, Keybinds::BindFlags flags = 0, Keybinds::SExtraBindArgs extra = {}) { return makeShared(makeBind(std::move(keys), flags, std::move(extra))); } class CScopedKeybindManager { public: CScopedKeybindManager() : m_previous(std::move(Keybinds::mgr())) { Keybinds::mgr() = makeUnique(); } ~CScopedKeybindManager() { Keybinds::mgr() = std::move(m_previous); } private: UP m_previous; }; class CLuaState { public: CLuaState() : m_lua(luaL_newstate()) { luaL_openlibs(m_lua); } ~CLuaState() { if (m_lua) lua_close(m_lua); } lua_State* get() const { return m_lua; } private: lua_State* m_lua = nullptr; }; int getGlobalInt(lua_State* L, const char* name) { lua_getglobal(L, name); const int v = sc(lua_tointeger(L, -1)); lua_pop(L, 1); return v; } bool getGlobalBool(lua_State* L, const char* name) { lua_getglobal(L, name); const bool v = lua_toboolean(L, -1) != 0; lua_pop(L, 1); return v; } bool isGlobalNil(lua_State* L, const char* name) { lua_getglobal(L, name); const bool isnil = lua_isnil(L, -1); lua_pop(L, 1); return isnil; } } TEST(ConfigLuaObjects, keybindCanToggleEnabledFromLua) { CLuaState S; const auto L = S.get(); Objects::CLuaKeybind{}.setup(L); auto keybind = makeBindPtr(); Objects::CLuaKeybind::push(L, keybind); lua_setglobal(L, "kb"); ASSERT_EQ(luaL_dostring(L, R"( assert(kb:is_enabled() == true) kb:set_enabled(false) assert(kb:is_enabled() == false) )"), LUA_OK); EXPECT_FALSE(keybind->enabled()); } TEST(ConfigLuaObjects, workspaceRuleCanToggleEnabledFromLua) { CLuaState S; const auto L = S.get(); Objects::CLuaWorkspaceRule{}.setup(L); auto rule = makeShared(); rule->setEnabled(true); Objects::CLuaWorkspaceRule::push(L, rule); lua_setglobal(L, "rule"); ASSERT_EQ(luaL_dostring(L, R"( assert(rule:is_enabled() == true) rule:set_enabled(false) assert(rule:is_enabled() == false) )"), LUA_OK); EXPECT_FALSE(rule->isEnabled()); } TEST(ConfigLuaObjects, keybindExposesMetadataAndRemoveMethods) { CScopedKeybindManager MANAGER; CLuaState S; const auto L = S.get(); Objects::CLuaKeybind{}.setup(L); const auto FLAGS = sc(Keybinds::BIND_FLAG_REPEAT | Keybinds::BIND_FLAG_LOCKED | Keybinds::BIND_FLAG_NON_CONSUMING | Keybinds::BIND_FLAG_DONT_INHIBIT | Keybinds::BIND_FLAG_DEVICE_INCLUSIVE | Keybinds::BIND_FLAG_ALLOW_INPUT_CAPTURE); const auto makeMetadataBind = [&] { return makeBind({"SUPER", "Q"}, FLAGS, { .devices = {"kbd-a", "kbd-b"}, .metadata = { .displayKey = "SUPER + Q", .description = "Close active window", .handler = "HL.Dispatcher(close)", .argument = "42", .submap = "default", }, }); }; const auto keybind = Keybinds::mgr()->addBind(makeMetadataBind()); const auto other = Keybinds::mgr()->addBind(makeMetadataBind()); Objects::CLuaKeybind::push(L, keybind); lua_setglobal(L, "kb"); ASSERT_EQ(luaL_dostring(L, R"-( assert(kb.enabled == true) assert(kb.description == "Close active window") assert(kb.display_key == "SUPER + Q") assert(kb.submap == "default") assert(kb.handler == "HL.Dispatcher(close)") assert(kb.arg == "42") assert(kb.modmask ~= nil) assert(kb.key == "Q") assert(kb.keycode == 0) assert(kb.repeating == true) assert(kb.locked == true) assert(kb.non_consuming == true) assert(kb.dont_inhibit == true) assert(type(kb.devices) == "table") assert(kb.allow_input_capture == true) kb:remove() kb:unbind() )-"), LUA_OK); ASSERT_EQ(Keybinds::mgr()->registry().size(), 1); EXPECT_EQ(Keybinds::mgr()->registry().binds().front(), other); } TEST(ConfigLuaObjects, keybindExposesKeycodeTrigger) { CLuaState S; const auto L = S.get(); Objects::CLuaKeybind{}.setup(L); auto keybind = makeBindPtr({"code:24"}); Objects::CLuaKeybind::push(L, keybind); lua_setglobal(L, "kb"); ASSERT_EQ(luaL_dostring(L, R"( assert(kb.key == "") assert(kb.keycode == 24) )"), LUA_OK); } TEST(ConfigLuaObjects, keybindRemovalDoesNotUnrefCallback) { CScopedKeybindManager MANAGER; CLuaState S; const auto L = S.get(); Objects::CLuaKeybind{}.setup(L); ASSERT_EQ(luaL_dostring(L, "return function() end"), LUA_OK); ASSERT_TRUE(lua_isfunction(L, -1)); const std::string HANDLER = luaL_tolstring(L, -1, nullptr); lua_pop(L, 1); const int REF = luaL_ref(L, LUA_REGISTRYINDEX); const auto keybind = Keybinds::mgr()->addBind(makeBind({"Q"}, 0, { .metadata = { .handler = HANDLER, .argument = std::to_string(REF), }, })); Objects::CLuaKeybind::push(L, keybind); lua_setglobal(L, "kb"); ASSERT_EQ(luaL_dostring(L, "kb:remove()"), LUA_OK); lua_rawgeti(L, LUA_REGISTRYINDEX, REF); EXPECT_TRUE(lua_isfunction(L, -1)); lua_pop(L, 1); luaL_unref(L, LUA_REGISTRYINDEX, REF); } TEST(ConfigLuaObjects, objectsAreReadOnlyFromLua) { CLuaState S; const auto L = S.get(); Objects::CLuaKeybind{}.setup(L); auto keybind = makeBindPtr(); Objects::CLuaKeybind::push(L, keybind); lua_setglobal(L, "kb"); luaL_dostring(L, "kb.foo = 1"); luaL_dostring(L, "x = kb.foo"); EXPECT_TRUE(isGlobalNil(L, "x")); } TEST(ConfigLuaObjects, keybindSupportsEqAndToString) { CLuaState S; const auto L = S.get(); Objects::CLuaKeybind{}.setup(L); auto keybindA = makeBindPtr(); auto keybindB = makeBindPtr(); Objects::CLuaKeybind::push(L, keybindA); lua_setglobal(L, "kb1"); Objects::CLuaKeybind::push(L, keybindA); lua_setglobal(L, "kb2"); Objects::CLuaKeybind::push(L, keybindB); lua_setglobal(L, "kb3"); ASSERT_EQ(luaL_dostring(L, R"( eq12 = (kb1 == kb2) neq13 = (kb1 ~= kb3) local s = tostring(kb1) hasPrefix = type(s) == "string" and string.sub(s, 1, 11) == "HL.Keybind(" )"), LUA_OK); EXPECT_TRUE(getGlobalBool(L, "eq12")); EXPECT_TRUE(getGlobalBool(L, "neq13")); EXPECT_TRUE(getGlobalBool(L, "hasPrefix")); } TEST(ConfigLuaObjects, eventSubscriptionRemoveAndIsActive) { CLuaState S; const auto L = S.get(); Objects::CLuaEventSubscription{}.setup(L); CLuaEventHandler handler(L); ASSERT_EQ(luaL_dostring(L, R"( count = 0 function onReload() count = count + 1 end )"), LUA_OK); lua_getglobal(L, "onReload"); ASSERT_TRUE(lua_isfunction(L, -1)); const int ref = luaL_ref(L, LUA_REGISTRYINDEX); const auto handle = handler.registerEvent("config.reloaded", ref); ASSERT_TRUE(handle.has_value()); Objects::CLuaEventSubscription::push(L, &handler, *handle); lua_setglobal(L, "sub"); Event::bus()->m_events.config.reloaded.emit(); EXPECT_EQ(getGlobalInt(L, "count"), 1); ASSERT_EQ(luaL_dostring(L, R"( assert(sub:is_active() == true) sub:remove() assert(sub:is_active() == false) sub:remove() )"), LUA_OK); Event::bus()->m_events.config.reloaded.emit(); EXPECT_EQ(getGlobalInt(L, "count"), 1); } TEST(ConfigLuaObjects, eventSubscriptionSupportsEqAndToString) { CLuaState S; const auto L = S.get(); Objects::CLuaEventSubscription{}.setup(L); CLuaEventHandler handler(L); ASSERT_EQ(luaL_dostring(L, R"( function cbA() end function cbB() end )"), LUA_OK); lua_getglobal(L, "cbA"); ASSERT_TRUE(lua_isfunction(L, -1)); const int refA = luaL_ref(L, LUA_REGISTRYINDEX); lua_getglobal(L, "cbB"); ASSERT_TRUE(lua_isfunction(L, -1)); const int refB = luaL_ref(L, LUA_REGISTRYINDEX); const auto handleA = handler.registerEvent("config.reloaded", refA); const auto handleB = handler.registerEvent("config.reloaded", refB); ASSERT_TRUE(handleA.has_value()); ASSERT_TRUE(handleB.has_value()); Objects::CLuaEventSubscription::push(L, &handler, *handleA); lua_setglobal(L, "sub1"); Objects::CLuaEventSubscription::push(L, &handler, *handleA); lua_setglobal(L, "sub2"); Objects::CLuaEventSubscription::push(L, &handler, *handleB); lua_setglobal(L, "sub3"); ASSERT_EQ(luaL_dostring(L, R"( eq12 = (sub1 == sub2) neq13 = (sub1 ~= sub3) local s = tostring(sub1) hasPrefix = type(s) == "string" and string.sub(s, 1, 21) == "HL.EventSubscription(" )"), LUA_OK); EXPECT_TRUE(getGlobalBool(L, "eq12")); EXPECT_TRUE(getGlobalBool(L, "neq13")); EXPECT_TRUE(getGlobalBool(L, "hasPrefix")); }