mirror of
https://github.com/hyprwm/Hyprland.git
synced 2026-08-24 02:24:14 -05:00
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:
@@ -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")
|
||||
|
||||
|
||||
@@ -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
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
@@ -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 ])
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user