diff --color -ruN a/include/modules/hyprland/backend.hpp b/include/modules/hyprland/backend.hpp --- a/include/modules/hyprland/backend.hpp 2026-10-04 00:13:00.535259516 +0200 +++ b/include/modules/hyprland/backend.hpp 2026-10-04 00:13:24.130275037 +0200 @@ -3,6 +3,7 @@ #include #include #include +#include #include #include #include @@ -34,9 +35,27 @@ Json::Value getSocket1JsonReply(const std::string& rq); static std::filesystem::path getSocketFolder(const char* instanceSig); + /// Dispatch a Hyprland command. Automatically uses the correct protocol + /// (legacy text or Lua-based) depending on the running Hyprland version. + static std::string dispatch(const std::string& dispatcher, const std::string& arg); + + /// Build a Lua-format dispatch command string. + static std::string buildLuaDispatch(const std::string& dispatcher, const std::string& arg); + protected: static std::filesystem::path socketFolder_; + /// Detect whether the running Hyprland uses the Lua-based IPC protocol. + /// Resolved once from the config manager it reports, then cached. + static bool isLuaProtocol(); + + /// Whether a "systeminfo" reply reports the Lua config manager. An absent + /// "configProvider:" field means the instance predates the Lua config + /// manager (< 0.55) and therefore speaks the legacy protocol. + static bool isLuaConfigProvider(const std::string& systemInfo); + + static std::optional s_luaProtocolDetected_; // cached detection result + private: void socketListener(); void parseIPC(const std::string&); diff --color -ruN a/src/modules/hyprland/backend.cpp b/src/modules/hyprland/backend.cpp --- a/src/modules/hyprland/backend.cpp 2026-10-04 00:13:00.537259511 +0200 +++ b/src/modules/hyprland/backend.cpp 2026-10-04 00:13:24.138620589 +0200 @@ -10,11 +10,16 @@ #include #include +#include #include +#include + +#include "util/string.hpp" namespace waybar::modules::hyprland { std::filesystem::path IPC::socketFolder_; +std::optional IPC::s_luaProtocolDetected_; std::filesystem::path IPC::getSocketFolder(const char* instanceSig) { static std::mutex folderMutex; @@ -243,4 +248,98 @@ return parser_.parse(reply); } +bool IPC::isLuaConfigProvider(const std::string& systemInfo) { + // Hyprland reports which config manager it actually loaded in "systeminfo", + // as a "configProvider: lua" / "configProvider: hyprlang" line. That is the + // authoritative signal: the version alone is not enough, because Hyprland + // only uses the Lua manager when the config file name ends in ".lua", so a + // >= 0.54 instance started with a traditional hyprland.conf still speaks the + // legacy dispatch protocol. The field landed together with the Lua config + // manager in 0.55, so its absence means the instance predates Lua support + // entirely and necessarily speaks the legacy protocol. + static constexpr std::string_view key = "configProvider:"; + + const size_t keyPos = systemInfo.find(key); + if (keyPos == std::string::npos) { + return false; + } + + const size_t valuePos = keyPos + key.size(); + const size_t lineEnd = systemInfo.find('\n', valuePos); + const std::string value = lineEnd == std::string::npos + ? systemInfo.substr(valuePos) + : systemInfo.substr(valuePos, lineEnd - valuePos); + + return trim(value) == "lua"; +} + +bool IPC::isLuaProtocol() { + if (s_luaProtocolDetected_.has_value()) { + return *s_luaProtocolDetected_; + } + + // The query is read-only, so detection has none of the side effects of an + // actual dispatch probe. + bool luaProto = false; + try { + luaProto = isLuaConfigProvider(getSocket1Reply("systeminfo")); + } catch (const std::exception& e) { + spdlog::warn("Hyprland IPC: could not read systeminfo ({}), assuming legacy protocol", + e.what()); + } + + if (luaProto) { + spdlog::info("Hyprland IPC: detected Lua-based dispatch protocol"); + } else { + spdlog::info("Hyprland IPC: detected legacy dispatch protocol"); + } + + s_luaProtocolDetected_ = luaProto; + return luaProto; +} + +std::string IPC::buildLuaDispatch(const std::string& dispatcher, const std::string& arg) { + // Map old-style dispatchers to the new Lua hl.dsp API. + // + // Old format: dispatch workspace 1 + // New format: /dispatch hl.dsp.focus({ workspace = "1" }) + // + // Old format: dispatch focusworkspaceoncurrentmonitor 2 + // New format: /dispatch hl.dsp.focus({ workspace = "2", on_current_monitor = true }) + // + // Old format: dispatch togglespecialworkspace name + // New format: /dispatch hl.dsp.workspace.toggle_special("name") + + if (dispatcher == "workspace") { + return "/dispatch hl.dsp.focus({ workspace = \"" + arg + "\" })"; + } + if (dispatcher == "focusworkspaceoncurrentmonitor") { + return "/dispatch hl.dsp.focus({ workspace = \"" + arg + "\", on_current_monitor = true })"; + } + if (dispatcher == "togglespecialworkspace") { + if (arg.empty()) { + return "/dispatch hl.dsp.workspace.toggle_special()"; + } + return "/dispatch hl.dsp.workspace.toggle_special(\"" + arg + "\")"; + } + + // Fallback for any other dispatcher: try the old format wrapped in dispatch(). + // This may not work for all dispatchers, but it's a reasonable default. + spdlog::warn("Hyprland IPC: unknown dispatcher '{}' in Lua mode, attempting generic format", + dispatcher); + return "/dispatch hl.dsp." + dispatcher + "(\"" + arg + "\")"; +} + +std::string IPC::dispatch(const std::string& dispatcher, const std::string& arg) { + if (isLuaProtocol()) { + return getSocket1Reply(buildLuaDispatch(dispatcher, arg)); + } + // Legacy format: "dispatch " + std::string cmd = "dispatch " + dispatcher; + if (!arg.empty()) { + cmd += " " + arg; + } + return getSocket1Reply(cmd); +} + } // namespace waybar::modules::hyprland diff --color -ruN a/src/modules/hyprland/workspace.cpp b/src/modules/hyprland/workspace.cpp --- a/src/modules/hyprland/workspace.cpp 2026-10-04 00:13:00.538259508 +0200 +++ b/src/modules/hyprland/workspace.cpp 2026-10-04 00:13:24.138815299 +0200 @@ -71,20 +71,20 @@ try { if (id() > 0) { // normal if (m_workspaceManager.moveToMonitor()) { - m_ipc.getSocket1Reply("dispatch focusworkspaceoncurrentmonitor " + std::to_string(id())); + IPC::dispatch("focusworkspaceoncurrentmonitor", std::to_string(id())); } else { - m_ipc.getSocket1Reply("dispatch workspace " + std::to_string(id())); + IPC::dispatch("workspace", std::to_string(id())); } } else if (!isSpecial()) { // named (this includes persistent) if (m_workspaceManager.moveToMonitor()) { - m_ipc.getSocket1Reply("dispatch focusworkspaceoncurrentmonitor name:" + name()); + IPC::dispatch("focusworkspaceoncurrentmonitor", "name:" + name()); } else { - m_ipc.getSocket1Reply("dispatch workspace name:" + name()); + IPC::dispatch("workspace", "name:" + name()); } } else if (id() != -99) { // named special - m_ipc.getSocket1Reply("dispatch togglespecialworkspace " + name()); + IPC::dispatch("togglespecialworkspace", name()); } else { // special - m_ipc.getSocket1Reply("dispatch togglespecialworkspace"); + IPC::dispatch("togglespecialworkspace", ""); } return true; } catch (const std::exception& e) {