hyprctl: add interactive Lua REPL mode (#15043)

* hyprctl: add lua repl and return for eval

* hook print function for repl

* add help

* nix fixes this

* on second thought don't break all tests

* tests
This commit is contained in:
Dregu
2026-06-13 17:18:20 +01:00
committed by GitHub
parent d121872736
commit f719bd6794
8 changed files with 105 additions and 13 deletions
+1 -1
View File
@@ -5,7 +5,7 @@ project(
DESCRIPTION "Control utility for Hyprland"
)
pkg_check_modules(hyprctl_deps REQUIRED IMPORTED_TARGET hyprutils>=0.2.4 hyprwire re2)
pkg_check_modules(hyprctl_deps REQUIRED IMPORTED_TARGET hyprutils>=0.2.4 hyprwire re2 readline)
file(GLOB_RECURSE HYPRCTL_SRCFILES CONFIGURE_DEPENDS "src/*.cpp" "hw-protocols/*.cpp" "include/*.hpp")
+3
View File
@@ -19,6 +19,7 @@ commands:
dismissnotify [amount] Dismisses all or up to AMOUNT notifications
dispatch <dispatcher> [args] Issue a dispatch to call a keybind
dispatcher with arguments
eval <code> Issue a Lua string to execute
getoption <option> Gets the config option status (values)
globalshortcuts Lists all global shortcuts
hyprpaper ... Issue a hyprpaper request
@@ -41,6 +42,8 @@ commands:
plugin ... Issue a plugin request
reload [config-only] Issue a reload to force reload the config. Pass
'config-only' to disable monitor reload
repl [code] Enter interactive Lua REPL mode (^D to exit)
or issue a Lua string and print the result
rollinglog Prints tail of the log. Also supports -f/--follow
option
setcursor <theme> <size> Sets the cursor theme and reloads the cursor
+23 -1
View File
@@ -33,6 +33,9 @@ using namespace Hyprutils::Memory;
#include "Strings.hpp"
#include "hyprpaper/Hyprpaper.hpp"
#include <readline/readline.h>
#include <readline/history.h>
std::string instanceSignature;
bool quiet = false;
@@ -252,6 +255,9 @@ int request(std::string_view arg, int minArgs = 0, bool needRoll = false) {
log(reply);
if (reply.starts_with("error:"))
return 7;
return 0;
}
@@ -532,7 +538,23 @@ int main(int argc, char** argv) {
std::println("{}", USAGE);
else if (fullRequest.contains("/rollinglog") && needRoll)
exitStatus = request(fullRequest, 0, true);
else {
else if (fullRequest.contains("/repl")) {
if (ARGS.size() > 1) {
// single command with output
exitStatus = request(fullRequest, 1);
} else {
// interactive REPL mode
char* input = nullptr;
while ((input = readline("> ")) != nullptr) {
std::string line(input);
if (!line.empty()) {
exitStatus = request("/repl " + line);
add_history(input);
}
free(input);
}
}
} else {
exitStatus = request(fullRequest);
}
+5
View File
@@ -166,3 +166,8 @@ TEST_CASE(hyprctlJsonErrors) {
jqProc.runSync();
EXPECT(jqProc.exitCode(), 0);
}
TEST_CASE(hyprctlREPL) {
EXPECT(getCommandStdOut("hyprctl repl 'print(type(hl))'"), "table");
EXPECT(getCommandStdOut("hyprctl eval 'print(type(hl))'"), "ok");
}
+2
View File
@@ -58,6 +58,7 @@
commit,
revCount,
date,
readline,
# deprecated flags
enableNvidiaPatches ? false,
nvidiaPatches ? false,
@@ -204,6 +205,7 @@ customStdenv.mkDerivation (finalAttrs: {
wayland
wayland-protocols
wayland-scanner
readline
]
(optionals customStdenv.hostPlatform.isBSD [ epoll-shim ])
(optionals customStdenv.hostPlatform.isMusl [ libexecinfo ])
+65 -7
View File
@@ -361,6 +361,33 @@ void CConfigManager::reinitLuaState() {
2);
lua_rawseti(m_lua, -2, 2); // replace package.searchers[2]
lua_pop(m_lua, 2); // pop searchers, package
// hook print function to print to hyprctl repl instead
lua_getglobal(m_lua, "print");
lua_pushcclosure(
m_lua,
[](lua_State* L) -> int {
auto* mgr = CConfigManager::fromLuaState(L);
int nstack = lua_gettop(L);
if (!mgr->isREPL()) {
// call original print function from upvalue if not repl
lua_pushvalue(L, lua_upvalueindex(1));
lua_insert(L, 1);
lua_call(L, nstack, LUA_MULTRET);
} else {
std::string out;
for (int i = 1; i <= nstack; ++i) {
out += std::format("{}\t", luaL_tolstring(L, i, nullptr));
lua_pop(L, 1);
}
lua_pop(L, nstack);
out.pop_back();
mgr->m_prints.emplace_back(out);
}
return 0;
},
1);
lua_setglobal(m_lua, "print");
}
void CConfigManager::init() {
@@ -614,26 +641,52 @@ void CConfigManager::addEvalIssue(const Config::SConfigError& err) {
m_evalIssues.emplace_back(err);
}
std::optional<std::string> CConfigManager::eval(const std::string& code) {
std::optional<std::string> CConfigManager::eval(const std::string& code, bool repl) {
if (!m_lua)
return "error: lua state not initialized";
m_errors.clear();
m_evalIssues.clear();
m_prints.clear();
m_isEvaluating = true;
m_isREPL = repl;
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);
Hyprutils::Utils::CScopeGuard x([this] {
m_isEvaluating = false;
m_isREPL = false;
});
if (luaL_loadstring(m_lua, code.starts_with("return") ? code.c_str() : std::format("return {};", code).c_str()) != LUA_OK) {
lua_pop(m_lua, 1);
return std::format("error: {}", err);
if (luaL_loadstring(m_lua, code.c_str()) != LUA_OK) {
std::string err = lua_tostring(m_lua, -1);
lua_pop(m_lua, 1);
return std::format("error: {}", err);
}
}
if (guardedPCall(0, 0, 0, LUA_TIMEOUT_EVAL_MS, "hyprctl eval") != LUA_OK) {
if (guardedPCall(0, LUA_MULTRET, 0, LUA_TIMEOUT_EVAL_MS, "hyprctl eval") != LUA_OK) {
std::string err = lua_tostring(m_lua, -1);
lua_pop(m_lua, 1);
return std::format("error: {}", err);
} else if (lua_gettop(m_lua) > 0) {
// print returned values to repl
int nstack = lua_gettop(m_lua);
std::string out;
for (int i = 1; i <= nstack; ++i) {
out += std::format("{}\t", luaL_tolstring(m_lua, i, nullptr));
lua_pop(m_lua, 1);
}
lua_pop(m_lua, nstack);
out.pop_back();
m_prints.emplace_back(out);
}
if (!m_prints.empty() && repl) {
std::string out;
for (auto& line : m_prints)
out += std::format("{}\n", line);
out.pop_back();
return out;
}
if (!m_errors.empty() || !m_evalIssues.empty()) {
@@ -658,6 +711,7 @@ std::optional<std::string> CConfigManager::eval(const std::string& code) {
m_errors.clear();
m_evalIssues.clear();
m_prints.clear();
return std::nullopt;
}
@@ -1082,6 +1136,10 @@ bool CConfigManager::isDynamicParse() const {
return !m_isParsingConfig || m_isEvaluating;
}
bool CConfigManager::isREPL() const {
return m_isREPL;
}
void CConfigManager::reregisterLuaPluginFns() {
for (auto& fn : m_pluginLuaFunctions) {
auto ret = registerPluginLuaFunctionInState(fn.id, fn.namespace_, fn.name);
+4 -2
View File
@@ -96,7 +96,7 @@ namespace Config::Lua {
std::expected<void, std::string> registerLuaLayoutProvider(std::string name, lua_State* L, int providerTableIdx);
// execute an arbitrary lua string on the current state.
std::optional<std::string> eval(const std::string& code);
std::optional<std::string> eval(const std::string& code, bool repl = false);
int guardedPCall(int nargs, int nresults, int errfunc, int timeoutMs, std::string_view context);
@@ -113,6 +113,7 @@ namespace Config::Lua {
bool isFirstLaunch() const;
bool isDynamicParse() const;
bool isREPL() const;
std::string m_currentSubmap;
std::string m_currentSubmapReset;
@@ -136,7 +137,7 @@ namespace Config::Lua {
};
std::unordered_map<std::string, SDeviceConfig> m_deviceConfigs;
std::vector<std::string> m_errors, m_configPaths;
std::vector<std::string> m_errors, m_configPaths, m_prints;
std::vector<Config::SConfigError> m_evalIssues;
// named window/layer rules for merge-on-redeclaration
@@ -167,6 +168,7 @@ namespace Config::Lua {
bool m_watchdogActive = false;
bool m_isParsingConfig = false;
bool m_isEvaluating = false;
bool m_isREPL = false;
std::chrono::steady_clock::time_point m_watchdogDeadline;
std::string m_watchdogContext;
+2 -2
View File
@@ -1098,7 +1098,7 @@ static std::string evalRequest(eHyprCtlOutputFormat format, std::string request)
// strip the command name ("eval ") from the request
auto code = request.substr(request.find_first_of(' ') + 1);
auto err = luaMgr->eval(code);
auto err = luaMgr->eval(code, request.starts_with("repl "));
if (err)
return *err;
@@ -2021,7 +2021,7 @@ CHyprCtl::CHyprCtl() {
registerCommand(SHyprCtlCommand{"decorations", false, decorationRequest});
registerCommand(SHyprCtlCommand{"[[BATCH]]", false, dispatchBatch});
registerCommand(SHyprCtlCommand{"eval", false, evalRequest});
registerCommand(SHyprCtlCommand{"repl", false, evalRequest});
startHyprCtlSocket();
}