Parent directory

dispatchers.cpp

31616 bytes
  1#include <optional>
  2#include <string>
  3#include <string_view>
  4
  5#include <hyprland/src/Compositor.hpp>
  6#include <hyprland/src/desktop/DesktopTypes.hpp>
  7#include <hyprland/src/plugins/PluginAPI.hpp>
  8#include <hyprutils/string/String.hpp>
  9#include <hyprutils/string/VarList.hpp>
 10#include <hyprland/src/config/lua/LuaBindings.hpp>
 11#include <hyprland/src/config/lua/bindings/LuaBindingsInternal.hpp>
 12#include <hyprland/src/managers/fullscreen/FullscreenController.hpp>
 13
 14#include "dispatchers.hpp"
 15#include "log.hpp"
 16#include "globals.hpp"
 17#include "src/SharedDefs.hpp"
 18
 19using Hyprutils::String::CVarList;
 20namespace LuaInternal = Config::Lua::Bindings::Internal;
 21
 22static Hy3Layout* hy3InstanceForAction(bool allow_fullscreen = false) {
 23	auto workspace = workspace_for_action(allow_fullscreen);
 24	if (!valid(workspace)) return nullptr;
 25	return hy3InstanceForWorkspace(workspace);
 26}
 27
 28static void luaCheckArgCount(lua_State* L, const char* fn, int min, int max) {
 29	const int count = lua_gettop(L);
 30	if (count < min || count > max) {
 31		if (min == max)
 32			luaL_error(L, "%s: expected %d argument(s), got %d", fn, min, count);
 33		else
 34			luaL_error(L, "%s: expected %d-%d argument(s), got %d", fn, min, max, count);
 35	}
 36}
 37
 38static bool luaHasOptionsTable(lua_State* L, int idx, const char* fn) {
 39	if (lua_isnoneornil(L, idx)) return false;
 40	if (!lua_istable(L, idx))
 41		luaL_error(L, "%s: options must be a table", fn);
 42	return true;
 43}
 44
 45static std::string luaStringArg(lua_State* L, int idx, const char* fn, const char* name) {
 46	if (lua_isnoneornil(L, idx)) {
 47		luaL_error(L, "%s: missing required argument '%s'", fn, name);
 48		return {};
 49	}
 50
 51	if (!lua_isstring(L, idx)) {
 52		luaL_error(L, "%s: argument '%s' must be a string", fn, name);
 53		return {};
 54	}
 55
 56	return lua_tostring(L, idx);
 57}
 58
 59static std::optional<bool> luaBoolLikeArg(lua_State* L, int idx, const char* fn, const char* name) {
 60	if (lua_isboolean(L, idx)) return lua_toboolean(L, idx);
 61
 62	if (lua_isstring(L, idx)) {
 63		std::string value = lua_tostring(L, idx);
 64		if (value == "true") return true;
 65		if (value == "false") return false;
 66	}
 67
 68	luaL_error(L, "%s: argument '%s' must be a boolean", fn, name);
 69	return {};
 70}
 71
 72static int luaOptionalBoolMode(std::optional<bool> value) {
 73	if (!value) return -1;
 74	return *value ? 1 : 0;
 75}
 76
 77static std::optional<bool> luaOptionalBoolFromUpvalue(lua_State* L, int idx) {
 78	auto mode = static_cast<int>(lua_tointeger(L, lua_upvalueindex(idx)));
 79	if (mode < 0) return {};
 80	return mode != 0;
 81}
 82
 83struct SMakeGroupAction {
 84	Hy3GroupLayout layout = Hy3GroupLayout::SplitH;
 85	bool opposite = false;
 86};
 87
 88static std::optional<SMakeGroupAction> parseMakeGroupArg(std::string_view arg) {
 89	if (arg == "h") return SMakeGroupAction { .layout = Hy3GroupLayout::SplitH };
 90	if (arg == "v") return SMakeGroupAction { .layout = Hy3GroupLayout::SplitV };
 91	if (arg == "tab") return SMakeGroupAction { .layout = Hy3GroupLayout::Tabbed };
 92	if (arg == "opposite") return SMakeGroupAction { .opposite = true };
 93	return {};
 94}
 95
 96static SMakeGroupAction luaMakeGroupActionArg(lua_State* L, int idx, const char* fn) {
 97	auto value = luaStringArg(L, idx, fn, "layout");
 98	auto action = parseMakeGroupArg(value);
 99	if (!action)
100		luaL_error(L, "%s: invalid layout '%s' (expected h/v/tab/opposite)", fn, value.c_str());
101	return *action;
102}
103
104static GroupEphemeralityOption luaTableEphemerality(lua_State* L, int idx, const char* fn) {
105	idx = lua_absindex(L, idx);
106
107	lua_getfield(L, idx, "ephemeral");
108	if (lua_isnil(L, -1)) {
109		lua_pop(L, 1);
110		return GroupEphemeralityOption::Standard;
111	}
112
113	if (lua_isboolean(L, -1)) {
114		const bool ephemeral = lua_toboolean(L, -1);
115		lua_pop(L, 1);
116		return ephemeral ? GroupEphemeralityOption::Ephemeral : GroupEphemeralityOption::Standard;
117	}
118
119	if (lua_isstring(L, -1)) {
120		std::string value = lua_tostring(L, -1);
121		lua_pop(L, 1);
122		if (value == "force") return GroupEphemeralityOption::ForceEphemeral;
123		luaL_error(L, "%s: invalid ephemeral option '%s' (expected true/false/\"force\")", fn, value.c_str());
124		return GroupEphemeralityOption::Standard;
125	}
126
127	lua_pop(L, 1);
128	luaL_error(L, "%s: ephemeral must be true, false, or \"force\"", fn);
129	return GroupEphemeralityOption::Standard;
130}
131
132static SDispatchResult makeGroup(SMakeGroupAction action, GroupEphemeralityOption ephemeral, bool toggle) {
133	auto* hy3 = hy3InstanceForAction();
134	if (!hy3) return SDispatchResult {};
135	auto ws = hy3->workspace();
136
137	if (action.opposite) {
138		hy3->makeOppositeGroupOnWorkspace(ws.get(), ephemeral);
139	} else {
140		hy3->makeGroupOnWorkspace(ws.get(), action.layout, ephemeral, toggle);
141	}
142
143	return SDispatchResult {};
144}
145
146static int luaMakeGroup(lua_State* L) {
147	static constexpr const char* FN = "hl.plugin.hy3.make_group";
148	luaCheckArgCount(L, FN, 1, 2);
149
150	auto action = luaMakeGroupActionArg(L, 1, FN);
151	bool toggle = false;
152	auto ephemeral = GroupEphemeralityOption::Standard;
153
154	if (luaHasOptionsTable(L, 2, FN)) {
155		toggle = LuaInternal::tableOptBool(L, 2, "toggle").value_or(false);
156		ephemeral = luaTableEphemerality(L, 2, FN);
157	}
158
159	auto dspMakeGroup = [](lua_State* L) -> int {
160		SMakeGroupAction action {
161			.layout = static_cast<Hy3GroupLayout>(lua_tointeger(L, lua_upvalueindex(1))),
162			.opposite = static_cast<bool>(lua_toboolean(L, lua_upvalueindex(2))),
163		};
164		auto ephemeral = static_cast<GroupEphemeralityOption>(lua_tointeger(L, lua_upvalueindex(3)));
165		bool toggle = lua_toboolean(L, lua_upvalueindex(4));
166		makeGroup(action, ephemeral, toggle);
167		return 0;
168	};
169
170	lua_pushinteger(L, static_cast<lua_Integer>(action.layout));
171	lua_pushboolean(L, action.opposite);
172	lua_pushinteger(L, static_cast<lua_Integer>(ephemeral));
173	lua_pushboolean(L, toggle);
174	lua_pushcclosure(L, dspMakeGroup, 4);
175	return 1;
176}
177
178static SDispatchResult dispatch_makegroup(std::string value) {
179	auto args = CVarList(value);
180
181	auto action = parseMakeGroupArg(args[0]);
182	if (!action) return SDispatchResult {};
183
184	auto toggle = args[1] == "toggle";
185	auto i = toggle ? 2 : 1;
186
187	GroupEphemeralityOption ephemeral = GroupEphemeralityOption::Standard;
188	if (args[i] == "ephemeral") {
189		ephemeral = GroupEphemeralityOption::Ephemeral;
190	} else if (args[i] == "force_ephemeral") {
191		ephemeral = GroupEphemeralityOption::ForceEphemeral;
192	}
193
194	return makeGroup(*action, ephemeral, toggle);
195}
196
197enum class ChangeGroupAction {
198	SplitH,
199	SplitV,
200	Tabbed,
201	Untab,
202	ToggleTab,
203	Opposite,
204};
205
206static std::optional<ChangeGroupAction> parseChangeGroupArg(std::string_view arg) {
207	if (arg == "h") return ChangeGroupAction::SplitH;
208	if (arg == "v") return ChangeGroupAction::SplitV;
209	if (arg == "tab") return ChangeGroupAction::Tabbed;
210	if (arg == "untab") return ChangeGroupAction::Untab;
211	if (arg == "toggletab") return ChangeGroupAction::ToggleTab;
212	if (arg == "opposite") return ChangeGroupAction::Opposite;
213	return {};
214}
215
216static ChangeGroupAction luaChangeGroupActionArg(lua_State* L, int idx, const char* fn) {
217	auto value = luaStringArg(L, idx, fn, "layout");
218	auto action = parseChangeGroupArg(value);
219	if (!action)
220		luaL_error(L, "%s: invalid layout '%s' (expected h/v/tab/untab/toggletab/opposite)", fn, value.c_str());
221	return *action;
222}
223
224static SDispatchResult changeGroup(ChangeGroupAction action) {
225	auto* hy3 = hy3InstanceForAction();
226	if (!hy3) return SDispatchResult {};
227	auto ws = hy3->workspace();
228
229	switch (action) {
230	case ChangeGroupAction::SplitH:
231		hy3->changeGroupOnWorkspace(ws.get(), Hy3GroupLayout::SplitH);
232		break;
233	case ChangeGroupAction::SplitV:
234		hy3->changeGroupOnWorkspace(ws.get(), Hy3GroupLayout::SplitV);
235		break;
236	case ChangeGroupAction::Tabbed:
237		hy3->changeGroupOnWorkspace(ws.get(), Hy3GroupLayout::Tabbed);
238		break;
239	case ChangeGroupAction::Untab:
240		hy3->untabGroupOnWorkspace(ws.get());
241		break;
242	case ChangeGroupAction::ToggleTab:
243		hy3->toggleTabGroupOnWorkspace(ws.get());
244		break;
245	case ChangeGroupAction::Opposite:
246		hy3->changeGroupToOppositeOnWorkspace(ws.get());
247		break;
248	}
249
250	return SDispatchResult {};
251}
252
253static int luaChangeGroup(lua_State* L) {
254	static constexpr const char* FN = "hl.plugin.hy3.change_group";
255	luaCheckArgCount(L, FN, 1, 1);
256
257	auto dspChangeGroup = [](lua_State* L) -> int {
258		auto action = static_cast<ChangeGroupAction>(lua_tointeger(L, lua_upvalueindex(1)));
259		changeGroup(action);
260		return 0;
261	};
262
263	lua_pushinteger(L, static_cast<lua_Integer>(luaChangeGroupActionArg(L, 1, FN)));
264	lua_pushcclosure(L, dspChangeGroup, 1);
265	return 1;
266}
267
268static SDispatchResult dispatch_changegroup(std::string value) {
269	auto args = CVarList(value);
270	auto action = parseChangeGroupArg(args[0]);
271	if (!action) return SDispatchResult {};
272	return changeGroup(*action);
273}
274
275static SDispatchResult setEphemeral(bool ephemeral) {
276	auto* hy3 = hy3InstanceForAction();
277	if (!hy3) return SDispatchResult {};
278
279	hy3->changeGroupEphemeralityOnWorkspace(hy3->workspace().get(), ephemeral);
280	return SDispatchResult {};
281}
282
283static int luaSetEphemeral(lua_State* L) {
284	static constexpr const char* FN = "hl.plugin.hy3.set_ephemeral";
285	luaCheckArgCount(L, FN, 1, 1);
286
287	auto ephemeral = luaBoolLikeArg(L, 1, FN, "value");
288
289	auto dspSetEphemeral = [](lua_State* L) -> int {
290		setEphemeral(lua_toboolean(L, lua_upvalueindex(1)));
291		return 0;
292	};
293
294	lua_pushboolean(L, ephemeral.value_or(false));
295	lua_pushcclosure(L, dspSetEphemeral, 1);
296	return 1;
297}
298
299static SDispatchResult dispatch_setephemeral(std::string value) {
300	auto args = CVarList(value);
301	return setEphemeral(args[0] == "true");
302}
303
304std::optional<ShiftDirection> parseShiftArg(std::string arg) {
305	if (arg == "l" || arg == "left") return ShiftDirection::Left;
306	else if (arg == "r" || arg == "right") return ShiftDirection::Right;
307	else if (arg == "u" || arg == "up") return ShiftDirection::Up;
308	else if (arg == "d" || arg == "down") return ShiftDirection::Down;
309	else return {};
310}
311
312static ShiftDirection luaShiftArg(lua_State* L, int idx, const char* fn) {
313	auto value = luaStringArg(L, idx, fn, "direction");
314	auto shift = parseShiftArg(value);
315	if (!shift)
316		luaL_error(L, "%s: invalid direction '%s' (expected left/right/up/down)", fn, value.c_str());
317	return *shift;
318}
319
320static SDispatchResult moveFocus(ShiftDirection shift, bool visible, std::optional<bool> warp_override) {
321	auto* hy3 = hy3InstanceForAction(true);
322	if (!hy3) return SDispatchResult {};
323	auto ws = hy3->workspace();
324
325	static const auto no_cursor_warps = CConfigValue<Config::INTEGER>("cursor:no_warps");
326	auto warp_cursor = warp_override.value_or(!*no_cursor_warps);
327
328	if (Fullscreen::controller()->hasFullscreen(ws)) {
329		hy3->focusMonitor(shift);
330		return SDispatchResult {};
331	}
332
333	hy3->shiftFocus(ws.get(), shift, visible, warp_cursor);
334	return SDispatchResult {};
335}
336
337static int luaMoveFocus(lua_State* L) {
338	static constexpr const char* FN = "hl.plugin.hy3.move_focus";
339	luaCheckArgCount(L, FN, 1, 2);
340
341	auto shift = luaShiftArg(L, 1, FN);
342	bool visible = false;
343	std::optional<bool> warp;
344
345	if (luaHasOptionsTable(L, 2, FN)) {
346		visible = LuaInternal::tableOptBool(L, 2, "visible").value_or(false);
347		warp = LuaInternal::tableOptBool(L, 2, "warp");
348	}
349
350	auto dspMoveFocus = [](lua_State* L) -> int {
351		auto shift = static_cast<ShiftDirection>(lua_tointeger(L, lua_upvalueindex(1)));
352		bool visible = lua_toboolean(L, lua_upvalueindex(2));
353		moveFocus(shift, visible, luaOptionalBoolFromUpvalue(L, 3));
354		return 0;
355	};
356
357	lua_pushinteger(L, static_cast<lua_Integer>(shift));
358	lua_pushboolean(L, visible);
359	lua_pushinteger(L, luaOptionalBoolMode(warp));
360	lua_pushcclosure(L, dspMoveFocus, 3);
361	return 1;
362}
363
364static SDispatchResult dispatch_movefocus(std::string value) {
365	auto args = CVarList(value);
366
367	int argi = 0;
368	auto shift = parseShiftArg(args[argi++]);
369	if (!shift) return SDispatchResult {};
370
371	auto visible = args[argi] == "visible";
372	if (visible) argi++;
373
374	std::optional<bool> warp_override;
375	if (args[argi] == "nowarp") warp_override = false;
376	else if (args[argi] == "warp") warp_override = true;
377
378	return moveFocus(*shift, visible, warp_override);
379}
380
381static SDispatchResult toggleFocusLayer(bool warp) {
382	auto* hy3 = hy3InstanceForAction();
383	if (!hy3) return SDispatchResult {};
384
385	hy3->toggleFocusLayer(hy3->workspace().get(), warp);
386	return SDispatchResult {};
387}
388
389static int luaToggleFocusLayer(lua_State* L) {
390	static constexpr const char* FN = "hl.plugin.hy3.toggle_focus_layer";
391	luaCheckArgCount(L, FN, 0, 1);
392
393	bool warp = true;
394	if (luaHasOptionsTable(L, 1, FN))
395		warp = LuaInternal::tableOptBool(L, 1, "warp").value_or(true);
396
397	auto dspToggleFocusLayer = [](lua_State* L) -> int {
398		toggleFocusLayer(lua_toboolean(L, lua_upvalueindex(1)));
399		return 0;
400	};
401
402	lua_pushboolean(L, warp);
403	lua_pushcclosure(L, dspToggleFocusLayer, 1);
404	return 1;
405}
406
407static SDispatchResult dispatch_togglefocuslayer(std::string value) {
408	return toggleFocusLayer(value != "nowarp");
409}
410
411static SDispatchResult warpCursor() {
412	auto* hy3 = hy3InstanceForAction(true);
413	if (!hy3) return SDispatchResult {};
414
415	hy3->warpCursor();
416	return SDispatchResult {};
417}
418
419static int luaWarpCursor(lua_State* L) {
420	static constexpr const char* FN = "hl.plugin.hy3.warp_cursor";
421	luaCheckArgCount(L, FN, 0, 0);
422
423	auto dspWarpCursor = [](lua_State* L) -> int {
424		warpCursor();
425		return 0;
426	};
427
428	lua_pushcclosure(L, dspWarpCursor, 0);
429	return 1;
430}
431
432static SDispatchResult dispatch_warpcursor(std::string value) {
433	return warpCursor();
434}
435
436static SDispatchResult moveWindow(ShiftDirection shift, bool once, bool visible) {
437	auto* hy3 = hy3InstanceForAction();
438	if (!hy3) return SDispatchResult {};
439
440	hy3->shiftWindow(hy3->workspace().get(), shift, once, visible);
441	return SDispatchResult {};
442}
443
444static int luaMoveWindow(lua_State* L) {
445	static constexpr const char* FN = "hl.plugin.hy3.move_window";
446	luaCheckArgCount(L, FN, 1, 2);
447
448	auto shift = luaShiftArg(L, 1, FN);
449	bool once = false;
450	bool visible = false;
451
452	if (luaHasOptionsTable(L, 2, FN)) {
453		once = LuaInternal::tableOptBool(L, 2, "once").value_or(false);
454		visible = LuaInternal::tableOptBool(L, 2, "visible").value_or(false);
455	}
456
457	auto dspMoveWindow = [](lua_State* L) -> int {
458		auto shift = static_cast<ShiftDirection>(lua_tointeger(L, lua_upvalueindex(1)));
459		bool once = lua_toboolean(L, lua_upvalueindex(2));
460		bool visible = lua_toboolean(L, lua_upvalueindex(3));
461		moveWindow(shift, once, visible);
462		return 0;
463	};
464
465	lua_pushinteger(L, static_cast<lua_Integer>(shift));
466	lua_pushboolean(L, once);
467	lua_pushboolean(L, visible);
468	lua_pushcclosure(L, dspMoveWindow, 3);
469	return 1;
470}
471
472static SDispatchResult dispatch_movewindow(std::string value) {
473	auto args = CVarList(value);
474	auto shift = parseShiftArg(args[0]);
475	if (!shift) return SDispatchResult {};
476
477	int i = 1;
478	bool once = false;
479	bool visible = false;
480
481	if (args[i] == "once") {
482		once = true;
483		i++;
484	}
485
486	if (args[i] == "visible") {
487		visible = true;
488		i++;
489	}
490
491	return moveWindow(*shift, once, visible);
492}
493
494static SDispatchResult moveToWorkspace(std::string workspace, bool follow, std::optional<bool> warp_override) {
495	auto* hy3 = hy3InstanceForAction(true);
496	if (!hy3) return SDispatchResult {};
497
498	static const auto no_cursor_warps = CConfigValue<Config::INTEGER>("cursor:no_warps");
499
500	auto warp_cursor = follow && warp_override.value_or(!*no_cursor_warps);
501
502	hy3->moveNodeToWorkspace(hy3->workspace().get(), workspace, follow, warp_cursor);
503	return SDispatchResult {};
504}
505
506static int luaMoveToWorkspace(lua_State* L) {
507	static constexpr const char* FN = "hl.plugin.hy3.move_to_workspace";
508	luaCheckArgCount(L, FN, 1, 2);
509
510	auto workspace = luaStringArg(L, 1, FN, "workspace");
511	if (workspace == "")
512		return luaL_error(L, "%s: workspace must not be empty", FN);
513
514	bool follow = false;
515	std::optional<bool> warp;
516
517	if (luaHasOptionsTable(L, 2, FN)) {
518		follow = LuaInternal::tableOptBool(L, 2, "follow").value_or(false);
519		warp = LuaInternal::tableOptBool(L, 2, "warp");
520	}
521
522	auto dspMoveToWorkspace = [](lua_State* L) -> int {
523		std::string workspace = lua_tostring(L, lua_upvalueindex(1));
524		bool follow = lua_toboolean(L, lua_upvalueindex(2));
525		moveToWorkspace(workspace, follow, luaOptionalBoolFromUpvalue(L, 3));
526		return 0;
527	};
528
529	lua_pushstring(L, workspace.c_str());
530	lua_pushboolean(L, follow);
531	lua_pushinteger(L, luaOptionalBoolMode(warp));
532	lua_pushcclosure(L, dspMoveToWorkspace, 3);
533	return 1;
534}
535
536static SDispatchResult dispatch_move_to_workspace(std::string value) {
537	auto args = CVarList(value);
538
539	auto workspace = args[0];
540	if (workspace == "") return SDispatchResult {};
541
542	auto follow = args[1] == "follow";
543
544	std::optional<bool> warp_override;
545	if (args[2] == "nowarp") warp_override = false;
546	else if (args[2] == "warp") warp_override = true;
547
548	return moveToWorkspace(workspace, follow, warp_override);
549}
550
551static std::optional<FocusShift> parseFocusShiftArg(std::string_view arg) {
552	if (arg == "top") return FocusShift::Top;
553	if (arg == "bottom") return FocusShift::Bottom;
554	if (arg == "raise") return FocusShift::Raise;
555	if (arg == "lower") return FocusShift::Lower;
556	if (arg == "tab") return FocusShift::Tab;
557	if (arg == "tabnode") return FocusShift::TabNode;
558	return {};
559}
560
561static FocusShift luaFocusShiftArg(lua_State* L, int idx, const char* fn) {
562	auto value = luaStringArg(L, idx, fn, "mode");
563	auto shift = parseFocusShiftArg(value);
564	if (!shift)
565		luaL_error(L, "%s: invalid mode '%s'", fn, value.c_str());
566	return *shift;
567}
568
569static SDispatchResult changeFocus(FocusShift shift) {
570	auto* hy3 = hy3InstanceForAction();
571	if (!hy3) return SDispatchResult {};
572	auto ws = hy3->workspace();
573
574	hy3->changeFocus(ws.get(), shift);
575	return SDispatchResult {};
576}
577
578static int luaChangeFocus(lua_State* L) {
579	static constexpr const char* FN = "hl.plugin.hy3.change_focus";
580	luaCheckArgCount(L, FN, 1, 1);
581
582	auto dspChangeFocus = [](lua_State* L) -> int {
583		auto shift = static_cast<FocusShift>(lua_tointeger(L, lua_upvalueindex(1)));
584		changeFocus(shift);
585		return 0;
586	};
587
588	lua_pushinteger(L, static_cast<lua_Integer>(luaFocusShiftArg(L, 1, FN)));
589	lua_pushcclosure(L, dspChangeFocus, 1);
590	return 1;
591}
592
593static SDispatchResult dispatch_changefocus(std::string arg) {
594	auto shift = parseFocusShiftArg(arg);
595	if (!shift) return SDispatchResult {};
596	return changeFocus(*shift);
597}
598
599static std::optional<TabFocusMousePriority> parseTabMouseArg(std::string_view arg) {
600	if (arg == "" || arg == "ignore") return TabFocusMousePriority::Ignore;
601	if (arg == "prioritize_hovered") return TabFocusMousePriority::Prioritize;
602	if (arg == "require_hovered") return TabFocusMousePriority::Require;
603	return {};
604}
605
606static SDispatchResult focusTab(TabFocus focus, TabFocusMousePriority mouse, bool wrap_scroll, int index) {
607	auto* hy3 = hy3InstanceForAction();
608	if (!hy3) return SDispatchResult {};
609	auto ws = hy3->workspace();
610
611	hy3->focusTab(ws.get(), focus, mouse, wrap_scroll, index);
612	return SDispatchResult {};
613}
614
615static int luaFocusTab(lua_State* L) {
616	static constexpr const char* FN = "hl.plugin.hy3.focus_tab";
617	luaCheckArgCount(L, FN, 1, 1);
618	if (!lua_istable(L, 1))
619		return luaL_error(L, "%s: expected a table", FN);
620
621	auto index = LuaInternal::tableOptNum(L, 1, "index");
622	auto direction = LuaInternal::tableOptStr(L, 1, "direction");
623	if (index && direction)
624		return luaL_error(L, "%s: options 'index' and 'direction' are mutually exclusive", FN);
625
626	TabFocus focus;
627	int focus_index = 0;
628
629	if (index) {
630		focus = TabFocus::Index;
631		focus_index = static_cast<int>(*index);
632	} else {
633		if (!direction)
634			return luaL_error(L, "%s: expected either 'direction' or 'index'", FN);
635		if (*direction == "l" || *direction == "left") focus = TabFocus::Left;
636		else if (*direction == "r" || *direction == "right") focus = TabFocus::Right;
637		else
638			return luaL_error(L, "%s: invalid direction '%s' (expected left/right)", FN, direction->c_str());
639	}
640
641	auto mouse = TabFocusMousePriority::Ignore;
642	if (auto mouse_arg = LuaInternal::tableOptStr(L, 1, "mouse")) {
643		auto parsed = parseTabMouseArg(*mouse_arg);
644		if (!parsed)
645			return luaL_error(L, "%s: invalid mouse option '%s'", FN, mouse_arg->c_str());
646		mouse = *parsed;
647	}
648
649	bool wrap = LuaInternal::tableOptBool(L, 1, "wrap").value_or(false);
650
651	auto dspFocusTab = [](lua_State* L) -> int {
652		auto focus = static_cast<TabFocus>(lua_tointeger(L, lua_upvalueindex(1)));
653		auto mouse = static_cast<TabFocusMousePriority>(lua_tointeger(L, lua_upvalueindex(2)));
654		bool wrap = lua_toboolean(L, lua_upvalueindex(3));
655		int index = static_cast<int>(lua_tointeger(L, lua_upvalueindex(4)));
656		focusTab(focus, mouse, wrap, index);
657		return 0;
658	};
659
660	lua_pushinteger(L, static_cast<lua_Integer>(focus));
661	lua_pushinteger(L, static_cast<lua_Integer>(mouse));
662	lua_pushboolean(L, wrap);
663	lua_pushinteger(L, focus_index);
664	lua_pushcclosure(L, dspFocusTab, 4);
665	return 1;
666}
667
668static SDispatchResult dispatch_focustab(std::string value) {
669	auto i = 0;
670	auto args = CVarList(value);
671
672	TabFocus focus;
673	auto mouse = TabFocusMousePriority::Ignore;
674	bool wrap_scroll = false;
675	int index = 0;
676
677	if (args[i] == "l" || args[i] == "left") focus = TabFocus::Left;
678	else if (args[i] == "r" || args[i] == "right") focus = TabFocus::Right;
679	else if (args[i] == "index") {
680		i++;
681		focus = TabFocus::Index;
682		if (!Hyprutils::String::isNumber(args[i])) return SDispatchResult {};
683		index = std::stoi(args[i]);
684		hy3_log(LOG, "Focus index '%s' -> %d, errno: %d", args[i].c_str(), index, errno);
685	} else return SDispatchResult {};
686
687	i++;
688
689	if (auto parsed_mouse = parseTabMouseArg(args[i]); parsed_mouse && *parsed_mouse != TabFocusMousePriority::Ignore) {
690		mouse = *parsed_mouse;
691		i++;
692	}
693
694	if (args[i++] == "wrap") wrap_scroll = true;
695
696	return focusTab(focus, mouse, wrap_scroll, index);
697}
698
699static std::optional<SetSwallowOption> parseSetSwallowArg(std::string_view arg) {
700	if (arg == "true") return SetSwallowOption::Swallow;
701	if (arg == "false") return SetSwallowOption::NoSwallow;
702	if (arg == "toggle") return SetSwallowOption::Toggle;
703	return {};
704}
705
706static SetSwallowOption luaSetSwallowArg(lua_State* L, int idx, const char* fn) {
707	if (lua_isboolean(L, idx))
708		return lua_toboolean(L, idx) ? SetSwallowOption::Swallow : SetSwallowOption::NoSwallow;
709
710	auto value = luaStringArg(L, idx, fn, "value");
711	auto option = parseSetSwallowArg(value);
712	if (!option)
713		luaL_error(L, "%s: invalid value '%s' (expected true/false/toggle)", fn, value.c_str());
714	return *option;
715}
716
717static SDispatchResult setSwallow(SetSwallowOption option) {
718	auto* hy3 = hy3InstanceForAction();
719	if (!hy3) return SDispatchResult {};
720
721	hy3->setNodeSwallow(hy3->workspace().get(), option);
722	return SDispatchResult {};
723}
724
725static int luaSetSwallow(lua_State* L) {
726	static constexpr const char* FN = "hl.plugin.hy3.set_swallow";
727	luaCheckArgCount(L, FN, 1, 1);
728
729	auto dspSetSwallow = [](lua_State* L) -> int {
730		auto option = static_cast<SetSwallowOption>(lua_tointeger(L, lua_upvalueindex(1)));
731		setSwallow(option);
732		return 0;
733	};
734
735	lua_pushinteger(L, static_cast<lua_Integer>(luaSetSwallowArg(L, 1, FN)));
736	lua_pushcclosure(L, dspSetSwallow, 1);
737	return 1;
738}
739
740static SDispatchResult dispatch_setswallow(std::string arg) {
741	auto option = parseSetSwallowArg(arg);
742	if (!option) return SDispatchResult {};
743	return setSwallow(*option);
744}
745
746static SDispatchResult killActive() {
747	auto* hy3 = hy3InstanceForAction(true);
748	if (!hy3) return SDispatchResult {};
749
750	hy3->killFocusedNode(hy3->workspace().get());
751	return SDispatchResult {};
752}
753
754static int luaKillActive(lua_State* L) {
755	static constexpr const char* FN = "hl.plugin.hy3.kill_active";
756	luaCheckArgCount(L, FN, 0, 0);
757
758	auto dspKillActive = [](lua_State* L) -> int {
759		killActive();
760		return 0;
761	};
762
763	lua_pushcclosure(L, dspKillActive, 0);
764	return 1;
765}
766
767static SDispatchResult dispatch_killactive(std::string value) {
768	return killActive();
769}
770
771static std::optional<ExpandOption> parseExpandArg(std::string_view arg) {
772	if (arg == "expand") return ExpandOption::Expand;
773	if (arg == "shrink") return ExpandOption::Shrink;
774	if (arg == "base") return ExpandOption::Base;
775	if (arg == "maximize") return ExpandOption::Maximize;
776	if (arg == "fullscreen") return ExpandOption::Fullscreen;
777	return {};
778}
779
780static std::optional<ExpandFullscreenOption> parseExpandFullscreenArg(std::string_view arg) {
781	if (arg == "" || arg == "intermediate_maximize") return ExpandFullscreenOption::MaximizeIntermediate;
782	if (arg == "fullscreen_maximize") return ExpandFullscreenOption::MaximizeAsFullscreen;
783	if (arg == "maximize_only") return ExpandFullscreenOption::MaximizeOnly;
784	return {};
785}
786
787static SDispatchResult expand(ExpandOption expand, ExpandFullscreenOption fs_expand) {
788	auto* hy3 = hy3InstanceForAction();
789	if (!hy3) return SDispatchResult {};
790
791	hy3->expand(hy3->workspace().get(), expand, fs_expand);
792	return SDispatchResult {};
793}
794
795static int luaExpand(lua_State* L) {
796	static constexpr const char* FN = "hl.plugin.hy3.expand";
797	luaCheckArgCount(L, FN, 1, 2);
798
799	auto value = luaStringArg(L, 1, FN, "mode");
800	auto option = parseExpandArg(value);
801	if (!option)
802		return luaL_error(L, "%s: invalid mode '%s'", FN, value.c_str());
803
804	auto fs_option = ExpandFullscreenOption::MaximizeIntermediate;
805	if (luaHasOptionsTable(L, 2, FN)) {
806		auto fs_value = LuaInternal::tableOptStr(L, 2, "fullscreen").value_or("intermediate_maximize");
807		auto parsed = parseExpandFullscreenArg(fs_value);
808		if (!parsed)
809			return luaL_error(L, "%s: invalid fullscreen option '%s'", FN, fs_value.c_str());
810		fs_option = *parsed;
811	}
812
813	auto dspExpand = [](lua_State* L) -> int {
814		auto option = static_cast<ExpandOption>(lua_tointeger(L, lua_upvalueindex(1)));
815		auto fs_option = static_cast<ExpandFullscreenOption>(lua_tointeger(L, lua_upvalueindex(2)));
816		expand(option, fs_option);
817		return 0;
818	};
819
820	lua_pushinteger(L, static_cast<lua_Integer>(*option));
821	lua_pushinteger(L, static_cast<lua_Integer>(fs_option));
822	lua_pushcclosure(L, dspExpand, 2);
823	return 1;
824}
825
826static SDispatchResult dispatch_expand(std::string value) {
827	auto args = CVarList(value);
828
829	auto expand_option = parseExpandArg(args[0]);
830	if (!expand_option) return SDispatchResult {};
831
832	auto fs_expand = parseExpandFullscreenArg(args[1]);
833	if (!fs_expand) return SDispatchResult {};
834
835	return expand(*expand_option, *fs_expand);
836}
837
838static std::optional<TabLockMode> parseTabLockModeArg(std::string_view arg) {
839	if (arg == "" || arg == "toggle") return TabLockMode::Toggle;
840	if (arg == "lock") return TabLockMode::Lock;
841	if (arg == "unlock") return TabLockMode::Unlock;
842	return {};
843}
844
845static SDispatchResult lockTab(TabLockMode mode) {
846	auto* hy3 = hy3InstanceForAction();
847	if (!hy3) return SDispatchResult {};
848
849	hy3->setTabLock(hy3->workspace().get(), mode);
850	return SDispatchResult {};
851}
852
853static int luaLockTab(lua_State* L) {
854	static constexpr const char* FN = "hl.plugin.hy3.lock_tab";
855	luaCheckArgCount(L, FN, 0, 1);
856
857	std::string mode_arg;
858	if (!lua_isnoneornil(L, 1))
859		mode_arg = luaStringArg(L, 1, FN, "mode");
860
861	auto mode = parseTabLockModeArg(mode_arg);
862	if (!mode)
863		return luaL_error(L, "%s: invalid mode '%s' (expected lock/unlock/toggle)", FN, mode_arg.c_str());
864
865	auto dspLockTab = [](lua_State* L) -> int {
866		auto mode = static_cast<TabLockMode>(lua_tointeger(L, lua_upvalueindex(1)));
867		lockTab(mode);
868		return 0;
869	};
870
871	lua_pushinteger(L, static_cast<lua_Integer>(*mode));
872	lua_pushcclosure(L, dspLockTab, 1);
873	return 1;
874}
875
876static SDispatchResult dispatch_locktab(std::string arg) {
877	auto mode = parseTabLockModeArg(arg);
878	if (!mode) return SDispatchResult {};
879	return lockTab(*mode);
880}
881
882static SDispatchResult equalize(bool recursive) {
883	auto* hy3 = hy3InstanceForAction();
884	if (!hy3) return SDispatchResult {};
885
886	hy3->equalize(hy3->workspace().get(), recursive);
887	return SDispatchResult {};
888}
889
890static int luaEqualize(lua_State* L) {
891	static constexpr const char* FN = "hl.plugin.hy3.equalize";
892	luaCheckArgCount(L, FN, 0, 1);
893
894	bool recursive = false;
895	if (luaHasOptionsTable(L, 1, FN)) {
896		if (auto scope = LuaInternal::tableOptStr(L, 1, "scope")) {
897			if (*scope == "workspace") recursive = true;
898			else if (*scope == "" || *scope == "group") recursive = false;
899			else
900				return luaL_error(L, "%s: invalid scope '%s' (expected group/workspace)", FN, scope->c_str());
901		}
902
903		if (auto workspace = LuaInternal::tableOptBool(L, 1, "workspace"); workspace)
904			recursive = *workspace;
905		if (auto recursive_opt = LuaInternal::tableOptBool(L, 1, "recursive"); recursive_opt)
906			recursive = *recursive_opt;
907	}
908
909	auto dspEqualize = [](lua_State* L) -> int {
910		equalize(lua_toboolean(L, lua_upvalueindex(1)));
911		return 0;
912	};
913
914	lua_pushboolean(L, recursive);
915	lua_pushcclosure(L, dspEqualize, 1);
916	return 1;
917}
918
919static SDispatchResult dispatch_equalize(std::string arg) {
920	return equalize(arg == "workspace");
921}
922
923static SDispatchResult debugNodes() {
924	auto output = Hy3Layout::debugNodes();
925
926	if (output.empty()) {
927		hy3_log(LOG, "DEBUG NODES: no nodes");
928		return { .success = false, .error = "no nodes" };
929	}
930
931	hy3_log(LOG, "DEBUG NODES\n{}", output);
932	return { .success = false, .error = output };
933}
934
935static int luaDebugNodes(lua_State* L) {
936	static constexpr const char* FN = "hl.plugin.hy3.debug_nodes";
937	luaCheckArgCount(L, FN, 0, 0);
938
939	auto dspDebugNodes = [](lua_State* L) -> int {
940		debugNodes();
941		return 0;
942	};
943
944	lua_pushcclosure(L, dspDebugNodes, 0);
945	return 1;
946}
947
948static SDispatchResult dispatch_debug(std::string arg) {
949	return debugNodes();
950}
951
952static void registerLuaDispatchers() {
953	HyprlandAPI::addLuaFunction(PHANDLE, "hy3", "make_group", luaMakeGroup);
954	HyprlandAPI::addLuaFunction(PHANDLE, "hy3", "change_group", luaChangeGroup);
955	HyprlandAPI::addLuaFunction(PHANDLE, "hy3", "set_ephemeral", luaSetEphemeral);
956	HyprlandAPI::addLuaFunction(PHANDLE, "hy3", "move_focus", luaMoveFocus);
957	HyprlandAPI::addLuaFunction(PHANDLE, "hy3", "toggle_focus_layer", luaToggleFocusLayer);
958	HyprlandAPI::addLuaFunction(PHANDLE, "hy3", "warp_cursor", luaWarpCursor);
959	HyprlandAPI::addLuaFunction(PHANDLE, "hy3", "move_window", luaMoveWindow);
960	HyprlandAPI::addLuaFunction(PHANDLE, "hy3", "move_to_workspace", luaMoveToWorkspace);
961	HyprlandAPI::addLuaFunction(PHANDLE, "hy3", "change_focus", luaChangeFocus);
962	HyprlandAPI::addLuaFunction(PHANDLE, "hy3", "focus_tab", luaFocusTab);
963	HyprlandAPI::addLuaFunction(PHANDLE, "hy3", "set_swallow", luaSetSwallow);
964	HyprlandAPI::addLuaFunction(PHANDLE, "hy3", "kill_active", luaKillActive);
965	HyprlandAPI::addLuaFunction(PHANDLE, "hy3", "expand", luaExpand);
966	HyprlandAPI::addLuaFunction(PHANDLE, "hy3", "lock_tab", luaLockTab);
967	HyprlandAPI::addLuaFunction(PHANDLE, "hy3", "equalize", luaEqualize);
968	HyprlandAPI::addLuaFunction(PHANDLE, "hy3", "debug_nodes", luaDebugNodes);
969}
970
971void registerDispatchers() {
972	HyprlandAPI::addDispatcherV2(PHANDLE, "hy3:makegroup", dispatch_makegroup);
973	HyprlandAPI::addDispatcherV2(PHANDLE, "hy3:changegroup", dispatch_changegroup);
974	HyprlandAPI::addDispatcherV2(PHANDLE, "hy3:setephemeral", dispatch_setephemeral);
975	HyprlandAPI::addDispatcherV2(PHANDLE, "hy3:movefocus", dispatch_movefocus);
976	HyprlandAPI::addDispatcherV2(PHANDLE, "hy3:togglefocuslayer", dispatch_togglefocuslayer);
977	HyprlandAPI::addDispatcherV2(PHANDLE, "hy3:warpcursor", dispatch_warpcursor);
978	HyprlandAPI::addDispatcherV2(PHANDLE, "hy3:movewindow", dispatch_movewindow);
979	HyprlandAPI::addDispatcherV2(PHANDLE, "hy3:movetoworkspace", dispatch_move_to_workspace);
980	HyprlandAPI::addDispatcherV2(PHANDLE, "hy3:changefocus", dispatch_changefocus);
981	HyprlandAPI::addDispatcherV2(PHANDLE, "hy3:focustab", dispatch_focustab);
982	HyprlandAPI::addDispatcherV2(PHANDLE, "hy3:setswallow", dispatch_setswallow);
983	HyprlandAPI::addDispatcherV2(PHANDLE, "hy3:killactive", dispatch_killactive);
984	HyprlandAPI::addDispatcherV2(PHANDLE, "hy3:expand", dispatch_expand);
985	HyprlandAPI::addDispatcherV2(PHANDLE, "hy3:locktab", dispatch_locktab);
986	HyprlandAPI::addDispatcherV2(PHANDLE, "hy3:equalize", dispatch_equalize);
987	HyprlandAPI::addDispatcherV2(PHANDLE, "hy3:debugnodes", dispatch_debug);
988	registerLuaDispatchers();
989}