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}