diff --git a/meta/generateLuaStubs.py b/meta/generateLuaStubs.py index a523496df..7ef70babb 100644 --- a/meta/generateLuaStubs.py +++ b/meta/generateLuaStubs.py @@ -469,7 +469,8 @@ def generate_stub(root: Path) -> str: api_signatures: dict[str, str] = { "hl.on": "fun(event: HL.EventName, cb: fun(...)): HL.EventSubscription", - "hl.bind": "fun(keys: string, dispatcher: function, opts?: HL.BindOptions): HL.Keybind", + "hl.bind": "fun(keys: string, dispatcher: HL.Dispatcher|function, opts?: HL.BindOptions): HL.Keybind", + "hl.dispatch": "fun(dispatcher: HL.Dispatcher|function): any", "hl.define_submap": "fun(name: string, reset_or_fn: string|function, fn?: function): nil", "hl.timer": "fun(callback: function, opts: HL.TimerOptions): HL.Timer", "hl.config": "fun(config: table): nil", @@ -523,6 +524,9 @@ def generate_stub(root: Path) -> str: lines.append("---@alias HL.CssGap integer|{top?:integer, right?:integer, bottom?:integer, left?:integer}") lines.append("---@alias HL.Gradient string|{colors:string[], angle?:number}") lines.append("") + lines.append("---@class HL.Dispatcher") + lines.append("local __HL_Dispatcher = {}") + lines.append("") lines.extend( emit_class_block( @@ -651,7 +655,8 @@ def generate_stub(root: Path) -> str: for method in sorted(node.methods): full_name = f"{full_prefix}.{method}" - method_type = api_signatures.get(full_name, "fun(...): any") + default_method_type = "fun(...): HL.Dispatcher" if path and path[0] == "dsp" else "fun(...): any" + method_type = api_signatures.get(full_name, default_method_type) fields.append((method, method_type, False)) for child_name in sorted(node.children.keys()): diff --git a/meta/hl.meta.lua b/meta/hl.meta.lua index e0fd096ae..a13655633 100644 --- a/meta/hl.meta.lua +++ b/meta/hl.meta.lua @@ -378,6 +378,9 @@ ---@alias HL.CssGap integer|{top?:integer, right?:integer, bottom?:integer, left?:integer} ---@alias HL.Gradient string|{colors:string[], angle?:number} +---@class HL.Dispatcher +local __HL_Dispatcher = {} + ---@class HL.Vec2 ---@field x number ---@field y number @@ -739,12 +742,12 @@ local __HL_Workspace = {} ---@class HL.API ---@field animation fun(...): any ----@field bind fun(keys: string, dispatcher: function, opts?: HL.BindOptions): HL.Keybind +---@field bind fun(keys: string, dispatcher: HL.Dispatcher|function, opts?: HL.BindOptions): HL.Keybind ---@field config fun(config: table): nil ---@field curve fun(...): any ---@field define_submap fun(name: string, reset_or_fn: string|function, fn?: function): nil ---@field device fun(spec: HL.DeviceSpec): nil ----@field dispatch fun(...): any +---@field dispatch fun(dispatcher: HL.Dispatcher|function): any ---@field env fun(...): any ---@field exec_cmd fun(cmd: string, rules?: table): nil ---@field gesture fun(spec: HL.GestureSpec): nil @@ -783,21 +786,21 @@ local __HL_Workspace = {} local __HL_API = {} ---@class HL.DspNamespace ----@field dpms fun(...): any ----@field event fun(...): any ----@field exec_cmd fun(...): any ----@field exec_raw fun(...): any ----@field exit fun(...): any ----@field focus fun(...): any ----@field force_idle fun(...): any ----@field force_renderer_reload fun(...): any ----@field global fun(...): any ----@field layout fun(...): any ----@field no_op fun(...): any ----@field pass fun(...): any ----@field send_key_state fun(...): any ----@field send_shortcut fun(...): any ----@field submap fun(...): any +---@field dpms fun(...): HL.Dispatcher +---@field event fun(...): HL.Dispatcher +---@field exec_cmd fun(...): HL.Dispatcher +---@field exec_raw fun(...): HL.Dispatcher +---@field exit fun(...): HL.Dispatcher +---@field focus fun(...): HL.Dispatcher +---@field force_idle fun(...): HL.Dispatcher +---@field force_renderer_reload fun(...): HL.Dispatcher +---@field global fun(...): HL.Dispatcher +---@field layout fun(...): HL.Dispatcher +---@field no_op fun(...): HL.Dispatcher +---@field pass fun(...): HL.Dispatcher +---@field send_key_state fun(...): HL.Dispatcher +---@field send_shortcut fun(...): HL.Dispatcher +---@field submap fun(...): HL.Dispatcher ---@field cursor HL.DspCursorNamespace ---@field group HL.DspGroupNamespace ---@field window HL.DspWindowNamespace @@ -805,48 +808,48 @@ local __HL_API = {} local __HL_DspNamespace = {} ---@class HL.DspCursorNamespace ----@field move fun(...): any ----@field move_to_corner fun(...): any +---@field move fun(...): HL.Dispatcher +---@field move_to_corner fun(...): HL.Dispatcher local __HL_DspCursorNamespace = {} ---@class HL.DspGroupNamespace ----@field active fun(...): any ----@field lock fun(...): any ----@field lock_active fun(...): any ----@field move_window fun(...): any ----@field next fun(...): any ----@field prev fun(...): any ----@field toggle fun(...): any +---@field active fun(...): HL.Dispatcher +---@field lock fun(...): HL.Dispatcher +---@field lock_active fun(...): HL.Dispatcher +---@field move_window fun(...): HL.Dispatcher +---@field next fun(...): HL.Dispatcher +---@field prev fun(...): HL.Dispatcher +---@field toggle fun(...): HL.Dispatcher local __HL_DspGroupNamespace = {} ---@class HL.DspWindowNamespace ----@field alter_zorder fun(...): any ----@field bring_to_top fun(...): any ----@field center fun(...): any ----@field close fun(...): any ----@field cycle_next fun(...): any ----@field deny_from_group fun(...): any ----@field drag fun(...): any ----@field float fun(...): any ----@field fullscreen fun(...): any ----@field fullscreen_state fun(...): any ----@field kill fun(...): any ----@field move fun(...): any ----@field pin fun(...): any ----@field pseudo fun(...): any ----@field resize fun(...): any ----@field set_prop fun(...): any ----@field signal fun(...): any ----@field swap fun(...): any ----@field tag fun(...): any ----@field toggle_swallow fun(...): any +---@field alter_zorder fun(...): HL.Dispatcher +---@field bring_to_top fun(...): HL.Dispatcher +---@field center fun(...): HL.Dispatcher +---@field close fun(...): HL.Dispatcher +---@field cycle_next fun(...): HL.Dispatcher +---@field deny_from_group fun(...): HL.Dispatcher +---@field drag fun(...): HL.Dispatcher +---@field float fun(...): HL.Dispatcher +---@field fullscreen fun(...): HL.Dispatcher +---@field fullscreen_state fun(...): HL.Dispatcher +---@field kill fun(...): HL.Dispatcher +---@field move fun(...): HL.Dispatcher +---@field pin fun(...): HL.Dispatcher +---@field pseudo fun(...): HL.Dispatcher +---@field resize fun(...): HL.Dispatcher +---@field set_prop fun(...): HL.Dispatcher +---@field signal fun(...): HL.Dispatcher +---@field swap fun(...): HL.Dispatcher +---@field tag fun(...): HL.Dispatcher +---@field toggle_swallow fun(...): HL.Dispatcher local __HL_DspWindowNamespace = {} ---@class HL.DspWorkspaceNamespace ----@field move fun(...): any ----@field rename fun(...): any ----@field swap_monitors fun(...): any ----@field toggle_special fun(...): any +---@field move fun(...): HL.Dispatcher +---@field rename fun(...): HL.Dispatcher +---@field swap_monitors fun(...): HL.Dispatcher +---@field toggle_special fun(...): HL.Dispatcher local __HL_DspWorkspaceNamespace = {} ---@class HL.NotificationNamespace diff --git a/src/config/lua/bindings/LuaBindingsDispatcherUtils.cpp b/src/config/lua/bindings/LuaBindingsDispatcherUtils.cpp new file mode 100644 index 000000000..a55dfc655 --- /dev/null +++ b/src/config/lua/bindings/LuaBindingsDispatcherUtils.cpp @@ -0,0 +1,144 @@ +#include "LuaBindingsInternal.hpp" + +using namespace Config::Lua::Bindings; + +static constexpr const char* DISPATCHER_MT = "HL.Dispatcher"; +static char DISPATCHER_TABLES_REGISTRY_KEY; + +namespace { + struct SDispatcherRef { + int ref = LUA_NOREF; + }; +} + +static int dispatcherGc(lua_State* L) { + auto* dispatcher = sc(luaL_checkudata(L, 1, DISPATCHER_MT)); + if (dispatcher->ref != LUA_NOREF) { + luaL_unref(L, LUA_REGISTRYINDEX, dispatcher->ref); + dispatcher->ref = LUA_NOREF; + } + + return 0; +} + +static int dispatcherCall(lua_State* L) { + return Internal::configError(L, "dispatcher objects cannot be called directly; use hl.dispatch(dispatcher)"); +} + +static int dispatcherToString(lua_State* L) { + lua_pushstring(L, "HL.Dispatcher"); + return 1; +} + +static void ensureDispatcherMetatable(lua_State* L) { + if (luaL_newmetatable(L, DISPATCHER_MT)) { + lua_pushcfunction(L, dispatcherGc); + lua_setfield(L, -2, "__gc"); + lua_pushcfunction(L, dispatcherCall); + lua_setfield(L, -2, "__call"); + lua_pushcfunction(L, dispatcherToString); + lua_setfield(L, -2, "__tostring"); + + lua_pushstring(L, DISPATCHER_MT); + lua_setfield(L, -2, "__metatable"); + } + + lua_pop(L, 1); +} + +static bool isDispatcherTable(lua_State* L, int idx) { + if (!lua_istable(L, idx)) + return false; + + idx = lua_absindex(L, idx); + lua_pushlightuserdata(L, &DISPATCHER_TABLES_REGISTRY_KEY); + lua_rawget(L, LUA_REGISTRYINDEX); + + if (!lua_istable(L, -1)) { + lua_pop(L, 1); + return false; + } + + lua_pushvalue(L, idx); + lua_rawget(L, -2); + const bool result = lua_toboolean(L, -1); + lua_pop(L, 2); + return result; +} + +static int dispatcherFactory(lua_State* L) { + const int nargs = lua_gettop(L); + + lua_pushvalue(L, lua_upvalueindex(1)); + lua_insert(L, 1); + lua_call(L, nargs, LUA_MULTRET); + + const int nresults = lua_gettop(L); + if (nresults == 1 && lua_isfunction(L, -1)) + return Internal::wrapDispatcher(L); + + return nresults; +} + +void Internal::setFn(lua_State* L, const char* name, lua_CFunction fn) { + if (isDispatcherTable(L, -1)) { + lua_pushcfunction(L, fn); + lua_pushcclosure(L, dispatcherFactory, 1); + } else + lua_pushcfunction(L, fn); + + lua_setfield(L, -2, name); +} + +void Internal::markDispatcherTable(lua_State* L) { + if (!lua_istable(L, -1)) + return; + + lua_pushlightuserdata(L, &DISPATCHER_TABLES_REGISTRY_KEY); + lua_rawget(L, LUA_REGISTRYINDEX); + + if (!lua_istable(L, -1)) { + lua_pop(L, 1); + lua_newtable(L); + lua_pushlightuserdata(L, &DISPATCHER_TABLES_REGISTRY_KEY); + lua_pushvalue(L, -2); + lua_rawset(L, LUA_REGISTRYINDEX); + } + + lua_pushvalue(L, -2); + lua_pushboolean(L, true); + lua_rawset(L, -3); + lua_pop(L, 1); +} + +int Internal::wrapDispatcher(lua_State* L) { + luaL_checktype(L, -1, LUA_TFUNCTION); + + const int ref = luaL_ref(L, LUA_REGISTRYINDEX); + + new (lua_newuserdata(L, sizeof(SDispatcherRef))) SDispatcherRef{.ref = ref}; + + ensureDispatcherMetatable(L); + luaL_getmetatable(L, DISPATCHER_MT); + lua_setmetatable(L, -2); + + return 1; +} + +bool Internal::pushDispatcherFunction(lua_State* L, int idx) { + if (lua_isfunction(L, idx)) { + lua_pushvalue(L, idx); + return true; + } + + auto* dispatcher = sc(luaL_testudata(L, idx, DISPATCHER_MT)); + if (!dispatcher || dispatcher->ref == LUA_NOREF) + return false; + + lua_rawgeti(L, LUA_REGISTRYINDEX, dispatcher->ref); + if (lua_isfunction(L, -1)) + return true; + + lua_pop(L, 1); + return false; +} diff --git a/src/config/lua/bindings/LuaBindingsDispatchers.cpp b/src/config/lua/bindings/LuaBindingsDispatchers.cpp index 4a981f4eb..4fd332eb7 100644 --- a/src/config/lua/bindings/LuaBindingsDispatchers.cpp +++ b/src/config/lua/bindings/LuaBindingsDispatchers.cpp @@ -1214,14 +1214,17 @@ static int hlWorkspaceSwapMonitors(lua_State* L) { void Internal::registerDispatcherBindings(lua_State* L) { lua_newtable(L); + Internal::markDispatcherTable(L); { lua_newtable(L); + Internal::markDispatcherTable(L); Internal::setFn(L, "move_to_corner", hlCursorMoveToCorner); Internal::setFn(L, "move", hlCursorMove); lua_setfield(L, -2, "cursor"); lua_newtable(L); + Internal::markDispatcherTable(L); Internal::setFn(L, "toggle", hlGroupToggle); Internal::setFn(L, "next", hlGroupNext); Internal::setFn(L, "prev", hlGroupPrev); @@ -1232,6 +1235,7 @@ void Internal::registerDispatcherBindings(lua_State* L) { lua_setfield(L, -2, "group"); lua_newtable(L); + Internal::markDispatcherTable(L); Internal::setFn(L, "close", hlWindowClose); Internal::setFn(L, "kill", hlWindowKill); Internal::setFn(L, "signal", hlWindowSignal); @@ -1255,6 +1259,7 @@ void Internal::registerDispatcherBindings(lua_State* L) { lua_setfield(L, -2, "window"); lua_newtable(L); + Internal::markDispatcherTable(L); Internal::setFn(L, "rename", hlWorkspaceRename); Internal::setFn(L, "move", hlWorkspaceMove); Internal::setFn(L, "swap_monitors", hlWorkspaceSwapMonitors); diff --git a/src/config/lua/bindings/LuaBindingsInternal.cpp b/src/config/lua/bindings/LuaBindingsInternal.cpp index e0cef1464..5f1958051 100644 --- a/src/config/lua/bindings/LuaBindingsInternal.cpp +++ b/src/config/lua/bindings/LuaBindingsInternal.cpp @@ -449,11 +449,6 @@ CA::eTogglableAction Internal::tableToggleAction(lua_State* L, int idx, const ch return CA::TOGGLE_ACTION_TOGGLE; } -void Internal::setFn(lua_State* L, const char* name, lua_CFunction fn) { - lua_pushcfunction(L, fn); - lua_setfield(L, -2, name); -} - void Internal::setMgrFn(lua_State* L, CConfigManager* mgr, const char* name, lua_CFunction fn) { lua_pushlightuserdata(L, mgr); lua_pushcclosure(L, fn, 1); diff --git a/src/config/lua/bindings/LuaBindingsInternal.hpp b/src/config/lua/bindings/LuaBindingsInternal.hpp index 024d72489..9b61360cb 100644 --- a/src/config/lua/bindings/LuaBindingsInternal.hpp +++ b/src/config/lua/bindings/LuaBindingsInternal.hpp @@ -181,6 +181,9 @@ namespace Config::Lua::Bindings::Internal { void setFn(lua_State* L, const char* name, lua_CFunction fn); void setMgrFn(lua_State* L, CConfigManager* mgr, const char* name, lua_CFunction fn); + void markDispatcherTable(lua_State* L); + int wrapDispatcher(lua_State* L); + bool pushDispatcherFunction(lua_State* L, int idx); template SParseError parseTableField(lua_State* L, int tableIdx, const char* field, T& parser) { diff --git a/src/config/lua/bindings/LuaBindingsToplevel.cpp b/src/config/lua/bindings/LuaBindingsToplevel.cpp index 2d54a7b3c..1b345cdef 100644 --- a/src/config/lua/bindings/LuaBindingsToplevel.cpp +++ b/src/config/lua/bindings/LuaBindingsToplevel.cpp @@ -132,13 +132,12 @@ static int hlBind(lua_State* L) { if (auto res = parseKeyString(kb, keys); !res) return Internal::configError(L, std::format("hl.bind: failed to parse key string: {}", res.error())); - if (!lua_isfunction(L, 2)) + if (!Internal::pushDispatcherFunction(L, 2)) return Internal::configError(L, "hl.bind: dispatcher must be a dispatcher (e.g. hl.dsp.window.close()) or a lua function"); if (kb.catchAll && mgr->m_currentSubmap.empty()) return Internal::configError(L, "hl.bind: catchall keybinds are only allowed in submaps."); - lua_pushvalue(L, 2); int ref = luaL_ref(L, LUA_REGISTRYINDEX); kb.handler = "__lua"; kb.arg = std::to_string(ref); @@ -293,10 +292,9 @@ static int hlExecCmd(lua_State* L) { } static int hlDispatch(lua_State* L) { - if (!lua_isfunction(L, 1)) - return Internal::configError(L, "hl.dispatch: expected a dispatcher function (e.g. hl.dsp.window.close())"); + if (!Internal::pushDispatcherFunction(L, 1)) + return Internal::configError(L, "hl.dispatch: expected a dispatcher (e.g. hl.dsp.window.close())"); - lua_pushvalue(L, 1); int status = LUA_OK; if (auto* mgr = CConfigManager::fromLuaState(L); mgr) status = mgr->guardedPCall(0, 1, 0, CConfigManager::LUA_TIMEOUT_DISPATCH_MS, "hl.dispatch");