summaryrefslogtreecommitdiff
path: root/src/config/lua/ConfigManager.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/ConfigManager.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/ConfigManager.cpp')
-rw-r--r--src/config/lua/ConfigManager.cpp1116
1 files changed, 1116 insertions, 0 deletions
diff --git a/src/config/lua/ConfigManager.cpp b/src/config/lua/ConfigManager.cpp
new file mode 100644
index 00000000..fad187db
--- /dev/null
+++ b/src/config/lua/ConfigManager.cpp
@@ -0,0 +1,1116 @@
+#include "ConfigManager.hpp"
+#include "LuaBindings.hpp"
+#include "DefaultConfig.hpp"
+#include "Emergency.hpp"
+
+#include <climits>
+#include <algorithm>
+#include <cctype>
+#include <functional>
+#include <fstream>
+#include <hyprutils/string/String.hpp>
+#include <hyprutils/string/Numeric.hpp>
+
+#include "types/LuaConfigUtils.hpp"
+#include "types/LuaConfigBool.hpp"
+
+#include "../values/ConfigValues.hpp"
+
+#include "../supplementary/jeremy/Jeremy.hpp"
+#include "../shared/workspace/WorkspaceRuleManager.hpp"
+#include "../shared/monitor/MonitorRuleManager.hpp"
+#include "../shared/animation/AnimationTree.hpp"
+#include "../shared/inotify/ConfigWatcher.hpp"
+
+#include "../../desktop/rule/Engine.hpp"
+#include "../../helpers/MiscFunctions.hpp"
+
+#include "../../event/EventBus.hpp"
+#include "../../Compositor.hpp"
+#include "../../managers/input/InputManager.hpp"
+#include "../../managers/KeybindManager.hpp"
+#include "../../helpers/Monitor.hpp"
+#include "../../layout/space/Space.hpp"
+#include "../../layout/supplementary/WorkspaceAlgoMatcher.hpp"
+#include "../../render/Renderer.hpp"
+#include "../../errorOverlay/Overlay.hpp"
+#include "../../xwayland/XWayland.hpp"
+#include "../../plugins/PluginSystem.hpp"
+#include "../../managers/EventManager.hpp"
+#include "../../managers/eventLoop/EventLoopManager.hpp"
+#include "../../managers/input/trackpad/TrackpadGestures.hpp"
+#include "../../notification/NotificationOverlay.hpp"
+#include "../../render/decorations/CHyprGroupBarDecoration.hpp"
+
+using namespace Config;
+using namespace Config::Lua;
+using namespace Hyprutils::String;
+
+static uint64_t nextPluginLuaFnID = 0x10000;
+
+//
+static bool isValidLuaIdentifier(const std::string& value) {
+ if (value.empty())
+ return false;
+
+ if (!std::isalpha(value[0]) && value[0] != '_')
+ return false;
+
+ return std::ranges::all_of(value, [](const char& c) { return std::isalnum(c) || c == '_'; });
+}
+
+static int pluginLuaFunctionDispatcher(lua_State* L) {
+ auto* mgr = CConfigManager::fromLuaState(L);
+ if (!mgr)
+ return luaL_error(L, "hl.plugin: internal error: config manager unavailable");
+
+ if (!lua_isinteger(L, lua_upvalueindex(1)))
+ return luaL_error(L, "hl.plugin: internal error: invalid callback id");
+
+ const auto id = sc<uint64_t>(lua_tointeger(L, lua_upvalueindex(1)));
+ return mgr->invokePluginLuaFunctionByID(id, L);
+}
+
+static void trackRequiredLuaModulePath(lua_State* L, CConfigManager* mgr, const std::string& moduleName) {
+ if (!L || !mgr || moduleName.empty())
+ return;
+
+ const int stackTop = lua_gettop(L);
+
+ lua_getglobal(L, "package");
+ if (!lua_istable(L, -1)) {
+ lua_settop(L, stackTop);
+ return;
+ }
+
+ lua_getfield(L, -1, "searchpath");
+ if (!lua_isfunction(L, -1)) {
+ lua_settop(L, stackTop);
+ return;
+ }
+
+ lua_pushstring(L, moduleName.c_str());
+ lua_getfield(L, -3, "path");
+ if (!lua_isstring(L, -1)) {
+ lua_settop(L, stackTop);
+ return;
+ }
+
+ if (lua_pcall(L, 2, 2, 0) == LUA_OK && lua_isstring(L, -2)) {
+ const auto* resolvedPath = lua_tostring(L, -2);
+ if (resolvedPath && std::ranges::find(mgr->m_configPaths, resolvedPath) == mgr->m_configPaths.end())
+ mgr->m_configPaths.emplace_back(resolvedPath);
+ }
+
+ lua_settop(L, stackTop);
+}
+
+static int safeLuaRequire(lua_State* L) {
+ const int nargs = lua_gettop(L);
+
+ std::string moduleName;
+ if (lua_isstring(L, 1))
+ moduleName = lua_tostring(L, 1);
+
+ lua_pushvalue(L, lua_upvalueindex(1));
+ lua_insert(L, 1);
+
+ const int status = lua_pcall(L, nargs, LUA_MULTRET, 0);
+ if (status == LUA_OK)
+ return lua_gettop(L);
+
+ std::string err;
+ {
+ size_t len = 0;
+ const char* str = luaL_tolstring(L, -1, &len);
+ if (str)
+ err.assign(str, len);
+ lua_pop(L, 1);
+ }
+
+ if (auto* mgr = CConfigManager::fromLuaState(L); mgr) {
+ if (!moduleName.empty()) {
+ trackRequiredLuaModulePath(L, mgr, moduleName);
+ mgr->addError(std::format("require(\"{}\"): {}", moduleName, err));
+ } else
+ mgr->addError(std::format("require: {}", err));
+ }
+
+ lua_pop(L, 1); // error object
+
+ lua_newtable(L); // fallback module
+ const int fallbackIdx = lua_gettop(L);
+
+ if (!moduleName.empty()) {
+ lua_getglobal(L, "package");
+ if (lua_istable(L, -1)) {
+ lua_getfield(L, -1, "loaded");
+ if (lua_istable(L, -1)) {
+ lua_pushstring(L, moduleName.c_str());
+ lua_pushvalue(L, fallbackIdx);
+ lua_settable(L, -3);
+ }
+ lua_pop(L, 1); // loaded
+ }
+ lua_pop(L, 1); // package
+ }
+
+ return 1;
+}
+
+WP<CConfigManager> Lua::mgr() {
+ auto& mgr = Config::mgr();
+ if (mgr->type() != CONFIG_LUA)
+ return nullptr;
+
+ return dynamicPointerCast<Lua::CConfigManager>(WP<IConfigManager>(mgr));
+}
+
+CConfigManager::SDeviceConfig::SDeviceConfig() {
+ auto mgr = Lua::mgr();
+
+ for (const auto& valRaw : Values::CONFIG_DEVICE_VALUE_NAMES) {
+ auto generic = std::ranges::find_if(Values::CONFIG_VALUES, [&valRaw](const auto& g) { return g->name() == valRaw; });
+
+ RASSERT(generic != Values::CONFIG_VALUES.end(), "Lua: device value has no generic counterpart, what the fuck happened?");
+
+ auto valLua = mgr->luaConfigValueName(valRaw);
+
+ auto valDevice = valLua.contains('.') ? valLua.substr(valLua.find_last_of('.') + 1) : valLua;
+
+ if (values.contains(valDevice))
+ continue; // some are duped
+
+ values.emplace(valDevice, fromGenericValue(*generic));
+ }
+
+ // emplace the special keybinds one
+ values.emplace("keybinds", makeUnique<CLuaConfigBool>(true));
+}
+
+CConfigManager::CConfigManager() : m_mainConfigPath(Supplementary::Jeremy::getMainConfigPath()->path) {
+ ;
+}
+
+CConfigManager* CConfigManager::fromLuaState(lua_State* L) {
+ if (!L)
+ return nullptr;
+
+ lua_getfield(L, LUA_REGISTRYINDEX, "hl_lua_manager");
+ auto* mgr = sc<CConfigManager*>(lua_touserdata(L, -1));
+ lua_pop(L, 1);
+ return mgr;
+}
+
+void CConfigManager::watchdogHook(lua_State* L, lua_Debug* /*ar*/) {
+ auto* mgr = fromLuaState(L);
+ if (!mgr || !mgr->m_watchdogActive)
+ return;
+
+ if (std::chrono::steady_clock::now() <= mgr->m_watchdogDeadline)
+ return;
+
+ const auto& context = mgr->m_watchdogContext;
+ if (context.empty())
+ luaL_error(L, "[Lua] execution timed out");
+
+ luaL_error(L, "[Lua] execution timed out in %s", context.c_str());
+}
+
+int CConfigManager::guardedPCall(int nargs, int nresults, int errfunc, int timeoutMs, std::string_view context) {
+ if (!m_lua)
+ return LUA_ERRERR;
+
+ const auto prevHook = lua_gethook(m_lua);
+ const int prevMask = lua_gethookmask(m_lua);
+ const int prevCount = lua_gethookcount(m_lua);
+
+ const bool prevWatchdogActive = m_watchdogActive;
+ const std::chrono::steady_clock::time_point prevWatchdogDeadline = m_watchdogDeadline;
+ const std::string prevWatchdogContext = m_watchdogContext;
+
+ m_watchdogActive = timeoutMs > 0;
+ m_watchdogDeadline = std::chrono::steady_clock::now() + std::chrono::milliseconds(std::max(timeoutMs, 1));
+ m_watchdogContext = context;
+
+ lua_sethook(m_lua, &CConfigManager::watchdogHook, LUA_MASKCOUNT, LUA_WATCHDOG_INSTRUCTION_INTERVAL);
+ const int result = lua_pcall(m_lua, nargs, nresults, errfunc);
+
+ lua_sethook(m_lua, prevHook, prevMask, prevCount);
+ m_watchdogActive = prevWatchdogActive;
+ m_watchdogDeadline = prevWatchdogDeadline;
+ m_watchdogContext = prevWatchdogContext;
+
+ return result;
+}
+
+eConfigManagerType CConfigManager::type() {
+ return CONFIG_LUA;
+}
+
+void CConfigManager::registerValue(const char* name, ILuaConfigValue* val) {
+ m_configValues.emplace(name, UP<ILuaConfigValue>(val));
+}
+
+void CConfigManager::cleanTimers() {
+ for (auto& t : m_luaTimers) {
+ t.timer->cancel();
+ g_pEventLoopManager->removeTimer(t.timer);
+
+ if (m_lua && t.luaRef != LUA_NOREF) {
+ luaL_unref(m_lua, LUA_REGISTRYINDEX, t.luaRef);
+ t.luaRef = LUA_NOREF;
+ }
+ }
+ m_luaTimers.clear();
+}
+
+void CConfigManager::reinitLuaState() {
+ // Destroy the event handler first so its luaL_unref calls happen while m_lua is still valid.
+ m_eventHandler.reset();
+
+ cleanTimers();
+
+ if (m_lua) {
+ lua_close(m_lua);
+ m_lua = nullptr;
+ }
+
+ m_lua = luaL_newstate();
+ luaL_openlibs(m_lua);
+
+ lua_getglobal(m_lua, "debug");
+ if (lua_istable(m_lua, -1)) {
+ lua_pushnil(m_lua);
+ lua_setfield(m_lua, -2, "sethook");
+ lua_pushnil(m_lua);
+ lua_setfield(m_lua, -2, "gethook");
+ }
+ lua_pop(m_lua, 1);
+
+ lua_pushlightuserdata(m_lua, this);
+ lua_setfield(m_lua, LUA_REGISTRYINDEX, "hl_lua_manager");
+
+ std::filesystem::path configDir = std::filesystem::path(m_mainConfigPath).parent_path();
+ const std::string luaPath = (configDir / "?.lua").string() + ";" + (configDir / "?/init.lua").string();
+ lua_getglobal(m_lua, "package");
+ lua_pushstring(m_lua, luaPath.c_str());
+ lua_setfield(m_lua, -2, "path");
+ lua_pop(m_lua, 1);
+
+ lua_getglobal(m_lua, "require");
+ if (lua_isfunction(m_lua, -1)) {
+ lua_pushcclosure(m_lua, safeLuaRequire, 1);
+ lua_setglobal(m_lua, "require");
+ } else
+ lua_pop(m_lua, 1);
+
+ Bindings::registerBindings(m_lua, this);
+
+ m_eventHandler = makeUnique<CLuaEventHandler>(m_lua);
+
+ // Hook package.searchers[2] (the Lua file searcher) to track require()'d paths.
+ lua_getglobal(m_lua, "package");
+ lua_getfield(m_lua, -1, "searchers");
+ lua_rawgeti(m_lua, -1, 2); // original file searcher
+ lua_pushlightuserdata(m_lua, this);
+ lua_pushcclosure(
+ m_lua,
+ [](lua_State* L) -> int {
+ // upvalue 1: original searcher, upvalue 2: CConfigManager*
+ lua_pushvalue(L, lua_upvalueindex(1));
+ lua_pushvalue(L, 1); // module name
+ lua_call(L, 1, 2); // -> loader?, filename?
+ if (lua_isfunction(L, -2) && lua_isstring(L, -1)) {
+ auto* self = sc<CConfigManager*>(lua_touserdata(L, lua_upvalueindex(2)));
+ self->m_configPaths.emplace_back(lua_tostring(L, -1));
+ }
+ return 2;
+ },
+ 2);
+ lua_rawseti(m_lua, -2, 2); // replace package.searchers[2]
+ lua_pop(m_lua, 2); // pop searchers, package
+}
+
+void CConfigManager::init() {
+ reinitLuaState();
+
+ for (const auto& v : Values::CONFIG_VALUES) {
+ m_configValues.emplace(luaConfigValueName(v->name()), fromGenericValue(v));
+ }
+
+ m_configValues["autogenerated"] = fromGenericValue(makeShared<Values::CIntValue>("autogenerated", "whether the config is autogenerated or not", 0));
+
+ Config::watcher()->setOnChange([this](const CConfigWatcher::SConfigWatchEvent& e) {
+ Log::logger->log(Log::DEBUG, "[lua] file {} modified, reloading", e.file);
+ reload();
+ });
+
+ reload();
+}
+
+void CConfigManager::reload() {
+ Event::bus()->m_events.config.preReload.emit();
+
+ Hyprutils::Utils::CScopeGuard x([this] {
+ m_isParsingConfig = false;
+ m_isFirstLaunch = false;
+ });
+ m_isParsingConfig = true;
+
+ m_mainConfigPath = Supplementary::Jeremy::getMainConfigPath()->path;
+
+ // reset tracked paths; the searcher hook will re-populate them as require() runs
+ m_configPaths.clear();
+ m_configPaths.emplace_back(m_mainConfigPath);
+
+ // phase 1: check syntax before clearing any state, so a broken syntax
+ // doesn't entirely fucking nuke the config and leave the user
+ // with no binds.
+ //
+ // this won't help if they are launching hyprland,
+ // which will be handled with emergency pcall
+ auto phase1Load = [this]() -> bool {
+ // clear package.loaded for user modules so require() re-executes them and
+ // the searcher hook can re-track their paths.
+ lua_getglobal(m_lua, "package");
+ lua_getfield(m_lua, -1, "loaded");
+ static constexpr std::string_view STDLIB[] = {"_G", "coroutine", "debug", "io", "math", "os", "package", "string", "table", "utf8", "bit32", "jit"};
+ lua_pushnil(m_lua);
+ while (lua_next(m_lua, -2)) {
+ lua_pop(m_lua, 1); // pop value, keep key
+ if (lua_isstring(m_lua, -1)) {
+ std::string_view mod = lua_tostring(m_lua, -1);
+ bool skip = false;
+ for (const auto& s : STDLIB) {
+ if (mod == s) {
+ skip = true;
+ break;
+ }
+ }
+ if (!skip) {
+ lua_pushvalue(m_lua, -1); // dup key
+ lua_pushnil(m_lua);
+ lua_settable(m_lua, -4); // package.loaded[key] = nil
+ }
+ }
+ }
+ lua_pop(m_lua, 2); // pop loaded, package
+
+ if (luaL_loadfile(m_lua, m_mainConfigPath.c_str()) != LUA_OK) {
+ m_errors.clear();
+ addError(lua_tostring(m_lua, -1));
+ lua_pop(m_lua, 1);
+ return false;
+ }
+
+ return true;
+ };
+
+ if (!phase1Load()) {
+ m_lastConfigVerificationWasSuccessful = false;
+ postConfigReload();
+ return;
+ }
+
+ // phase 2: syntax is valid, reset and load.
+ Config::animationTree()->reset();
+ Config::workspaceRuleMgr()->clear();
+ Config::monitorRuleMgr()->clear();
+ Desktop::Rule::ruleEngine()->clearAllRules();
+ g_pTrackpadGestures->clearGestures();
+ cleanTimers();
+ m_luaWindowRules.clear();
+ m_luaLayerRules.clear();
+ m_errors.clear();
+ m_deviceConfigs.clear();
+ m_registeredPlugins.clear();
+ m_eventHandler->clearEvents();
+ clearHeldLuaRefs();
+
+ if (g_pKeybindManager) {
+ for (const auto& kb : g_pKeybindManager->m_keybinds) {
+ if (kb->handler == "__lua")
+ luaL_unref(m_lua, LUA_REGISTRYINDEX, std::stoi(kb->arg));
+ }
+ g_pKeybindManager->clearKeybinds();
+ }
+
+ // refresh lua state so we get rid of any stale state
+ reinitLuaState();
+
+ // restart phase 1 load, because our state is gone now.
+ if (!phase1Load()) {
+ m_lastConfigVerificationWasSuccessful = false;
+ postConfigReload();
+ return;
+ }
+
+ // re-register plugin functions
+ reregisterLuaPluginFns();
+
+ for (const auto& v : m_configValues) {
+ v.second->reset();
+ }
+
+ lua_pushcfunction(m_lua, [](lua_State* L) -> int {
+ luaL_traceback(L, L, lua_tostring(L, 1), 1);
+ return 1;
+ });
+ lua_insert(m_lua, 1);
+
+ if (guardedPCall(0, 0, 1, LUA_TIMEOUT_CONFIG_RELOAD_MS, "config reload") != LUA_OK) {
+ addError(lua_tostring(m_lua, -1));
+ lua_pop(m_lua, 1);
+ m_lastConfigVerificationWasSuccessful = false;
+ } else
+ m_lastConfigVerificationWasSuccessful = m_errors.empty();
+
+ lua_remove(m_lua, 1);
+
+ // collect stale userdata after reload so HL.Notification __gc can
+ // promptly release pause leases from dropped references
+ lua_gc(m_lua, LUA_GCCOLLECT, 0);
+
+ postConfigReload();
+}
+
+void CConfigManager::postConfigReload() {
+
+ static auto PZOOMFACTOR = CConfigValue<Config::FLOAT>("cursor.zoom_factor");
+ static auto PSUPPRESSERRORS = CConfigValue<Config::INTEGER>("debug.suppress_errors");
+ static auto PXWAYLAND = CConfigValue<Config::INTEGER>("xwayland.enabled");
+ static auto PMANUALCRASH = CConfigValue<Config::INTEGER>("debug.manual_crash");
+ static auto PENABLESTDOUT = CConfigValue<Config::INTEGER>("debug.enable_stdout_logs");
+ static auto PAUTOGENERATED = CConfigValue<Config::INTEGER>("autogenerated");
+
+ Config::watcher()->update();
+
+ for (auto const& w : g_pCompositor->m_windows) {
+ w->uncacheWindowDecos();
+ }
+
+ for (auto const& m : g_pCompositor->m_monitors) {
+ *(m->m_cursorZoom) = *PZOOMFACTOR;
+ if (m->m_activeWorkspace)
+ m->m_activeWorkspace->m_space->recalculate();
+ }
+
+ // Update the keyboard layout to the cfg'd one if this is not the first launch
+ if (!m_isFirstLaunch) {
+ g_pInputManager->setKeyboardLayout();
+ g_pInputManager->setPointerConfigs();
+ g_pInputManager->setTouchDeviceConfigs();
+ g_pInputManager->setTabletConfigs();
+
+ g_pHyprRenderer->m_reloadScreenShader = true;
+ }
+
+ const bool emergencyModeTripped = !m_errors.empty() && g_pKeybindManager->m_keybinds.empty();
+
+ if (emergencyModeTripped)
+ luaL_dostring(m_lua, EMERGENCY_PCALL);
+
+ // parseError will be displayed next frame
+ if (!m_errors.empty() && !*PSUPPRESSERRORS) {
+ std::string errorStr;
+
+ if (emergencyModeTripped) {
+ errorStr = "⚠ Emergency mode tripped: A lua config error resulted in no binds being registered. Emergency binds active: SUPER + Q → any known terminal, SUPER + R "
+ "→ hyprland-run, SUPER + M → Exit\n";
+ }
+
+ errorStr += "Your config has errors:\n";
+
+ for (const auto& e : m_errors) {
+ errorStr += e + "\n";
+
+ if (std::ranges::count(errorStr, '\n') > 15) {
+ errorStr += "... more";
+ break;
+ }
+ }
+
+ if (!errorStr.empty() && errorStr.back() == '\n')
+ errorStr.pop_back();
+
+ ErrorOverlay::overlay()->queueCreate(errorStr, ErrorOverlay::Colors::ERROR);
+ } else if (*PAUTOGENERATED)
+ ErrorOverlay::overlay()->queueCreate(
+ "Warning: You're using an autogenerated config! Edit the config file to get rid of this message. (config file: " + getMainConfigPath() +
+ " )\nSUPER+Q -> kitty (if it doesn't launch, make sure it's installed or choose a different terminal in the config)\nSUPER+M -> exit Hyprland",
+ ErrorOverlay::Colors::WARNING);
+ else
+ ErrorOverlay::overlay()->destroy();
+
+ // Set the modes for all monitors as we configured them
+ // not on first launch because monitors might not exist yet
+ // and they'll be taken care of in the newMonitor event
+ if (!m_isFirstLaunch) {
+ // check
+ Config::monitorRuleMgr()->scheduleReload();
+ Config::monitorRuleMgr()->ensureMonitorStatus();
+ Config::monitorRuleMgr()->ensureVRR();
+ }
+
+#ifndef NO_XWAYLAND
+ g_pCompositor->m_wantsXwayland = *PXWAYLAND;
+ // enable/disable xwayland usage
+ if (!m_isFirstLaunch &&
+ g_pXWayland /* XWayland has to be initialized by CCompositor::initManagers for this to make sense, and it doesn't have to be (e.g. very early plugin load) */) {
+ bool prevEnabledXwayland = g_pXWayland->enabled();
+ if (g_pCompositor->m_wantsXwayland != prevEnabledXwayland)
+ g_pXWayland = makeUnique<CXWayland>(g_pCompositor->m_wantsXwayland);
+ } else
+ g_pCompositor->m_wantsXwayland = *PXWAYLAND;
+#endif
+
+ if (!m_isFirstLaunch && !g_pCompositor->m_unsafeState)
+ refreshGroupBarGradients();
+
+ for (auto const& w : g_pCompositor->getWorkspaces()) {
+ if (w->inert())
+ continue;
+ w->updateWindows();
+ w->updateWindowData();
+ }
+
+ g_pCompositor->updateAllWindowsAnimatedDecorationValues();
+
+ if (*PMANUALCRASH && !m_manualCrashInitiated) {
+ m_manualCrashInitiated = true;
+ Notification::overlay()->addNotification("Manual crash has been set up. Set debug.manual_crash back to 0 in order to crash the compositor.", CHyprColor(0), 5000,
+ ICON_INFO);
+ } else if (m_manualCrashInitiated && !*PMANUALCRASH) {
+ // cowabunga it is
+ g_pHyprRenderer->initiateManualCrash();
+ }
+
+ auto disableStdout = !*PENABLESTDOUT;
+ if (disableStdout && m_isFirstLaunch)
+ Log::logger->log(Log::DEBUG, "Disabling stdout logs! Check the log for further logs.");
+
+ for (auto const& m : g_pCompositor->m_monitors) {
+ // mark blur dirty
+ m->m_blurFBDirty = true;
+
+ g_pCompositor->scheduleFrameForMonitor(m);
+
+ // Force the compositor to fully re-render all monitors
+ m->m_forceFullFrames = 2;
+
+ // also force mirrors, as the aspect ratio could've changed
+ for (auto const& mirror : m->m_mirrors)
+ mirror->m_forceFullFrames = 3;
+ }
+
+ handlePluginLoads();
+
+ if (!m_isFirstLaunch)
+ g_pCompositor->ensurePersistentWorkspacesPresent();
+
+ Layout::Supplementary::algoMatcher()->updateWorkspaceLayouts();
+
+ Event::bus()->m_events.config.reloaded.emit();
+ if (g_pEventManager)
+ g_pEventManager->postEvent(SHyprIPCEvent{"configreloaded", ""});
+}
+
+void CConfigManager::addError(std::string&& str) {
+ if (m_isParsingConfig) {
+ m_errors.emplace_back(std::move(str));
+ return;
+ }
+
+ if (m_isEvaluating) {
+ m_errors.emplace_back(std::move(str));
+ return;
+ }
+
+ // pop a notification
+ Notification::overlay()->addNotification(std::format("Runtime error in lua:\n{}", std::move(str)), 0, 5000, ICON_WARNING);
+}
+
+std::optional<std::string> CConfigManager::eval(const std::string& code) {
+ if (!m_lua)
+ return "lua state not initialized";
+
+ m_errors.clear();
+ m_isEvaluating = true;
+
+ Hyprutils::Utils::CScopeGuard x([this] { m_isEvaluating = false; });
+
+ if (luaL_loadstring(m_lua, code.c_str()) != LUA_OK) {
+ std::string err = lua_tostring(m_lua, -1);
+ lua_pop(m_lua, 1);
+ return err;
+ }
+
+ if (guardedPCall(0, 0, 0, LUA_TIMEOUT_EVAL_MS, "hyprctl eval") != LUA_OK) {
+ std::string err = lua_tostring(m_lua, -1);
+ lua_pop(m_lua, 1);
+ return err;
+ }
+
+ if (!m_errors.empty()) {
+ std::string out;
+ out.reserve(256);
+
+ for (size_t i = 0; i < m_errors.size(); ++i) {
+ out += m_errors.at(i);
+ out += "\n";
+ }
+
+ out.pop_back();
+
+ return out;
+ }
+
+ m_errors.clear();
+
+ return std::nullopt;
+}
+
+std::string CConfigManager::verify() {
+ if (m_errors.empty())
+ return "config ok";
+
+ std::string fullStr = "";
+ for (const auto& e : m_errors) {
+ fullStr += e;
+ fullStr += "\n";
+ }
+
+ fullStr.pop_back();
+ return fullStr;
+}
+
+static std::string normalizeDeviceName(const std::string& dev) {
+ auto copy = dev;
+ std::ranges::replace(copy, ' ', '-');
+ return copy;
+}
+
+ILuaConfigValue* CConfigManager::findDeviceValue(const std::string& dev, const std::string& field) {
+ const auto devIt = m_deviceConfigs.find(dev);
+ if (devIt == m_deviceConfigs.end())
+ return nullptr;
+ const auto valIt = devIt->second.values.find(field);
+ return valIt != devIt->second.values.end() ? valIt->second.get() : nullptr;
+}
+
+int CConfigManager::getDeviceInt(const std::string& dev, const std::string& field, const std::string& fb) {
+ std::string fallback = luaConfigValueName(fb);
+ if (auto* v = findDeviceValue(normalizeDeviceName(dev), luaConfigValueName(field)); v && v->setByUser())
+ return (int)*sc<const Config::INTEGER*>(v->data());
+ if (!fallback.empty() && m_configValues.contains(fallback))
+ return (int)*sc<const Config::INTEGER*>(m_configValues.at(fallback)->data());
+ return 0;
+}
+
+float CConfigManager::getDeviceFloat(const std::string& dev, const std::string& field, const std::string& fb) {
+ std::string fallback = luaConfigValueName(fb);
+ if (auto* v = findDeviceValue(normalizeDeviceName(dev), luaConfigValueName(field)); v && v->setByUser())
+ return *sc<const Config::FLOAT*>(v->data());
+ if (!fallback.empty() && m_configValues.contains(fallback))
+ return *sc<const Config::FLOAT*>(m_configValues.at(fallback)->data());
+ return 0.F;
+}
+
+Vector2D CConfigManager::getDeviceVec(const std::string& dev, const std::string& field, const std::string& fb) {
+ std::string fallback = luaConfigValueName(fb);
+ auto toVec = [](const Config::VEC2& v) -> Vector2D { return {v.x, v.y}; };
+ if (auto* v = findDeviceValue(normalizeDeviceName(dev), luaConfigValueName(field)); v && v->setByUser())
+ return toVec(*sc<const Config::VEC2*>(v->data()));
+ if (!fallback.empty() && m_configValues.contains(fallback))
+ return toVec(*sc<const Config::VEC2*>(m_configValues.at(fallback)->data()));
+ return {0, 0};
+}
+
+std::string CConfigManager::getDeviceString(const std::string& dev, const std::string& field, const std::string& fb) {
+ std::string fallback = luaConfigValueName(fb);
+ auto clean = [](const Config::STRING& s) -> std::string { return s == STRVAL_EMPTY ? "" : s; };
+ if (auto* v = findDeviceValue(normalizeDeviceName(dev), luaConfigValueName(field)); v && v->setByUser())
+ return clean(*sc<const Config::STRING*>(v->data()));
+ if (!fallback.empty() && m_configValues.contains(fallback))
+ return clean(*sc<const Config::STRING*>(m_configValues.at(fallback)->data()));
+ return "";
+}
+
+bool CConfigManager::deviceConfigExplicitlySet(const std::string& dev, const std::string& field) {
+ auto v = findDeviceValue(normalizeDeviceName(dev), luaConfigValueName(field));
+ return v && v->setByUser();
+}
+
+bool CConfigManager::deviceConfigExists(const std::string& dev) {
+ return m_deviceConfigs.contains(normalizeDeviceName(dev));
+}
+
+SConfigOptionReply CConfigManager::getConfigValue(const std::string& s) {
+
+ if (m_configValues.contains(s)) {
+
+ auto& cv = m_configValues[s];
+
+ m_configPtrMap[s] = cv->data();
+
+ return SConfigOptionReply{.dataptr = cc<void* const*>(&m_configPtrMap.at(s)), .type = cv->underlying(), .setByUser = cv->setByUser()};
+ }
+
+ // try replacing all . with : unless col.
+ std::string s2 = s;
+
+ for (size_t i = 0; i < s2.length(); ++i) {
+ if (s2[i] != ':')
+ continue;
+
+ if (i <= 3) {
+ s2[i] = '.';
+ continue;
+ }
+
+ if (s2[i - 1] == 'l' && s2[i - 2] == 'o' && s2[i - 3] == 'c')
+ continue;
+
+ s2[i] = '.';
+ }
+
+ if (!m_configValues.contains(s2))
+ return SConfigOptionReply{.dataptr = nullptr};
+
+ auto& cv = m_configValues[s2];
+
+ m_configPtrMap[s2] = cv->data();
+
+ return SConfigOptionReply{.dataptr = cc<void* const*>(&m_configPtrMap.at(s2)), .type = cv->underlying(), .setByUser = cv->setByUser()};
+}
+
+std::string CConfigManager::getMainConfigPath() {
+ return m_mainConfigPath;
+}
+
+std::string CConfigManager::getErrors() {
+ std::string errStr;
+ for (const auto& e : m_errors) {
+ errStr += e + "\n";
+ }
+
+ if (!errStr.empty())
+ errStr.pop_back();
+
+ return errStr;
+}
+
+std::string CConfigManager::getConfigString() {
+ return "Not supported under lua";
+}
+
+std::string CConfigManager::currentConfigPath() {
+ return m_mainConfigPath;
+}
+
+const std::vector<std::string>& CConfigManager::getConfigPaths() {
+ return m_configPaths;
+}
+
+std::expected<void, std::string> CConfigManager::generateDefaultConfig(const std::filesystem::path& path, bool safeMode) {
+ std::string parentPath = std::filesystem::path(path).parent_path();
+
+ if (!parentPath.empty()) {
+ std::error_code ec;
+ bool created = std::filesystem::create_directories(parentPath, ec);
+ if (ec) {
+ Log::logger->log(Log::ERR, "Couldn't create config home directory ({}): {}", ec.message(), parentPath);
+ return std::unexpected("Config could not be generated.");
+ }
+ if (created)
+ Log::logger->log(Log::WARN, "Creating config home directory");
+ }
+
+ Log::logger->log(Log::WARN, "No config file found; attempting to generate.");
+ std::ofstream ofs;
+ ofs.open(path, std::ios::trunc);
+
+ if (!ofs.good())
+ return std::unexpected("Config could not be generated.");
+
+ if (!safeMode) {
+ ofs << AUTOGENERATED_PREFIX;
+ ofs << EXAMPLE_CONFIG;
+ } else {
+ std::string n = std::string{EXAMPLE_CONFIG};
+ replaceInString(n, "\nlocal menu = \"hyprlauncher\"\n", "\nlocal menu = \"hyprland-run\"\n");
+ ofs << n;
+ }
+
+ ofs.close();
+
+ if (ofs.fail())
+ return std::unexpected("Config could not be generated.");
+
+ return {};
+}
+
+void CConfigManager::handlePluginLoads() {
+ if (!g_pPluginSystem)
+ return;
+
+ bool pluginsChanged = false;
+ g_pPluginSystem->updateConfigPlugins(m_registeredPlugins, pluginsChanged);
+
+ if (pluginsChanged) {
+ ErrorOverlay::overlay()->destroy();
+ reload();
+ }
+}
+
+bool CConfigManager::configVerifPassed() {
+ return m_lastConfigVerificationWasSuccessful;
+}
+
+std::string CConfigManager::luaConfigValueName(const std::string& s) {
+ std::string cpy = s;
+ std::ranges::replace(cpy, ':', '.');
+ std::ranges::replace(cpy, '-', '_');
+ return cpy;
+}
+
+bool CConfigManager::isFirstLaunch() const {
+ return m_isFirstLaunch;
+}
+
+std::expected<void, std::string> CConfigManager::registerPluginValue(void* handle, SP<Config::Values::IValue> value) {
+
+ const auto NAME = luaConfigValueName(value->name());
+
+ if (m_configValues.contains(NAME))
+ return std::unexpected("name collision: already registered");
+
+ auto val = fromGenericValue(value);
+
+ if (!val)
+ return std::unexpected("unsupported value type");
+
+ m_configValues.emplace(NAME, std::move(val));
+
+ m_pluginValues[handle].emplace_back(NAME);
+
+ return {};
+}
+
+std::expected<void, std::string> CConfigManager::registerPluginLuaFunctionInState(uint64_t id, const std::string& namespace_, const std::string& name) {
+ if (!m_lua)
+ return std::unexpected("lua state not initialized");
+
+ lua_getglobal(m_lua, "hl");
+ if (!lua_istable(m_lua, -1)) {
+ lua_pop(m_lua, 1);
+ return std::unexpected("missing global table 'hl'");
+ }
+
+ lua_getfield(m_lua, -1, "plugin");
+ if (!lua_istable(m_lua, -1)) {
+ lua_pop(m_lua, 2);
+ return std::unexpected("missing global table 'hl.plugin'");
+ }
+
+ const int pluginTableIdx = lua_gettop(m_lua);
+
+ lua_getfield(m_lua, pluginTableIdx, namespace_.c_str());
+ if (lua_isnil(m_lua, -1)) {
+ lua_pop(m_lua, 1);
+ lua_newtable(m_lua);
+ lua_pushvalue(m_lua, -1);
+ lua_setfield(m_lua, pluginTableIdx, namespace_.c_str());
+ } else if (!lua_istable(m_lua, -1)) {
+ lua_pop(m_lua, 3);
+ return std::unexpected(std::format("hl.plugin.{} already exists and is not a namespace table", namespace_));
+ }
+
+ const int namespaceTableIdx = lua_gettop(m_lua);
+
+ lua_getfield(m_lua, namespaceTableIdx, name.c_str());
+ const bool exists = !lua_isnil(m_lua, -1);
+ lua_pop(m_lua, 1);
+
+ if (exists) {
+ lua_pop(m_lua, 3);
+ return std::unexpected(std::format("hl.plugin.{}.{} already exists", namespace_, name));
+ }
+
+ lua_pushinteger(m_lua, sc<lua_Integer>(id));
+ lua_pushcclosure(m_lua, pluginLuaFunctionDispatcher, 1);
+ lua_setfield(m_lua, namespaceTableIdx, name.c_str());
+
+ lua_pop(m_lua, 3);
+ return {};
+}
+
+std::expected<void, std::string> CConfigManager::unregisterPluginLuaFunctionInState(const std::string& namespace_, const std::string& name) {
+ if (!m_lua)
+ return std::unexpected("lua state not initialized");
+
+ lua_getglobal(m_lua, "hl");
+ if (!lua_istable(m_lua, -1)) {
+ lua_pop(m_lua, 1);
+ return {};
+ }
+
+ lua_getfield(m_lua, -1, "plugin");
+ if (!lua_istable(m_lua, -1)) {
+ lua_pop(m_lua, 2);
+ return {};
+ }
+
+ const int pluginTableIdx = lua_gettop(m_lua);
+
+ lua_getfield(m_lua, pluginTableIdx, namespace_.c_str());
+ if (!lua_istable(m_lua, -1)) {
+ lua_pop(m_lua, 3);
+ return {};
+ }
+
+ const int namespaceTableIdx = lua_gettop(m_lua);
+
+ lua_pushnil(m_lua);
+ lua_setfield(m_lua, namespaceTableIdx, name.c_str());
+
+ bool isEmpty = true;
+ lua_pushnil(m_lua);
+ if (lua_next(m_lua, namespaceTableIdx) != 0) {
+ isEmpty = false;
+ lua_pop(m_lua, 2);
+ }
+
+ if (isEmpty) {
+ lua_pushnil(m_lua);
+ lua_setfield(m_lua, pluginTableIdx, namespace_.c_str());
+ }
+
+ lua_pop(m_lua, 3);
+ return {};
+}
+
+void CConfigManager::erasePluginLuaFunction(uint64_t id) {
+ std::erase_if(m_pluginLuaFunctions, [&id](const SPluginLuaFunction& f) { return f.id == id; });
+}
+
+int CConfigManager::invokePluginLuaFunctionByID(uint64_t id, lua_State* L) {
+ const auto REGIT = std::ranges::find_if(m_pluginLuaFunctions, [&id](const SPluginLuaFunction& r) { return r.id == id; });
+ if (REGIT == m_pluginLuaFunctions.end())
+ return luaL_error(L, "hl.plugin: this function is no longer available (plugin unloaded)");
+
+ const auto FN = REGIT->fn;
+ if (!FN)
+ return luaL_error(L, "hl.plugin: this function is not callable");
+
+ return FN(L);
+}
+
+std::expected<void, std::string> CConfigManager::registerPluginLuaFunction(void* handle, const std::string& namespace_, const std::string& name, Config::PLUGIN_LUA_FN fn) {
+ if (!handle)
+ return std::unexpected("invalid handle");
+
+ if (!fn)
+ return std::unexpected("function pointer cannot be null");
+
+ if (!isValidLuaIdentifier(namespace_))
+ return std::unexpected("namespace must match [A-Za-z_][A-Za-z0-9_]*");
+
+ if (!isValidLuaIdentifier(name))
+ return std::unexpected("name must match [A-Za-z_][A-Za-z0-9_]*");
+
+ if (namespace_ == "load")
+ return std::unexpected("namespace 'load' is reserved");
+
+ const auto key = namespace_ + "." + name;
+ if (std::ranges::find_if(m_pluginLuaFunctions, [&key](const SPluginLuaFunction& r) { return r.namespace_ + "." + r.name == key; }) != m_pluginLuaFunctions.end())
+ return std::unexpected("name collision: already registered");
+
+ const uint64_t id = nextPluginLuaFnID++;
+ if (const auto REGISTERED = registerPluginLuaFunctionInState(id, namespace_, name); !REGISTERED)
+ return REGISTERED;
+
+ m_pluginLuaFunctions.emplace_back(SPluginLuaFunction{.id = id, .handle = handle, .namespace_ = namespace_, .name = name, .fn = fn});
+
+ return {};
+}
+
+std::expected<void, std::string> CConfigManager::unregisterPluginLuaFunction(void* handle, const std::string& namespace_, const std::string& name) {
+ if (!handle)
+ return std::unexpected("invalid handle");
+
+ const auto key = namespace_ + "." + name;
+ auto it = std::ranges::find_if(m_pluginLuaFunctions, [&key](const SPluginLuaFunction& r) { return r.namespace_ + "." + r.name == key; });
+
+ if (it == m_pluginLuaFunctions.end())
+ return std::unexpected("no such function");
+
+ if (it->handle != handle)
+ return std::unexpected("function belongs to a different plugin");
+
+ const auto removedFromState = unregisterPluginLuaFunctionInState(namespace_, name);
+ erasePluginLuaFunction(it->id);
+
+ if (!removedFromState)
+ return removedFromState;
+
+ return {};
+}
+
+void CConfigManager::onPluginUnload(void* handle) {
+ if (!handle)
+ return;
+
+ if (const auto it = m_pluginValues.find(handle); it != m_pluginValues.end()) {
+ for (const auto& name : it->second) {
+ m_configValues.erase(name);
+ }
+
+ m_pluginValues.erase(it);
+ }
+
+ std::erase_if(m_pluginLuaFunctions, [&handle, this](const SPluginLuaFunction& f) {
+ const bool NEEDS_REMOVE = f.handle == handle;
+
+ if (NEEDS_REMOVE) // NOLINTNEXTLINE
+ unregisterPluginLuaFunctionInState(f.namespace_, f.name);
+
+ return NEEDS_REMOVE;
+ });
+}
+
+void CConfigManager::registerLuaRef(int ref) {
+ m_heldLuaRefs.emplace_back(ref);
+}
+
+void CConfigManager::callLuaFn(int ref) {
+ lua_rawgeti(m_lua, LUA_REGISTRYINDEX, ref);
+
+ int status = guardedPCall(0, 0, 0, CConfigManager::LUA_TIMEOUT_KEYBIND_CALLBACK_MS, "keybind callback");
+
+ if (status != LUA_OK) {
+ addError(std::format("error in callLuaFn: {}", lua_tostring(m_lua, -1)));
+ lua_pop(m_lua, 1);
+ }
+}
+
+void CConfigManager::clearHeldLuaRefs() {
+ for (const auto& r : m_heldLuaRefs) {
+ luaL_unref(m_lua, LUA_REGISTRYINDEX, r);
+ }
+
+ m_heldLuaRefs.clear();
+}
+
+bool CConfigManager::isDynamicParse() const {
+ return !m_isParsingConfig || m_isEvaluating;
+}
+
+void CConfigManager::reregisterLuaPluginFns() {
+ for (auto& fn : m_pluginLuaFunctions) {
+ auto ret = registerPluginLuaFunctionInState(fn.id, fn.namespace_, fn.name);
+ if (!ret)
+ Log::logger->log(Log::ERR, "[lua] failed to reregister plugin fn for {}.{}: {}", fn.namespace_, fn.name, ret.error());
+ }
+}