From f7daeb7f79d48745a2894367e9e73229ac162abe Mon Sep 17 00:00:00 2001 From: Lucas Ritzdorf <42657792+LRitzdorf@users.noreply.github.com> Date: Sun, 9 Aug 2026 12:00:17 -0600 Subject: [PATCH] hyprctl: random Lua-adjacent fixes (#15767) * hyprctl(repl): go interactive if no Lua was given via args The previous check would mess up if `-i` was provided, since it'd see *an argument* and refuse to go interactive, even when it should've (i.e. `hyprctl -i 0 repl`). * ipc(socket1): new escape parsing for batch commands Semicolons are still the command separator, so those have to be escaped if they appear anywhere inside of commands (e.g. multi-statement Lua code). Also, literal backslashes need to be escaped as well. * tests(ipc): add test for hyprctl batch mode This test case includes slightly weird stuff (semicolons in Lua code; square brackets) that would break the old parsing logic. * format: dang braces --- hyprctl/src/main.cpp | 4 ++-- hyprtester/src/tests/main/hyprctl.cpp | 13 +++++++++++++ src/ipc/s1/S1.cpp | 28 +++++++++++++-------------- 3 files changed, 29 insertions(+), 16 deletions(-) diff --git a/hyprctl/src/main.cpp b/hyprctl/src/main.cpp index 676f9c846..f986ef67b 100644 --- a/hyprctl/src/main.cpp +++ b/hyprctl/src/main.cpp @@ -566,8 +566,8 @@ int main(int argc, char** argv) { std::println("{}", USAGE); else if (fullRequest.contains("/rollinglog") && needRoll) exitStatus = request(fullRequest, 0, true); - else if (fullRequest.contains("/repl")) { - if (ARGS.size() > 1) { + else if (auto pos = fullRequest.find("/repl"); pos != std::string::npos) { + if (fullRequest.length() > pos + 5) { // single command with output exitStatus = request(fullRequest, 1); } else { diff --git a/hyprtester/src/tests/main/hyprctl.cpp b/hyprtester/src/tests/main/hyprctl.cpp index b84a10e70..df86af592 100644 --- a/hyprtester/src/tests/main/hyprctl.cpp +++ b/hyprtester/src/tests/main/hyprctl.cpp @@ -206,3 +206,16 @@ TEST_CASE(hyprctlREPL) { EXPECT(getCommandStdOut("hyprctl repl 'print(type(hl))'"), "table"); EXPECT(getCommandStdOut("hyprctl eval 'print(type(hl))'"), "ok"); } + +TEST_CASE(hyprctlBatch) { + const auto command = R"([[BATCH]] activewindow; repl local i = 42\; print(i, "hello\\nworld ]"); clients)"; + const auto expected = R"(Invalid + + +42 hello +world ] + + +no open windows)"; + EXPECT(getFromSocket(command), expected); +} diff --git a/src/ipc/s1/S1.cpp b/src/ipc/s1/S1.cpp index 0576269ae..53abd9530 100644 --- a/src/ipc/s1/S1.cpp +++ b/src/ipc/s1/S1.cpp @@ -7,6 +7,7 @@ #include #include #include +#include #include using namespace IPC::Socket1; @@ -129,25 +130,24 @@ SResponse CSocket1::dispatchBatch(std::string request, pid_t pid) { request = request.substr(BATCH_TOKEN.size()); std::vector commands; - size_t commandStart = 0; - int bracketDepth = 0; + std::stringstream parsedCommand(""); for (size_t i = 0; i <= request.size(); ++i) { const bool atEnd = i == request.size(); - - if (!atEnd) { - if (request[i] == '[') - ++bracketDepth; - else if (request[i] == ']') - --bracketDepth; + if (atEnd || request[i] == ';') { + commands.emplace_back(Hyprutils::String::trim(parsedCommand.str())); + parsedCommand.str(""); + parsedCommand.clear(); + continue; } - if (!atEnd && (request[i] != ';' || bracketDepth != 0)) - continue; - - if (commandStart < i) - commands.emplace_back(Hyprutils::String::trim(request.substr(commandStart, i - commandStart))); - commandStart = i + 1; + if (request[i] == '\\') { + if (i < request.size() && (request[i + 1] == '\\' || request[i + 1] == ';')) + ++i; + else + Log::logger->log(Log::ERR, "Malformed socket1 request: invalid escape sequence {} at position {}, using it verbatim", request.subview(i, 2), i); + } + parsedCommand << request[i]; } std::vector responses;