diff --git a/.clang-tidy b/.clang-tidy index c97a138f3..8d887ec69 100644 --- a/.clang-tidy +++ b/.clang-tidy @@ -201,6 +201,5 @@ CheckOptions: readability-identifier-naming.EnumConstantCase: UPPER_CASE readability-identifier-naming.FunctionCase: camelBack readability-identifier-naming.NamespaceCase: CamelCase - readability-identifier-naming.NamespacePrefix: N readability-identifier-naming.StructPrefix: S readability-identifier-naming.StructCase: CamelCase diff --git a/.github/actions/setup_base/action.yml b/.github/actions/setup_base/action.yml index 9fcaabfbc..20e192f33 100644 --- a/.github/actions/setup_base/action.yml +++ b/.github/actions/setup_base/action.yml @@ -45,6 +45,7 @@ runs: libxkbcommon \ libxkbfile \ lld \ + lua \ meson \ muparser \ ninja \ diff --git a/.github/workflows/nix-test.yml b/.github/workflows/nix-test.yml index 666d971c1..2a47656cd 100644 --- a/.github/workflows/nix-test.yml +++ b/.github/workflows/nix-test.yml @@ -18,7 +18,10 @@ jobs: run: nix build 'github:${{ github.repository }}?ref=${{ github.ref }}#checks.x86_64-linux.tests' -L --extra-substituters "https://hyprland.cachix.org" - name: Check exit status - run: grep 0 result/exit_status + run: | + grep 0 result/exit_status || echo "hyprtester failed" + grep 0 result/exit_status_gtests || echo "gtests failed" + [ 0 = $(cat result/exit_status) ] && [ 0 = $(cat result/exit_status_gtests) ] - name: Upload artifacts if: always() diff --git a/CMakeLists.txt b/CMakeLists.txt index abca4e9b6..c1ffc6b3a 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -34,6 +34,7 @@ if(NOT HYPR_SHADER_GEN_RESULT EQUAL 0) endif() find_package(PkgConfig REQUIRED) +find_package(Python3 COMPONENTS Interpreter QUIET) # Try to find canihavesomecoffee's udis86 using pkgconfig vmd/udis86 does not # provide a .pc file and won't be detected this way @@ -130,7 +131,7 @@ find_package(glslang CONFIG REQUIRED) set(AQUAMARINE_MINIMUM_VERSION 0.9.3) set(HYPRLANG_MINIMUM_VERSION 0.6.7) set(HYPRCURSOR_MINIMUM_VERSION 0.1.7) -set(HYPRUTILS_MINIMUM_VERSION 0.11.1) +set(HYPRUTILS_MINIMUM_VERSION 0.13.0) set(HYPRGRAPHICS_MINIMUM_VERSION 0.5.1) pkg_check_modules(aquamarine_dep REQUIRED IMPORTED_TARGET aquamarine>=${AQUAMARINE_MINIMUM_VERSION}) @@ -268,7 +269,10 @@ pkg_check_modules( gio-2.0 re2 muparser - lcms2) + lcms2 +) + +pkg_search_module(LUA REQUIRED IMPORTED_TARGET GLOBAL lua55 lua5.5 lua-55 lua-5.5 lua>=5.5 lua<5.6) find_package(hyprwayland-scanner 0.3.10 REQUIRED) @@ -286,8 +290,8 @@ add_library(hyprland_lib STATIC ${SRCFILES}) add_executable(Hyprland src/main.cpp ${TRACY_CPP_FILES}) target_link_libraries(Hyprland hyprland_lib) -target_include_directories(hyprland_lib PUBLIC ${deps_INCLUDE_DIRS}) -target_include_directories(Hyprland PUBLIC ${deps_INCLUDE_DIRS}) +target_include_directories(hyprland_lib PUBLIC ${deps_INCLUDE_DIRS} ${LUA_INCLUDE_DIRS}) +target_include_directories(Hyprland PUBLIC ${deps_INCLUDE_DIRS} ${LUA_INCLUDE_DIRS}) set(USE_GPROF OFF) @@ -421,6 +425,7 @@ target_link_libraries( PkgConfig::hyprcursor_dep PkgConfig::hyprgraphics_dep PkgConfig::deps + PkgConfig::LUA ) target_link_libraries( @@ -437,6 +442,28 @@ endif() # used by `make installheaders`, to ensure the headers are generated add_custom_target(generate-protocol-headers) + +if(Python3_Interpreter_FOUND) + add_custom_target( + generate-lua-stubs ALL + COMMAND ${Python3_EXECUTABLE} ${CMAKE_SOURCE_DIR}/meta/generateLuaStubs.py --root ${CMAKE_SOURCE_DIR} + WORKING_DIRECTORY ${CMAKE_SOURCE_DIR} + COMMENT "Generating Lua API stubs for LuaLS" + VERBATIM) + + add_dependencies(hyprland_lib generate-lua-stubs) + add_dependencies(Hyprland generate-lua-stubs) + + add_custom_target( + check-lua-stubs + COMMAND ${Python3_EXECUTABLE} ${CMAKE_SOURCE_DIR}/meta/generateLuaStubs.py --root ${CMAKE_SOURCE_DIR} --check + WORKING_DIRECTORY ${CMAKE_SOURCE_DIR} + COMMENT "Checking Lua API stubs are up to date" + VERBATIM) +else() + message(FATAL_ERROR "Python3 interpreter not found, which is required for the build") +endif() + set(PROTOCOL_SOURCES "") function(protocolnew protoPath protoName external) @@ -605,6 +632,14 @@ install(FILES ${INSTALLABLE_ASSETS} install(FILES ${CMAKE_SOURCE_DIR}/example/hyprland.conf DESTINATION ${CMAKE_INSTALL_DATAROOTDIR}/hypr) +# default lua config +install(FILES ${CMAKE_SOURCE_DIR}/example/hyprland.lua + DESTINATION ${CMAKE_INSTALL_DATAROOTDIR}/hypr) + +# LuaLS stubs +install(FILES ${CMAKE_SOURCE_DIR}/meta/hl.meta.lua + DESTINATION ${CMAKE_INSTALL_DATAROOTDIR}/hypr/stubs) + # portal config install(FILES ${CMAKE_SOURCE_DIR}/assets/hyprland-portals.conf DESTINATION ${CMAKE_INSTALL_DATAROOTDIR}/xdg-desktop-portal) diff --git a/Makefile b/Makefile index c3cd8df04..a03142264 100644 --- a/Makefile +++ b/Makefile @@ -108,4 +108,4 @@ format-fix: test: $(MAKE) debug - ./build/hyprtester/hyprtester -c hyprtester/test.conf -b ./build/Hyprland -p hyprtester/plugin/hyprtestplugin.so + ./build/hyprtester/hyprtester -c hyprtester/test.lua -b ./build/Hyprland -p hyprtester/plugin/hyprtestplugin.so diff --git a/docs/hyprctl.1 b/docs/hyprctl.1 index 61cb84fdb..dc00db1b6 100644 --- a/docs/hyprctl.1 +++ b/docs/hyprctl.1 @@ -124,6 +124,12 @@ Lists all the layers. .PP Returns the current random splash. .RE +.PP +\f[B]status\f[R] +.RS +.PP +Returns internal status information like config format or backend. +.RE .SH OPTIONS .PP \f[B]--batch\f[R] diff --git a/docs/hyprctl.1.rst b/docs/hyprctl.1.rst index 4db0807e2..bc5a9ddcd 100644 --- a/docs/hyprctl.1.rst +++ b/docs/hyprctl.1.rst @@ -88,6 +88,10 @@ INFO COMMANDS Returns the current random splash. +**status** + + Returns internal status information like config format or backend. + OPTIONS ======= diff --git a/example/hyprland.conf b/example/hyprland.conf index 15b0e4f7b..cd50c3e2a 100644 --- a/example/hyprland.conf +++ b/example/hyprland.conf @@ -179,7 +179,6 @@ animations { # See https://wiki.hypr.land/Configuring/Dwindle-Layout/ for more dwindle { - pseudotile = true # Master switch for pseudotiling. Enabling is bound to mainMod + P in the keybinds section below preserve_split = true # You probably want this } diff --git a/example/hyprland.lua b/example/hyprland.lua new file mode 100644 index 000000000..dcc153428 --- /dev/null +++ b/example/hyprland.lua @@ -0,0 +1,356 @@ +-- This is an example Hyprland Lua config file. +-- Refer to the wiki for more information. +-- https://wiki.hypr.land/Configuring/Start/ + +-- Please note not all available settings / options are set here. +-- For a full list, see the wiki + +-- You can (and should!!) split this configuration into multiple files +-- Create your files separately and then require them like this: +-- require("myColors") + + +------------------ +---- MONITORS ---- +------------------ + +-- See https://wiki.hypr.land/Configuring/Basics/Monitors/ +hl.monitor({ + output = "", + mode = "preferred", + position = "auto", + scale = "auto", +}) + + +--------------------- +---- MY PROGRAMS ---- +--------------------- + +-- Set programs that you use +local terminal = "kitty" +local fileManager = "dolphin" +local menu = "hyprlauncher" + + +------------------- +---- AUTOSTART ---- +------------------- + +-- See https://wiki.hypr.land/Configuring/Basics/Autostart/ + +-- Autostart necessary processes (like notifications daemons, status bars, etc.) +-- Or execute your favorite apps at launch like this: +-- +-- hl.on("hyprland.start", function () +-- hl.exec_cmd(terminal) +-- hl.exec_cmd("nm-applet") +-- hl.exec_cmd("waybar & hyprpaper & firefox") +-- end) + + +------------------------------- +---- ENVIRONMENT VARIABLES ---- +------------------------------- + +-- See https://wiki.hypr.land/Configuring/Advanced-and-Cool/Environment-variables/ + +hl.env("XCURSOR_SIZE", "24") +hl.env("HYPRCURSOR_SIZE", "24") + + +----------------------- +----- PERMISSIONS ----- +----------------------- + +-- See https://wiki.hypr.land/Configuring/Advanced-and-Cool/Permissions/ +-- Please note permission changes here require a Hyprland restart and are not applied on-the-fly +-- for security reasons + +-- hl.config({ +-- ecosystem = { +-- enforce_permissions = true, +-- }, +-- }) + +-- hl.permission("/usr/(bin|local/bin)/grim", "screencopy", "allow") +-- hl.permission("/usr/(lib|libexec|lib64)/xdg-desktop-portal-hyprland", "screencopy", "allow") +-- hl.permission("/usr/(bin|local/bin)/hyprpm", "plugin", "allow") + + +----------------------- +---- LOOK AND FEEL ---- +----------------------- + +-- Refer to https://wiki.hypr.land/Configuring/Basics/Variables/ +hl.config({ + general = { + gaps_in = 5, + gaps_out = 20, + + border_size = 2, + + col = { + active_border = { colors = {"rgba(33ccffee)", "rgba(00ff99ee)"}, angle = 45 }, + inactive_border = "rgba(595959aa)", + }, + + -- Set to true to enable resizing windows by clicking and dragging on borders and gaps + resize_on_border = false, + + -- Please see https://wiki.hypr.land/Configuring/Advanced-and-Cool/Tearing/ before you turn this on + allow_tearing = false, + + layout = "dwindle", + }, + + decoration = { + rounding = 10, + rounding_power = 2, + + -- Change transparency of focused and unfocused windows + active_opacity = 1.0, + inactive_opacity = 1.0, + + shadow = { + enabled = true, + range = 4, + render_power = 3, + color = 0xee1a1a1a, + }, + + blur = { + enabled = true, + size = 3, + passes = 1, + vibrancy = 0.1696, + }, + }, + + animations = { + enabled = true, + }, +}) + +-- Default curves and animations, see https://wiki.hypr.land/Configuring/Advanced-and-Cool/Animations/ +hl.curve("easeOutQuint", { type = "bezier", points = { {0.23, 1}, {0.32, 1} } }) +hl.curve("easeInOutCubic", { type = "bezier", points = { {0.65, 0.05}, {0.36, 1} } }) +hl.curve("linear", { type = "bezier", points = { {0, 0}, {1, 1} } }) +hl.curve("almostLinear", { type = "bezier", points = { {0.5, 0.5}, {0.75, 1} } }) +hl.curve("quick", { type = "bezier", points = { {0.15, 0}, {0.1, 1} } }) + +-- Default springs +hl.curve("easy", { type = "spring", mass = 1, stiffness = 71.2633, dampening = 15.8273644 }) + +hl.animation({ leaf = "global", enabled = true, speed = 10, bezier = "default" }) +hl.animation({ leaf = "border", enabled = true, speed = 5.39, bezier = "easeOutQuint" }) +hl.animation({ leaf = "windows", enabled = true, speed = 4.79, spring = "easy" }) +hl.animation({ leaf = "windowsIn", enabled = true, speed = 4.1, spring = "easy", style = "popin 87%" }) +hl.animation({ leaf = "windowsOut", enabled = true, speed = 1.49, bezier = "linear", style = "popin 87%" }) +hl.animation({ leaf = "fadeIn", enabled = true, speed = 1.73, bezier = "almostLinear" }) +hl.animation({ leaf = "fadeOut", enabled = true, speed = 1.46, bezier = "almostLinear" }) +hl.animation({ leaf = "fade", enabled = true, speed = 3.03, bezier = "quick" }) +hl.animation({ leaf = "layers", enabled = true, speed = 3.81, bezier = "easeOutQuint" }) +hl.animation({ leaf = "layersIn", enabled = true, speed = 4, bezier = "easeOutQuint", style = "fade" }) +hl.animation({ leaf = "layersOut", enabled = true, speed = 1.5, bezier = "linear", style = "fade" }) +hl.animation({ leaf = "fadeLayersIn", enabled = true, speed = 1.79, bezier = "almostLinear" }) +hl.animation({ leaf = "fadeLayersOut", enabled = true, speed = 1.39, bezier = "almostLinear" }) +hl.animation({ leaf = "workspaces", enabled = true, speed = 1.94, bezier = "almostLinear", style = "fade" }) +hl.animation({ leaf = "workspacesIn", enabled = true, speed = 1.21, bezier = "almostLinear", style = "fade" }) +hl.animation({ leaf = "workspacesOut", enabled = true, speed = 1.94, bezier = "almostLinear", style = "fade" }) +hl.animation({ leaf = "zoomFactor", enabled = true, speed = 7, bezier = "quick" }) + +-- Ref https://wiki.hypr.land/Configuring/Basics/Workspace-Rules/ +-- "Smart gaps" / "No gaps when only" +-- uncomment all if you wish to use that. +-- hl.workspace_rule({ workspace = "w[tv1]", gaps_out = 0, gaps_in = 0 }) +-- hl.workspace_rule({ workspace = "f[1]", gaps_out = 0, gaps_in = 0 }) +-- hl.window_rule({ +-- name = "no-gaps-wtv1", +-- match = { float = false, workspace = "w[tv1]" }, +-- border_size = 0, +-- rounding = 0, +-- }) +-- hl.window_rule({ +-- name = "no-gaps-f1", +-- match = { float = false, workspace = "f[1]" }, +-- border_size = 0, +-- rounding = 0, +-- }) + +-- See https://wiki.hypr.land/Configuring/Layouts/Dwindle-Layout/ for more +hl.config({ + dwindle = { + preserve_split = true, -- You probably want this + }, +}) + +-- See https://wiki.hypr.land/Configuring/Layouts/Master-Layout/ for more +hl.config({ + master = { + new_status = "master", + }, +}) + +-- See https://wiki.hypr.land/Configuring/Layouts/Scrolling-Layout/ for more +hl.config({ + scrolling = { + fullscreen_on_one_column = true, + }, +}) + +---------------- +---- MISC ---- +---------------- + +hl.config({ + misc = { + force_default_wallpaper = -1, -- Set to 0 or 1 to disable the anime mascot wallpapers + disable_hyprland_logo = false, -- If true disables the random hyprland logo / anime girl background. :( + }, +}) + + +--------------- +---- INPUT ---- +--------------- + +hl.config({ + input = { + kb_layout = "us", + kb_variant = "", + kb_model = "", + kb_options = "", + kb_rules = "", + + follow_mouse = 1, + + sensitivity = 0, -- -1.0 - 1.0, 0 means no modification. + + touchpad = { + natural_scroll = false, + }, + }, +}) + +hl.gesture({ + fingers = 3, + direction = "horizontal", + action = "workspace" +}) + +-- Example per-device config +-- See https://wiki.hypr.land/Configuring/Advanced-and-Cool/Devices/ for more +hl.device({ + name = "epic-mouse-v1", + sensitivity = -0.5, +}) + + +--------------------- +---- KEYBINDINGS ---- +--------------------- + +local mainMod = "SUPER" -- Sets "Windows" key as main modifier + +-- Example binds, see https://wiki.hypr.land/Configuring/Basics/Binds/ for more +hl.bind(mainMod .. " + Q", hl.dsp.exec_cmd(terminal)) +local closeWindowBind = hl.bind(mainMod .. " + C", hl.dsp.window.close()) +-- closeWindowBind:set_enabled(false) +hl.bind(mainMod .. " + M", hl.dsp.exec_cmd("command -v hyprshutdown >/dev/null 2>&1 && hyprshutdown || hyprctl dispatch 'hl.dsp.exit()'")) +hl.bind(mainMod .. " + E", hl.dsp.exec_cmd(fileManager)) +hl.bind(mainMod .. " + V", hl.dsp.window.float({ action = "toggle" })) +hl.bind(mainMod .. " + R", hl.dsp.exec_cmd(menu)) +hl.bind(mainMod .. " + P", hl.dsp.window.pseudo()) +hl.bind(mainMod .. " + J", hl.dsp.layout("togglesplit")) -- dwindle only + +-- Move focus with mainMod + arrow keys +hl.bind(mainMod .. " + left", hl.dsp.focus({ direction = "left" })) +hl.bind(mainMod .. " + right", hl.dsp.focus({ direction = "right" })) +hl.bind(mainMod .. " + up", hl.dsp.focus({ direction = "up" })) +hl.bind(mainMod .. " + down", hl.dsp.focus({ direction = "down" })) + +-- Switch workspaces with mainMod + [0-9] +-- Move active window to a workspace with mainMod + SHIFT + [0-9] +for i = 1, 10 do + local key = i % 10 -- 10 maps to key 0 + hl.bind(mainMod .. " + " .. key, hl.dsp.focus({ workspace = i})) + hl.bind(mainMod .. " + SHIFT + " .. key, hl.dsp.window.move({ workspace = i })) +end + +-- Example special workspace (scratchpad) +hl.bind(mainMod .. " + S", hl.dsp.workspace.toggle_special("magic")) +hl.bind(mainMod .. " + SHIFT + S", hl.dsp.window.move({ workspace = "special:magic" })) + +-- Scroll through existing workspaces with mainMod + scroll +hl.bind(mainMod .. " + mouse_down", hl.dsp.focus({ workspace = "e+1" })) +hl.bind(mainMod .. " + mouse_up", hl.dsp.focus({ workspace = "e-1" })) + +-- Move/resize windows with mainMod + LMB/RMB and dragging +hl.bind(mainMod .. " + mouse:272", hl.dsp.window.drag(), { mouse = true }) +hl.bind(mainMod .. " + mouse:273", hl.dsp.window.resize(), { mouse = true }) + +-- Laptop multimedia keys for volume and LCD brightness +hl.bind("XF86AudioRaiseVolume", hl.dsp.exec_cmd("wpctl set-volume -l 1 @DEFAULT_AUDIO_SINK@ 5%+"), { locked = true, repeating = true }) +hl.bind("XF86AudioLowerVolume", hl.dsp.exec_cmd("wpctl set-volume @DEFAULT_AUDIO_SINK@ 5%-"), { locked = true, repeating = true }) +hl.bind("XF86AudioMute", hl.dsp.exec_cmd("wpctl set-mute @DEFAULT_AUDIO_SINK@ toggle"), { locked = true, repeating = true }) +hl.bind("XF86AudioMicMute", hl.dsp.exec_cmd("wpctl set-mute @DEFAULT_AUDIO_SOURCE@ toggle"), { locked = true, repeating = true }) +hl.bind("XF86MonBrightnessUp", hl.dsp.exec_cmd("brightnessctl -e4 -n2 set 5%+"), { locked = true, repeating = true }) +hl.bind("XF86MonBrightnessDown",hl.dsp.exec_cmd("brightnessctl -e4 -n2 set 5%-"), { locked = true, repeating = true }) + +-- Requires playerctl +hl.bind("XF86AudioNext", hl.dsp.exec_cmd("playerctl next"), { locked = true }) +hl.bind("XF86AudioPause", hl.dsp.exec_cmd("playerctl play-pause"), { locked = true }) +hl.bind("XF86AudioPlay", hl.dsp.exec_cmd("playerctl play-pause"), { locked = true }) +hl.bind("XF86AudioPrev", hl.dsp.exec_cmd("playerctl previous"), { locked = true }) + + +-------------------------------- +---- WINDOWS AND WORKSPACES ---- +-------------------------------- + +-- See https://wiki.hypr.land/Configuring/Basics/Window-Rules/ +-- and https://wiki.hypr.land/Configuring/Basics/Workspace-Rules/ + +-- Example window rules that are useful + +local suppressMaximizeRule = hl.window_rule({ + -- Ignore maximize requests from all apps. You'll probably like this. + name = "suppress-maximize-events", + match = { class = ".*" }, + + suppress_event = "maximize", +}) +-- suppressMaximizeRule:set_enabled(false) + +hl.window_rule({ + -- Fix some dragging issues with XWayland + name = "fix-xwayland-drags", + match = { + class = "^$", + title = "^$", + xwayland = true, + float = true, + fullscreen = false, + pin = false, + }, + + no_focus = true, +}) + +-- Layer rules also return a handle. +-- local overlayLayerRule = hl.layer_rule({ +-- name = "no-anim-overlay", +-- match = { namespace = "^my-overlay$" }, +-- no_anim = true, +-- }) +-- overlayLayerRule:set_enabled(false) + +-- Hyprland-run windowrule +hl.window_rule({ + name = "move-hyprland-run", + match = { class = "hyprland-run" }, + + move = "20 monitor_h-120", + float = true, +}) diff --git a/flake.lock b/flake.lock index 45ecbad47..8ebb11f87 100644 --- a/flake.lock +++ b/flake.lock @@ -16,11 +16,11 @@ ] }, "locked": { - "lastModified": 1776702787, - "narHash": "sha256-qc5uwEWbuubzYthmZcfCapooZGXhoYZWfTQ24TozbCQ=", + "lastModified": 1776876344, + "narHash": "sha256-Ubqb/agkuMJK+k19gjQgHux/eOYRc1sRGoOZOho8+VY=", "owner": "hyprwm", "repo": "aquamarine", - "rev": "9a1ca6b8cb4d86a599787a55b78f2ddf809bf945", + "rev": "648a13d0ee1e03a843b3e145b8ece15393058701", "type": "github" }, "original": { @@ -261,11 +261,11 @@ ] }, "locked": { - "lastModified": 1776428866, - "narHash": "sha256-XfRlBolGtjvalTHJp3XvvpYLBjkMhaZLLU0WqZ91Fcg=", + "lastModified": 1777492286, + "narHash": "sha256-PwuoEJQcjSKJNP5T55qhfDwIP0tw5zxEhfu8GDfKfeg=", "owner": "hyprwm", "repo": "hyprutils", - "rev": "eedd60805cd96d4442586f2ba5fe51d549b12674", + "rev": "ec5c0c709706bad5b82f667fd8758eae442577ce", "type": "github" }, "original": { @@ -284,11 +284,11 @@ ] }, "locked": { - "lastModified": 1776430932, - "narHash": "sha256-Yv3RPiUvl7CAsJgwIVsqcj7akn1gLyJP1F/mocof5hA=", + "lastModified": 1777148232, + "narHash": "sha256-Uv0WZLhu89SafuSOmYDA7akrPt4wBRmsa1ucasO5aXg=", "owner": "hyprwm", "repo": "hyprwayland-scanner", - "rev": "4c2fcc06dc9722c97dbb54ba649c69b18ce83d2e", + "rev": "fec9cf1abcc1011e46f0a0986f46bf93c6bf8b92", "type": "github" }, "original": { @@ -325,11 +325,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1776548001, - "narHash": "sha256-ZSK0NL4a1BwVbbTBoSnWgbJy9HeZFXLYQizjb2DPF24=", + "lastModified": 1776877367, + "narHash": "sha256-EHq1/OX139R1RvBzOJ0aMRT3xnWyqtHBRUBuO1gFzjI=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "b12141ef619e0a9c1c84dc8c684040326f27cdcc", + "rev": "0726a0ecb6d4e08f6adced58726b95db924cef57", "type": "github" }, "original": { @@ -415,11 +415,11 @@ ] }, "locked": { - "lastModified": 1776608502, - "narHash": "sha256-UH8YoQxx4hFOm6qjMdjRQNRvSejFIR/wBZ8fW1p9sME=", + "lastModified": 1777035886, + "narHash": "sha256-m1TNuBoSXUBSKhD9UVMkU90M0wFTPTfvIOOltO8IM8A=", "owner": "hyprwm", "repo": "xdg-desktop-portal-hyprland", - "rev": "4a293523d36dfa367e67ec304cc718ea66a8fec2", + "rev": "ecfcdcc781f48821d83e1e2a0e30d7beca0eeb5e", "type": "github" }, "original": { diff --git a/hyprctl/hyprctl.bash b/hyprctl/hyprctl.bash index ba5416538..2564d32a2 100644 --- a/hyprctl/hyprctl.bash +++ b/hyprctl/hyprctl.bash @@ -23,7 +23,7 @@ _hyprctl () { local words cword _get_comp_words_by_ref -n "$COMP_WORDBREAKS" words cword - declare -a literals=(resizeactive 2 changegroupactive -r moveintogroup forceallowsinput 4 ::= systeminfo all layouts setprop animationstyle switchxkblayout create denywindowfromgroup headless activebordercolor exec setcursor wayland focusurgentorlast workspacerules movecurrentworkspacetomonitor movetoworkspacesilent hyprpaper alpha inactivebordercolor movegroupwindow movecursortocorner movewindowpixel prev movewindow globalshortcuts clients dimaround setignoregrouplock splash execr monitors 0 forcenoborder -q animations 1 nomaxsize splitratio moveactive pass swapnext devices layers rounding lockactivegroup 5 moveworkspacetomonitor -f -i --quiet forcenodim pin 0 1 forceopaque forcenoshadow setfloating minsize alphaoverride sendshortcut workspaces cyclenext alterzorder togglegroup lockgroups bordersize dpms focuscurrentorlast -1 --batch notify remove instances 1 3 moveoutofgroup killactive 2 movetoworkspace movecursor configerrors closewindow swapwindow tagwindow forcerendererreload centerwindow auto focuswindow seterror nofocus alphafullscreen binds version -h togglespecialworkspace fullscreen windowdancecompat 0 keyword toggleopaque 3 --instance togglefloating renameworkspace alphafullscreenoverride activeworkspace x11 kill forceopaqueoverriden output global dispatch reload forcenoblur -j event --help disable -1 activewindow keepaspectratio dismissnotify focusmonitor movefocus plugin exit workspace fullscreenstate getoption alphainactiveoverride alphainactive decorations settiled config-only descriptions resizewindowpixel fakefullscreen rollinglog swapactiveworkspaces submap next movewindoworgroup cursorpos forcenoanims focusworkspaceoncurrentmonitor maxsize sendkeystate) + declare -a literals=(resizeactive 2 changegroupactive -r moveintogroup forceallowsinput 4 ::= systeminfo all layouts setprop animationstyle switchxkblayout create denywindowfromgroup headless activebordercolor exec setcursor wayland focusurgentorlast workspacerules movecurrentworkspacetomonitor movetoworkspacesilent hyprpaper alpha inactivebordercolor movegroupwindow movecursortocorner movewindowpixel prev movewindow globalshortcuts clients dimaround splash execr monitors 0 forcenoborder -q animations 1 nomaxsize splitratio moveactive pass swapnext devices layers rounding lockactivegroup 5 moveworkspacetomonitor -f -i --quiet forcenodim pin 0 1 forceopaque forcenoshadow setfloating minsize alphaoverride sendshortcut workspaces cyclenext alterzorder togglegroup lockgroups bordersize dpms focuscurrentorlast -1 --batch notify remove instances 1 3 moveoutofgroup killactive 2 movetoworkspace movecursor configerrors closewindow swapwindow tagwindow forcerendererreload centerwindow auto focuswindow seterror nofocus alphafullscreen binds version -h togglespecialworkspace fullscreen windowdancecompat 0 keyword toggleopaque 3 --instance togglefloating renameworkspace alphafullscreenoverride activeworkspace x11 kill forceopaqueoverriden output global dispatch reload forcenoblur -j event --help disable -1 activewindow keepaspectratio dismissnotify focusmonitor movefocus plugin exit workspace fullscreenstate getoption alphainactiveoverride alphainactive decorations settiled config-only descriptions resizewindowpixel fakefullscreen rollinglog swapactiveworkspaces submap next movewindoworgroup cursorpos forcenoanims focusworkspaceoncurrentmonitor maxsize sendkeystate) declare -A literal_transitions literal_transitions[0]="([120]=14 [43]=2 [125]=21 [81]=2 [3]=21 [51]=2 [50]=2 [128]=2 [89]=2 [58]=21 [8]=2 [10]=2 [11]=3 [130]=4 [13]=5 [97]=6 [101]=2 [102]=21 [133]=7 [100]=2 [137]=2 [22]=2 [19]=2 [140]=8 [25]=2 [143]=2 [107]=9 [146]=10 [69]=2 [33]=2 [34]=2 [78]=21 [114]=2 [37]=2 [151]=2 [116]=2 [121]=13 [123]=21 [39]=11 [42]=21 [79]=15 [118]=12)" literal_transitions[1]="([81]=2 [51]=2 [50]=2 [128]=2 [8]=2 [89]=2 [10]=2 [11]=3 [130]=4 [13]=5 [97]=6 [101]=2 [133]=7 [100]=2 [22]=2 [19]=2 [137]=2 [140]=8 [25]=2 [143]=2 [107]=9 [146]=10 [69]=2 [33]=2 [34]=2 [114]=2 [37]=2 [151]=2 [116]=2 [39]=11 [118]=12 [121]=13 [120]=14 [79]=15 [43]=2)" diff --git a/hyprctl/hyprctl.fish b/hyprctl/hyprctl.fish index 6ad32041d..99d0e8743 100644 --- a/hyprctl/hyprctl.fish +++ b/hyprctl/hyprctl.fish @@ -29,7 +29,7 @@ function _hyprctl set COMP_CWORD (count $COMP_WORDS) end - set literals "resizeactive" "2" "changegroupactive" "-r" "moveintogroup" "forceallowsinput" "4" "::=" "systeminfo" "all" "layouts" "setprop" "animationstyle" "switchxkblayout" "create" "denywindowfromgroup" "headless" "activebordercolor" "exec" "setcursor" "wayland" "focusurgentorlast" "workspacerules" "movecurrentworkspacetomonitor" "movetoworkspacesilent" "hyprpaper" "alpha" "inactivebordercolor" "movegroupwindow" "movecursortocorner" "movewindowpixel" "prev" "movewindow" "globalshortcuts" "clients" "dimaround" "setignoregrouplock" "splash" "execr" "monitors" "0" "forcenoborder" "-q" "animations" "1" "nomaxsize" "splitratio" "moveactive" "pass" "swapnext" "devices" "layers" "rounding" "lockactivegroup" "5" "moveworkspacetomonitor" "-f" "-i" "--quiet" "forcenodim" "pin" "0" "1" "forceopaque" "forcenoshadow" "setfloating" "minsize" "alphaoverride" "sendshortcut" "workspaces" "cyclenext" "alterzorder" "togglegroup" "lockgroups" "bordersize" "dpms" "focuscurrentorlast" "-1" "--batch" "notify" "remove" "instances" "1" "3" "moveoutofgroup" "killactive" "2" "movetoworkspace" "movecursor" "configerrors" "closewindow" "swapwindow" "tagwindow" "forcerendererreload" "centerwindow" "auto" "focuswindow" "seterror" "nofocus" "alphafullscreen" "binds" "version" "-h" "togglespecialworkspace" "fullscreen" "windowdancecompat" "0" "keyword" "toggleopaque" "3" "--instance" "togglefloating" "renameworkspace" "alphafullscreenoverride" "activeworkspace" "x11" "kill" "forceopaqueoverriden" "output" "global" "dispatch" "reload" "forcenoblur" "-j" "event" "--help" "disable" "-1" "activewindow" "keepaspectratio" "dismissnotify" "focusmonitor" "movefocus" "plugin" "exit" "workspace" "fullscreenstate" "getoption" "alphainactiveoverride" "alphainactive" "decorations" "settiled" "config-only" "descriptions" "resizewindowpixel" "fakefullscreen" "rollinglog" "swapactiveworkspaces" "submap" "next" "movewindoworgroup" "cursorpos" "forcenoanims" "focusworkspaceoncurrentmonitor" "maxsize" "sendkeystate" + set literals "resizeactive" "2" "changegroupactive" "-r" "moveintogroup" "forceallowsinput" "4" "::=" "systeminfo" "all" "layouts" "setprop" "animationstyle" "switchxkblayout" "create" "denywindowfromgroup" "headless" "activebordercolor" "exec" "setcursor" "wayland" "focusurgentorlast" "workspacerules" "movecurrentworkspacetomonitor" "movetoworkspacesilent" "hyprpaper" "alpha" "inactivebordercolor" "movegroupwindow" "movecursortocorner" "movewindowpixel" "prev" "movewindow" "globalshortcuts" "clients" "dimaround" "splash" "execr" "monitors" "0" "forcenoborder" "-q" "animations" "1" "nomaxsize" "splitratio" "moveactive" "pass" "swapnext" "devices" "layers" "rounding" "lockactivegroup" "5" "moveworkspacetomonitor" "-f" "-i" "--quiet" "forcenodim" "pin" "0" "1" "forceopaque" "forcenoshadow" "setfloating" "minsize" "alphaoverride" "sendshortcut" "workspaces" "cyclenext" "alterzorder" "togglegroup" "lockgroups" "bordersize" "dpms" "focuscurrentorlast" "-1" "--batch" "notify" "remove" "instances" "1" "3" "moveoutofgroup" "killactive" "2" "movetoworkspace" "movecursor" "configerrors" "closewindow" "swapwindow" "tagwindow" "forcerendererreload" "centerwindow" "auto" "focuswindow" "seterror" "nofocus" "alphafullscreen" "binds" "version" "-h" "togglespecialworkspace" "fullscreen" "windowdancecompat" "0" "keyword" "toggleopaque" "3" "--instance" "togglefloating" "renameworkspace" "alphafullscreenoverride" "activeworkspace" "x11" "kill" "forceopaqueoverriden" "output" "global" "dispatch" "reload" "forcenoblur" "-j" "event" "--help" "disable" "-1" "activewindow" "keepaspectratio" "dismissnotify" "focusmonitor" "movefocus" "plugin" "exit" "workspace" "fullscreenstate" "getoption" "alphainactiveoverride" "alphainactive" "decorations" "settiled" "config-only" "descriptions" "resizewindowpixel" "fakefullscreen" "rollinglog" "swapactiveworkspaces" "submap" "next" "movewindoworgroup" "cursorpos" "forcenoanims" "focusworkspaceoncurrentmonitor" "maxsize" "sendkeystate" set descriptions set descriptions[1] "Resize the active window" diff --git a/hyprctl/hyprctl.usage b/hyprctl/hyprctl.usage index e13fc9d5d..cacdfa53e 100644 --- a/hyprctl/hyprctl.usage +++ b/hyprctl/hyprctl.usage @@ -93,6 +93,7 @@ hyprctl []... | (version) "Print the Hyprland version: flags, commit and branch of build" | (workspacerules) "Get the list of defined workspace rules" | (workspaces) "List all workspaces with their properties" + | (status) "Get internal status information like config format or backend" ; ::= (-1) "Current" @@ -157,7 +158,6 @@ hyprctl []... | (movewindoworgroup) "Behave as moveintogroup" | (movegroupwindow) "Swap the active window with the next or previous in a group" | (denywindowfromgroup) "Prohibit the active window from becoming or being inserted into group" - | (setignoregrouplock) "Temporarily enable or disable binds:ignore_group_lock" | (global) "Execute a Global Shortcut using the GlobalShortcuts portal" | (submap) "Change the current mapping group" | (event) "Emits a custom event to socket2" diff --git a/hyprctl/hyprctl.zsh b/hyprctl/hyprctl.zsh index e0c48ab07..d05e88611 100644 --- a/hyprctl/hyprctl.zsh +++ b/hyprctl/hyprctl.zsh @@ -17,7 +17,7 @@ _hyprctl_cmd_0 () { } _hyprctl () { - local -a literals=("resizeactive" "2" "changegroupactive" "-r" "moveintogroup" "forceallowsinput" "4" "::=" "systeminfo" "all" "layouts" "setprop" "animationstyle" "switchxkblayout" "create" "denywindowfromgroup" "headless" "activebordercolor" "exec" "setcursor" "wayland" "focusurgentorlast" "workspacerules" "movecurrentworkspacetomonitor" "movetoworkspacesilent" "hyprpaper" "alpha" "inactivebordercolor" "movegroupwindow" "movecursortocorner" "movewindowpixel" "prev" "movewindow" "globalshortcuts" "clients" "dimaround" "setignoregrouplock" "splash" "execr" "monitors" "0" "forcenoborder" "-q" "animations" "1" "nomaxsize" "splitratio" "moveactive" "pass" "swapnext" "devices" "layers" "rounding" "lockactivegroup" "5" "moveworkspacetomonitor" "-f" "-i" "--quiet" "forcenodim" "pin" "0" "1" "forceopaque" "forcenoshadow" "setfloating" "minsize" "alphaoverride" "sendshortcut" "workspaces" "cyclenext" "alterzorder" "togglegroup" "lockgroups" "bordersize" "dpms" "focuscurrentorlast" "-1" "--batch" "notify" "remove" "instances" "1" "3" "moveoutofgroup" "killactive" "2" "movetoworkspace" "movecursor" "configerrors" "closewindow" "swapwindow" "tagwindow" "forcerendererreload" "centerwindow" "auto" "focuswindow" "seterror" "nofocus" "alphafullscreen" "binds" "version" "-h" "togglespecialworkspace" "fullscreen" "windowdancecompat" "0" "keyword" "toggleopaque" "3" "--instance" "togglefloating" "renameworkspace" "alphafullscreenoverride" "activeworkspace" "x11" "kill" "forceopaqueoverriden" "output" "global" "dispatch" "reload" "forcenoblur" "-j" "event" "--help" "disable" "-1" "activewindow" "keepaspectratio" "dismissnotify" "focusmonitor" "movefocus" "plugin" "exit" "workspace" "fullscreenstate" "getoption" "alphainactiveoverride" "alphainactive" "decorations" "settiled" "config-only" "descriptions" "resizewindowpixel" "fakefullscreen" "rollinglog" "swapactiveworkspaces" "submap" "next" "movewindoworgroup" "cursorpos" "forcenoanims" "focusworkspaceoncurrentmonitor" "maxsize" "sendkeystate") + local -a literals=("resizeactive" "2" "changegroupactive" "-r" "moveintogroup" "forceallowsinput" "4" "::=" "systeminfo" "all" "layouts" "setprop" "animationstyle" "switchxkblayout" "create" "denywindowfromgroup" "headless" "activebordercolor" "exec" "setcursor" "wayland" "focusurgentorlast" "workspacerules" "movecurrentworkspacetomonitor" "movetoworkspacesilent" "hyprpaper" "alpha" "inactivebordercolor" "movegroupwindow" "movecursortocorner" "movewindowpixel" "prev" "movewindow" "globalshortcuts" "clients" "dimaround" "splash" "execr" "monitors" "0" "forcenoborder" "-q" "animations" "1" "nomaxsize" "splitratio" "moveactive" "pass" "swapnext" "devices" "layers" "rounding" "lockactivegroup" "5" "moveworkspacetomonitor" "-f" "-i" "--quiet" "forcenodim" "pin" "0" "1" "forceopaque" "forcenoshadow" "setfloating" "minsize" "alphaoverride" "sendshortcut" "workspaces" "cyclenext" "alterzorder" "togglegroup" "lockgroups" "bordersize" "dpms" "focuscurrentorlast" "-1" "--batch" "notify" "remove" "instances" "1" "3" "moveoutofgroup" "killactive" "2" "movetoworkspace" "movecursor" "configerrors" "closewindow" "swapwindow" "tagwindow" "forcerendererreload" "centerwindow" "auto" "focuswindow" "seterror" "nofocus" "alphafullscreen" "binds" "version" "-h" "togglespecialworkspace" "fullscreen" "windowdancecompat" "0" "keyword" "toggleopaque" "3" "--instance" "togglefloating" "renameworkspace" "alphafullscreenoverride" "activeworkspace" "x11" "kill" "forceopaqueoverriden" "output" "global" "dispatch" "reload" "forcenoblur" "-j" "event" "--help" "disable" "-1" "activewindow" "keepaspectratio" "dismissnotify" "focusmonitor" "movefocus" "plugin" "exit" "workspace" "fullscreenstate" "getoption" "alphainactiveoverride" "alphainactive" "decorations" "settiled" "config-only" "descriptions" "resizewindowpixel" "fakefullscreen" "rollinglog" "swapactiveworkspaces" "submap" "next" "movewindoworgroup" "cursorpos" "forcenoanims" "focusworkspaceoncurrentmonitor" "maxsize" "sendkeystate") local -A descriptions descriptions[1]="Resize the active window" diff --git a/hyprctl/src/Strings.hpp b/hyprctl/src/Strings.hpp index 549d84bb6..779a2ee3f 100644 --- a/hyprctl/src/Strings.hpp +++ b/hyprctl/src/Strings.hpp @@ -51,6 +51,7 @@ commands: setprop ... → Sets a window property getprop ... → Gets a window property splash → Get the current splash + status → Get internal status information switchxkblayout ... → Sets the xkb layout index for a keyboard systeminfo → Get system info version → Prints the hyprland version, meaning flags, commit diff --git a/hyprctl/src/main.cpp b/hyprctl/src/main.cpp index 0a33f3ed8..a504bf93f 100644 --- a/hyprctl/src/main.cpp +++ b/hyprctl/src/main.cpp @@ -526,6 +526,8 @@ int main(int argc, char** argv) { exitStatus = request(fullRequest, 2); else if (fullRequest.contains("/decorations")) exitStatus = request(fullRequest, 1); + else if (fullRequest.contains("/eval")) + exitStatus = request(fullRequest, 1); else if (fullRequest.contains("/--help")) std::println("{}", USAGE); else if (fullRequest.contains("/rollinglog") && needRoll) diff --git a/hyprtester/CMakeLists.txt b/hyprtester/CMakeLists.txt index 599eee585..1484e728c 100644 --- a/hyprtester/CMakeLists.txt +++ b/hyprtester/CMakeLists.txt @@ -24,7 +24,7 @@ target_link_libraries(hyprtester PUBLIC PkgConfig::hyprtester_deps) install(TARGETS hyprtester) -install(FILES ${CMAKE_CURRENT_SOURCE_DIR}/test.conf +install(FILES ${CMAKE_CURRENT_SOURCE_DIR}/test.lua DESTINATION ${CMAKE_INSTALL_DATAROOTDIR}/hypr) install(FILES ${CMAKE_CURRENT_SOURCE_DIR}/plugin/hyprtestplugin.so diff --git a/hyprtester/plugin/src/main.cpp b/hyprtester/plugin/src/main.cpp index 35d2eea4c..c7d6f4fdf 100644 --- a/hyprtester/plugin/src/main.cpp +++ b/hyprtester/plugin/src/main.cpp @@ -4,8 +4,6 @@ #include #define private public -#include -#include #include #include #include @@ -23,6 +21,11 @@ using namespace Hyprutils::Utils; using namespace Hyprutils::String; +extern "C" { +#include +#include +} + #include "globals.hpp" // Do NOT change this function. @@ -31,18 +34,7 @@ APICALL EXPORT std::string PLUGIN_API_VERSION() { } static SDispatchResult test(std::string in) { - bool success = true; - std::string errors = ""; - - if (Config::Legacy::mgr()->m_configValueNumber != Config::Supplementary::CONFIG_OPTIONS.size() + 1 /* autogenerated is special */) { - errors += "config value number mismatches descriptions size\n"; - success = false; - } - - return SDispatchResult{ - .success = success, - .error = errors, - }; + return {.success = true}; } // Trigger a snap move event for the active window @@ -344,7 +336,7 @@ static SDispatchResult floatingFocusOnFullscreen(std::string in) { if (!PLASTWINDOW->m_isFloating) return {.success = false, .error = "Window must be floating"}; - if (PLASTWINDOW->m_alpha != 1.f) + if (PLASTWINDOW->alphaTotal() != 1.F) return {.success = false, .error = "floating window doesnt restore it opacity when focused on fullscreen workspace"}; if (!PLASTWINDOW->m_createdOverFullscreen) @@ -353,22 +345,94 @@ static SDispatchResult floatingFocusOnFullscreen(std::string in) { return {}; } +static int luaResult(lua_State* L, const SDispatchResult& result) { + if (result.success) + return 0; + + lua_pushstring(L, result.error.empty() ? "plugin function failed" : result.error.c_str()); + return lua_error(L); +} + +static int luaTest(lua_State* L) { + return luaResult(L, ::test("")); +} + +static int luaSnapMove(lua_State* L) { + return luaResult(L, ::snapMove("")); +} + +static int luaVkb(lua_State* L) { + return luaResult(L, ::vkb("")); +} + +static int luaAlt(lua_State* L) { + return luaResult(L, ::pressAlt(std::to_string((int)luaL_checkinteger(L, 1)))); +} + +static int luaGesture(lua_State* L) { + const auto direction = std::string{luaL_checkstring(L, 1)}; + const auto fingers = (int)luaL_optinteger(L, 2, 3); + return luaResult(L, ::simulateGesture(std::format("{},{}", direction, fingers))); +} + +static int luaScroll(lua_State* L) { + return luaResult(L, ::scroll(std::to_string((double)luaL_checknumber(L, 1)))); +} + +static int luaClick(lua_State* L) { + const auto button = (int)luaL_checkinteger(L, 1); + const auto pressed = (int)luaL_checkinteger(L, 2); + return luaResult(L, ::click(std::format("{},{}", button, pressed))); +} + +static int luaKeybind(lua_State* L) { + const auto press = (int)luaL_checkinteger(L, 1); + const auto modifier = (int)luaL_checkinteger(L, 2); + const auto key = (int)luaL_checkinteger(L, 3); + return luaResult(L, ::keybind(std::format("{},{},{}", press, modifier, key))); +} + +static int luaAddWindowRule(lua_State* L) { + return luaResult(L, ::addWindowRule("")); +} + +static int luaCheckWindowRule(lua_State* L) { + return luaResult(L, ::checkWindowRule("")); +} + +static int luaAddLayerRule(lua_State* L) { + return luaResult(L, ::addLayerRule("")); +} + +static int luaCheckLayerRule(lua_State* L) { + return luaResult(L, ::checkLayerRule("")); +} + +static int luaFloatingFocusOnFullscreen(lua_State* L) { + return luaResult(L, ::floatingFocusOnFullscreen("")); +} + APICALL EXPORT PLUGIN_DESCRIPTION_INFO PLUGIN_INIT(HANDLE handle) { PHANDLE = handle; - HyprlandAPI::addDispatcherV2(PHANDLE, "plugin:test:test", ::test); - HyprlandAPI::addDispatcherV2(PHANDLE, "plugin:test:snapmove", ::snapMove); - HyprlandAPI::addDispatcherV2(PHANDLE, "plugin:test:vkb", ::vkb); - HyprlandAPI::addDispatcherV2(PHANDLE, "plugin:test:alt", ::pressAlt); - HyprlandAPI::addDispatcherV2(PHANDLE, "plugin:test:gesture", ::simulateGesture); - HyprlandAPI::addDispatcherV2(PHANDLE, "plugin:test:scroll", ::scroll); - HyprlandAPI::addDispatcherV2(PHANDLE, "plugin:test:click", ::click); - HyprlandAPI::addDispatcherV2(PHANDLE, "plugin:test:keybind", ::keybind); - HyprlandAPI::addDispatcherV2(PHANDLE, "plugin:test:add_window_rule", ::addWindowRule); - HyprlandAPI::addDispatcherV2(PHANDLE, "plugin:test:check_window_rule", ::checkWindowRule); - HyprlandAPI::addDispatcherV2(PHANDLE, "plugin:test:add_layer_rule", ::addLayerRule); - HyprlandAPI::addDispatcherV2(PHANDLE, "plugin:test:check_layer_rule", ::checkLayerRule); - HyprlandAPI::addDispatcherV2(PHANDLE, "plugin:test:floating_focus_on_fullscreen", ::floatingFocusOnFullscreen); + auto addLuaFn = [](const std::string& name, PLUGIN_LUA_FN fn) { + if (!HyprlandAPI::addLuaFunction(PHANDLE, "test", name, fn)) + Log::logger->log(Log::ERR, "hyprtester plugin: failed to register hl.plugin.test.{}", name); + }; + + addLuaFn("test", ::luaTest); + addLuaFn("snapmove", ::luaSnapMove); + addLuaFn("vkb", ::luaVkb); + addLuaFn("alt", ::luaAlt); + addLuaFn("gesture", ::luaGesture); + addLuaFn("scroll", ::luaScroll); + addLuaFn("click", ::luaClick); + addLuaFn("keybind", ::luaKeybind); + addLuaFn("add_window_rule", ::luaAddWindowRule); + addLuaFn("check_window_rule", ::luaCheckWindowRule); + addLuaFn("add_layer_rule", ::luaAddLayerRule); + addLuaFn("check_layer_rule", ::luaCheckLayerRule); + addLuaFn("floating_focus_on_fullscreen", ::luaFloatingFocusOnFullscreen); // init mouse g_mouse = CTestMouse::create(false); diff --git a/hyprtester/src/Log.hpp b/hyprtester/src/Log.hpp index 7eeb3ece0..87f0890d2 100644 --- a/hyprtester/src/Log.hpp +++ b/hyprtester/src/Log.hpp @@ -3,6 +3,8 @@ #include #include +#include "shared.hpp" + namespace NLog { template //NOLINTNEXTLINE @@ -11,7 +13,7 @@ namespace NLog { logMsg += std::vformat(fmt.get(), std::make_format_args(args...)); - std::println("{}", logMsg); + std::println("{}{}", logMsg, Colors::RESET); std::fflush(stdout); } -} \ No newline at end of file +} diff --git a/hyprtester/src/hyprctlCompat.cpp b/hyprtester/src/hyprctlCompat.cpp index 2c8e06449..dbeee93d5 100644 --- a/hyprtester/src/hyprctlCompat.cpp +++ b/hyprtester/src/hyprctlCompat.cpp @@ -43,7 +43,7 @@ std::vector instances() { } catch (std::exception& e) { return {}; } for (const auto& el : std::filesystem::directory_iterator(getRuntimeDir())) { - if (!el.is_directory() || !std::filesystem::exists(el.path().string() + "/hyprland.lock")) + if (!std::filesystem::exists(el.path() / "hyprland.lock")) continue; // read lock @@ -74,7 +74,7 @@ std::vector instances() { std::erase_if(result, [&](const auto& el) { return kill(el.pid, 0) != 0 && errno == ESRCH; }); - std::sort(result.begin(), result.end(), [&](const auto& a, const auto& b) { return a.time < b.time; }); + std::ranges::sort(result, [&](const auto& a, const auto& b) { return a.time < b.time; }); return result; } @@ -135,4 +135,4 @@ std::string getFromSocket(const std::string& cmd) { close(SERVERSOCKET); return reply; -} \ No newline at end of file +} diff --git a/hyprtester/src/main.cpp b/hyprtester/src/main.cpp index 1ef9bf0c2..6c30fea07 100644 --- a/hyprtester/src/main.cpp +++ b/hyprtester/src/main.cpp @@ -17,14 +17,17 @@ #include "shared.hpp" #include "hyprctlCompat.hpp" #include "tests/main/tests.hpp" +#undef TEST_CASES_STORAGE // Prevent redefinition warning #include "tests/clients/tests.hpp" +#undef TEST_CASES_STORAGE // Prevent redefinition warning #include "tests/plugin/plugin.hpp" +#include "tests/shared.hpp" +#include #include #include #include #include -using namespace Hyprutils::Memory; #include #include @@ -38,35 +41,13 @@ using namespace Hyprutils::Memory; using namespace Hyprutils::OS; using namespace Hyprutils::Memory; +using Path = std::filesystem::path; #define SP CSharedPointer -static int ret = 0; -static SP hyprlandProc; -static const std::string cwd = std::filesystem::current_path().string(); - -// -static bool launchHyprland(std::string configPath, std::string binaryPath) { - if (binaryPath == "") { - std::error_code ec; - if (!std::filesystem::exists(cwd + "/../build/Hyprland", ec) || ec) { - NLog::log("{}No Hyprland binary", Colors::RED); - return false; - } - - binaryPath = cwd + "/../build/Hyprland"; - } - - if (configPath == "") { - std::error_code ec; - if (!std::filesystem::exists(cwd + "/test.conf", ec) || ec) { - NLog::log("{}No test config", Colors::RED); - return false; - } - - configPath = cwd + "/test.conf"; - } +static SP hyprlandProc; +static bool launchHyprland(Path configPath, Path binaryPath) { NLog::log("{}Launching Hyprland", Colors::YELLOW); hyprlandProc = makeShared(binaryPath, std::vector{"--config", configPath}); hyprlandProc->addEnv("HYPRLAND_HEADLESS_ONLY", "1"); @@ -78,121 +59,147 @@ static bool launchHyprland(std::string configPath, std::string binaryPath) { static bool hyprlandAlive() { NLog::log("{}hyprlandAlive", Colors::YELLOW); - kill(hyprlandProc->pid(), 0); - return errno != ESRCH; + return kill(hyprlandProc->pid(), 0) == 0 || errno != ESRCH; } -static void help() { +[[noreturn]] static void helpAndDie(int exit_code) { NLog::log("usage: hyprtester [arg [...]].\n"); NLog::log(R"(Arguments: --help -h - Show this message again - --config FILE -c FILE - Specify config file to use - --binary FILE -b FILE - Specify Hyprland binary to use - --plugin FILE -p FILE - Specify the location of the test plugin)"); + --config FILE -c FILE - Specify config file to use (default: './test.lua') + --binary FILE -b FILE - Specify Hyprland binary to use (default: '../build/Hyprland') + --plugin FILE -p FILE - Specify the location of the test plugin (default: './'))"); + + std::exit(exit_code); +} + +static Path validatePathOrDie(Path path) { + try { + if (!std::filesystem::is_regular_file(path)) { + throw std::exception(); + } + } catch (...) { + std::println(stderr, "[ ERROR ] File '{}' is not accessible or not a regular file", path.string()); + helpAndDie(EXIT_FAILURE); + } + return path; +} + +namespace { + struct SSettings { + Path configPath; + Path binaryPath; + Path pluginPath; + }; +} + +static SSettings parseSettings(const std::span args) { + static const auto cwd = std::filesystem::current_path(); + SSettings settings{}; + + for (auto it = args.begin(); it < args.end(); it++) { + std::string_view value = *it; + + if (value == "--config" || value == "-c") { + if (std::next(it) == args.end()) { + helpAndDie(EXIT_FAILURE); + } + + settings.configPath = validatePathOrDie(*std::next(it)); + it++; + } else if (value == "--binary" || value == "-b") { + if (std::next(it) == args.end()) { + helpAndDie(EXIT_FAILURE); + } + + settings.binaryPath = validatePathOrDie(*std::next(it)); + it++; + } else if (value == "--plugin" || value == "-p") { + if (std::next(it) == args.end()) { + helpAndDie(EXIT_FAILURE); + } + + settings.pluginPath = validatePathOrDie(*std::next(it)); + it++; + } else if (value == "--help" || value == "-h") { + helpAndDie(EXIT_SUCCESS); + } else { + std::println(stderr, "[ ERROR ] Unknown option '{}' !", *it); + helpAndDie(EXIT_SUCCESS); + } + } + + // Default options + if (settings.configPath.empty()) + settings.configPath = validatePathOrDie(cwd / "test.lua"); + if (settings.binaryPath.empty()) + settings.binaryPath = validatePathOrDie(cwd / "../build/Hyprland"); + if (settings.pluginPath.empty()) + settings.pluginPath = cwd; + + return settings; +} + +static bool preTestCleanup() { + bool failed = false; + + if (!Tests::killAllWindows()) { + NLog::log("{}Internal failure: failed to kill all windows", Colors::RED); + failed = true; + } + if (!Tests::killAllLayers()) { + NLog::log("{}Internal failure: failed to kill all layers", Colors::RED); + failed = true; + } + if (getFromSocket("/reload") != "ok") { + NLog::log("{}Internal failure: failed to reload", Colors::RED); + failed = true; + } + if (!getFromSocket("/activeworkspace").contains("workspace ID 1 (1)")) { + if (getFromSocket("/dispatch hl.dsp.focus({ workspace = '1' })") != "ok") { + NLog::log("{}Internal failure: failed to switch to workspace 1", Colors::RED); + failed = true; + } + } + if (getFromSocket("/dispatch hl.dsp.cursor.move({ x = 960, y = 540 })") != "ok") { + NLog::log("{}Internal failure: failed to reset cursor position", Colors::RED); + failed = true; + } + + return !failed; +} + +static void runTests(std::map& testCases) { + for (auto& [name, tc] : testCases) { + // Clean up before every test + NLog::log("{}Cleaning up", Colors::YELLOW); + (void)preTestCleanup(); + + NLog::log("{}Running test {}", Colors::BLUE, name); + tc.test(); + if (tc.failed) + NLog::log("{}Test failed: {}", Colors::RED, name); + else + NLog::log("{}Test passed: {}", Colors::GREEN, name); + } +} + +static long long countFailed(const std::map& testCases) { + long long ans = 0; + for (const auto& [_, tc] : testCases) { + if (tc.failed) + ans++; + } + return ans; } int main(int argc, char** argv, char** envp) { - std::string configPath = ""; - std::string binaryPath = ""; - std::string pluginPath = std::filesystem::current_path().string(); - - if (argc > 1) { - std::span args{argv + 1, sc(argc - 1)}; - - for (auto it = args.begin(); it != args.end(); it++) { - std::string_view value = *it; - - if (value == "--config" || value == "-c") { - if (std::next(it) == args.end()) { - help(); - - return 1; - } - - configPath = *std::next(it); - - try { - configPath = std::filesystem::canonical(configPath); - - if (!std::filesystem::is_regular_file(configPath)) { - throw std::exception(); - } - } catch (...) { - std::println(stderr, "[ ERROR ] Config file '{}' doesn't exist!", configPath); - help(); - - return 1; - } - - it++; - - continue; - } else if (value == "--binary" || value == "-b") { - if (std::next(it) == args.end()) { - help(); - - return 1; - } - - binaryPath = *std::next(it); - - try { - binaryPath = std::filesystem::canonical(binaryPath); - - if (!std::filesystem::is_regular_file(binaryPath)) { - throw std::exception(); - } - } catch (...) { - std::println(stderr, "[ ERROR ] Binary '{}' doesn't exist!", binaryPath); - help(); - - return 1; - } - - it++; - - continue; - } else if (value == "--plugin" || value == "-p") { - if (std::next(it) == args.end()) { - help(); - - return 1; - } - - pluginPath = *std::next(it); - - try { - pluginPath = std::filesystem::canonical(pluginPath); - - if (!std::filesystem::is_regular_file(pluginPath)) { - throw std::exception(); - } - } catch (...) { - std::println(stderr, "[ ERROR ] plugin '{}' doesn't exist!", pluginPath); - help(); - - return 1; - } - - it++; - - continue; - } else if (value == "--help" || value == "-h") { - help(); - - return 0; - } else { - std::println(stderr, "[ ERROR ] Unknown option '{}' !", *it); - help(); - - return 1; - } - } - } + std::span args{const_cast(argv + 1), sc(argc - 1)}; + const SSettings settings = parseSettings(args); NLog::log("{}launching hl", Colors::YELLOW); - if (!launchHyprland(configPath, binaryPath)) { + if (!launchHyprland(settings.configPath, settings.binaryPath)) { NLog::log("{}well it failed", Colors::RED); std::cout << "\033[37m"; return 1; @@ -221,42 +228,58 @@ int main(int argc, char** argv, char** envp) { getFromSocket("/output create headless"); NLog::log("{}trying to load plugin", Colors::YELLOW); - if (const auto R = getFromSocket(std::format("/plugin load {}", pluginPath)); R != "ok") { + if (const auto R = getFromSocket(std::format("/plugin load {}", settings.pluginPath.string())); R != "ok") { NLog::log("{}Failed to load the test plugin: {}", Colors::RED, R); - getFromSocket("/dispatch exit 1"); + getFromSocket("/dispatch hl.dsp.exit()"); return 1; } NLog::log("{}Loaded plugin", Colors::YELLOW); - NLog::log("{}Running main tests", Colors::YELLOW); + long long failedTests = 0, totalTests = 0; - for (const auto& fn : testFns) { - EXPECT(fn(), true); - } + NLog::log("{}Running main tests", Colors::YELLOW); + runTests(mainTestCases); + failedTests += countFailed(mainTestCases); + totalTests += mainTestCases.size(); NLog::log("{}Running protocol client tests", Colors::YELLOW); + runTests(clientTestCases); + failedTests += countFailed(clientTestCases); + totalTests += clientTestCases.size(); - for (const auto& fn : clientTestFns) { - EXPECT(fn(), true); - } + // TODO: the two tests below should not be hardcoded, include them somewhere NLog::log("{}running plugin test", Colors::YELLOW); - EXPECT(testPlugin(), true); + if (!testPlugin()) { + NLog::log("{}Test failed: plugin test", Colors::RED); + failedTests++; + } else { + NLog::log("{}Test passed: plugin test", Colors::GREEN); + } + totalTests++; NLog::log("{}running vkb test from plugin", Colors::YELLOW); - EXPECT(testVkb(), true); + if (!testVkb()) { + NLog::log("{}Test failed: vkb test from plugin", Colors::RED); + failedTests++; + } else { + NLog::log("{}Test passed: vkb test from plugin", Colors::GREEN); + } + totalTests++; // kill hyprland NLog::log("{}dispatching exit", Colors::YELLOW); - getFromSocket("/dispatch exit"); + getFromSocket("/dispatch hl.dsp.exit()"); - NLog::log("\n{}Summary:\n\tPASSED: {}{}{}/{}\n\tFAILED: {}{}{}/{}\n{}", Colors::RESET, Colors::GREEN, TESTS_PASSED, Colors::RESET, TESTS_PASSED + TESTS_FAILED, Colors::RED, - TESTS_FAILED, Colors::RESET, TESTS_PASSED + TESTS_FAILED, (TESTS_FAILED > 0 ? std::string{Colors::RED} + "\nSome tests failed.\n" : "")); + NLog::log("\nSummary:\n\tPASSED: {}{}{}/{}", Colors::GREEN, totalTests - failedTests, Colors::RESET, totalTests); + NLog::log("\tFAILED: {}{}{}/{}", Colors::RED, failedTests, Colors::RESET, totalTests); + if (failedTests > 0) + NLog::log("{}Some tests failed.", Colors::RED); kill(hyprlandProc->pid(), SIGKILL); hyprlandProc.reset(); - return ret || TESTS_FAILED; + return failedTests > 0; } diff --git a/hyprtester/src/shared.hpp b/hyprtester/src/shared.hpp index 941788fde..d40b2f1fc 100644 --- a/hyprtester/src/shared.hpp +++ b/hyprtester/src/shared.hpp @@ -1,12 +1,12 @@ // Stolen from hyprutils #pragma once -#include +#include +#include -inline std::string HIS = ""; -inline std::string WLDISPLAY = ""; -inline int TESTS_PASSED = 0; -inline int TESTS_FAILED = 0; +// TODO: localize these global variables +inline std::string HIS = ""; +inline std::string WLDISPLAY = ""; namespace Colors { constexpr const char* RED = "\x1b[31m"; @@ -18,94 +18,229 @@ namespace Colors { constexpr const char* RESET = "\x1b[0m"; }; +// ================================= +// TEST CASES DEFINITION +// ================================= + +class CTestCase { + public: + CTestCase() = default; + CTestCase(const CTestCase&) = delete; // Test cases probably should not be copied + bool failed = false; + virtual ~CTestCase() = default; + + // TODO: `test` will be protected + virtual void test() = 0; +}; + +#define TEST_CASE(name) \ + namespace { \ + class TestCase_##name : public CTestCase { \ + public: \ + void test() override; \ + }; \ + } \ + \ + static TestCase_##name test_case_##name{}; \ + static auto register_test_case_##name = [] { \ + /* `TEST_CASES_STORAGE` must be defined by the caller */ \ + TEST_CASES_STORAGE.emplace(#name, test_case_##name); \ + return 1; \ + }(); \ + \ + void TestCase_##name::test() + +#define SUBTEST(name, ...) \ + namespace { \ + class Subtest_##name { \ + public: \ + bool failed = false; \ + \ + void main(__VA_ARGS__); \ + }; \ + } \ + \ + void Subtest_##name::main(__VA_ARGS__) + +#define CALL_SUBTEST(name, ...) \ + do { \ + auto subtest_##name = Subtest_##name{}; \ + subtest_##name.main(__VA_ARGS__); \ + if (subtest_##name.failed) { \ + NLog::log("{}Subtest {}({}) failed", Colors::RED, #name, #__VA_ARGS__); \ + this->failed = true; \ + return; \ + } \ + } while (0) + +// ================================= +// IN-TEST MACROS +// ================================= + +/// Marks the test as failed without terminating it +#define MARK_TEST_FAILED_SILENT() this->failed = true + +/// Prints a failure message and makrs the test as failed without terminating it +#define MARK_TEST_FAILED(fmt, ...) \ + do { \ + NLog::log("{}Failed:{} " fmt ". Source: {}@{}.", Colors::RED, Colors::RESET __VA_OPT__(, ) __VA_ARGS__, __FILE__, __LINE__); \ + MARK_TEST_FAILED_SILENT(); \ + } while (0) + +/// Terminates the test execution and marks it as failed +#define FAIL_TEST_SILENT() \ + do { \ + MARK_TEST_FAILED_SILENT(); \ + return; \ + } while (0) + +/// Prints a failure message, terminates the test execution, and marks it as failed +#define FAIL_TEST(fmt, ...) \ + do { \ + MARK_TEST_FAILED(fmt, __VA_ARGS__); \ + return; \ + } while (0) + +#define LOG_OK(fmt, ...) \ + do { \ + NLog::log("{}OK:{} " fmt ". Source: {}@{}", Colors::GREEN, Colors::RESET, __VA_ARGS__, __FILE__, __LINE__); \ + } while (0) + +// In case of failure: +// - All the `EXPECT*` macros will print a message and mark the test as failed without terminating its execution; +// - All the `ASSERT*` macros will print a message, mark the test as failed, and terminate it. +// +// `OK` acts like `ASSERT_OK`. + #define EXPECT_MAX_DELTA(expr, desired, delta) \ if (const auto RESULT = expr; std::abs(RESULT - (desired)) > delta) { \ - NLog::log("{}Failed: {}{}, expected max delta of {}, got delta {} ({} - {}). Source: {}@{}.", Colors::RED, Colors::RESET, #expr, delta, (RESULT - (desired)), RESULT, \ - desired, __FILE__, __LINE__); \ - ret = 1; \ - TESTS_FAILED++; \ + MARK_TEST_FAILED("{}, expected max delta of {}, got delta {} ({} - {})", #expr, delta, (RESULT - (desired)), RESULT, desired); \ } else { \ - NLog::log("{}Passed: {}{}. Got {}", Colors::GREEN, Colors::RESET, #expr, (RESULT - (desired))); \ - TESTS_PASSED++; \ + LOG_OK("{}. Got {}", #expr, (RESULT - (desired))); \ } #define EXPECT(expr, val) \ - if (const auto RESULT = expr; RESULT != (val)) { \ - NLog::log("{}Failed: {}{}, expected {}, got {}. Source: {}@{}.", Colors::RED, Colors::RESET, #expr, val, RESULT, __FILE__, __LINE__); \ - ret = 1; \ - TESTS_FAILED++; \ - } else { \ - NLog::log("{}Passed: {}{}. Got {}", Colors::GREEN, Colors::RESET, #expr, val); \ - TESTS_PASSED++; \ - } + do { \ + if (const auto RESULT = expr; RESULT != (val)) { \ + MARK_TEST_FAILED("{}, expected {}, got {}", #expr, val, RESULT); \ + } else { \ + LOG_OK("{}. Got {}", #expr, val); \ + } \ + } while (0) #define EXPECT_NOT(expr, val) \ if (const auto RESULT = expr; RESULT == (val)) { \ - NLog::log("{}Failed: {}{}, expected not {}, got {}. Source: {}@{}.", Colors::RED, Colors::RESET, #expr, val, RESULT, __FILE__, __LINE__); \ - ret = 1; \ - TESTS_FAILED++; \ + MARK_TEST_FAILED("{}, expected not {}, got {}", #expr, val, RESULT); \ } else { \ - NLog::log("{}Passed: {}{}. Got {}", Colors::GREEN, Colors::RESET, #expr, val); \ - TESTS_PASSED++; \ + LOG_OK("{}. Got {}", #expr, val); \ } #define EXPECT_VECTOR2D(expr, val) \ do { \ const auto& RESULT = expr; \ - const auto& EXPECTED = val; \ - if (!(std::abs(RESULT.x - EXPECTED.x) < 1e-6 && std::abs(RESULT.y - EXPECTED.y) < 1e-6)) { \ - NLog::log("{}Failed: {}{}, expected [{}, {}], got [{}, {}]. Source: {}@{}.", Colors::RED, Colors::RESET, #expr, EXPECTED.x, EXPECTED.y, RESULT.x, RESULT.y, __FILE__, \ - __LINE__); \ - ret = 1; \ - TESTS_FAILED++; \ + const auto& ASSERTED = val; \ + if (!(std::abs(RESULT.x - ASSERTED.x) < 1e-6 && std::abs(RESULT.y - ASSERTED.y) < 1e-6)) { \ + MARK_TEST_FAILED("{}, expected [{}, {}], got [{}, {}]", #expr, ASSERTED.x, ASSERTED.y, RESULT.x, RESULT.y); \ } else { \ - NLog::log("{}Passed: {}{}. Got [{}, {}].", Colors::GREEN, Colors::RESET, #expr, RESULT.x, RESULT.y); \ - TESTS_PASSED++; \ + LOG_OK("{}. Got [{}, {}].", #expr, RESULT.x, RESULT.y); \ } \ } while (0) +// String check macros below use a special error message layout, putting the Source reference before `haystack`, +// since `haystack` may be a large multi-line string. Thus, they use bare `NLog::log` + `MARK_TEST_FAILED_SILENT`. + #define EXPECT_CONTAINS(haystack, needle) \ - if (const auto EXPECTED = needle; !std::string{haystack}.contains(EXPECTED)) { \ + if (const auto ASSERTED = needle; !std::string{haystack}.contains(ASSERTED)) { \ NLog::log("{}Failed: {}{} should contain {} but doesn't. Source: {}@{}. Haystack is:\n{}", Colors::RED, Colors::RESET, #haystack, #needle, __FILE__, __LINE__, \ std::string{haystack}); \ - ret = 1; \ - TESTS_FAILED++; \ + MARK_TEST_FAILED_SILENT(); \ } else { \ - NLog::log("{}Passed: {}{} contains {}.", Colors::GREEN, Colors::RESET, #haystack, EXPECTED); \ - TESTS_PASSED++; \ + LOG_OK("{} contains {}.", #haystack, ASSERTED); \ } #define EXPECT_NOT_CONTAINS(haystack, needle) \ if (std::string{haystack}.contains(needle)) { \ NLog::log("{}Failed: {}{} shouldn't contain {} but does. Source: {}@{}. Haystack is:\n{}", Colors::RED, Colors::RESET, #haystack, #needle, __FILE__, __LINE__, \ std::string{haystack}); \ - ret = 1; \ - TESTS_FAILED++; \ + MARK_TEST_FAILED_SILENT(); \ } else { \ - NLog::log("{}Passed: {}{} doesn't contain {}.", Colors::GREEN, Colors::RESET, #haystack, #needle); \ - TESTS_PASSED++; \ + LOG_OK("{} doesn't contain {}.", #haystack, #needle); \ } #define EXPECT_STARTS_WITH(str, what) \ if (!std::string{str}.starts_with(what)) { \ NLog::log("{}Failed: {}{} should start with {} but doesn't. Source: {}@{}. String is:\n{}", Colors::RED, Colors::RESET, #str, #what, __FILE__, __LINE__, \ std::string{str}); \ - ret = 1; \ - TESTS_FAILED++; \ + MARK_TEST_FAILED_SILENT(); \ } else { \ - NLog::log("{}Passed: {}{} starts with {}.", Colors::GREEN, Colors::RESET, #str, #what); \ - TESTS_PASSED++; \ + LOG_OK("{} starts with {}.", #str, #what); \ } #define EXPECT_COUNT_STRING(str, what, no) \ if (Tests::countOccurrences(str, what) != no) { \ NLog::log("{}Failed: {}{} should contain {} {} times, but doesn't. Source: {}@{}. String is:\n{}", Colors::RED, Colors::RESET, #str, #what, no, __FILE__, __LINE__, \ std::string{str}); \ - ret = 1; \ - TESTS_FAILED++; \ + MARK_TEST_FAILED_SILENT(); \ } else { \ - NLog::log("{}Passed: {}{} contains {} {} times.", Colors::GREEN, Colors::RESET, #str, #what, no); \ - TESTS_PASSED++; \ + LOG_OK("{} contains {} {} times.", #str, #what, no); \ } -#define OK(x) EXPECT(x, "ok") +#define EXPECT_OK(x) EXPECT(x, "ok") + +#define ASSERT_MAX_DELTA(expr, desired, delta) \ + do { \ + EXPECT_MAX_DELTA(expr, desired, delta); \ + if (this->failed) \ + return; \ + } while (0) + +#define ASSERT(expr, val) \ + do { \ + EXPECT(expr, val); \ + if (this->failed) \ + return; \ + } while (0) + +#define ASSERT_NOT(expr, val) \ + do { \ + EXPECT_NOT(expr, val); \ + if (this->failed) \ + return; \ + } while (0) + +#define ASSERT_VECTOR2D(expr, val) \ + do { \ + EXPECT_VECTOR2D(expr, val); \ + if (this->failed) \ + return; \ + } while (0) + +#define ASSERT_CONTAINS(haystack, needle) \ + do { \ + EXPECT_CONTAINS(haystack, needle); \ + if (this->failed) \ + return; \ + } while (0) + +#define ASSERT_NOT_CONTAINS(haystack, needle) \ + do { \ + EXPECT_NOT_CONTAINS(haystack, needle); \ + if (this->failed) \ + return; \ + } while (0) + +#define ASSERT_STARTS_WITH(str, what) \ + do { \ + EXPECT_STARTS_WITH(str, what); \ + if (this->failed) \ + return; \ + } while (0) + +#define ASSERT_COUNT_STRING(str, what, no) \ + do { \ + EXPECT_COUNT_STRING(str, what, no); \ + if (this->failed) \ + return; \ + } while (0) + +#define OK(x) ASSERT(x, "ok") diff --git a/hyprtester/src/tests/clients/child-window.cpp b/hyprtester/src/tests/clients/child-window.cpp index a5680d4f0..f97ffde18 100644 --- a/hyprtester/src/tests/clients/child-window.cpp +++ b/hyprtester/src/tests/clients/child-window.cpp @@ -1,4 +1,3 @@ -#include "../../shared.hpp" #include "../../hyprctlCompat.hpp" #include "../shared.hpp" #include "tests.hpp" @@ -7,6 +6,7 @@ #include #include +#include #include #include #include @@ -17,15 +17,6 @@ using namespace Hyprutils::Memory; #define SP CSharedPointer -struct SClient { - SP proc; - std::array readBuf; - CFileDescriptor readFd, writeFd; - struct pollfd fds; -}; - -static int ret = 0; - static bool waitForWindow(SP proc, int windowsBefore) { int counter = 0; while (Tests::processAlive(proc->pid()) && Tests::windowCount() == windowsBefore) { @@ -40,59 +31,72 @@ static bool waitForWindow(SP proc, int windowsBefore) { return Tests::processAlive(proc->pid()); } -static bool startClient(SClient& client) { +namespace { + class CClient { + SP proc; + std::array readBuf; + CFileDescriptor readFd, writeFd; + struct pollfd fds; + + public: + CClient(); + ~CClient(); + bool createChild(); + }; +} + +CClient::CClient() { NLog::log("{}Attempting to start child-window client", Colors::YELLOW); - client.proc = makeShared(binaryDir + "/child-window", std::vector{}); + this->proc = makeShared(binaryDir + "/child-window", std::vector{}); - client.proc->addEnv("WAYLAND_DISPLAY", WLDISPLAY); + this->proc->addEnv("WAYLAND_DISPLAY", WLDISPLAY); int procInPipeFd[2], procOutPipeFd[2]; if (pipe(procInPipeFd) != 0 || pipe(procOutPipeFd) != 0) { NLog::log("{}Unable to open pipe to client", Colors::RED); - return false; + throw std::exception(); } - client.writeFd = CFileDescriptor(procInPipeFd[1]); - client.proc->setStdinFD(procInPipeFd[0]); + this->writeFd = CFileDescriptor(procInPipeFd[1]); + this->proc->setStdinFD(procInPipeFd[0]); - client.readFd = CFileDescriptor(procOutPipeFd[0]); - client.proc->setStdoutFD(procOutPipeFd[1]); + this->readFd = CFileDescriptor(procOutPipeFd[0]); + this->proc->setStdoutFD(procOutPipeFd[1]); - if (!client.proc->runAsync()) { + if (!this->proc->runAsync()) { NLog::log("{}Failed to run client", Colors::RED); - return false; + throw std::exception(); } close(procInPipeFd[0]); close(procOutPipeFd[1]); - if (!waitForWindow(client.proc, Tests::windowCount())) { + if (!waitForWindow(this->proc, Tests::windowCount())) { NLog::log("{}Window took too long to open", Colors::RED); - return false; + throw std::exception(); } NLog::log("{}Started child-window client", Colors::YELLOW); - return true; } -static void stopClient(SClient& client) { +CClient::~CClient() { std::string cmd = "exit\n"; - write(client.writeFd.get(), cmd.c_str(), cmd.length()); + write(this->writeFd.get(), cmd.c_str(), cmd.length()); - kill(client.proc->pid(), SIGKILL); - client.proc.reset(); + kill(this->proc->pid(), SIGKILL); + this->proc.reset(); } -static bool createChild(SClient& client) { +bool CClient::createChild() { std::string cmd = "toplevel\n"; - if ((size_t)write(client.writeFd.get(), cmd.c_str(), cmd.length()) != cmd.length()) + if ((size_t)write(this->writeFd.get(), cmd.c_str(), cmd.length()) != cmd.length()) return false; - if (!waitForWindow(client.proc, Tests::windowCount())) + if (!waitForWindow(this->proc, Tests::windowCount())) NLog::log("{}Child window took too long to open", Colors::RED); - if (getFromSocket("/dispatch focuswindow class:child-test-child") != "ok") { + if (getFromSocket("/dispatch hl.dsp.focus({ window = 'class:child-test-child' })") != "ok") { NLog::log("{}Failed to focus child window", Colors::RED); return false; } @@ -100,19 +104,21 @@ static bool createChild(SClient& client) { return true; } -static bool test() { - SClient client; +TEST_CASE(childWindow) { + { + std::optional client; + try { + client.emplace(); + } catch (...) { FAIL_TEST("Couldn't start the client"); } - if (!startClient(client)) - return false; - OK(getFromSocket("/dispatch setfloating class:child-test-parent")); - OK(getFromSocket("/dispatch pin class:child-test-parent")); + OK(getFromSocket("/dispatch hl.dsp.window.float({ action = 'set', window = 'class:child-test-parent' })")); + OK(getFromSocket("/dispatch hl.dsp.window.pin({ action = 'set', window = 'class:child-test-parent' })")); - createChild(client); - EXPECT(Tests::windowCount(), 2) - EXPECT_COUNT_STRING(getFromSocket("/clients"), "pinned: 1", 2); + client->createChild(); + EXPECT(Tests::windowCount(), 2); + EXPECT_COUNT_STRING(getFromSocket("/clients"), "pinned: 1", 2); + } - stopClient(client); NLog::log("{}Reloading config", Colors::YELLOW); OK(getFromSocket("/reload")); Tests::killAllWindows(); @@ -122,30 +128,27 @@ static bool test() { NLog::log("{}Test child windows are not auto-grouped", Colors::GREEN); auto kitty = Tests::spawnKitty(); if (!kitty) { - NLog::log("{}Error: kitty did not spawn", Colors::RED); - return false; + FAIL_TEST("Couldn't spawn kitty"); } // create group and enable auto-grouping - OK(getFromSocket("/dispatch togglegroup")); - OK(getFromSocket("/keyword group:auto_group true")); + OK(getFromSocket("/dispatch hl.dsp.group.toggle()")); + OK(getFromSocket("/eval hl.config({ group = { auto_group = true } })")); - SClient client2; - if (!startClient(client2)) - return false; + { + std::optional client2; + try { + client2.emplace(); + } catch (...) { FAIL_TEST("Couldn't start the client"); } - EXPECT(Tests::windowCount(), 2); - createChild(client2); - EXPECT(Tests::windowCount(), 3); + EXPECT(Tests::windowCount(), 2); + client2->createChild(); + EXPECT(Tests::windowCount(), 3); - // child has set_parent so shouldBeFloated returns true, it should not be auto-grouped - EXPECT_COUNT_STRING(getFromSocket("/clients"), "grouped: 0", 1); + // child has set_parent so shouldBeFloated returns true, it should not be auto-grouped + EXPECT_COUNT_STRING(getFromSocket("/clients"), "grouped: 0", 1); + } - stopClient(client2); Tests::killAllWindows(); EXPECT(Tests::windowCount(), 0); - - return !ret; } - -REGISTER_CLIENT_TEST_FN(test); diff --git a/hyprtester/src/tests/clients/pointer-scroll.cpp b/hyprtester/src/tests/clients/pointer-scroll.cpp index b5fb68fbc..c468deb3f 100644 --- a/hyprtester/src/tests/clients/pointer-scroll.cpp +++ b/hyprtester/src/tests/clients/pointer-scroll.cpp @@ -1,4 +1,3 @@ -#include "../../shared.hpp" #include "../../hyprctlCompat.hpp" #include "../shared.hpp" #include "tests.hpp" @@ -7,6 +6,7 @@ #include #include +#include #include #include #include @@ -17,100 +17,103 @@ using namespace Hyprutils::Memory; #define SP CSharedPointer -struct SClient { - SP proc; - std::array readBuf; - CFileDescriptor readFd, writeFd; - struct pollfd fds; -}; +namespace { + class CClient { + SP proc; + std::array readBuf; + CFileDescriptor readFd, writeFd; + struct pollfd fds; -static int ret = 0; + public: + CClient(); + ~CClient(); + int getLastDelta(); + }; +} -static bool startClient(SClient& client) { - client.proc = makeShared(binaryDir + "/pointer-scroll", std::vector{}); +CClient::CClient() { + this->proc = makeShared(binaryDir + "/pointer-scroll", std::vector{}); - client.proc->addEnv("WAYLAND_DISPLAY", WLDISPLAY); + this->proc->addEnv("WAYLAND_DISPLAY", WLDISPLAY); int pipeFds1[2], pipeFds2[2]; if (pipe(pipeFds1) != 0 || pipe(pipeFds2) != 0) { NLog::log("{}Unable to open pipe to client", Colors::RED); - return false; + throw std::exception(); } - client.writeFd = CFileDescriptor(pipeFds1[1]); - client.proc->setStdinFD(pipeFds1[0]); + this->writeFd = CFileDescriptor(pipeFds1[1]); + this->proc->setStdinFD(pipeFds1[0]); - client.readFd = CFileDescriptor(pipeFds2[0]); - client.proc->setStdoutFD(pipeFds2[1]); + this->readFd = CFileDescriptor(pipeFds2[0]); + this->proc->setStdoutFD(pipeFds2[1]); const int COUNT_BEFORE = Tests::windowCount(); - client.proc->runAsync(); + this->proc->runAsync(); close(pipeFds1[0]); close(pipeFds2[1]); - client.fds = {.fd = client.readFd.get(), .events = POLLIN}; - if (poll(&client.fds, 1, 1000) != 1 || !(client.fds.revents & POLLIN)) - return false; + this->fds = {.fd = this->readFd.get(), .events = POLLIN}; + if (poll(&this->fds, 1, 1000) != 1 || !(this->fds.revents & POLLIN)) + throw std::exception(); - client.readBuf.fill(0); - if (read(client.readFd.get(), client.readBuf.data(), client.readBuf.size() - 1) == -1) - return false; + this->readBuf.fill(0); + if (read(this->readFd.get(), this->readBuf.data(), this->readBuf.size() - 1) == -1) + throw std::exception(); - std::string ret = std::string{client.readBuf.data()}; + std::string ret = std::string{this->readBuf.data()}; if (ret.find("started") == std::string::npos) { NLog::log("{}Failed to start pointer-scroll client, read {}", Colors::RED, ret); - return false; + throw std::exception(); } // wait for window to appear int counter = 0; - while (Tests::processAlive(client.proc->pid()) && Tests::windowCount() == COUNT_BEFORE) { + while (Tests::processAlive(this->proc->pid()) && Tests::windowCount() == COUNT_BEFORE) { counter++; std::this_thread::sleep_for(std::chrono::milliseconds(100)); if (counter > 50) { NLog::log("{}pointer-scroll client took too long to open", Colors::RED); - return false; + throw std::exception(); } } - if (getFromSocket(std::format("/dispatch setprop pid:{} no_anim 1", client.proc->pid())) != "ok") { + if (getFromSocket(std::format("/dispatch hl.dsp.window.set_prop({{ window = 'pid:{}', prop = 'no_anim', value = '1' }})", this->proc->pid())) != "ok") { NLog::log("{}Failed to disable animations for client window", Colors::RED, ret); - return false; + throw std::exception(); } - if (getFromSocket(std::format("/dispatch focuswindow pid:{}", client.proc->pid())) != "ok") { + if (getFromSocket(std::format("/dispatch hl.dsp.focus({{ window = 'pid:{}' }})", this->proc->pid())) != "ok") { NLog::log("{}Failed to focus pointer-scroll client", Colors::RED, ret); - return false; + throw std::exception(); } NLog::log("{}Started pointer-scroll client", Colors::YELLOW); - - return true; } -static void stopClient(SClient& client) { +CClient::~CClient() { std::string cmd = "exit\n"; - write(client.writeFd.get(), cmd.c_str(), cmd.length()); + write(this->writeFd.get(), cmd.c_str(), cmd.length()); - kill(client.proc->pid(), SIGKILL); - client.proc.reset(); + kill(this->proc->pid(), SIGKILL); + this->proc.reset(); } -static int getLastDelta(SClient& client) { +int CClient::getLastDelta() { std::string cmd = "hypr"; - if ((size_t)write(client.writeFd.get(), cmd.c_str(), cmd.length()) != cmd.length()) + if ((size_t)write(this->writeFd.get(), cmd.c_str(), cmd.length()) != cmd.length()) return false; - if (poll(&client.fds, 1, 1500) != 1 || !(client.fds.revents & POLLIN)) + if (poll(&this->fds, 1, 1500) != 1 || !(this->fds.revents & POLLIN)) return false; - ssize_t bytesRead = read(client.fds.fd, client.readBuf.data(), 1023); + ssize_t bytesRead = read(this->fds.fd, this->readBuf.data(), 1023); if (bytesRead == -1) return false; - client.readBuf[bytesRead] = 0; - std::string received = std::string{client.readBuf.data()}; + this->readBuf[bytesRead] = 0; + std::string received = std::string{this->readBuf.data()}; received.pop_back(); try { @@ -119,38 +122,32 @@ static int getLastDelta(SClient& client) { } static bool sendScroll(int delta) { - return getFromSocket(std::format("/dispatch plugin:test:scroll {}", delta)) == "ok"; + return getFromSocket(std::format("/eval hl.plugin.test.scroll({})", delta)) == "ok"; } -static bool test() { - SClient client; +TEST_CASE(pointerScroll) { + std::optional client; + try { + client.emplace(); + } catch (...) { FAIL_TEST("Couldn't start the client"); } - if (!startClient(client)) - return false; - - EXPECT(getFromSocket("/keyword input:emulate_discrete_scroll 0"), "ok"); + EXPECT(getFromSocket("r/eval hl.config({ input = { emulate_discrete_scroll = 0 } })"), "ok"); EXPECT(sendScroll(10), true); - EXPECT(getLastDelta(client), 10); + EXPECT(client->getLastDelta(), 10); - EXPECT(getFromSocket("/keyword input:scroll_factor 2"), "ok"); + EXPECT(getFromSocket("r/eval hl.config({ input = { scroll_factor = 2 } })"), "ok"); EXPECT(sendScroll(10), true); - EXPECT(getLastDelta(client), 20); + EXPECT(client->getLastDelta(), 20); - EXPECT(getFromSocket("r/keyword device[test-mouse-1]:scroll_factor 3"), "ok"); + EXPECT(getFromSocket("r/eval hl.device({ name = 'test-mouse-1', scroll_factor = 3 })"), "ok"); EXPECT(sendScroll(10), true); - EXPECT(getLastDelta(client), 30); + EXPECT(client->getLastDelta(), 30); - EXPECT(getFromSocket("r/dispatch setprop active scroll_mouse 4"), "ok"); + EXPECT(getFromSocket("r/dispatch hl.dsp.window.set_prop({ window = 'active', prop = 'scroll_mouse', value = '4' })"), "ok"); EXPECT(sendScroll(10), true); - EXPECT(getLastDelta(client), 40); - - stopClient(client); + EXPECT(client->getLastDelta(), 40); NLog::log("{}Reloading the config", Colors::YELLOW); OK(getFromSocket("/reload")); - - return !ret; } - -REGISTER_CLIENT_TEST_FN(test); diff --git a/hyprtester/src/tests/clients/pointer-warp.cpp b/hyprtester/src/tests/clients/pointer-warp.cpp index be992566d..e13e03f69 100644 --- a/hyprtester/src/tests/clients/pointer-warp.cpp +++ b/hyprtester/src/tests/clients/pointer-warp.cpp @@ -1,4 +1,3 @@ -#include "../../shared.hpp" #include "../../hyprctlCompat.hpp" #include "../shared.hpp" #include "tests.hpp" @@ -7,6 +6,7 @@ #include #include +#include #include #include #include @@ -17,102 +17,105 @@ using namespace Hyprutils::Memory; #define SP CSharedPointer -struct SClient { - SP proc; - std::array readBuf; - CFileDescriptor readFd, writeFd; - struct pollfd fds; -}; +namespace { + class CClient { + SP proc; + std::array readBuf; + CFileDescriptor readFd, writeFd; + struct pollfd fds; -static int ret = 0; + public: + CClient(); + ~CClient(); + bool sendWarp(int x, int y); + }; +} -static bool startClient(SClient& client) { - client.proc = makeShared(binaryDir + "/pointer-warp", std::vector{}); +CClient::CClient() { + this->proc = makeShared(binaryDir + "/pointer-warp", std::vector{}); - client.proc->addEnv("WAYLAND_DISPLAY", WLDISPLAY); + this->proc->addEnv("WAYLAND_DISPLAY", WLDISPLAY); int pipeFds1[2], pipeFds2[2]; if (pipe(pipeFds1) != 0 || pipe(pipeFds2) != 0) { NLog::log("{}Unable to open pipe to client", Colors::RED); - return false; + throw std::exception(); } - client.writeFd = CFileDescriptor(pipeFds1[1]); - client.proc->setStdinFD(pipeFds1[0]); + this->writeFd = CFileDescriptor(pipeFds1[1]); + this->proc->setStdinFD(pipeFds1[0]); - client.readFd = CFileDescriptor(pipeFds2[0]); - client.proc->setStdoutFD(pipeFds2[1]); + this->readFd = CFileDescriptor(pipeFds2[0]); + this->proc->setStdoutFD(pipeFds2[1]); const int COUNT_BEFORE = Tests::windowCount(); - client.proc->runAsync(); + this->proc->runAsync(); close(pipeFds1[0]); close(pipeFds2[1]); - client.fds = {.fd = client.readFd.get(), .events = POLLIN}; - if (poll(&client.fds, 1, 1000) != 1 || !(client.fds.revents & POLLIN)) - return false; + this->fds = {.fd = this->readFd.get(), .events = POLLIN}; + if (poll(&this->fds, 1, 1000) != 1 || !(this->fds.revents & POLLIN)) + throw std::exception(); - client.readBuf.fill(0); - if (read(client.readFd.get(), client.readBuf.data(), client.readBuf.size() - 1) == -1) - return false; + this->readBuf.fill(0); + if (read(this->readFd.get(), this->readBuf.data(), this->readBuf.size() - 1) == -1) + throw std::exception(); - std::string ret = std::string{client.readBuf.data()}; + std::string ret = std::string{this->readBuf.data()}; if (ret.find("started") == std::string::npos) { NLog::log("{}Failed to start pointer-warp client, read {}", Colors::RED, ret); - return false; + throw std::exception(); } // wait for window to appear int counter = 0; - while (Tests::processAlive(client.proc->pid()) && Tests::windowCount() == COUNT_BEFORE) { + while (Tests::processAlive(this->proc->pid()) && Tests::windowCount() == COUNT_BEFORE) { counter++; std::this_thread::sleep_for(std::chrono::milliseconds(100)); - if (counter > 50) { - NLog::log("{}pointer-warp client took too long to open", Colors::RED); - return false; + if (counter > 100) { + NLog::log("{}pointer-warp client took too long to open, continuing", Colors::YELLOW); + throw std::exception(); } } - if (getFromSocket(std::format("/dispatch setprop pid:{} no_anim 1", client.proc->pid())) != "ok") { + if (getFromSocket(std::format("/dispatch hl.dsp.window.set_prop({{ window = 'pid:{}', prop = 'no_anim', value = '1' }})", this->proc->pid())) != "ok") { NLog::log("{}Failed to disable animations for client window", Colors::RED, ret); - return false; + throw std::exception(); } - if (getFromSocket(std::format("/dispatch focuswindow pid:{}", client.proc->pid())) != "ok") { + if (getFromSocket(std::format("/dispatch hl.dsp.focus({{ window = 'pid:{}' }})", this->proc->pid())) != "ok") { NLog::log("{}Failed to focus pointer-warp client", Colors::RED, ret); - return false; + throw std::exception(); } NLog::log("{}Started pointer-warp client", Colors::YELLOW); - - return true; } -static void stopClient(SClient& client) { +CClient::~CClient() { std::string cmd = "exit\n"; - write(client.writeFd.get(), cmd.c_str(), cmd.length()); + write(this->writeFd.get(), cmd.c_str(), cmd.length()); - kill(client.proc->pid(), SIGKILL); - client.proc.reset(); + kill(this->proc->pid(), SIGKILL); + this->proc.reset(); } // format is like below // "warp 20 20\n" would ask to warp cursor to x=20,y=20 in surface local coords -static bool sendWarp(SClient& client, int x, int y) { +bool CClient::sendWarp(int x, int y) { std::string cmd = std::format("warp {} {}\n", x, y); - if ((size_t)write(client.writeFd.get(), cmd.c_str(), cmd.length()) != cmd.length()) + if ((size_t)write(this->writeFd.get(), cmd.c_str(), cmd.length()) != cmd.length()) return false; - if (poll(&client.fds, 1, 1500) != 1 || !(client.fds.revents & POLLIN)) + if (poll(&this->fds, 1, 1500) != 1 || !(this->fds.revents & POLLIN)) return false; - ssize_t bytesRead = read(client.fds.fd, client.readBuf.data(), 1023); + ssize_t bytesRead = read(this->fds.fd, this->readBuf.data(), 1023); if (bytesRead == -1) return false; - client.readBuf[bytesRead] = 0; - std::string recieved = std::string{client.readBuf.data()}; + this->readBuf[bytesRead] = 0; + std::string recieved = std::string{this->readBuf.data()}; recieved.pop_back(); return true; @@ -147,48 +150,40 @@ static bool isCursorPos(int x, int y) { return clientX == x && clientY == y; } -static bool test() { - SClient client; +TEST_CASE(pointerWarp) { + std::optional client; - if (!startClient(client)) - return false; + try { + client.emplace(); + } catch (...) { FAIL_TEST("Couldn't start the client"); } - EXPECT(sendWarp(client, 100, 100), true); + EXPECT(client->sendWarp(100, 100), true); EXPECT(isCursorPos(100, 100), true); - EXPECT(sendWarp(client, 0, 0), true); + EXPECT(client->sendWarp(0, 0), true); EXPECT(isCursorPos(0, 0), true); - EXPECT(sendWarp(client, 200, 200), true); + EXPECT(client->sendWarp(200, 200), true); EXPECT(isCursorPos(200, 200), true); - EXPECT(sendWarp(client, 100, -100), true); + EXPECT(client->sendWarp(100, -100), true); EXPECT(isCursorPos(200, 200), true); - EXPECT(sendWarp(client, 234, 345), true); + EXPECT(client->sendWarp(234, 345), true); EXPECT(isCursorPos(234, 345), true); - EXPECT(sendWarp(client, -1, -1), true); + EXPECT(client->sendWarp(-1, -1), true); EXPECT(isCursorPos(234, 345), true); - EXPECT(sendWarp(client, 1, -1), true); + EXPECT(client->sendWarp(1, -1), true); EXPECT(isCursorPos(234, 345), true); - EXPECT(sendWarp(client, 13, 37), true); + EXPECT(client->sendWarp(13, 37), true); EXPECT(isCursorPos(13, 37), true); - EXPECT(sendWarp(client, -100, 100), true); + EXPECT(client->sendWarp(-100, 100), true); EXPECT(isCursorPos(13, 37), true); - EXPECT(sendWarp(client, -1, 1), true); + EXPECT(client->sendWarp(-1, 1), true); EXPECT(isCursorPos(13, 37), true); - - stopClient(client); - - NLog::log("{}Reloading the config", Colors::YELLOW); - OK(getFromSocket("/reload")); - - return !ret; } - -REGISTER_CLIENT_TEST_FN(test); diff --git a/hyprtester/src/tests/clients/shortcut-inhibitor.cpp b/hyprtester/src/tests/clients/shortcut-inhibitor.cpp index 91c3376c5..2e0296644 100644 --- a/hyprtester/src/tests/clients/shortcut-inhibitor.cpp +++ b/hyprtester/src/tests/clients/shortcut-inhibitor.cpp @@ -1,4 +1,3 @@ -#include "../../shared.hpp" #include "../../hyprctlCompat.hpp" #include "../shared.hpp" #include "tests.hpp" @@ -7,6 +6,7 @@ #include #include +#include #include #include #include @@ -17,106 +17,109 @@ using namespace Hyprutils::Memory; #define SP CSharedPointer -struct SClient { - SP proc; - std::array readBuf; - CFileDescriptor readFd, writeFd; - struct pollfd fds; -}; +namespace { -static int ret = 0; + class CClient { + SP proc; + std::array readBuf; + CFileDescriptor readFd, writeFd; + struct pollfd fds; -static bool startClient(SClient& client) { + public: + CClient(); + ~CClient(); + }; +} + +CClient::CClient() { Tests::killAllWindows(); - client.proc = makeShared(binaryDir + "/shortcut-inhibitor", std::vector{}); + this->proc = makeShared(binaryDir + "/shortcut-inhibitor", std::vector{}); - client.proc->addEnv("WAYLAND_DISPLAY", WLDISPLAY); + this->proc->addEnv("WAYLAND_DISPLAY", WLDISPLAY); int pipeFds1[2], pipeFds2[2]; if (pipe(pipeFds1) != 0 || pipe(pipeFds2) != 0) { NLog::log("{}Unable to open pipe to client", Colors::RED); - return false; + throw std::exception(); } - client.writeFd = CFileDescriptor(pipeFds1[1]); - client.proc->setStdinFD(pipeFds1[0]); + this->writeFd = CFileDescriptor(pipeFds1[1]); + this->proc->setStdinFD(pipeFds1[0]); - client.readFd = CFileDescriptor(pipeFds2[0]); - client.proc->setStdoutFD(pipeFds2[1]); + this->readFd = CFileDescriptor(pipeFds2[0]); + this->proc->setStdoutFD(pipeFds2[1]); const int COUNT_BEFORE = Tests::windowCount(); - client.proc->runAsync(); + this->proc->runAsync(); close(pipeFds1[0]); close(pipeFds2[1]); - client.fds = {.fd = client.readFd.get(), .events = POLLIN}; - if (poll(&client.fds, 1, 1000) != 1 || !(client.fds.revents & POLLIN)) { + this->fds = {.fd = this->readFd.get(), .events = POLLIN}; + if (poll(&this->fds, 1, 1000) != 1 || !(this->fds.revents & POLLIN)) { NLog::log("{}shortcut-inhibitor client failed poll", Colors::RED); - return false; + throw std::exception(); } - client.readBuf.fill(0); - if (read(client.readFd.get(), client.readBuf.data(), client.readBuf.size() - 1) == -1) { + this->readBuf.fill(0); + if (read(this->readFd.get(), this->readBuf.data(), this->readBuf.size() - 1) == -1) { NLog::log("{}shortcut-inhibitor client read failed", Colors::RED); - return false; + throw std::exception(); } - std::string ret = std::string{client.readBuf.data()}; + std::string ret = std::string{this->readBuf.data()}; if (ret.find("started") == std::string::npos) { NLog::log("{}Failed to start shortcut-inhibitor client, read {}", Colors::RED, ret); - return false; + throw std::exception(); } // wait for window to appear int counter = 0; - while (Tests::processAlive(client.proc->pid()) && Tests::windowCount() == COUNT_BEFORE) { + while (Tests::processAlive(this->proc->pid()) && Tests::windowCount() == COUNT_BEFORE) { counter++; std::this_thread::sleep_for(std::chrono::milliseconds(100)); if (counter > 50) { NLog::log("{}shortcut-inhibitor client took too long to open", Colors::RED); - return false; + throw std::exception(); } } - if (!Tests::processAlive(client.proc->pid())) { + if (!Tests::processAlive(this->proc->pid())) { NLog::log("{}shortcut-inhibitor client not alive", Colors::RED); - return false; + throw std::exception(); } - if (getFromSocket(std::format("/dispatch focuswindow pid:{}", client.proc->pid())) != "ok") { + if (getFromSocket(std::format("/dispatch hl.dsp.focus({{ window = 'pid:{}' }})", this->proc->pid())) != "ok") { NLog::log("{}Failed to focus shortcut-inhibitor client", Colors::RED, ret); - return false; + throw std::exception(); } std::string command = "on\n"; - if (write(client.writeFd.get(), command.c_str(), command.length()) == -1) { + if (write(this->writeFd.get(), command.c_str(), command.length()) == -1) { NLog::log("{}shortcut-inhibitor client write failed", Colors::RED); - return false; + throw std::exception(); } - client.readBuf.fill(0); - if (read(client.readFd.get(), client.readBuf.data(), client.readBuf.size() - 1) == -1) - return false; + this->readBuf.fill(0); + if (read(this->readFd.get(), this->readBuf.data(), this->readBuf.size() - 1) == -1) + throw std::exception(); - ret = std::string{client.readBuf.data()}; + ret = std::string{this->readBuf.data()}; if (ret.find("inhibiting") == std::string::npos) { NLog::log("{}shortcut-inhibitor client didn't return inhibiting", Colors::RED); - return false; + throw std::exception(); } NLog::log("{}Started shortcut-inhibitor client", Colors::YELLOW); - - return true; } -static void stopClient(SClient& client) { +CClient::~CClient() { std::string cmd = "off\n"; - write(client.writeFd.get(), cmd.c_str(), cmd.length()); + write(this->writeFd.get(), cmd.c_str(), cmd.length()); - kill(client.proc->pid(), SIGKILL); - client.proc.reset(); + kill(this->proc->pid(), SIGKILL); + this->proc.reset(); } static std::string flagFile = "/tmp/hyprtester-keybinds.txt"; @@ -138,43 +141,36 @@ static bool attemptCheckFlag(int attempts, int intervalMs) { return false; } -static bool test() { - SClient client; - if (!startClient(client)) - return false; +TEST_CASE(shortcutInhibitor) { + std::optional client; + + try { + client.emplace(); + } catch (...) { FAIL_TEST("Couldn't start the client"); } NLog::log("{}Testing keybinds", Colors::GREEN); //basic keybind test EXPECT(checkFlag(), false); - EXPECT(getFromSocket("/keyword bind SUPER,Y,exec,touch " + flagFile), "ok"); - OK(getFromSocket("/dispatch plugin:test:keybind 1,7,29")); + EXPECT(getFromSocket("/eval hl.bind('SUPER + Y', hl.dsp.exec_cmd('touch " + flagFile + "'))"), "ok"); + OK(getFromSocket("/eval hl.plugin.test.keybind(1, 7, 29)")); EXPECT(attemptCheckFlag(20, 50), false); - OK(getFromSocket("/dispatch plugin:test:keybind 0,0,29")); - EXPECT(getFromSocket("/keyword unbind SUPER,Y"), "ok"); + OK(getFromSocket("/eval hl.plugin.test.keybind(0, 0, 29)")); + EXPECT(getFromSocket("/eval hl.unbind('SUPER + Y')"), "ok"); //keybind bypass flag test EXPECT(checkFlag(), false); - EXPECT(getFromSocket("/keyword bindp SUPER,Y,exec,touch " + flagFile), "ok"); - OK(getFromSocket("/dispatch plugin:test:keybind 1,7,29")); + EXPECT(getFromSocket("/eval hl.bind('SUPER + Y', hl.dsp.exec_cmd('touch " + flagFile + "'), { dont_inhibit = true })"), "ok"); + OK(getFromSocket("/eval hl.plugin.test.keybind(1, 7, 29)")); EXPECT(attemptCheckFlag(20, 50), true); - OK(getFromSocket("/dispatch plugin:test:keybind 0,0,29")); - EXPECT(getFromSocket("/keyword unbind SUPER,Y"), "ok"); + OK(getFromSocket("/eval hl.plugin.test.keybind(0, 0, 29)")); + EXPECT(getFromSocket("/eval hl.unbind('SUPER + Y')"), "ok"); NLog::log("{}Testing gestures", Colors::GREEN); //basic gesture test - OK(getFromSocket("/dispatch plugin:test:gesture right,3")); + OK(getFromSocket("/eval hl.plugin.test.gesture('right', 3)")); EXPECT_NOT_CONTAINS(getFromSocket("/activewindow"), "floating: 1"); //gesture bypass flag test - OK(getFromSocket("/dispatch plugin:test:gesture right,2")); + OK(getFromSocket("/eval hl.plugin.test.gesture('right', 2)")); EXPECT_CONTAINS(getFromSocket("/activewindow"), "floating: 1"); - - stopClient(client); - - NLog::log("{}Reloading the config", Colors::YELLOW); - OK(getFromSocket("/reload")); - - return !ret; } - -REGISTER_CLIENT_TEST_FN(test); diff --git a/hyprtester/src/tests/clients/tests.hpp b/hyprtester/src/tests/clients/tests.hpp index 31746fe56..7f10fba76 100644 --- a/hyprtester/src/tests/clients/tests.hpp +++ b/hyprtester/src/tests/clients/tests.hpp @@ -1,12 +1,9 @@ #pragma once +#include -#include -#include +#include "../../shared.hpp" -inline std::vector> clientTestFns; +inline std::map clientTestCases; -#define REGISTER_CLIENT_TEST_FN(fn) \ - static auto _register_fn = [] { \ - clientTestFns.emplace_back(fn); \ - return 1; \ - }(); +// Where `TEST_CASE` macros will store generated test cases: +#define TEST_CASES_STORAGE clientTestCases diff --git a/hyprtester/src/tests/main/animations.cpp b/hyprtester/src/tests/main/animations.cpp index e464dcbd9..2f918d2b1 100644 --- a/hyprtester/src/tests/main/animations.cpp +++ b/hyprtester/src/tests/main/animations.cpp @@ -5,18 +5,11 @@ #include #include -static int ret = 0; - using namespace Hyprutils::OS; using namespace Hyprutils::Memory; -static bool test() { - NLog::log("{}Testing animations", Colors::GREEN); - +TEST_CASE(animationsTrivial) { auto str = getFromSocket("/animations"); NLog::log("{}Testing bezier curve output from `hyprctl animations`", Colors::YELLOW); - {EXPECT_CONTAINS(str, std::format("beziers:\n\n\tname: quick\n\t\tX0: 0.15\n\t\tY0: 0.00\n\t\tX1: 0.10\n\t\tY1: 1.00"))}; - return !ret; + ASSERT_CONTAINS(str, std::format("beziers:\n\n\tname: quick\n\t\tX0: 0.15\n\t\tY0: 0.00\n\t\tX1: 0.10\n\t\tY1: 1.00")); } - -REGISTER_TEST_FN(test) diff --git a/hyprtester/src/tests/main/colors.cpp b/hyprtester/src/tests/main/colors.cpp index 5fa9eee6d..e430145c5 100644 --- a/hyprtester/src/tests/main/colors.cpp +++ b/hyprtester/src/tests/main/colors.cpp @@ -6,32 +6,24 @@ #include #include -static int ret = 0; - -static bool test() { - NLog::log("{}Testing hyprctl monitors", Colors::GREEN); - +TEST_CASE(monitorsColorManagement) { std::string monitorsSpec = getFromSocket("j/monitors"); - EXPECT_CONTAINS(monitorsSpec, R"("colorManagementPreset")"); + ASSERT_CONTAINS(monitorsSpec, R"("colorManagementPreset": )"); - EXPECT_CONTAINS(getFromSocket("/keyword monitor HEADLESS-2,1920x1080x60.00000,0x0,1.0,bitdepth,10,cm,wide"), "ok") + ASSERT_CONTAINS(getFromSocket("/eval hl.monitor({ output = 'HEADLESS-2', bitdepth = 10, cm = 'wide' })"), "ok"); // monitor settings are applied after a frame is pushed. std::this_thread::sleep_for(std::chrono::milliseconds(500)); monitorsSpec = getFromSocket("j/monitors"); - EXPECT_CONTAINS(monitorsSpec, R"("colorManagementPreset": "wide")"); + ASSERT_CONTAINS(monitorsSpec, R"("colorManagementPreset": )"); - EXPECT_CONTAINS(getFromSocket("/keyword monitor HEADLESS-2,1920x1080x60.00000,0x0,1.0,bitdepth,10,cm,srgb,sdrbrightness,1.2,sdrsaturation,0.98"), "ok") + ASSERT_CONTAINS(getFromSocket("/eval hl.monitor({ output = 'HEADLESS-2', bitdepth = 10, cm = 'srgb', sdrbrightness = 1.2, sdrsaturation = 0.98 })"), "ok"); monitorsSpec = getFromSocket("j/monitors"); std::this_thread::sleep_for(std::chrono::milliseconds(500)); - EXPECT_CONTAINS(monitorsSpec, "colorManagementPreset"); - EXPECT_CONTAINS(monitorsSpec, "sdrBrightness"); - EXPECT_CONTAINS(monitorsSpec, "sdrSaturation"); - - return !ret; + ASSERT_CONTAINS(monitorsSpec, R"("colorManagementPreset": )"); + ASSERT_CONTAINS(monitorsSpec, R"("sdrBrightness": )"); + ASSERT_CONTAINS(monitorsSpec, R"("sdrSaturation": )"); } - -REGISTER_TEST_FN(test) diff --git a/hyprtester/src/tests/main/dwindle.cpp b/hyprtester/src/tests/main/dwindle.cpp index cd8528547..e962efd1b 100644 --- a/hyprtester/src/tests/main/dwindle.cpp +++ b/hyprtester/src/tests/main/dwindle.cpp @@ -3,25 +3,20 @@ #include "../../hyprctlCompat.hpp" #include "tests.hpp" -static int ret = 0; - -static void testFloatClamp() { +TEST_CASE(dwindleFloatClamp) { for (auto const& win : {"a", "b", "c"}) { if (!Tests::spawnKitty(win)) { - NLog::log("{}Failed to spawn kitty with win class `{}`", Colors::RED, win); - ++TESTS_FAILED; - ret = 1; - return; + FAIL_TEST("Could not spawn kitty with win class `{}`", win); } } - OK(getFromSocket("/keyword dwindle:force_split 2")); - OK(getFromSocket("/keyword monitor HEADLESS-2, addreserved, 0, 20, 0, 20")); - OK(getFromSocket("/dispatch focuswindow class:c")); - OK(getFromSocket("/dispatch setfloating class:c")); - OK(getFromSocket("/dispatch resizewindowpixel exact 1200 900,class:c")); - OK(getFromSocket("/dispatch settiled class:c")); - OK(getFromSocket("/dispatch setfloating class:c")); + OK(getFromSocket("/eval hl.config({ dwindle = { force_split = 2 } })")); + OK(getFromSocket("/eval hl.monitor({ output = 'HEADLESS-2', reserved = { top = 0, right = 20, bottom = 0, left = 20 } })")); + OK(getFromSocket("/dispatch hl.dsp.focus({ window = 'class:c' })")); + OK(getFromSocket("/dispatch hl.dsp.window.float({ action = 'set', window = 'class:c' })")); + OK(getFromSocket("/dispatch hl.dsp.window.resize({ x = 1200, y = 900, window = 'class:c' })")); + OK(getFromSocket("/dispatch hl.dsp.window.float({ action = 'unset', window = 'class:c' })")); + OK(getFromSocket("/dispatch hl.dsp.window.float({ action = 'set', window = 'class:c' })")); { auto str = getFromSocket("/clients"); @@ -29,30 +24,21 @@ static void testFloatClamp() { EXPECT_CONTAINS(str, "size: 1200,900"); } - OK(getFromSocket("/keyword dwindle:force_split 0")); - - // clean up - NLog::log("{}Killing all windows", Colors::YELLOW); - Tests::killAllWindows(); - - OK(getFromSocket("/reload")); + OK(getFromSocket("/eval hl.config({ dwindle = { force_split = 0 } })")); } -static void test13349() { +TEST_CASE(dwindleIssue13349) { // Test if dwindle properly uses a focal point to place a new window. // exposed by #13349 as a regression from #12890 for (auto const& win : {"a", "b", "c"}) { if (!Tests::spawnKitty(win)) { - NLog::log("{}Failed to spawn kitty with win class `{}`", Colors::RED, win); - ++TESTS_FAILED; - ret = 1; - return; + FAIL_TEST("Could not spawn kitty with win class `{}`", win); } } - OK(getFromSocket("/dispatch focuswindow class:c")); + OK(getFromSocket("/dispatch hl.dsp.focus({ window = 'class:c' })")); { auto str = getFromSocket("/activewindow"); @@ -60,7 +46,7 @@ static void test13349() { EXPECT_CONTAINS(str, "size: 931,511"); } - OK(getFromSocket("/dispatch movewindow l")); + OK(getFromSocket("/dispatch hl.dsp.window.move({ direction = 'left' })")); { auto str = getFromSocket("/activewindow"); @@ -68,32 +54,28 @@ static void test13349() { EXPECT_CONTAINS(str, "size: 931,511"); } - OK(getFromSocket("/dispatch movewindow r")); + OK(getFromSocket("/dispatch hl.dsp.window.move({ direction = 'right' })")); { auto str = getFromSocket("/activewindow"); EXPECT_CONTAINS(str, "at: 967,547"); EXPECT_CONTAINS(str, "size: 931,511"); } - - // clean up - NLog::log("{}Killing all windows", Colors::YELLOW); - Tests::killAllWindows(); } -static void testSplit() { +TEST_CASE(dwindleSplit) { // Test various split methods Tests::spawnKitty("a"); // these must not crash - EXPECT_NOT(getFromSocket("/dispatch layoutmsg swapsplit"), "ok"); - EXPECT_NOT(getFromSocket("/dispatch layoutmsg splitratio 1 exact"), "ok"); + EXPECT_NOT(getFromSocket("/dispatch hl.dsp.layout('swapsplit')"), "ok"); + EXPECT_NOT(getFromSocket("/dispatch hl.dsp.layout('splitratio 1 exact')"), "ok"); Tests::spawnKitty("b"); - OK(getFromSocket("/dispatch focuswindow class:a")); - OK(getFromSocket("/dispatch layoutmsg splitratio -0.2")); + OK(getFromSocket("/dispatch hl.dsp.focus({ window = 'class:a' })")); + OK(getFromSocket("/dispatch hl.dsp.layout('splitratio -0.2')")); { auto str = getFromSocket("/activewindow"); @@ -101,7 +83,7 @@ static void testSplit() { EXPECT_CONTAINS(str, "size: 743,1036"); } - OK(getFromSocket("/dispatch layoutmsg splitratio 1.6 exact")); + OK(getFromSocket("/dispatch hl.dsp.layout('splitratio 1.6 exact')")); { auto str = getFromSocket("/activewindow"); @@ -109,13 +91,13 @@ static void testSplit() { EXPECT_CONTAINS(str, "size: 1495,1036"); } - EXPECT_NOT(getFromSocket("/dispatch layoutmsg splitratio fhne exact"), "ok"); - EXPECT_NOT(getFromSocket("/dispatch layoutmsg splitratio exact"), "ok"); - EXPECT_NOT(getFromSocket("/dispatch layoutmsg splitratio -....9"), "ok"); - EXPECT_NOT(getFromSocket("/dispatch layoutmsg splitratio ..9"), "ok"); - EXPECT_NOT(getFromSocket("/dispatch layoutmsg splitratio"), "ok"); + EXPECT_NOT(getFromSocket("/dispatch hl.dsp.layout('splitratio fhne exact')"), "ok"); + EXPECT_NOT(getFromSocket("/dispatch hl.dsp.layout('splitratio exact')"), "ok"); + EXPECT_NOT(getFromSocket("/dispatch hl.dsp.layout('splitratio -....9')"), "ok"); + EXPECT_NOT(getFromSocket("/dispatch hl.dsp.layout('splitratio ..9')"), "ok"); + EXPECT_NOT(getFromSocket("/dispatch hl.dsp.layout('splitratio')"), "ok"); - OK(getFromSocket("/dispatch layoutmsg togglesplit")); + OK(getFromSocket("/dispatch hl.dsp.layout('togglesplit')")); { auto str = getFromSocket("/activewindow"); @@ -123,29 +105,23 @@ static void testSplit() { EXPECT_CONTAINS(str, "size: 1876,823"); } - OK(getFromSocket("/dispatch layoutmsg swapsplit")); + OK(getFromSocket("/dispatch hl.dsp.layout('swapsplit')")); { auto str = getFromSocket("/activewindow"); EXPECT_CONTAINS(str, "at: 22,859"); EXPECT_CONTAINS(str, "size: 1876,199"); } - - NLog::log("{}Killing all windows", Colors::YELLOW); - Tests::killAllWindows(); } -static void testRotatesplit() { - OK(getFromSocket("r/keyword general:gaps_in 0")); - OK(getFromSocket("r/keyword general:gaps_out 0")); - OK(getFromSocket("r/keyword general:border_size 0")); +TEST_CASE(dwindleRotateSplit) { + OK(getFromSocket("r/eval hl.config({ general = { gaps_in = 0 } })")); + OK(getFromSocket("r/eval hl.config({ general = { gaps_out = 0 } })")); + OK(getFromSocket("r/eval hl.config({ general = { border_size = 0 } })")); for (auto const& win : {"a", "b"}) { if (!Tests::spawnKitty(win)) { - NLog::log("{}Failed to spawn kitty with win class `{}`", Colors::RED, win); - ++TESTS_FAILED; - ret = 1; - return; + FAIL_TEST("Could not spawn kitty with win class `{}`", win); } } @@ -156,28 +132,28 @@ static void testRotatesplit() { } // test 4 repeated rotations by 90 degrees - OK(getFromSocket("/dispatch layoutmsg rotatesplit")); + OK(getFromSocket("/dispatch hl.dsp.layout('rotatesplit')")); { auto str = getFromSocket("/clients"); EXPECT_CONTAINS(str, "at: 0,0"); EXPECT_CONTAINS(str, "size: 1920,540"); } - OK(getFromSocket("/dispatch layoutmsg rotatesplit")); + OK(getFromSocket("/dispatch hl.dsp.layout('rotatesplit')")); { auto str = getFromSocket("/clients"); EXPECT_CONTAINS(str, "at: 960,0"); EXPECT_CONTAINS(str, "size: 960,1080"); } - OK(getFromSocket("/dispatch layoutmsg rotatesplit")); + OK(getFromSocket("/dispatch hl.dsp.layout('rotatesplit')")); { auto str = getFromSocket("/clients"); EXPECT_CONTAINS(str, "at: 0,540"); EXPECT_CONTAINS(str, "size: 1920,540"); } - OK(getFromSocket("/dispatch layoutmsg rotatesplit")); + OK(getFromSocket("/dispatch hl.dsp.layout('rotatesplit')")); { auto str = getFromSocket("/clients"); EXPECT_CONTAINS(str, "at: 0,0"); @@ -185,21 +161,21 @@ static void testRotatesplit() { } // test different angles - OK(getFromSocket("/dispatch layoutmsg rotatesplit 180")); + OK(getFromSocket("/dispatch hl.dsp.layout('rotatesplit 180')")); { auto str = getFromSocket("/clients"); EXPECT_CONTAINS(str, "at: 960,0"); EXPECT_CONTAINS(str, "size: 960,1080"); } - OK(getFromSocket("/dispatch layoutmsg rotatesplit 270")); + OK(getFromSocket("/dispatch hl.dsp.layout('rotatesplit 270')")); { auto str = getFromSocket("/clients"); EXPECT_CONTAINS(str, "at: 0,540"); EXPECT_CONTAINS(str, "size: 1920,540"); } - OK(getFromSocket("/dispatch layoutmsg rotatesplit 360")); + OK(getFromSocket("/dispatch hl.dsp.layout('rotatesplit 360')")); { auto str = getFromSocket("/clients"); EXPECT_CONTAINS(str, "at: 0,0"); @@ -207,73 +183,34 @@ static void testRotatesplit() { } // test negative angles - OK(getFromSocket("/dispatch layoutmsg rotatesplit -90")); + OK(getFromSocket("/dispatch hl.dsp.layout('rotatesplit -90')")); { auto str = getFromSocket("/clients"); EXPECT_CONTAINS(str, "at: 0,0"); EXPECT_CONTAINS(str, "size: 960,1080"); } - OK(getFromSocket("/dispatch layoutmsg rotatesplit -180")); + OK(getFromSocket("/dispatch hl.dsp.layout('rotatesplit -180')")); { auto str = getFromSocket("/clients"); EXPECT_CONTAINS(str, "at: 960,0"); EXPECT_CONTAINS(str, "size: 960,1080"); } - - NLog::log("{}Killing all windows", Colors::YELLOW); - Tests::killAllWindows(); - - OK(getFromSocket("/reload")); } -static void testForceSplitOnMoveToWorkspace() { - OK(getFromSocket("/dispatch workspace 2")); - EXPECT(!!Tests::spawnKitty("kitty"), true); +TEST_CASE(dwindleForceSplitOnMoveToWorkspace) { + OK(getFromSocket("/dispatch hl.dsp.focus({ workspace = '2' })")); + ASSERT(!!Tests::spawnKitty("kitty"), true); - OK(getFromSocket("/dispatch workspace 1")); - EXPECT(!!Tests::spawnKitty("kitty"), true); - std::string posBefore = Tests::getWindowAttribute(getFromSocket("/activewindow"), "at:"); + OK(getFromSocket("/dispatch hl.dsp.focus({ workspace = '1' })")); + ASSERT(!!Tests::spawnKitty("kitty"), true); + std::string posBefore = "at: " + Tests::getAttribute(getFromSocket("/activewindow"), "at"); - OK(getFromSocket("/keyword dwindle:force_split 2")); - OK(getFromSocket("/dispatch movecursortocorner 3")); // top left - OK(getFromSocket("/dispatch movetoworkspace 2")); + OK(getFromSocket("/eval hl.config({ dwindle = { force_split = 2 } })")); + OK(getFromSocket("/dispatch hl.dsp.cursor.move_to_corner({ corner = 3 })")); // top left + OK(getFromSocket("/dispatch hl.dsp.window.move({ workspace = '2' })")); // Should be moved to the right, so the position should change std::string activeWindow = getFromSocket("/activewindow"); EXPECT(activeWindow.contains(posBefore), false); - - // clean up - OK(getFromSocket("/reload")); - Tests::killAllWindows(); - Tests::waitUntilWindowsN(0); } - -static bool test() { - NLog::log("{}Testing Dwindle layout", Colors::GREEN); - - // test - NLog::log("{}Testing float clamp", Colors::GREEN); - testFloatClamp(); - - NLog::log("{}Testing #13349", Colors::GREEN); - test13349(); - - NLog::log("{}Testing splits", Colors::GREEN); - testSplit(); - - NLog::log("{}Testing rotatesplit", Colors::GREEN); - testRotatesplit(); - - NLog::log("{}Testing force_split on move to workspace", Colors::GREEN); - testForceSplitOnMoveToWorkspace(); - - // clean up - NLog::log("Cleaning up", Colors::YELLOW); - getFromSocket("/dispatch workspace 1"); - OK(getFromSocket("/reload")); - - return !ret; -} - -REGISTER_TEST_FN(test); diff --git a/hyprtester/src/tests/main/exec.cpp b/hyprtester/src/tests/main/exec.cpp index a410494ab..ff19edfe6 100644 --- a/hyprtester/src/tests/main/exec.cpp +++ b/hyprtester/src/tests/main/exec.cpp @@ -8,8 +8,6 @@ #include #include "../shared.hpp" -static int ret = 0; - using namespace Hyprutils::OS; using namespace Hyprutils::Memory; @@ -18,13 +16,11 @@ using namespace Hyprutils::Memory; const static auto SLEEP_DURATIONS = std::array{1, 10}; -static bool test() { - NLog::log("{}Testing process spawning", Colors::GREEN); - +TEST_CASE(processSpawning) { for (const auto duration : SLEEP_DURATIONS) { // Note: POSIX sleep does not support fractional seconds, so // can't sleep for less than 1 second. - OK(getFromSocket(std::format("/dispatch exec sleep {}", duration))); + OK(getFromSocket(std::format("/dispatch hl.dsp.exec_cmd('sleep {}')", duration))); // Ensure that sleep is our child const std::string sleepPidS = Tests::execAndGet("pgrep sleep"); @@ -45,17 +41,9 @@ static bool test() { // Ensure that sleep did not become a zombie EXPECT(Tests::processAlive(sleepPid), false); - // kill all - NLog::log("{}Killing all windows", Colors::YELLOW); - Tests::killAllWindows(); - - NLog::log("{}Expecting 0 windows", Colors::YELLOW); - EXPECT(Tests::windowCount(), 0); - - return !ret; + // Test succeeded + return; } - return false; + FAIL_TEST_SILENT(); } - -REGISTER_TEST_FN(test) diff --git a/hyprtester/src/tests/main/follow_mouse_shrink.cpp b/hyprtester/src/tests/main/follow_mouse_shrink.cpp index 61b1e5c12..600643c6b 100644 --- a/hyprtester/src/tests/main/follow_mouse_shrink.cpp +++ b/hyprtester/src/tests/main/follow_mouse_shrink.cpp @@ -7,22 +7,11 @@ #include "../shared.hpp" #include "tests.hpp" -static int ret = 0; - -static bool spawnKitty(const std::string& class_) { - NLog::log("{}Spawning {}", Colors::YELLOW, class_); - if (!Tests::spawnKitty(class_)) { - NLog::log("{}Error: {} did not spawn", Colors::RED, class_); - return false; - } - return true; -} - static bool isActiveWindow(const std::string& class_, char fullscreen = '0', bool log = true) { std::string activeWin = getFromSocket("/activewindow"); - auto winClass = Tests::getWindowAttribute(activeWin, "class:"); - auto winFullscreen = Tests::getWindowAttribute(activeWin, "fullscreen:").back(); - if (winClass.substr(strlen("class: ")) == class_ && winFullscreen == fullscreen) + auto winClass = Tests::getAttribute(activeWin, "class"); + auto winFullscreen = Tests::getAttribute(activeWin, "fullscreen").back(); + if (winClass == class_ && winFullscreen == fullscreen) return true; else { if (log) @@ -43,89 +32,82 @@ static bool waitForActiveWindow(const std::string& class_, char fullscreen = '0' return true; } -static bool test() { +// TODO: split this into multiple test cases +TEST_CASE(followMouseShrink) { NLog::log("{}Testing follow_mouse_shrink", Colors::GREEN); - getFromSocket("/dispatch workspace name:follow_mouse_shrink"); + getFromSocket("/dispatch hl.dsp.focus({ workspace = 'name:follow_mouse_shrink' })"); // follow_mouse 2 so cursor position determines focus (mode 1's delta threshold // is unreliable with movecursor/simulateMouseMovement). float_switch_override_focus 2 // enables focus switching between floating windows. - OK(getFromSocket("/keyword input:follow_mouse 2")); - OK(getFromSocket("/keyword input:float_switch_override_focus 2")); + OK(getFromSocket("/eval hl.config({ input = { follow_mouse = 2 } })")); + OK(getFromSocket("/eval hl.config({ input = { float_switch_override_focus = 2 } })")); // Spawn two floating windows with a 20px gap // fms_a: position (100,100), size 400x400 -> hitbox [100,499] x [100,499] - if (!spawnKitty("fms_a")) - return false; - OK(getFromSocket("/dispatch setfloating")); - OK(getFromSocket("/dispatch resizewindowpixel exact 400 400,activewindow")); - OK(getFromSocket("/dispatch movewindowpixel exact 100 100,activewindow")); + if (!Tests::spawnKitty("fms_a")) + FAIL_TEST("Couldn't spawn kitty"); + OK(getFromSocket("/dispatch hl.dsp.window.float({ action = 'set' })")); + OK(getFromSocket("/dispatch hl.dsp.window.resize({ x = 400, y = 400 })")); + OK(getFromSocket("/dispatch hl.dsp.window.move({ x = 100, y = 100 })")); // fms_b: position (520,100), size 400x400 -> hitbox [520,919] x [100,499] - if (!spawnKitty("fms_b")) - return false; - OK(getFromSocket("/dispatch setfloating")); - OK(getFromSocket("/dispatch resizewindowpixel exact 400 400,activewindow")); - OK(getFromSocket("/dispatch movewindowpixel exact 520 100,activewindow")); + if (!Tests::spawnKitty("fms_b")) + FAIL_TEST("Couldn't spawn kitty"); + OK(getFromSocket("/dispatch hl.dsp.window.float({ action = 'set' })")); + OK(getFromSocket("/dispatch hl.dsp.window.resize({ x = 400, y = 400 })")); + OK(getFromSocket("/dispatch hl.dsp.window.move({ x = 520, y = 100 })")); // --- Test 1: Baseline shrink=0, edge focus works --- NLog::log("{}Test 1: shrink=0, cursor at B's left edge focuses B", Colors::GREEN); - OK(getFromSocket("/keyword input:follow_mouse_shrink 0")); + OK(getFromSocket("/eval hl.config({ input = { follow_mouse_shrink = 0 } })")); // Focus A explicitly, then move cursor inside A so follow_mouse tracks it - OK(getFromSocket("/dispatch focuswindow class:fms_a")); - EXPECT(waitForActiveWindow("fms_a"), true); - OK(getFromSocket("/dispatch movecursor 300 300")); + OK(getFromSocket("/dispatch hl.dsp.focus({ window = 'class:fms_a' })")); + ASSERT(waitForActiveWindow("fms_a"), true); + OK(getFromSocket("/dispatch hl.dsp.cursor.move({ x = 300, y = 300 })")); std::this_thread::sleep_for(std::chrono::milliseconds(200)); // Move to just inside B's left edge - OK(getFromSocket("/dispatch movecursor 521 300")); - EXPECT(waitForActiveWindow("fms_b"), true); + OK(getFromSocket("/dispatch hl.dsp.cursor.move({ x = 521, y = 300 })")); + ASSERT(waitForActiveWindow("fms_b"), true); // --- Test 2: Shrink=20, cursor in dead zone does NOT change focus --- NLog::log("{}Test 2: shrink=20, cursor in B's dead zone stays on A", Colors::GREEN); - OK(getFromSocket("/keyword input:follow_mouse_shrink 20")); + OK(getFromSocket("/eval hl.config({ input = { follow_mouse_shrink = 20 } })")); // Focus A explicitly - OK(getFromSocket("/dispatch focuswindow class:fms_a")); - EXPECT(waitForActiveWindow("fms_a"), true); - OK(getFromSocket("/dispatch movecursor 300 300")); + OK(getFromSocket("/dispatch hl.dsp.focus({ window = 'class:fms_a' })")); + ASSERT(waitForActiveWindow("fms_a"), true); + OK(getFromSocket("/dispatch hl.dsp.cursor.move({ x = 300, y = 300 })")); std::this_thread::sleep_for(std::chrono::milliseconds(200)); // Move to 530,300 -- 10px inside B, within 20px shrink zone (B's shrunk hitbox starts at 540) - OK(getFromSocket("/dispatch movecursor 530 300")); + OK(getFromSocket("/dispatch hl.dsp.cursor.move({ x = 530, y = 300 })")); std::this_thread::sleep_for(std::chrono::milliseconds(500)); - EXPECT(isActiveWindow("fms_a"), true); + ASSERT(isActiveWindow("fms_a"), true); // --- Test 3: Shrink=20, cursor well inside inactive window DOES focus it --- NLog::log("{}Test 3: shrink=20, cursor at B's center focuses B", Colors::GREEN); // Still focused on A from test 2 - OK(getFromSocket("/dispatch movecursor 720 300")); - EXPECT(waitForActiveWindow("fms_b"), true); + OK(getFromSocket("/dispatch hl.dsp.cursor.move({ x = 720, y = 300 })")); + ASSERT(waitForActiveWindow("fms_b"), true); // --- Test 4: Focused window's hitbox is NOT shrunk --- NLog::log("{}Test 4a: focused window hitbox is not shrunk", Colors::GREEN); // Focus A explicitly, move cursor inside A, then move near A's right edge - OK(getFromSocket("/dispatch focuswindow class:fms_a")); - EXPECT(waitForActiveWindow("fms_a"), true); - OK(getFromSocket("/dispatch movecursor 300 300")); + OK(getFromSocket("/dispatch hl.dsp.focus({ window = 'class:fms_a' })")); + ASSERT(waitForActiveWindow("fms_a"), true); + OK(getFromSocket("/dispatch hl.dsp.cursor.move({ x = 300, y = 300 })")); std::this_thread::sleep_for(std::chrono::milliseconds(200)); - OK(getFromSocket("/dispatch movecursor 490 300")); + OK(getFromSocket("/dispatch hl.dsp.cursor.move({ x = 490, y = 300 })")); std::this_thread::sleep_for(std::chrono::milliseconds(500)); - EXPECT(isActiveWindow("fms_a"), true); + ASSERT(isActiveWindow("fms_a"), true); NLog::log("{}Test 4b: inactive window hitbox IS shrunk at same position", Colors::GREEN); // Focus B explicitly, then move to (490,300). Now A is inactive with shrunk box ending at 479, so 490 is outside A. - OK(getFromSocket("/dispatch focuswindow class:fms_b")); - EXPECT(waitForActiveWindow("fms_b"), true); - OK(getFromSocket("/dispatch movecursor 720 300")); + OK(getFromSocket("/dispatch hl.dsp.focus({ window = 'class:fms_b' })")); + ASSERT(waitForActiveWindow("fms_b"), true); + OK(getFromSocket("/dispatch hl.dsp.cursor.move({ x = 720, y = 300 })")); std::this_thread::sleep_for(std::chrono::milliseconds(200)); - OK(getFromSocket("/dispatch movecursor 490 300")); + OK(getFromSocket("/dispatch hl.dsp.cursor.move({ x = 490, y = 300 })")); std::this_thread::sleep_for(std::chrono::milliseconds(500)); - EXPECT(isActiveWindow("fms_b"), true); - - // Cleanup - OK(getFromSocket("/reload")); - Tests::killAllWindows(); - - return ret == 0; + ASSERT(isActiveWindow("fms_b"), true); } - -REGISTER_TEST_FN(test) diff --git a/hyprtester/src/tests/main/gestures.cpp b/hyprtester/src/tests/main/gestures.cpp index 07cc4ca96..f83247123 100644 --- a/hyprtester/src/tests/main/gestures.cpp +++ b/hyprtester/src/tests/main/gestures.cpp @@ -10,14 +10,13 @@ #include #include "../shared.hpp" -static int ret = 0; - using namespace Hyprutils::OS; using namespace Hyprutils::Memory; #define UP CUniquePointer #define SP CSharedPointer +// TODO: refactor and reuse `Tests::waitUntilWindowsN` static bool waitForWindowCount(int expectedWindowCnt, std::string_view expectation, int waitMillis = 100, int maxWaitCnt = 50) { int counter = 0; while (Tests::windowCount() != expectedWindowCnt) { @@ -32,68 +31,61 @@ static bool waitForWindowCount(int expectedWindowCnt, std::string_view expectati return true; } -static bool test() { - NLog::log("{}Testing gestures", Colors::GREEN); - - EXPECT(Tests::windowCount(), 0); - - // test on workspace "window" - NLog::log("{}Switching to workspace 1", Colors::YELLOW); - getFromSocket("/dispatch workspace 1"); // no OK: we might be on 1 already - +// TODO: decompose this into multiple test cases +TEST_CASE(gestures) { Tests::spawnKitty(); - EXPECT(Tests::windowCount(), 1); + ASSERT(Tests::windowCount(), 1); // Give the shell a moment to initialize std::this_thread::sleep_for(std::chrono::milliseconds(500)); - OK(getFromSocket("/dispatch plugin:test:gesture up,5")); - OK(getFromSocket("/dispatch plugin:test:gesture down,5")); - OK(getFromSocket("/dispatch plugin:test:gesture left,5")); - OK(getFromSocket("/dispatch plugin:test:gesture right,5")); - OK(getFromSocket("/dispatch plugin:test:gesture right,4")); + OK(getFromSocket("/eval hl.plugin.test.gesture('up', 5)")); + OK(getFromSocket("/eval hl.plugin.test.gesture('down', 5)")); + OK(getFromSocket("/eval hl.plugin.test.gesture('left', 5)")); + OK(getFromSocket("/eval hl.plugin.test.gesture('right', 5)")); + OK(getFromSocket("/eval hl.plugin.test.gesture('right', 4)")); EXPECT(waitForWindowCount(0, "Gesture sent paste exit + enter to kitty"), true); EXPECT(Tests::windowCount(), 0); - OK(getFromSocket("/dispatch plugin:test:gesture left,3")); + OK(getFromSocket("/eval hl.plugin.test.gesture('left', 3)")); EXPECT(waitForWindowCount(1, "Gesture spawned kitty"), true); EXPECT(Tests::windowCount(), 1); - OK(getFromSocket("/dispatch plugin:test:gesture right,3")); + OK(getFromSocket("/eval hl.plugin.test.gesture('right', 3)")); { auto str = getFromSocket("/clients"); EXPECT_CONTAINS(str, "floating: 1"); } - OK(getFromSocket("/dispatch plugin:test:gesture down,3")); + OK(getFromSocket("/eval hl.plugin.test.gesture('down', 3)")); { auto str = getFromSocket("/clients"); EXPECT_CONTAINS(str, "fullscreen: 2"); } - OK(getFromSocket("/dispatch plugin:test:gesture down,3")); + OK(getFromSocket("/eval hl.plugin.test.gesture('down', 3)")); { auto str = getFromSocket("/clients"); EXPECT_CONTAINS(str, "fullscreen: 0"); } - OK(getFromSocket("/dispatch plugin:test:alt 1")); + OK(getFromSocket("/eval hl.plugin.test.alt(1)")); - OK(getFromSocket("/dispatch plugin:test:gesture left,3")); + OK(getFromSocket("/eval hl.plugin.test.gesture('left', 3)")); { auto str = getFromSocket("/workspaces"); EXPECT_CONTAINS(str, "ID 2 (2)"); } - OK(getFromSocket("/dispatch plugin:test:gesture right,3")); + OK(getFromSocket("/eval hl.plugin.test.gesture('right', 3)")); { auto str = getFromSocket("/workspaces"); @@ -101,33 +93,33 @@ static bool test() { } // check for crashes - OK(getFromSocket("/dispatch plugin:test:gesture right,3")); + OK(getFromSocket("/eval hl.plugin.test.gesture('right', 3)")); { auto str = getFromSocket("/workspaces"); EXPECT_NOT_CONTAINS(str, "ID 2 (2)"); } - OK(getFromSocket("/keyword gestures:workspace_swipe_invert 0")); + OK(getFromSocket("/eval hl.config({ gestures = { workspace_swipe_invert = 0 } })")); - OK(getFromSocket("/dispatch plugin:test:gesture right,3")); + OK(getFromSocket("/eval hl.plugin.test.gesture('right', 3)")); { auto str = getFromSocket("/workspaces"); EXPECT_CONTAINS(str, "ID 2 (2)"); } - OK(getFromSocket("/dispatch plugin:test:gesture left,3")); + OK(getFromSocket("/eval hl.plugin.test.gesture('left', 3)")); { auto str = getFromSocket("/workspaces"); EXPECT_NOT_CONTAINS(str, "ID 2 (2)"); } - OK(getFromSocket("/keyword gestures:workspace_swipe_invert 1")); - OK(getFromSocket("/keyword gestures:workspace_swipe_create_new 0")); + OK(getFromSocket("/eval hl.config({ gestures = { workspace_swipe_invert = 1 } })")); + OK(getFromSocket("/eval hl.config({ gestures = { workspace_swipe_create_new = 0 } })")); - OK(getFromSocket("/dispatch plugin:test:gesture left,3")); + OK(getFromSocket("/eval hl.plugin.test.gesture('left', 3)")); { auto str = getFromSocket("/workspaces"); @@ -135,69 +127,55 @@ static bool test() { EXPECT_CONTAINS(str, "ID 1 (1)"); } - OK(getFromSocket("/dispatch plugin:test:gesture down,3")); + OK(getFromSocket("/eval hl.plugin.test.gesture('down', 3)")); { auto str = getFromSocket("/clients"); EXPECT_CONTAINS(str, "floating: 0"); } - OK(getFromSocket("/dispatch plugin:test:alt 0")); + OK(getFromSocket("/eval hl.plugin.test.alt(0)")); - OK(getFromSocket("/dispatch plugin:test:gesture up,3")); + OK(getFromSocket("/eval hl.plugin.test.gesture('up', 3)")); EXPECT(waitForWindowCount(0, "Gesture closed kitty"), true); - EXPECT(Tests::windowCount(), 0); + ASSERT(Tests::windowCount(), 0); // This test ensures that `movecursortocorner`, which expects // a single-character direction argument, is parsed correctly. Tests::spawnKitty(); - OK(getFromSocket("/dispatch movecursortocorner 0")); + OK(getFromSocket("/dispatch hl.dsp.cursor.move_to_corner({ corner = 0, window = 'activewindow' })")); const std::string cursorPos1 = getFromSocket("/cursorpos"); - OK(getFromSocket("/dispatch plugin:test:gesture left,4")); + OK(getFromSocket("/eval hl.plugin.test.gesture('left', 4)")); const std::string cursorPos2 = getFromSocket("/cursorpos"); // The cursor should have moved because of the gesture EXPECT(cursorPos1 != cursorPos2, true); // Test that `workspace previous` works correctly after a workspace gesture. { - OK(getFromSocket("/keyword gestures:workspace_swipe_invert 0")); - OK(getFromSocket("/keyword gestures:workspace_swipe_create_new 1")); - OK(getFromSocket("/dispatch workspace 3")); + OK(getFromSocket("/eval hl.config({ gestures = { workspace_swipe_invert = 0 } })")); + OK(getFromSocket("/eval hl.config({ gestures = { workspace_swipe_create_new = 1 } })")); + OK(getFromSocket("/dispatch hl.dsp.focus({ workspace = '3' })")); // Come to workspace 5 from workspace 3: 5 will remember that. - OK(getFromSocket("/dispatch workspace 5")); + OK(getFromSocket("/dispatch hl.dsp.focus({ workspace = '5' })")); Tests::spawnKitty(); // Keep workspace 5 open // Swipe from 1 to 5: 5 shall remember that. - OK(getFromSocket("/dispatch workspace 1")); - OK(getFromSocket("/dispatch plugin:test:alt 1")); - OK(getFromSocket("/dispatch plugin:test:gesture right,3")); - OK(getFromSocket("/dispatch plugin:test:alt 0")); + OK(getFromSocket("/dispatch hl.dsp.focus({ workspace = '1' })")); + OK(getFromSocket("/eval hl.plugin.test.alt(1)")); + OK(getFromSocket("/eval hl.plugin.test.gesture('right', 3)")); + OK(getFromSocket("/eval hl.plugin.test.alt(0)")); EXPECT_CONTAINS(getFromSocket("/activeworkspace"), "ID 5 (5)"); // Must return to 1 rather than 3 - OK(getFromSocket("/dispatch workspace previous")); + OK(getFromSocket("/dispatch hl.dsp.focus({ workspace = 'previous' })")); EXPECT_CONTAINS(getFromSocket("/activeworkspace"), "ID 1 (1)"); - OK(getFromSocket("/dispatch workspace previous")); + OK(getFromSocket("/dispatch hl.dsp.focus({ workspace = 'previous' })")); EXPECT_CONTAINS(getFromSocket("/activeworkspace"), "ID 5 (5)"); - OK(getFromSocket("/dispatch workspace 1")); + OK(getFromSocket("/dispatch hl.dsp.focus({ workspace = '1' })")); } - - // kill all - NLog::log("{}Killing all windows", Colors::YELLOW); - Tests::killAllWindows(); - - NLog::log("{}Expecting 0 windows", Colors::YELLOW); - EXPECT(Tests::windowCount(), 0); - - // reload cfg - OK(getFromSocket("/reload")); - - return !ret; } - -REGISTER_TEST_FN(test) diff --git a/hyprtester/src/tests/main/groups.cpp b/hyprtester/src/tests/main/groups.cpp index 86ffc7f75..7e1c63b6b 100644 --- a/hyprtester/src/tests/main/groups.cpp +++ b/hyprtester/src/tests/main/groups.cpp @@ -10,26 +10,22 @@ #include #include "../shared.hpp" -static int ret = 0; - using namespace Hyprutils::OS; using namespace Hyprutils::Memory; #define UP CUniquePointer #define SP CSharedPointer -static bool test() { - NLog::log("{}Testing groups", Colors::GREEN); - +// TODO: decompose this into multiple test cases +TEST_CASE(groups) { // test on workspace "window" NLog::log("{}Dispatching workspace `groups`", Colors::YELLOW); - getFromSocket("/dispatch workspace name:groups"); + getFromSocket("/dispatch hl.dsp.focus({ workspace = 'name:groups' })"); NLog::log("{}Testing movewindoworgroup from group to group", Colors::YELLOW); auto kittyA = Tests::spawnKitty("kittyA"); if (!kittyA) { - NLog::log("{}Error: kitty did not spawn", Colors::RED); - return false; + FAIL_TEST("Could not spawn kitty"); } // check kitty properties. One kitty should take the entire screen, minus the gaps. @@ -43,14 +39,13 @@ static bool test() { auto kittyB = Tests::spawnKitty("kittyB"); if (!kittyB) { - NLog::log("{}Error: kitty did not spawn", Colors::RED); - return false; + FAIL_TEST("Could not spawn kitty"); } - OK(getFromSocket("/dispatch focuswindow class:kittyB")); - OK(getFromSocket("/dispatch togglegroup")); - OK(getFromSocket("/dispatch focuswindow class:kittyA")); - OK(getFromSocket("/dispatch togglegroup")); + OK(getFromSocket("/dispatch hl.dsp.focus({ window = 'class:kittyB' })")); + OK(getFromSocket("/dispatch hl.dsp.group.toggle()")); + OK(getFromSocket("/dispatch hl.dsp.focus({ window = 'class:kittyA' })")); + OK(getFromSocket("/dispatch hl.dsp.group.toggle()")); NLog::log("{}Check kittyB dimensions", Colors::YELLOW); { @@ -61,8 +56,7 @@ static bool test() { auto kittyC = Tests::spawnKitty("kittyC"); if (!kittyC) { - NLog::log("{}Error: kitty did not spawn", Colors::RED); - return false; + FAIL_TEST("Could not spawn kitty"); } NLog::log("{}Check kittyC dimensions", Colors::YELLOW); @@ -72,7 +66,7 @@ static bool test() { EXPECT_COUNT_STRING(str, "fullscreen: 0", 1); } - OK(getFromSocket("/dispatch movewindoworgroup r")); + OK(getFromSocket("/dispatch hl.dsp.window.move({ direction = 'right', group_aware = true })")); NLog::log("{}Check that dimensions remain the same after move", Colors::YELLOW); { auto str = getFromSocket("/activewindow"); @@ -87,12 +81,11 @@ static bool test() { NLog::log("{}Spawning kittyProcA", Colors::YELLOW); auto kittyProcA = Tests::spawnKitty(); if (!kittyProcA) { - NLog::log("{}Error: kitty did not spawn", Colors::RED); - return false; + FAIL_TEST("Could not spawn kitty"); } NLog::log("{}Expecting 1 window", Colors::YELLOW); - EXPECT(Tests::windowCount(), 1); + ASSERT(Tests::windowCount(), 1); // check kitty properties. One kitty should take the entire screen, minus the gaps. NLog::log("{}Check kitty dimensions", Colors::YELLOW); @@ -105,8 +98,8 @@ static bool test() { // group the kitty NLog::log("{}Enable group and groupbar", Colors::YELLOW); - OK(getFromSocket("/dispatch togglegroup")); - OK(getFromSocket("/keyword group:groupbar:enabled 1")); + OK(getFromSocket("/dispatch hl.dsp.group.toggle()")); + OK(getFromSocket("/eval hl.config({ group = { groupbar = { enabled = 1 } } })")); // check the height of the window now NLog::log("{}Recheck kitty dimensions", Colors::YELLOW); @@ -118,7 +111,7 @@ static bool test() { // disable the groupbar for ease of testing for now NLog::log("{}Disable groupbar", Colors::YELLOW); - OK(getFromSocket("r/keyword group:groupbar:enabled 0")); + OK(getFromSocket("r/eval hl.config({ group = { groupbar = { enabled = 0 } } })")); // kill all NLog::log("{}Kill windows", Colors::YELLOW); @@ -127,12 +120,11 @@ static bool test() { NLog::log("{}Spawn kitty again", Colors::YELLOW); kittyProcA = Tests::spawnKitty(); if (!kittyProcA) { - NLog::log("{}Error: kitty did not spawn", Colors::RED); - return false; + FAIL_TEST("Could not spawn kitty"); } NLog::log("{}Group kitty", Colors::YELLOW); - OK(getFromSocket("/dispatch togglegroup")); + OK(getFromSocket("/dispatch hl.dsp.group.toggle()")); // check the height of the window now NLog::log("{}Check kitty dimensions 2", Colors::YELLOW); @@ -145,12 +137,11 @@ static bool test() { NLog::log("{}Spawn kittyProcB", Colors::YELLOW); auto kittyProcB = Tests::spawnKitty(); if (!kittyProcB) { - NLog::log("{}Error: kitty did not spawn", Colors::RED); - return false; + FAIL_TEST("Could not spawn kitty"); } NLog::log("{}Expecting 2 windows", Colors::YELLOW); - EXPECT(Tests::windowCount(), 2); + ASSERT(Tests::windowCount(), 2); size_t lastActiveKittyIdx = 0; @@ -158,95 +149,78 @@ static bool test() { try { auto str = getFromSocket("/activewindow"); lastActiveKittyIdx = std::stoull(str.substr(7, str.find(" -> ") - 7), nullptr, 16); - } catch (...) { - NLog::log("{}Fail at getting prop", Colors::RED); - ret = 1; - } + } catch (...) { FAIL_TEST("Could not extract the active window id"); } // test cycling through NLog::log("{}Test cycling through grouped windows", Colors::YELLOW); - OK(getFromSocket("/dispatch changegroupactive f")); + OK(getFromSocket("/dispatch hl.dsp.group.next()")); try { auto str = getFromSocket("/activewindow"); - EXPECT(lastActiveKittyIdx != std::stoull(str.substr(7, str.find(" -> ") - 7), nullptr, 16), true); - } catch (...) { - NLog::log("{}Fail at getting prop", Colors::RED); - ret = 1; - } + ASSERT(lastActiveKittyIdx != std::stoull(str.substr(7, str.find(" -> ") - 7), nullptr, 16), true); + } catch (...) { FAIL_TEST("Could not extract the active window id"); } - getFromSocket("/dispatch changegroupactive f"); + getFromSocket("/dispatch hl.dsp.group.next()"); try { auto str = getFromSocket("/activewindow"); - EXPECT(lastActiveKittyIdx, std::stoull(str.substr(7, str.find(" -> ") - 7), nullptr, 16)); - } catch (...) { - NLog::log("{}Fail at getting prop", Colors::RED); - ret = 1; - } + ASSERT(lastActiveKittyIdx, std::stoull(str.substr(7, str.find(" -> ") - 7), nullptr, 16)); + } catch (...) { FAIL_TEST("Could not extract the active window id"); } // test movegroupwindow: focus should follow the moved window NLog::log("{}Test movegroupwindow focus follows window", Colors::YELLOW); try { auto str = getFromSocket("/activewindow"); auto activeBeforeMove = std::stoull(str.substr(7, str.find(" -> ") - 7), nullptr, 16); - OK(getFromSocket("/dispatch movegroupwindow f")); + OK(getFromSocket("/dispatch hl.dsp.group.move_window({ forward = true })")); str = getFromSocket("/activewindow"); auto activeAfterMove = std::stoull(str.substr(7, str.find(" -> ") - 7), nullptr, 16); EXPECT(activeAfterMove, activeBeforeMove); - } catch (...) { - NLog::log("{}Fail at getting prop", Colors::RED); - ret = 1; - } + } catch (...) { FAIL_TEST("Could not extract the active window id"); } // and backwards NLog::log("{}Test movegroupwindow backwards", Colors::YELLOW); try { auto str = getFromSocket("/activewindow"); auto activeBeforeMove = std::stoull(str.substr(7, str.find(" -> ") - 7), nullptr, 16); - OK(getFromSocket("/dispatch movegroupwindow b")); + OK(getFromSocket("/dispatch hl.dsp.group.move_window({ forward = false })")); str = getFromSocket("/activewindow"); auto activeAfterMove = std::stoull(str.substr(7, str.find(" -> ") - 7), nullptr, 16); EXPECT(activeAfterMove, activeBeforeMove); - } catch (...) { - NLog::log("{}Fail at getting prop", Colors::RED); - ret = 1; - } + } catch (...) { FAIL_TEST("Could not extract the active window id"); } NLog::log("{}Disable autogrouping", Colors::YELLOW); - OK(getFromSocket("/keyword group:auto_group false")); + OK(getFromSocket("/eval hl.config({ group = { auto_group = false } })")); NLog::log("{}Spawn kittyProcC", Colors::YELLOW); auto kittyProcC = Tests::spawnKitty(); if (!kittyProcC) { - NLog::log("{}Error: kitty did not spawn", Colors::RED); - return false; + FAIL_TEST("Could not spawn kitty"); } NLog::log("{}Expecting 3 windows 2", Colors::YELLOW); - EXPECT(Tests::windowCount(), 3); + ASSERT(Tests::windowCount(), 3); { auto str = getFromSocket("/clients"); EXPECT_COUNT_STRING(str, "at: 22,22", 2); } - OK(getFromSocket("/dispatch movefocus l")); - OK(getFromSocket("/dispatch changegroupactive 1")); - OK(getFromSocket("/keyword group:auto_group true")); - OK(getFromSocket("/keyword group:insert_after_current false")); + OK(getFromSocket("/dispatch hl.dsp.focus({ direction = 'left' })")); + OK(getFromSocket("/dispatch hl.dsp.group.active({ index = 1 })")); + OK(getFromSocket("/eval hl.config({ group = { auto_group = true } })")); + OK(getFromSocket("/eval hl.config({ group = { insert_after_current = false } })")); NLog::log("{}Spawn kittyProcD", Colors::YELLOW); auto kittyProcD = Tests::spawnKitty(); if (!kittyProcD) { - NLog::log("{}Error: kitty did not spawn", Colors::RED); - return false; + FAIL_TEST("Could not spawn kitty"); } NLog::log("{}Expecting 4 windows", Colors::YELLOW); - EXPECT(Tests::windowCount(), 4); + ASSERT(Tests::windowCount(), 4); - OK(getFromSocket("/dispatch changegroupactive 3")); + OK(getFromSocket("/dispatch hl.dsp.group.active({ index = 3 })")); { auto str = getFromSocket("/activewindow"); @@ -258,27 +232,25 @@ static bool test() { Tests::killAllWindows(); NLog::log("{}Expecting 0 windows", Colors::YELLOW); - EXPECT(Tests::windowCount(), 0); + ASSERT(Tests::windowCount(), 0); // test movewindoworgroup: direction should be respected when extracting from group NLog::log("{}Test movewindoworgroup respects direction out of group", Colors::YELLOW); - OK(getFromSocket("/keyword group:groupbar:enabled 0")); + OK(getFromSocket("/eval hl.config({ group = { groupbar = { enabled = 0 } } })")); { auto kittyE = Tests::spawnKitty(); if (!kittyE) { - NLog::log("{}Error: kitty did not spawn", Colors::RED); - return false; + FAIL_TEST("Could not spawn kitty"); } // group kitty, and new windows should be auto-grouped - OK(getFromSocket("/dispatch togglegroup")); + OK(getFromSocket("/dispatch hl.dsp.group.toggle()")); auto kittyF = Tests::spawnKitty(); if (!kittyF) { - NLog::log("{}Error: kitty did not spawn", Colors::RED); - return false; + FAIL_TEST("Could not spawn kitty"); } - EXPECT(Tests::windowCount(), 2); + ASSERT(Tests::windowCount(), 2); // both windows should be grouped at the same position { @@ -288,7 +260,7 @@ static bool test() { // move active window out of group to the right NLog::log("{}Test movewindoworgroup r", Colors::YELLOW); - OK(getFromSocket("/dispatch movewindoworgroup r")); + OK(getFromSocket("/dispatch hl.dsp.window.move({ direction = 'right', group_aware = true })")); // the group should stay at x=22, the extracted window should be to the right { @@ -297,11 +269,11 @@ static bool test() { } // move it back into the group - OK(getFromSocket("/dispatch moveintogroup l")); + OK(getFromSocket("/dispatch hl.dsp.window.move({ into_group = 'left' })")); // move active window out of group downward NLog::log("{}Test movewindoworgroup d", Colors::YELLOW); - OK(getFromSocket("/dispatch movewindoworgroup d")); + OK(getFromSocket("/dispatch hl.dsp.window.move({ direction = 'down', group_aware = true })")); // the group should stay at y=22, the extracted window should be below { @@ -310,37 +282,34 @@ static bool test() { } Tests::killAllWindows(); - EXPECT(Tests::windowCount(), 0); + ASSERT(Tests::windowCount(), 0); } // test that we deny a floated window getting auto-grouped into a tiled group. NLog::log("{}Test that we deny a floated window getting auto-grouped into a tiled group.", Colors::GREEN); - OK(getFromSocket("/keyword windowrule[kitty-tiled]:match:class kitty_tiled")); - OK(getFromSocket("/keyword windowrule[kitty-tiled]:tile yes")); + OK(getFromSocket("/eval hl.window_rule({ name = 'kitty-tiled', match = { class = 'kitty_tiled' } })")); + OK(getFromSocket("/eval hl.window_rule({ name = 'kitty-tiled', tile = true })")); auto kittyProcE = Tests::spawnKitty("kitty_tiled"); if (!kittyProcE) { - NLog::log("{}Error: kitty did not spawn", Colors::RED); - return false; + FAIL_TEST("Could not spawn kitty"); } - OK(getFromSocket("/dispatch togglegroup")); + OK(getFromSocket("/dispatch hl.dsp.group.toggle()")); - OK(getFromSocket("/keyword windowrule[kitty-floated]:match:class kitty_floated")); - OK(getFromSocket("/keyword windowrule[kitty-floated]:float yes")); + OK(getFromSocket("/eval hl.window_rule({ name = 'kitty-floated', match = { class = 'kitty_floated' } })")); + OK(getFromSocket("/eval hl.window_rule({ name = 'kitty-floated', float = true })")); auto kittyProcF = Tests::spawnKitty("kitty_floated"); if (!kittyProcF) { - NLog::log("{}Error: kitty did not spawn", Colors::RED); - return false; + FAIL_TEST("Could not spawn kitty"); } - EXPECT(Tests::windowCount(), 2); + ASSERT(Tests::windowCount(), 2); { auto clients = getFromSocket("/clients"); auto classPos = clients.find("class: kitty_floated"); if (classPos == std::string::npos) { - NLog::log("{}Could not find kitty_floated in clients output", Colors::RED); - ret = 1; + FAIL_TEST("Could not find kitty_floated in clients output"); } else { auto entryStart = clients.rfind("Window ", classPos); auto entryEnd = clients.find("\n\n", classPos); @@ -351,27 +320,25 @@ static bool test() { } Tests::killAllWindows(); - EXPECT(Tests::windowCount(), 0); + ASSERT(Tests::windowCount(), 0); // Tests for grouping/merging logic NLog::log("{}Testing locked groups w/ invade", Colors::GREEN); Tests::killAllWindows(); - EXPECT(Tests::windowCount(), 0); + ASSERT(Tests::windowCount(), 0); // Test normal, unlocked groups { auto winA = Tests::spawnKitty("unlocked"); if (!winA) { - NLog::log("{}Error: unlocked kitty did not spawn", Colors::RED); - return false; + FAIL_TEST("Could not spawn unlocked kitty"); } - OK(getFromSocket("/dispatch togglegroup")); + OK(getFromSocket("/dispatch hl.dsp.group.toggle()")); auto winB = Tests::spawnKitty("top"); if (!winB) { - NLog::log("{}Error: top kitty did not spawn", Colors::RED); - return false; + FAIL_TEST("Could not spawn top kitty"); } // Verify it DID merge into a group @@ -382,23 +349,21 @@ static bool test() { } Tests::killAllWindows(); - EXPECT(Tests::windowCount(), 0); + ASSERT(Tests::windowCount(), 0); // Test locked groups { auto lockedWin = Tests::spawnKitty("locked"); if (!lockedWin) { - NLog::log("{}Error: locked kitty did not spawn", Colors::RED); - return false; + FAIL_TEST("Could not spawn locked kitty"); } - OK(getFromSocket("/dispatch togglegroup")); - OK(getFromSocket(std::format("/dispatch focuswindow pid:{}", lockedWin->pid()))); - OK(getFromSocket("/dispatch lockactivegroup lock")); + OK(getFromSocket("/dispatch hl.dsp.group.toggle()")); + OK(getFromSocket(std::format("/dispatch hl.dsp.focus({{ window = 'pid:{}' }})", lockedWin->pid()))); + OK(getFromSocket("/dispatch hl.dsp.group.lock_active({ action = 'set' })")); auto winB = Tests::spawnKitty("top"); if (!winB) { - NLog::log("{}Error: top kitty did not spawn", Colors::RED); - return false; + FAIL_TEST("Could not spawn top kitty"); } // Verify it did NOT merge into the locked group @@ -409,23 +374,21 @@ static bool test() { } Tests::killAllWindows(); - EXPECT(Tests::windowCount(), 0); + ASSERT(Tests::windowCount(), 0); // Test locked groups WITH invade rule { - OK(getFromSocket("/keyword windowrule[locked-im]:match:class ^locked|invade$")); - OK(getFromSocket("/keyword windowrule[locked-im]:group set always lock invade")); + OK(getFromSocket("/eval hl.window_rule({ name = 'locked-im', match = { class = '^locked|invade$' } })")); + OK(getFromSocket("/eval hl.window_rule({ name = 'locked-im', group = 'set always lock invade' })")); auto lockedWin = Tests::spawnKitty("locked"); if (!lockedWin) { - NLog::log("{}Error: locked kitty did not spawn", Colors::RED); - return false; + FAIL_TEST("Could not spawn locked kitty"); } auto invadingWin = Tests::spawnKitty("invade"); if (!invadingWin) { - NLog::log("{}Error: invading kitty did not spawn", Colors::RED); - return false; + FAIL_TEST("Could not spawn invading kitty"); } // Verify it DID merge into the locked group @@ -434,9 +397,5 @@ static bool test() { } Tests::killAllWindows(); - EXPECT(Tests::windowCount(), 0); - - return !ret; + ASSERT(Tests::windowCount(), 0); } - -REGISTER_TEST_FN(test) diff --git a/hyprtester/src/tests/main/hyprctl.cpp b/hyprtester/src/tests/main/hyprctl.cpp index 4f3fba752..c67c8af93 100644 --- a/hyprtester/src/tests/main/hyprctl.cpp +++ b/hyprtester/src/tests/main/hyprctl.cpp @@ -12,8 +12,6 @@ #include #include "../shared.hpp" -static int ret = 0; - using namespace Hyprutils::OS; using namespace Hyprutils::Memory; @@ -31,11 +29,13 @@ static std::string getCommandStdOut(std::string command) { return stdOut.substr(0, stdOut.length() - 1); } -static bool testDevicesActiveLayoutIndex() { - NLog::log("{}Testing hyprctl devices active_layout_index", Colors::GREEN); +static void setWindowProp(const std::string& selector, const std::string& prop, const std::string& value) { + getFromSocket("/dispatch hl.dsp.window.set_prop({ window = '" + selector + "', prop = '" + prop + "', value = '" + value + "' })"); +} +TEST_CASE(hyprctlDevicesActiveLayoutIndex) { // configure layouts - getFromSocket("/keyword input:kb_layout us,pl,ua"); + OK(getFromSocket("r/eval hl.config({ input = { kb_layout = \"us,pl,ua\" } })")); for (uint8_t i = 0; i < 3; i++) { // set layout @@ -45,115 +45,111 @@ static bool testDevicesActiveLayoutIndex() { // check layout index EXPECT_CONTAINS(devicesJson, expected); } - - return true; } -static bool testGetprop() { - NLog::log("{}Testing hyprctl getprop", Colors::GREEN); +TEST_CASE(hyprctlGetprop) { if (!Tests::spawnKitty()) { - NLog::log("{}Error: kitty did not spawn", Colors::RED); - return false; + FAIL_TEST("Could not spawn kitty"); } // animation EXPECT(getCommandStdOut("hyprctl getprop class:kitty animation"), "(unset)"); EXPECT(getCommandStdOut("hyprctl getprop class:kitty animation -j"), R"({"animation": ""})"); - getFromSocket("/dispatch setprop class:kitty animation teststyle"); + setWindowProp("class:kitty", "animation", "teststyle"); EXPECT(getCommandStdOut("hyprctl getprop class:kitty animation"), "teststyle"); EXPECT(getCommandStdOut("hyprctl getprop class:kitty animation -j"), R"({"animation": "teststyle"})"); // max_size EXPECT(getCommandStdOut("hyprctl getprop class:kitty max_size"), "inf inf"); EXPECT(getCommandStdOut("hyprctl getprop class:kitty max_size -j"), R"({"max_size": [null,null]})"); - getFromSocket("/dispatch setprop class:kitty max_size 200 150"); + setWindowProp("class:kitty", "max_size", "200 150"); EXPECT(getCommandStdOut("hyprctl getprop class:kitty max_size"), "200 150"); EXPECT(getCommandStdOut("hyprctl getprop class:kitty max_size -j"), R"({"max_size": [200,150]})"); // min_size EXPECT(getCommandStdOut("hyprctl getprop class:kitty min_size"), "20 20"); EXPECT(getCommandStdOut("hyprctl getprop class:kitty min_size -j"), R"({"min_size": [20,20]})"); - getFromSocket("/dispatch setprop class:kitty min_size 100 50"); + setWindowProp("class:kitty", "min_size", "100 50"); EXPECT(getCommandStdOut("hyprctl getprop class:kitty min_size"), "100 50"); EXPECT(getCommandStdOut("hyprctl getprop class:kitty min_size -j"), R"({"min_size": [100,50]})"); // expr-based min/max _size - getFromSocket("/dispatch setfloating class:kitty"); // need to set floating for tests below - getFromSocket("/dispatch setprop class:kitty max_size 90+10 25*2"); // set max to the same as min above, forcing window to 100*50 + getFromSocket("/dispatch hl.dsp.window.float({ action = 'set', window = 'class:kitty' })"); // need to set floating for tests below + setWindowProp("class:kitty", "max_size", "90+10 25*2"); // set max to the same as min above, forcing window to 100*50 EXPECT(getCommandStdOut("hyprctl getprop class:kitty max_size"), "100 50"); EXPECT(getCommandStdOut("hyprctl getprop class:kitty max_size -j"), R"({"max_size": [100,50]})"); - getFromSocket("/dispatch setprop class:kitty min_size window_w*0.5 window_h-10"); + setWindowProp("class:kitty", "min_size", "window_w*0.5 window_h-10"); EXPECT(getCommandStdOut("hyprctl getprop class:kitty min_size"), "50 40"); EXPECT(getCommandStdOut("hyprctl getprop class:kitty min_size -j"), R"({"min_size": [50,40]})"); - getFromSocket("/dispatch settiled class:kitty"); // go back to tiled for consistency + getFromSocket("/dispatch hl.dsp.window.float({ action = 'unset', window = 'class:kitty' })"); // go back to tiled for consistency // opacity EXPECT(getCommandStdOut("hyprctl getprop class:kitty opacity"), "1"); EXPECT(getCommandStdOut("hyprctl getprop class:kitty opacity -j"), R"({"opacity": 1})"); - getFromSocket("/dispatch setprop class:kitty opacity 0.3"); + setWindowProp("class:kitty", "opacity", "0.3"); EXPECT(getCommandStdOut("hyprctl getprop class:kitty opacity"), "0.3"); EXPECT(getCommandStdOut("hyprctl getprop class:kitty opacity -j"), R"({"opacity": 0.3})"); // opacity_inactive EXPECT(getCommandStdOut("hyprctl getprop class:kitty opacity_inactive"), "1"); EXPECT(getCommandStdOut("hyprctl getprop class:kitty opacity_inactive -j"), R"({"opacity_inactive": 1})"); - getFromSocket("/dispatch setprop class:kitty opacity_inactive 0.5"); + setWindowProp("class:kitty", "opacity_inactive", "0.5"); EXPECT(getCommandStdOut("hyprctl getprop class:kitty opacity_inactive"), "0.5"); EXPECT(getCommandStdOut("hyprctl getprop class:kitty opacity_inactive -j"), R"({"opacity_inactive": 0.5})"); // opacity_fullscreen EXPECT(getCommandStdOut("hyprctl getprop class:kitty opacity_fullscreen"), "1"); EXPECT(getCommandStdOut("hyprctl getprop class:kitty opacity_fullscreen -j"), R"({"opacity_fullscreen": 1})"); - getFromSocket("/dispatch setprop class:kitty opacity_fullscreen 0.75"); + setWindowProp("class:kitty", "opacity_fullscreen", "0.75"); EXPECT(getCommandStdOut("hyprctl getprop class:kitty opacity_fullscreen"), "0.75"); EXPECT(getCommandStdOut("hyprctl getprop class:kitty opacity_fullscreen -j"), R"({"opacity_fullscreen": 0.75})"); // opacity_override EXPECT(getCommandStdOut("hyprctl getprop class:kitty opacity_override"), "false"); EXPECT(getCommandStdOut("hyprctl getprop class:kitty opacity_override -j"), R"({"opacity_override": false})"); - getFromSocket("/dispatch setprop class:kitty opacity_override true"); + setWindowProp("class:kitty", "opacity_override", "true"); EXPECT(getCommandStdOut("hyprctl getprop class:kitty opacity_override"), "true"); EXPECT(getCommandStdOut("hyprctl getprop class:kitty opacity_override -j"), R"({"opacity_override": true})"); // opacity_inactive_override EXPECT(getCommandStdOut("hyprctl getprop class:kitty opacity_inactive_override"), "false"); EXPECT(getCommandStdOut("hyprctl getprop class:kitty opacity_inactive_override -j"), R"({"opacity_inactive_override": false})"); - getFromSocket("/dispatch setprop class:kitty opacity_inactive_override true"); + setWindowProp("class:kitty", "opacity_inactive_override", "true"); EXPECT(getCommandStdOut("hyprctl getprop class:kitty opacity_inactive_override"), "true"); EXPECT(getCommandStdOut("hyprctl getprop class:kitty opacity_inactive_override -j"), R"({"opacity_inactive_override": true})"); // opacity_fullscreen_override EXPECT(getCommandStdOut("hyprctl getprop class:kitty opacity_fullscreen_override"), "false"); EXPECT(getCommandStdOut("hyprctl getprop class:kitty opacity_fullscreen_override -j"), R"({"opacity_fullscreen_override": false})"); - getFromSocket("/dispatch setprop class:kitty opacity_fullscreen_override true"); + setWindowProp("class:kitty", "opacity_fullscreen_override", "true"); EXPECT(getCommandStdOut("hyprctl getprop class:kitty opacity_fullscreen_override"), "true"); EXPECT(getCommandStdOut("hyprctl getprop class:kitty opacity_fullscreen_override -j"), R"({"opacity_fullscreen_override": true})"); // active_border_color EXPECT(getCommandStdOut("hyprctl getprop class:kitty active_border_color"), "ee33ccff ee00ff99 45deg"); EXPECT(getCommandStdOut("hyprctl getprop class:kitty active_border_color -j"), R"({"active_border_color": "ee33ccff ee00ff99 45deg"})"); - getFromSocket("/dispatch setprop class:kitty active_border_color rgb(abcdef)"); + setWindowProp("class:kitty", "active_border_color", "rgb(abcdef)"); EXPECT(getCommandStdOut("hyprctl getprop class:kitty active_border_color"), "ffabcdef 0deg"); EXPECT(getCommandStdOut("hyprctl getprop class:kitty active_border_color -j"), R"({"active_border_color": "ffabcdef 0deg"})"); // bool window properties EXPECT(getCommandStdOut("hyprctl getprop class:kitty allows_input"), "false"); EXPECT(getCommandStdOut("hyprctl getprop class:kitty allows_input -j"), R"({"allows_input": false})"); - getFromSocket("/dispatch setprop class:kitty allows_input true"); + setWindowProp("class:kitty", "allows_input", "true"); EXPECT(getCommandStdOut("hyprctl getprop class:kitty allows_input"), "true"); EXPECT(getCommandStdOut("hyprctl getprop class:kitty allows_input -j"), R"({"allows_input": true})"); // int window properties EXPECT(getCommandStdOut("hyprctl getprop class:kitty rounding"), "10"); EXPECT(getCommandStdOut("hyprctl getprop class:kitty rounding -j"), R"({"rounding": 10})"); - getFromSocket("/dispatch setprop class:kitty rounding 4"); + setWindowProp("class:kitty", "rounding", "4"); EXPECT(getCommandStdOut("hyprctl getprop class:kitty rounding"), "4"); EXPECT(getCommandStdOut("hyprctl getprop class:kitty rounding -j"), R"({"rounding": 4})"); // float window properties EXPECT(getCommandStdOut("hyprctl getprop class:kitty rounding_power"), "2"); EXPECT(getCommandStdOut("hyprctl getprop class:kitty rounding_power -j"), R"({"rounding_power": 2})"); - getFromSocket("/dispatch setprop class:kitty rounding_power 1.25"); + setWindowProp("class:kitty", "rounding_power", "1.25"); EXPECT(getCommandStdOut("hyprctl getprop class:kitty rounding_power"), "1.25"); EXPECT(getCommandStdOut("hyprctl getprop class:kitty rounding_power -j"), R"({"rounding_power": 1.25})"); @@ -162,41 +158,16 @@ static bool testGetprop() { EXPECT(getCommandStdOut("hyprctl getprop class:kitty"), "not enough args"); EXPECT(getCommandStdOut("hyprctl getprop class:nonexistantclass animation"), "window not found"); EXPECT(getCommandStdOut("hyprctl getprop class:kitty nonexistantprop"), "prop not found"); - - // kill all - NLog::log("{}Killing all windows", Colors::YELLOW); - Tests::killAllWindows(); - - NLog::log("{}Expecting 0 windows", Colors::YELLOW); - EXPECT(Tests::windowCount(), 0); - - return true; } -static void testSubmap() { - NLog::log("{}Testing hyprctl submap", Colors::GREEN); - +TEST_CASE(hyprctlSubmap) { EXPECT(getCommandStdOut("hyprctl submap"), "default\n"); EXPECT(getCommandStdOut("hyprctl submap -j | jq -r \".\""), "default"); } -static bool test() { - NLog::log("{}Testing hyprctl", Colors::GREEN); - - { - NLog::log("{}Testing hyprctl descriptions for any json errors", Colors::GREEN); - CProcess jqProc("bash", {"-c", "hyprctl descriptions | jq"}); - jqProc.addEnv("HYPRLAND_INSTANCE_SIGNATURE", HIS); - jqProc.runSync(); - EXPECT(jqProc.exitCode(), 0); - } - - testGetprop(); - testDevicesActiveLayoutIndex(); - testSubmap(); - getFromSocket("/reload"); - - return !ret; +TEST_CASE(hyprctlJsonErrors) { + CProcess jqProc("bash", {"-c", "hyprctl descriptions | jq"}); + jqProc.addEnv("HYPRLAND_INSTANCE_SIGNATURE", HIS); + jqProc.runSync(); + EXPECT(jqProc.exitCode(), 0); } - -REGISTER_TEST_FN(test); diff --git a/hyprtester/src/tests/main/keybinds.cpp b/hyprtester/src/tests/main/keybinds.cpp index dcde9839d..567d078bc 100644 --- a/hyprtester/src/tests/main/keybinds.cpp +++ b/hyprtester/src/tests/main/keybinds.cpp @@ -8,10 +8,16 @@ using namespace Hyprutils::OS; using namespace Hyprutils::Memory; - -static int ret = 0; static std::string flagFile = "/tmp/hyprtester-keybinds.txt"; +static std::string pluginKeybindCmd(bool pressed, uint32_t modifier, uint32_t key) { + return "/eval hl.plugin.test.keybind(" + std::to_string(pressed ? 1 : 0) + ", " + std::to_string(modifier) + ", " + std::to_string(key) + ")"; +} + +static std::string pluginScrollCmd(int delta) { + return "/eval hl.plugin.test.scroll(" + std::to_string(delta) + ")"; +} + // Because i don't feel like changing someone elses code. enum eKeyboardModifierIndex : uint8_t { MOD_SHIFT = 1, @@ -80,36 +86,40 @@ static CUniquePointer spawnRemoteControlKitty() { return kittyProc; } -static void testBind() { +// All the `SUBTEST`s below are supposed to be independent `TEST_CASE`s. +// But if isolated trivially, some of them fail. +// TODO: investigate and isolate tests by turning `SUBTEST`s into `TEST_CASE`s. + +SUBTEST(bind) { EXPECT(checkFlag(), false); - EXPECT(getFromSocket("/keyword bind SUPER,Y,exec,touch " + flagFile), "ok"); + EXPECT(getFromSocket("/eval hl.bind('SUPER + Y', hl.dsp.exec_cmd('touch " + flagFile + "'))"), "ok"); // press keybind - OK(getFromSocket("/dispatch plugin:test:keybind 1,7,29")); + OK(getFromSocket(pluginKeybindCmd(true, 7, 29))); // await flag EXPECT(attemptCheckFlag(20, 50), true); // release keybind - OK(getFromSocket("/dispatch plugin:test:keybind 0,0,29")); - EXPECT(getFromSocket("/keyword unbind SUPER,Y"), "ok"); + OK(getFromSocket(pluginKeybindCmd(false, 0, 29))); + EXPECT(getFromSocket("/eval hl.unbind('SUPER + Y')"), "ok"); } -static void testBindKey() { +SUBTEST(bindKey) { EXPECT(checkFlag(), false); - EXPECT(getFromSocket("/keyword bind ,Y,exec,touch " + flagFile), "ok"); + EXPECT(getFromSocket("/eval hl.bind('Y', hl.dsp.exec_cmd('touch " + flagFile + "'))"), "ok"); // press keybind - OK(getFromSocket("/dispatch plugin:test:keybind 1,0,29")); + OK(getFromSocket(pluginKeybindCmd(true, 0, 29))); // await flag EXPECT(attemptCheckFlag(20, 50), true); // release keybind - OK(getFromSocket("/dispatch plugin:test:keybind 0,0,29")); - EXPECT(getFromSocket("/keyword unbind ,Y"), "ok"); + OK(getFromSocket(pluginKeybindCmd(false, 0, 29))); + EXPECT(getFromSocket("/eval hl.unbind('Y')"), "ok"); } -static void testLongPress() { +SUBTEST(longPress) { EXPECT(checkFlag(), false); - EXPECT(getFromSocket("/keyword bindo SUPER,Y,exec,touch " + flagFile), "ok"); - EXPECT(getFromSocket("/keyword input:repeat_delay 100"), "ok"); + EXPECT(getFromSocket("/eval hl.bind('SUPER + Y', hl.dsp.exec_cmd('touch " + flagFile + "'), { long_press = true })"), "ok"); + EXPECT(getFromSocket("r/eval hl.config({ input = { repeat_delay = 100 } })"), "ok"); // press keybind - OK(getFromSocket("/dispatch plugin:test:keybind 1,7,29")); + OK(getFromSocket(pluginKeybindCmd(true, 7, 29))); // check no flag on short press std::this_thread::sleep_for(std::chrono::milliseconds(50)); EXPECT(checkFlag(), false); @@ -117,16 +127,15 @@ static void testLongPress() { std::this_thread::sleep_for(std::chrono::milliseconds(200)); EXPECT(checkFlag(), true); // release keybind - OK(getFromSocket("/dispatch plugin:test:keybind 0,0,29")); - EXPECT(getFromSocket("/keyword unbind SUPER,Y"), "ok"); + OK(getFromSocket(pluginKeybindCmd(false, 0, 29))); + EXPECT(getFromSocket("/eval hl.unbind('SUPER + Y')"), "ok"); } - -static void testKeyLongPress() { +SUBTEST(keyLongPress) { EXPECT(checkFlag(), false); - EXPECT(getFromSocket("/keyword bindo ,Y,exec,touch " + flagFile), "ok"); - EXPECT(getFromSocket("/keyword input:repeat_delay 100"), "ok"); + EXPECT(getFromSocket("/eval hl.bind('Y', hl.dsp.exec_cmd('touch " + flagFile + "'), { long_press = true })"), "ok"); + EXPECT(getFromSocket("r/eval hl.config({ input = { repeat_delay = 100 } })"), "ok"); // press keybind - OK(getFromSocket("/dispatch plugin:test:keybind 1,0,29")); + OK(getFromSocket(pluginKeybindCmd(true, 0, 29))); // check no flag on short press std::this_thread::sleep_for(std::chrono::milliseconds(50)); EXPECT(checkFlag(), false); @@ -134,51 +143,50 @@ static void testKeyLongPress() { std::this_thread::sleep_for(std::chrono::milliseconds(200)); EXPECT(checkFlag(), true); // release keybind - OK(getFromSocket("/dispatch plugin:test:keybind 0,0,29")); - EXPECT(getFromSocket("/keyword unbind ,Y"), "ok"); + OK(getFromSocket(pluginKeybindCmd(false, 0, 29))); + EXPECT(getFromSocket("/eval hl.unbind('Y')"), "ok"); } -static void testLongPressRelease() { +SUBTEST(longPressRelease) { EXPECT(checkFlag(), false); - EXPECT(getFromSocket("/keyword bindo SUPER,Y,exec,touch " + flagFile), "ok"); - EXPECT(getFromSocket("/keyword input:repeat_delay 100"), "ok"); + EXPECT(getFromSocket("/eval hl.bind('SUPER + Y', hl.dsp.exec_cmd('touch " + flagFile + "'), { long_press = true })"), "ok"); + EXPECT(getFromSocket("r/eval hl.config({ input = { repeat_delay = 100 } })"), "ok"); // press keybind - OK(getFromSocket("/dispatch plugin:test:keybind 1,7,29")); + OK(getFromSocket(pluginKeybindCmd(true, 7, 29))); // check no flag on short press std::this_thread::sleep_for(std::chrono::milliseconds(50)); EXPECT(checkFlag(), false); // release keybind - OK(getFromSocket("/dispatch plugin:test:keybind 0,0,29")); + OK(getFromSocket(pluginKeybindCmd(false, 0, 29))); // await repeat delay std::this_thread::sleep_for(std::chrono::milliseconds(200)); EXPECT(checkFlag(), false); - EXPECT(getFromSocket("/keyword unbind SUPER,Y"), "ok"); + EXPECT(getFromSocket("/eval hl.unbind('SUPER + Y')"), "ok"); } - -static void testLongPressOnlyKeyRelease() { +SUBTEST(longPressOnlyKeyRelease) { EXPECT(checkFlag(), false); - EXPECT(getFromSocket("/keyword bindo SUPER,Y,exec,touch " + flagFile), "ok"); - EXPECT(getFromSocket("/keyword input:repeat_delay 100"), "ok"); + EXPECT(getFromSocket("/eval hl.bind('SUPER + Y', hl.dsp.exec_cmd('touch " + flagFile + "'), { long_press = true })"), "ok"); + EXPECT(getFromSocket("r/eval hl.config({ input = { repeat_delay = 100 } })"), "ok"); // press keybind - OK(getFromSocket("/dispatch plugin:test:keybind 1,7,29")); + OK(getFromSocket(pluginKeybindCmd(true, 7, 29))); // check no flag on short press std::this_thread::sleep_for(std::chrono::milliseconds(50)); EXPECT(checkFlag(), false); // release key, keep modifier - OK(getFromSocket("/dispatch plugin:test:keybind 0,7,29")); + OK(getFromSocket(pluginKeybindCmd(false, 7, 29))); // await repeat delay std::this_thread::sleep_for(std::chrono::milliseconds(200)); EXPECT(checkFlag(), false); - OK(getFromSocket("/dispatch plugin:test:keybind 0,0,29")); - EXPECT(getFromSocket("/keyword unbind SUPER,Y"), "ok"); + OK(getFromSocket(pluginKeybindCmd(false, 0, 29))); + EXPECT(getFromSocket("/eval hl.unbind('SUPER + Y')"), "ok"); } -static void testRepeat() { +SUBTEST(repeat) { EXPECT(checkFlag(), false); - EXPECT(getFromSocket("/keyword binde SUPER,Y,exec,touch " + flagFile), "ok"); - EXPECT(getFromSocket("/keyword input:repeat_delay 100"), "ok"); + EXPECT(getFromSocket("/eval hl.bind('SUPER + Y', hl.dsp.exec_cmd('touch " + flagFile + "'), { repeating = true })"), "ok"); + EXPECT(getFromSocket("r/eval hl.config({ input = { repeat_delay = 100 } })"), "ok"); // press keybind - OK(getFromSocket("/dispatch plugin:test:keybind 1,7,29")); + OK(getFromSocket(pluginKeybindCmd(true, 7, 29))); // await flag std::this_thread::sleep_for(std::chrono::milliseconds(200)); EXPECT(checkFlag(), true); @@ -189,16 +197,16 @@ static void testRepeat() { std::this_thread::sleep_for(std::chrono::milliseconds(200)); EXPECT(checkFlag(), true); // release keybind - OK(getFromSocket("/dispatch plugin:test:keybind 0,0,29")); - EXPECT(getFromSocket("/keyword unbind SUPER,Y"), "ok"); + OK(getFromSocket(pluginKeybindCmd(false, 0, 29))); + EXPECT(getFromSocket("/eval hl.unbind('SUPER + Y')"), "ok"); } -static void testKeyRepeat() { +SUBTEST(keyRepeat) { EXPECT(checkFlag(), false); - EXPECT(getFromSocket("/keyword binde ,Y,exec,touch " + flagFile), "ok"); - EXPECT(getFromSocket("/keyword input:repeat_delay 100"), "ok"); + EXPECT(getFromSocket("/eval hl.bind('Y', hl.dsp.exec_cmd('touch " + flagFile + "'), { repeating = true })"), "ok"); + EXPECT(getFromSocket("r/eval hl.config({ input = { repeat_delay = 100 } })"), "ok"); // press keybind - OK(getFromSocket("/dispatch plugin:test:keybind 1,0,29")); + OK(getFromSocket(pluginKeybindCmd(true, 0, 29))); // await flag std::this_thread::sleep_for(std::chrono::milliseconds(50)); EXPECT(checkFlag(), true); @@ -209,11 +217,11 @@ static void testKeyRepeat() { std::this_thread::sleep_for(std::chrono::milliseconds(200)); EXPECT(checkFlag(), true); // release keybind - OK(getFromSocket("/dispatch plugin:test:keybind 0,0,29")); - EXPECT(getFromSocket("/keyword unbind ,Y"), "ok"); + OK(getFromSocket(pluginKeybindCmd(false, 0, 29))); + EXPECT(getFromSocket("/eval hl.unbind('Y')"), "ok"); } -static void testRepeatRelease() { +SUBTEST(repeatRelease) { // wait until flag becomes false (CI timing can vary) bool ok = false; for (int i = 0; i < 20; ++i) { @@ -225,15 +233,15 @@ static void testRepeatRelease() { } EXPECT(ok, true); - EXPECT(getFromSocket("/keyword binde SUPER,Y,exec,touch " + flagFile), "ok"); - EXPECT(getFromSocket("/keyword input:repeat_delay 100"), "ok"); + EXPECT(getFromSocket("/eval hl.bind('SUPER + Y', hl.dsp.exec_cmd('touch " + flagFile + "'), { repeating = true })"), "ok"); + EXPECT(getFromSocket("r/eval hl.config({ input = { repeat_delay = 100 } })"), "ok"); // press keybind - OK(getFromSocket("/dispatch plugin:test:keybind 1,7,29")); + OK(getFromSocket(pluginKeybindCmd(true, 7, 29))); // await flag std::this_thread::sleep_for(std::chrono::milliseconds(50)); EXPECT(checkFlag(), true); // release keybind - OK(getFromSocket("/dispatch plugin:test:keybind 0,0,29")); + OK(getFromSocket(pluginKeybindCmd(false, 0, 29))); // await repeat delay std::this_thread::sleep_for(std::chrono::milliseconds(200)); clearFlag(); @@ -242,20 +250,20 @@ static void testRepeatRelease() { // check that it is not repeating std::this_thread::sleep_for(std::chrono::milliseconds(200)); EXPECT(checkFlag(), false); - EXPECT(getFromSocket("/keyword unbind SUPER,Y"), "ok"); + EXPECT(getFromSocket("/eval hl.unbind('SUPER + Y')"), "ok"); } -static void testRepeatOnlyKeyRelease() { +SUBTEST(repeatOnlyKeyRelease) { EXPECT(checkFlag(), false); - EXPECT(getFromSocket("/keyword binde SUPER,Y,exec,touch " + flagFile), "ok"); - EXPECT(getFromSocket("/keyword input:repeat_delay 100"), "ok"); + EXPECT(getFromSocket("/eval hl.bind('SUPER + Y', hl.dsp.exec_cmd('touch " + flagFile + "'), { repeating = true })"), "ok"); + EXPECT(getFromSocket("r/eval hl.config({ input = { repeat_delay = 100 } })"), "ok"); // press keybind - OK(getFromSocket("/dispatch plugin:test:keybind 1,7,29")); + OK(getFromSocket(pluginKeybindCmd(true, 7, 29))); // await flag std::this_thread::sleep_for(std::chrono::milliseconds(200)); EXPECT(checkFlag(), true); // release key, keep modifier - OK(getFromSocket("/dispatch plugin:test:keybind 0,7,29")); + OK(getFromSocket(pluginKeybindCmd(false, 7, 29))); // await repeat delay std::this_thread::sleep_for(std::chrono::milliseconds(200)); clearFlag(); @@ -264,71 +272,65 @@ static void testRepeatOnlyKeyRelease() { // check that it is not repeating std::this_thread::sleep_for(std::chrono::milliseconds(200)); EXPECT(checkFlag(), false); - OK(getFromSocket("/dispatch plugin:test:keybind 0,0,29")); - EXPECT(getFromSocket("/keyword unbind SUPER,Y"), "ok"); + OK(getFromSocket(pluginKeybindCmd(false, 0, 29))); + EXPECT(getFromSocket("/eval hl.unbind('SUPER + Y')"), "ok"); } -static void testShortcutBind() { +SUBTEST(shortcutBind) { auto kittyProc = spawnRemoteControlKitty(); if (!kittyProc) { - NLog::log("{}Error: kitty did not spawn", Colors::RED); - ret = 1; - return; + FAIL_TEST("Could not spawn kitty"); } - EXPECT(getFromSocket("/dispatch focuswindow class:keybinds_test"), "ok"); - EXPECT(getFromSocket("/keyword bind SUPER,Y,sendshortcut,,q,"), "ok"); + EXPECT(getFromSocket("/dispatch hl.dsp.focus({ window = 'class:keybinds_test' })"), "ok"); + EXPECT(getFromSocket("/eval hl.bind('SUPER + Y', hl.dsp.send_shortcut({ mods = '', key = 'q', window = 'activewindow' }))"), "ok"); // press keybind - OK(getFromSocket("/dispatch plugin:test:keybind 1,7,29")); + OK(getFromSocket(pluginKeybindCmd(true, 7, 29))); // release keybind std::this_thread::sleep_for(std::chrono::milliseconds(50)); - OK(getFromSocket("/dispatch plugin:test:keybind 0,0,29")); + OK(getFromSocket(pluginKeybindCmd(false, 0, 29))); std::this_thread::sleep_for(std::chrono::milliseconds(50)); const std::string output = readKittyOutput(); EXPECT_COUNT_STRING(output, "y", 0); - EXPECT_COUNT_STRING(output, "q", 1); - EXPECT(getFromSocket("/keyword unbind SUPER,Y"), "ok"); + EXPECT(output.find("q") != std::string::npos, true); + EXPECT(getFromSocket("/eval hl.unbind('SUPER + Y')"), "ok"); Tests::killAllWindows(); } -static void testShortcutBindKey() { +SUBTEST(shortcutBindKey) { auto kittyProc = spawnRemoteControlKitty(); if (!kittyProc) { - NLog::log("{}Error: kitty did not spawn", Colors::RED); - ret = 1; - return; + FAIL_TEST("Could not spawn kitty"); } - EXPECT(getFromSocket("/dispatch focuswindow class:keybinds_test"), "ok"); - EXPECT(getFromSocket("/keyword bind ,Y,sendshortcut,,q,"), "ok"); + EXPECT(getFromSocket("/dispatch hl.dsp.focus({ window = 'class:keybinds_test' })"), "ok"); + EXPECT(getFromSocket("/eval hl.bind('Y', hl.dsp.send_shortcut({ mods = '', key = 'q', window = 'activewindow' }))"), "ok"); // press keybind - OK(getFromSocket("/dispatch plugin:test:keybind 1,0,29")); + OK(getFromSocket(pluginKeybindCmd(true, 0, 29))); // release keybind std::this_thread::sleep_for(std::chrono::milliseconds(50)); - OK(getFromSocket("/dispatch plugin:test:keybind 0,0,29")); + OK(getFromSocket(pluginKeybindCmd(false, 0, 29))); std::this_thread::sleep_for(std::chrono::milliseconds(50)); const std::string output = readKittyOutput(); EXPECT_COUNT_STRING(output, "y", 0); // disabled: doesn't work in CI // EXPECT_COUNT_STRING(output, "q", 1); - EXPECT(getFromSocket("/keyword unbind ,Y"), "ok"); + EXPECT(getFromSocket("/eval hl.unbind('Y')"), "ok"); Tests::killAllWindows(); } -static void testShortcutLongPress() { +SUBTEST(shortcutLongPress) { auto kittyProc = spawnRemoteControlKitty(); if (!kittyProc) { - NLog::log("{}Error: kitty did not spawn", Colors::RED); - ret = 1; - return; + FAIL_TEST("Could not spawn kitty"); } - EXPECT(getFromSocket("/dispatch focuswindow class:keybinds_test"), "ok"); - EXPECT(getFromSocket("/keyword bindo SUPER,Y,sendshortcut,,q,"), "ok"); - EXPECT(getFromSocket("/keyword input:repeat_delay 100"), "ok"); - EXPECT(getFromSocket("/keyword input:repeat_rate 10"), "ok"); + EXPECT(getFromSocket("/dispatch hl.dsp.focus({ window = 'class:keybinds_test' })"), "ok"); + EXPECT(getFromSocket("/eval hl.bind('SUPER + Y', hl.dsp.send_shortcut({ mods = '', key = 'q', window = 'activewindow' }), { long_press = true })"), "ok"); + EXPECT(getFromSocket("r/eval hl.config({ input = { repeat_delay = 100 } })"), "ok"); + EXPECT(getFromSocket("r/eval hl.config({ input = { repeat_rate = 10 } })"), "ok"); // press keybind - OK(getFromSocket("/dispatch plugin:test:keybind 1,7,29")); + OK(getFromSocket(pluginKeybindCmd(true, 7, 29))); // await repeat delay std::this_thread::sleep_for(std::chrono::milliseconds(200)); - OK(getFromSocket("/dispatch plugin:test:keybind 0,0,29")); + OK(getFromSocket(pluginKeybindCmd(false, 0, 29))); std::this_thread::sleep_for(std::chrono::milliseconds(200)); const std::string output = readKittyOutput(); int yCount = Tests::countOccurrences(output, "y"); @@ -338,54 +340,50 @@ static void testShortcutLongPress() { // final release stop repeats, and shouldn't send any more EXPECT(true, yCount == 1 || yCount == 2); EXPECT_COUNT_STRING(output, "q", 1); - EXPECT(getFromSocket("/keyword unbind SUPER,Y"), "ok"); + EXPECT(getFromSocket("/eval hl.unbind('SUPER + Y')"), "ok"); Tests::killAllWindows(); } -static void testShortcutLongPressKeyRelease() { +SUBTEST(shortcutLongPressKeyRelease) { auto kittyProc = spawnRemoteControlKitty(); if (!kittyProc) { - NLog::log("{}Error: kitty did not spawn", Colors::RED); - ret = 1; - return; + FAIL_TEST("Could not spawn kitty"); } - EXPECT(getFromSocket("/dispatch focuswindow class:keybinds_test"), "ok"); - EXPECT(getFromSocket("/keyword bindo SUPER,Y,sendshortcut,,q,"), "ok"); - EXPECT(getFromSocket("/keyword input:repeat_delay 100"), "ok"); - EXPECT(getFromSocket("/keyword input:repeat_rate 10"), "ok"); + EXPECT(getFromSocket("/dispatch hl.dsp.focus({ window = 'class:keybinds_test' })"), "ok"); + EXPECT(getFromSocket("/eval hl.bind('SUPER + Y', hl.dsp.send_shortcut({ mods = '', key = 'q', window = 'activewindow' }), { long_press = true })"), "ok"); + EXPECT(getFromSocket("r/eval hl.config({ input = { repeat_delay = 100 } })"), "ok"); + EXPECT(getFromSocket("r/eval hl.config({ input = { repeat_rate = 10 } })"), "ok"); // press keybind - OK(getFromSocket("/dispatch plugin:test:keybind 1,7,29")); + OK(getFromSocket(pluginKeybindCmd(true, 7, 29))); std::this_thread::sleep_for(std::chrono::milliseconds(10)); // release key, keep modifier - OK(getFromSocket("/dispatch plugin:test:keybind 0,7,29")); + OK(getFromSocket(pluginKeybindCmd(false, 7, 29))); // await repeat delay std::this_thread::sleep_for(std::chrono::milliseconds(200)); const std::string output = readKittyOutput(); // disabled: doesn't work on CI // EXPECT_COUNT_STRING(output, "y", 1); EXPECT_COUNT_STRING(output, "q", 0); - OK(getFromSocket("/dispatch plugin:test:keybind 0,0,29")); - EXPECT(getFromSocket("/keyword unbind SUPER,Y"), "ok"); + OK(getFromSocket(pluginKeybindCmd(false, 0, 29))); + EXPECT(getFromSocket("/eval hl.unbind('SUPER + Y')"), "ok"); Tests::killAllWindows(); } -static void testShortcutRepeat() { +SUBTEST(shortcutRepeat) { auto kittyProc = spawnRemoteControlKitty(); if (!kittyProc) { - NLog::log("{}Error: kitty did not spawn", Colors::RED); - ret = 1; - return; + FAIL_TEST("Could not spawn kitty"); } - EXPECT(getFromSocket("/dispatch focuswindow class:keybinds_test"), "ok"); - EXPECT(getFromSocket("/keyword binde SUPER,Y,sendshortcut,,q,"), "ok"); - EXPECT(getFromSocket("/keyword input:repeat_rate 5"), "ok"); - EXPECT(getFromSocket("/keyword input:repeat_delay 200"), "ok"); + EXPECT(getFromSocket("/dispatch hl.dsp.focus({ window = 'class:keybinds_test' })"), "ok"); + EXPECT(getFromSocket("/eval hl.bind('SUPER + Y', hl.dsp.send_shortcut({ mods = '', key = 'q', window = 'activewindow' }), { repeating = true })"), "ok"); + EXPECT(getFromSocket("r/eval hl.config({ input = { repeat_rate = 5 } })"), "ok"); + EXPECT(getFromSocket("r/eval hl.config({ input = { repeat_delay = 200 } })"), "ok"); // press keybind - OK(getFromSocket("/dispatch plugin:test:keybind 1,7,29")); + OK(getFromSocket(pluginKeybindCmd(true, 7, 29))); // await repeat std::this_thread::sleep_for(std::chrono::milliseconds(210)); // release keybind - OK(getFromSocket("/dispatch plugin:test:keybind 0,0,29")); + OK(getFromSocket(pluginKeybindCmd(false, 0, 29))); std::this_thread::sleep_for(std::chrono::milliseconds(450)); const std::string output = readKittyOutput(); EXPECT_COUNT_STRING(output, "y", 0); @@ -395,26 +393,24 @@ static void testShortcutRepeat() { // then repeat triggers, sending 1 q // final release stop repeats, and shouldn't send any more EXPECT(true, qCount == 2 || qCount == 3); - EXPECT(getFromSocket("/keyword unbind SUPER,Y"), "ok"); + EXPECT(getFromSocket("/eval hl.unbind('SUPER + Y')"), "ok"); Tests::killAllWindows(); } -static void testShortcutRepeatKeyRelease() { +SUBTEST(shortcutRepeatKeyRelease) { auto kittyProc = spawnRemoteControlKitty(); if (!kittyProc) { - NLog::log("{}Error: kitty did not spawn", Colors::RED); - ret = 1; - return; + FAIL_TEST("Could not spawn kitty"); } - EXPECT(getFromSocket("/dispatch focuswindow class:keybinds_test"), "ok"); - EXPECT(getFromSocket("/keyword binde SUPER,Y,sendshortcut,,q,"), "ok"); - EXPECT(getFromSocket("/keyword input:repeat_rate 5"), "ok"); - EXPECT(getFromSocket("/keyword input:repeat_delay 200"), "ok"); + EXPECT(getFromSocket("/dispatch hl.dsp.focus({ window = 'class:keybinds_test' })"), "ok"); + EXPECT(getFromSocket("/eval hl.bind('SUPER + Y', hl.dsp.send_shortcut({ mods = '', key = 'q', window = 'activewindow' }), { repeating = true })"), "ok"); + EXPECT(getFromSocket("r/eval hl.config({ input = { repeat_rate = 5 } })"), "ok"); + EXPECT(getFromSocket("r/eval hl.config({ input = { repeat_delay = 200 } })"), "ok"); // press keybind - OK(getFromSocket("/dispatch plugin:test:keybind 1,7,29")); + OK(getFromSocket(pluginKeybindCmd(true, 7, 29))); std::this_thread::sleep_for(std::chrono::milliseconds(210)); // release key, keep modifier - OK(getFromSocket("/dispatch plugin:test:keybind 0,7,29")); + OK(getFromSocket(pluginKeybindCmd(false, 7, 29))); // if repeat was still active, we'd get 2 more q's here std::this_thread::sleep_for(std::chrono::milliseconds(450)); // release modifier @@ -426,16 +422,16 @@ static void testShortcutRepeatKeyRelease() { // then repeat triggers, sending 1 q // final release stop repeats, and shouldn't send any more EXPECT(true, qCount == 2 || qCount == 3); - OK(getFromSocket("/dispatch plugin:test:keybind 0,0,29")); - EXPECT(getFromSocket("/keyword unbind SUPER,Y"), "ok"); + OK(getFromSocket(pluginKeybindCmd(false, 0, 29))); + EXPECT(getFromSocket("/eval hl.unbind('SUPER + Y')"), "ok"); Tests::killAllWindows(); } -static void testSubmap() { +SUBTEST(submap) { const auto press = [](const uint32_t key, const uint32_t mod = 0) { // +8 because udev -> XKB keycode. - getFromSocket("/dispatch plugin:test:keybind 1," + std::to_string(mod) + "," + std::to_string(key + 8)); - getFromSocket("/dispatch plugin:test:keybind 0," + std::to_string(mod) + "," + std::to_string(key + 8)); + getFromSocket(pluginKeybindCmd(true, mod, key + 8)); + getFromSocket(pluginKeybindCmd(false, mod, key + 8)); }; NLog::log("{}Testing submaps", Colors::GREEN); @@ -466,119 +462,128 @@ static void testSubmap() { Tests::killAllWindows(); } -static void testBindsAfterScroll() { +SUBTEST(bindsAfterScroll) { NLog::log("{}Testing binds after scroll", Colors::GREEN); clearFlag(); - OK(getFromSocket("/keyword binds Alt_R,w,exec,touch " + flagFile)); + OK(getFromSocket("/eval hl.bind('ALT + w', hl.dsp.exec_cmd('touch " + flagFile + "'))")); // press keybind before scroll - OK(getFromSocket("/dispatch plugin:test:keybind 1,0,108")); // Alt_R press - OK(getFromSocket("/dispatch plugin:test:keybind 1,4,25")); // w press + OK(getFromSocket(pluginKeybindCmd(true, 0, 108))); // Alt_R press + OK(getFromSocket(pluginKeybindCmd(true, 4, 25))); // w press EXPECT(attemptCheckFlag(20, 50), true); - OK(getFromSocket("/dispatch plugin:test:keybind 0,4,25")); // w release - OK(getFromSocket("/dispatch plugin:test:keybind 0,0,108")); // Alt_R release + OK(getFromSocket(pluginKeybindCmd(false, 4, 25))); // w release + OK(getFromSocket(pluginKeybindCmd(false, 0, 108))); // Alt_R release // scroll - OK(getFromSocket("/dispatch plugin:test:scroll 120")); - OK(getFromSocket("/dispatch plugin:test:scroll -120")); - OK(getFromSocket("/dispatch plugin:test:scroll 120")); + OK(getFromSocket(pluginScrollCmd(120))); + OK(getFromSocket(pluginScrollCmd(-120))); + OK(getFromSocket(pluginScrollCmd(120))); // press keybind after scroll - OK(getFromSocket("/dispatch plugin:test:keybind 1,0,108")); // Alt_R press - OK(getFromSocket("/dispatch plugin:test:keybind 1,4,25")); // w press + OK(getFromSocket(pluginKeybindCmd(true, 0, 108))); // Alt_R press + OK(getFromSocket(pluginKeybindCmd(true, 4, 25))); // w press EXPECT(attemptCheckFlag(20, 50), true); - OK(getFromSocket("/dispatch plugin:test:keybind 0,4,25")); // w release - OK(getFromSocket("/dispatch plugin:test:keybind 0,0,108")); // Alt_R release + OK(getFromSocket(pluginKeybindCmd(false, 4, 25))); // w release + OK(getFromSocket(pluginKeybindCmd(false, 0, 108))); // Alt_R release clearFlag(); - OK(getFromSocket("/keyword unbind Alt_R,w")); + OK(getFromSocket("/eval hl.unbind('ALT + w')")); } -static void testSubmapUniversal() { +SUBTEST(submapUniversal) { NLog::log("{}Testing submap universal", Colors::GREEN); EXPECT(checkFlag(), false); - EXPECT(getFromSocket("/keyword bindu SUPER,Y,exec,touch " + flagFile), "ok"); + EXPECT(getFromSocket("/eval hl.bind('SUPER + Y', hl.dsp.exec_cmd('touch " + flagFile + "'), { submap_universal = true })"), "ok"); EXPECT_CONTAINS(getFromSocket("/submap"), "default"); // keybind works on default submap - OK(getFromSocket("/dispatch plugin:test:keybind 1,7,29")); - OK(getFromSocket("/dispatch plugin:test:keybind 0,7,29")); + OK(getFromSocket(pluginKeybindCmd(true, 7, 29))); + OK(getFromSocket(pluginKeybindCmd(false, 7, 29))); EXPECT(attemptCheckFlag(30, 5), true); // keybind works on submap1 - getFromSocket("/dispatch plugin:test:keybind 1,7,30"); - getFromSocket("/dispatch plugin:test:keybind 0,7,30"); + getFromSocket(pluginKeybindCmd(true, 7, 30)); + getFromSocket(pluginKeybindCmd(false, 7, 30)); EXPECT_CONTAINS(getFromSocket("/submap"), "submap1"); - OK(getFromSocket("/dispatch plugin:test:keybind 1,7,29")); - OK(getFromSocket("/dispatch plugin:test:keybind 0,7,29")); + OK(getFromSocket(pluginKeybindCmd(true, 7, 29))); + OK(getFromSocket(pluginKeybindCmd(false, 7, 29))); EXPECT(attemptCheckFlag(30, 5), true); // reset to default submap - getFromSocket("/dispatch plugin:test:keybind 1,0,33"); - getFromSocket("/dispatch plugin:test:keybind 0,0,33"); + getFromSocket(pluginKeybindCmd(true, 0, 33)); + getFromSocket(pluginKeybindCmd(false, 0, 33)); EXPECT_CONTAINS(getFromSocket("/submap"), "default"); - EXPECT(getFromSocket("/keyword unbind SUPER,Y"), "ok"); + EXPECT(getFromSocket("/eval hl.unbind('SUPER + Y')"), "ok"); } -static void testPerDeviceKeybind() { +SUBTEST(perDeviceKeybind) { NLog::log("{}Testing per-device binds", Colors::GREEN); // Inclusive EXPECT(checkFlag(), false); - EXPECT(getFromSocket("/keyword bindk SUPER,Y,test-keyboard-1,exec,touch " + flagFile), "ok"); - OK(getFromSocket("/dispatch plugin:test:keybind 1,7,29")); + EXPECT(getFromSocket("/eval hl.bind('SUPER + Y', hl.dsp.exec_cmd('touch " + flagFile + "'), { device = { inclusive = true, list = { 'test-keyboard-1' } } })"), "ok"); + OK(getFromSocket(pluginKeybindCmd(true, 7, 29))); EXPECT(attemptCheckFlag(20, 50), true); - OK(getFromSocket("/dispatch plugin:test:keybind 0,0,29")); - EXPECT(getFromSocket("/keyword unbind SUPER,Y"), "ok"); + OK(getFromSocket(pluginKeybindCmd(false, 0, 29))); + EXPECT(getFromSocket("/eval hl.unbind('SUPER + Y')"), "ok"); // Exclusive EXPECT(checkFlag(), false); - EXPECT(getFromSocket("/keyword bindk SUPER,Y,!test-keyboard-1,exec,touch " + flagFile), "ok"); - OK(getFromSocket("/dispatch plugin:test:keybind 1,7,29")); + EXPECT(getFromSocket("/eval hl.bind('SUPER + Y', hl.dsp.exec_cmd('touch " + flagFile + "'), { device = { inclusive = false, list = { 'test-keyboard-1' } } })"), "ok"); + OK(getFromSocket(pluginKeybindCmd(true, 7, 29))); EXPECT(attemptCheckFlag(20, 50), false); - OK(getFromSocket("/dispatch plugin:test:keybind 0,0,29")); - EXPECT(getFromSocket("/keyword unbind SUPER,Y"), "ok"); + OK(getFromSocket(pluginKeybindCmd(false, 0, 29))); + EXPECT(getFromSocket("/eval hl.unbind('SUPER + Y')"), "ok"); // With description EXPECT(checkFlag(), false); - EXPECT(getFromSocket("/keyword binddk SUPER,Y,test-keyboard-1,test description,exec,touch " + flagFile), "ok"); - OK(getFromSocket("/dispatch plugin:test:keybind 1,7,29")); + EXPECT(getFromSocket("/eval hl.bind('SUPER + Y', hl.dsp.exec_cmd('touch " + flagFile + + "'), { description = 'test description', device = { inclusive = true, list = { 'test-keyboard-1' } } })"), + "ok"); + OK(getFromSocket(pluginKeybindCmd(true, 7, 29))); EXPECT(attemptCheckFlag(20, 50), true); - OK(getFromSocket("/dispatch plugin:test:keybind 0,0,29")); - EXPECT(getFromSocket("/keyword unbind SUPER,Y"), "ok"); + OK(getFromSocket(pluginKeybindCmd(false, 0, 29))); + EXPECT(getFromSocket("/eval hl.unbind('SUPER + Y')"), "ok"); } -static bool test() { - NLog::log("{}Testing keybinds", Colors::GREEN); +SUBTEST(unbind) { + NLog::log("{}Testing unbind behavior", Colors::GREEN); - clearFlag(); + // unbind should normalize the string: no spaces, lowercase OK + EXPECT(checkFlag(), false); + EXPECT(getFromSocket("/eval hl.bind('SUPER + Y', hl.dsp.exec_cmd('touch " + flagFile + "'), { device = { inclusive = true, list = { 'test-keyboard-1' } } })"), "ok"); + EXPECT(getFromSocket("/eval hl.unbind(' super + y ')"), "ok"); - testBind(); - testBindKey(); - testLongPress(); - testKeyLongPress(); - testLongPressRelease(); - testLongPressOnlyKeyRelease(); - testRepeat(); - testKeyRepeat(); - testRepeatRelease(); - testRepeatOnlyKeyRelease(); - testShortcutBind(); - testShortcutBindKey(); - testShortcutLongPress(); - testShortcutLongPressKeyRelease(); - testShortcutRepeat(); - testShortcutRepeatKeyRelease(); - testSubmap(); - testSubmapUniversal(); - testBindsAfterScroll(); - testPerDeviceKeybind(); + OK(getFromSocket(pluginKeybindCmd(true, 7, 29))); + OK(getFromSocket(pluginKeybindCmd(false, 0, 29))); - clearFlag(); - return !ret; + EXPECT(attemptCheckFlag(20, 50), false); } -REGISTER_TEST_FN(test) +// TODO: remove this test after subtests above are properly isolated into independent tests +TEST_CASE(keybinds) { + CALL_SUBTEST(bind); + CALL_SUBTEST(bindKey); + CALL_SUBTEST(longPress); + CALL_SUBTEST(keyLongPress); + CALL_SUBTEST(longPressRelease); + CALL_SUBTEST(longPressOnlyKeyRelease); + CALL_SUBTEST(repeat); + CALL_SUBTEST(keyRepeat); + CALL_SUBTEST(repeatRelease); + CALL_SUBTEST(repeatOnlyKeyRelease); + CALL_SUBTEST(shortcutBind); + CALL_SUBTEST(shortcutBindKey); + CALL_SUBTEST(shortcutLongPress); + CALL_SUBTEST(shortcutLongPressKeyRelease); + CALL_SUBTEST(shortcutRepeat); + CALL_SUBTEST(shortcutRepeatKeyRelease); + CALL_SUBTEST(submap); + CALL_SUBTEST(submapUniversal); + CALL_SUBTEST(bindsAfterScroll); + CALL_SUBTEST(perDeviceKeybind); + CALL_SUBTEST(unbind); +} diff --git a/hyprtester/src/tests/main/layer.cpp b/hyprtester/src/tests/main/layer.cpp index 73e30ba67..f39c007c6 100644 --- a/hyprtester/src/tests/main/layer.cpp +++ b/hyprtester/src/tests/main/layer.cpp @@ -6,8 +6,6 @@ #include #include -static int ret = 0; - using namespace Hyprutils::OS; using namespace Hyprutils::Memory; @@ -20,34 +18,18 @@ static bool spawnLayer(const std::string& namespace_) { return true; } -static bool test() { - NLog::log("{}Testing plugin layerrules", Colors::GREEN); +TEST_CASE(plugin_layerrules) { - if (!spawnLayer("rule-layer")) - return false; + EXPECT(spawnLayer("rule-layer"), true); - OK(getFromSocket("/dispatch plugin:test:add_layer_rule")); + OK(getFromSocket("/eval hl.plugin.test.add_layer_rule()")); OK(getFromSocket("/reload")); - OK(getFromSocket("/keyword layerrule match:namespace rule-layer, plugin_rule effect")); + OK(getFromSocket("/eval hl.layer_rule({ match = { namespace = 'rule-layer' }, plugin_rule = 'effect' })")); - if (!spawnLayer("rule-layer")) - return false; + EXPECT(spawnLayer("rule-layer"), true); - if (!spawnLayer("norule-layer")) - return false; + EXPECT(spawnLayer("norule-layer"), true); - OK(getFromSocket("/dispatch plugin:test:check_layer_rule")); - - OK(getFromSocket("/reload")); - - NLog::log("{}Killing all layers", Colors::YELLOW); - Tests::killAllLayers(); - - NLog::log("{}Expecting 0 layers", Colors::YELLOW); - EXPECT(Tests::layerCount(), 0); - - return !ret; + OK(getFromSocket("/eval hl.plugin.test.check_layer_rule()")); } - -REGISTER_TEST_FN(test) diff --git a/hyprtester/src/tests/main/layout.cpp b/hyprtester/src/tests/main/layout.cpp index 208bee30c..4d604fa45 100644 --- a/hyprtester/src/tests/main/layout.cpp +++ b/hyprtester/src/tests/main/layout.cpp @@ -3,10 +3,8 @@ #include "../../hyprctlCompat.hpp" #include "tests.hpp" -static int ret = 0; - -static void swar() { - OK(getFromSocket("/keyword layout:single_window_aspect_ratio 1 1")); +TEST_CASE(single_window_aspect_ratio) { + OK(getFromSocket("/eval hl.config({ layout = { single_window_aspect_ratio = '1 1' } })")); Tests::spawnKitty(); @@ -18,7 +16,7 @@ static void swar() { Tests::spawnKitty(); - OK(getFromSocket("/dispatch killwindow activewindow")); + OK(getFromSocket("/dispatch hl.dsp.window.kill({ window = 'activewindow' })")); Tests::waitUntilWindowsN(1); @@ -29,44 +27,35 @@ static void swar() { } // don't use swar on maximized - OK(getFromSocket("/dispatch fullscreen 1")); + OK(getFromSocket("/dispatch hl.dsp.window.fullscreen({ mode = 'maximized' })")); { auto str = getFromSocket("/activewindow"); EXPECT_CONTAINS(str, "at: 22,22"); EXPECT_CONTAINS(str, "size: 1876,1036"); } - - // clean up - NLog::log("{}Killing all windows", Colors::YELLOW); - Tests::killAllWindows(); } // Don't crash when focus after global geometry changes -static void testCrashOnGeomUpdate() { +TEST_CASE(crashOnGeomUpdate) { Tests::spawnKitty(); Tests::spawnKitty(); Tests::spawnKitty(); // move the layout - OK(getFromSocket("/keyword monitor HEADLESS-2,1920x1080@60,1000x0,1")); + OK(getFromSocket("/eval hl.monitor({ output = 'HEADLESS-2', mode = '1920x1080@60', position = '1000x0', scale = '1' })")); // shouldnt crash - OK(getFromSocket("/dispatch movefocus r")); - - OK(getFromSocket("/reload")); - - NLog::log("{}Killing all windows", Colors::YELLOW); - Tests::killAllWindows(); + OK(getFromSocket("/dispatch hl.dsp.focus({ direction = 'right' })")); } // Test if size + pos is preserved after fs cycle -static void testPosPreserve() { +TEST_CASE(posPreserve) { Tests::spawnKitty(); - OK(getFromSocket("/dispatch setfloating class:kitty")); - OK(getFromSocket("/dispatch resizewindowpixel exact 1337 69, class:kitty")); - OK(getFromSocket("/dispatch movewindowpixel exact 420 420, class:kitty")); + OK(getFromSocket("/dispatch hl.dsp.window.float({ action = 'set', window = 'class:kitty' })")); + OK(getFromSocket("/dispatch hl.dsp.window.resize({ x = 1337, y = 69, window = 'class:kitty' })")); + OK(getFromSocket("/dispatch hl.dsp.window.move({ x = 420, y = 420, window = 'class:kitty' })")); { auto str = getFromSocket("/activewindow"); @@ -74,15 +63,15 @@ static void testPosPreserve() { EXPECT_CONTAINS(str, "size: 1337,69"); } - OK(getFromSocket("/dispatch fullscreen")); - OK(getFromSocket("/dispatch fullscreen")); + OK(getFromSocket("/dispatch hl.dsp.window.fullscreen()")); + OK(getFromSocket("/dispatch hl.dsp.window.fullscreen()")); { auto str = getFromSocket("/activewindow"); EXPECT_CONTAINS(str, "size: 1337,69"); } - OK(getFromSocket("/dispatch movewindow r")); + OK(getFromSocket("/dispatch hl.dsp.window.move({ direction = 'right' })")); { auto str = getFromSocket("/activewindow"); @@ -90,35 +79,32 @@ static void testPosPreserve() { EXPECT_CONTAINS(str, "size: 1337,69"); } - OK(getFromSocket("/dispatch fullscreen")); - OK(getFromSocket("/dispatch fullscreen")); + OK(getFromSocket("/dispatch hl.dsp.window.fullscreen()")); + OK(getFromSocket("/dispatch hl.dsp.window.fullscreen()")); { auto str = getFromSocket("/activewindow"); EXPECT_CONTAINS(str, "at: 581,420"); EXPECT_CONTAINS(str, "size: 1337,69"); } - - NLog::log("{}Killing all windows", Colors::YELLOW); - Tests::killAllWindows(); } -static bool testFocusMRUAfterClose() { +TEST_CASE(focusMRUAfterClose) { NLog::log("{}Testing focus after close (MRU order)", Colors::GREEN); OK(getFromSocket("/reload")); - OK(getFromSocket("/keyword dwindle:default_split_ratio 1.25")); - OK(getFromSocket("/keyword input:focus_on_close 2")); + OK(getFromSocket("/eval hl.config({ dwindle = { default_split_ratio = 1.25 } })")); + OK(getFromSocket("/eval hl.config({ input = { focus_on_close = 2 } })")); - EXPECT(!!Tests::spawnKitty("kitty_A"), true); - EXPECT(!!Tests::spawnKitty("kitty_B"), true); - EXPECT(!!Tests::spawnKitty("kitty_C"), true); + ASSERT(!!Tests::spawnKitty("kitty_A"), true); + ASSERT(!!Tests::spawnKitty("kitty_B"), true); + ASSERT(!!Tests::spawnKitty("kitty_C"), true); - OK(getFromSocket("/dispatch focuswindow class:kitty_A")); - OK(getFromSocket("/dispatch focuswindow class:kitty_B")); - OK(getFromSocket("/dispatch focuswindow class:kitty_C")); + OK(getFromSocket("/dispatch hl.dsp.focus({ window = 'class:kitty_A' })")); + OK(getFromSocket("/dispatch hl.dsp.focus({ window = 'class:kitty_B' })")); + OK(getFromSocket("/dispatch hl.dsp.focus({ window = 'class:kitty_C' })")); - OK(getFromSocket("/dispatch killactive")); + OK(getFromSocket("/dispatch hl.dsp.window.kill()")); Tests::waitUntilWindowsN(2); { @@ -126,66 +112,29 @@ static bool testFocusMRUAfterClose() { EXPECT(str.contains("class: kitty_B"), true); } - OK(getFromSocket("/dispatch killactive")); + OK(getFromSocket("/dispatch hl.dsp.window.kill()")); Tests::waitUntilWindowsN(1); { auto str = getFromSocket("/activewindow"); EXPECT(str.contains("class: kitty_A"), true); } - - NLog::log("{}Killing all windows", Colors::YELLOW); - Tests::killAllWindows(); - EXPECT(Tests::windowCount(), 0); - return true; } -static bool testFocusPreservedLayoutChange() { - NLog::log("{}Testing focus is preserved on layout change", Colors::GREEN); +TEST_CASE(focusPreservedLayoutChange) { + OK(getFromSocket("r/eval hl.config({ general = { layout = 'master' } })")); - OK(getFromSocket("/keyword general:layout master")); + ASSERT(!!Tests::spawnKitty("kitty_A"), true); + ASSERT(!!Tests::spawnKitty("kitty_B"), true); + ASSERT(!!Tests::spawnKitty("kitty_C"), true); + ASSERT(!!Tests::spawnKitty("kitty_D"), true); - EXPECT(!!Tests::spawnKitty("kitty_A"), true); - EXPECT(!!Tests::spawnKitty("kitty_B"), true); - EXPECT(!!Tests::spawnKitty("kitty_C"), true); - EXPECT(!!Tests::spawnKitty("kitty_D"), true); + OK(getFromSocket("/dispatch hl.dsp.focus({ window = 'class:kitty_C' })")); - OK(getFromSocket("/dispatch focuswindow class:kitty_C")); - - OK(getFromSocket("/keyword general:layout monocle")); + OK(getFromSocket("r/eval hl.config({ general = { layout = 'monocle' } })")); { auto str = getFromSocket("/activewindow"); EXPECT(str.contains("class: kitty_C"), true); } - - NLog::log("{}Killing all windows", Colors::YELLOW); - Tests::killAllWindows(); - EXPECT(Tests::windowCount(), 0); - return true; } - -static bool test() { - NLog::log("{}Testing layout generic", Colors::GREEN); - - // setup - OK(getFromSocket("/dispatch workspace 10")); - - // test - NLog::log("{}Testing `single_window_aspect_ratio`", Colors::GREEN); - swar(); - - testCrashOnGeomUpdate(); - testPosPreserve(); - testFocusMRUAfterClose(); - testFocusPreservedLayoutChange(); - - // clean up - NLog::log("Cleaning up", Colors::YELLOW); - OK(getFromSocket("/dispatch workspace 1")); - OK(getFromSocket("/reload")); - - return !ret; -} - -REGISTER_TEST_FN(test); diff --git a/hyprtester/src/tests/main/master.cpp b/hyprtester/src/tests/main/master.cpp index 9aaa4cf0f..b9364c455 100644 --- a/hyprtester/src/tests/main/master.cpp +++ b/hyprtester/src/tests/main/master.cpp @@ -3,11 +3,45 @@ #include "../../hyprctlCompat.hpp" #include "tests.hpp" -static int ret = 0; +TEST_CASE(focusMasterPrevious) { + OK(getFromSocket("r/eval hl.config({ general = { layout = 'master' } })")); -// reqs 1 master 3 slaves -static void testOrientations() { - OK(getFromSocket("/keyword master:orientation top")); + // setup + NLog::log("{}Spawning 1 master and 3 slave windows", Colors::YELLOW); + // order of windows set according to new_status = master (set in test.lua) + for (auto const& win : {"slave1", "slave2", "slave3", "master"}) { + if (!Tests::spawnKitty(win)) { + FAIL_TEST("Could not spawn kitty with win class `{}`", win); + } + } + NLog::log("{}Ensuring focus is on master before testing", Colors::YELLOW); + OK(getFromSocket("/dispatch hl.dsp.layout('focusmaster master')")); + ASSERT_CONTAINS(getFromSocket("/activewindow"), "class: master"); + + // test + NLog::log("{}Testing fallback to focusmaster auto", Colors::YELLOW); + + OK(getFromSocket("/dispatch hl.dsp.layout('focusmaster previous')")); + ASSERT_CONTAINS(getFromSocket("/activewindow"), "class: slave1"); + + NLog::log("{}Testing focusing from slave to master", Colors::YELLOW); + + OK(getFromSocket("/dispatch hl.dsp.layout('cyclenext noloop')")); + ASSERT_CONTAINS(getFromSocket("/activewindow"), "class: slave2"); + OK(getFromSocket("/dispatch hl.dsp.layout('focusmaster previous')")); + ASSERT_CONTAINS(getFromSocket("/activewindow"), "class: master"); + + NLog::log("{}Testing focusing on previous window", Colors::YELLOW); + + OK(getFromSocket("/dispatch hl.dsp.layout('focusmaster previous')")); + ASSERT_CONTAINS(getFromSocket("/activewindow"), "class: slave2"); + + NLog::log("{}Testing focusing back to master", Colors::YELLOW); + + OK(getFromSocket("/dispatch hl.dsp.layout('focusmaster previous')")); + ASSERT_CONTAINS(getFromSocket("/activewindow"), "class: master"); + + OK(getFromSocket("r/eval hl.config({ master = { orientation = 'top' } })")); // top { @@ -19,7 +53,7 @@ static void testOrientations() { // cycle = top, right, bottom, center, left // right - OK(getFromSocket("/dispatch layoutmsg orientationnext")); + OK(getFromSocket("/dispatch hl.dsp.layout('orientationnext')")); { auto str = getFromSocket("/activewindow"); EXPECT_CONTAINS(str, "at: 873,22"); @@ -27,7 +61,7 @@ static void testOrientations() { } // bottom - OK(getFromSocket("/dispatch layoutmsg orientationnext")); + OK(getFromSocket("/dispatch hl.dsp.layout('orientationnext')")); { auto str = getFromSocket("/activewindow"); EXPECT_CONTAINS(str, "at: 22,495"); @@ -35,7 +69,7 @@ static void testOrientations() { } // center - OK(getFromSocket("/dispatch layoutmsg orientationnext")); + OK(getFromSocket("/dispatch hl.dsp.layout('orientationnext')")); { auto str = getFromSocket("/activewindow"); EXPECT_CONTAINS(str, "at: 450,22"); @@ -43,7 +77,7 @@ static void testOrientations() { } // left - OK(getFromSocket("/dispatch layoutmsg orientationnext")); + OK(getFromSocket("/dispatch hl.dsp.layout('orientationnext')")); { auto str = getFromSocket("/activewindow"); EXPECT_CONTAINS(str, "at: 22,22"); @@ -51,67 +85,20 @@ static void testOrientations() { } } -static void focusMasterPrevious() { - // setup - NLog::log("{}Spawning 1 master and 3 slave windows", Colors::YELLOW); - // order of windows set according to new_status = master (set in test.conf) - for (auto const& win : {"slave1", "slave2", "slave3", "master"}) { - if (!Tests::spawnKitty(win)) { - NLog::log("{}Failed to spawn kitty with win class `{}`", Colors::RED, win); - ++TESTS_FAILED; - ret = 1; - return; - } - } - NLog::log("{}Ensuring focus is on master before testing", Colors::YELLOW); - OK(getFromSocket("/dispatch layoutmsg focusmaster master")); - EXPECT_CONTAINS(getFromSocket("/activewindow"), "class: master"); - - // test - NLog::log("{}Testing fallback to focusmaster auto", Colors::YELLOW); - - OK(getFromSocket("/dispatch layoutmsg focusmaster previous")); - EXPECT_CONTAINS(getFromSocket("/activewindow"), "class: slave1"); - - NLog::log("{}Testing focusing from slave to master", Colors::YELLOW); - - OK(getFromSocket("/dispatch layoutmsg cyclenext noloop")); - EXPECT_CONTAINS(getFromSocket("/activewindow"), "class: slave2"); - OK(getFromSocket("/dispatch layoutmsg focusmaster previous")); - EXPECT_CONTAINS(getFromSocket("/activewindow"), "class: master"); - - NLog::log("{}Testing focusing on previous window", Colors::YELLOW); - - OK(getFromSocket("/dispatch layoutmsg focusmaster previous")); - EXPECT_CONTAINS(getFromSocket("/activewindow"), "class: slave2"); - - NLog::log("{}Testing focusing back to master", Colors::YELLOW); - - OK(getFromSocket("/dispatch layoutmsg focusmaster previous")); - EXPECT_CONTAINS(getFromSocket("/activewindow"), "class: master"); - - testOrientations(); - - // clean up - NLog::log("{}Killing all windows", Colors::YELLOW); - Tests::killAllWindows(); -} - -static void testFsBehavior() { +TEST_CASE(fsBehavior) { // Master will re-send data to fullscreen / maximized windows, which can interfere with misc:on_focus_under_fullscreen // check that it doesn't. + OK(getFromSocket("r/eval hl.config({ general = { layout = 'master' } })")); + for (auto const& win : {"master", "slave1", "slave2"}) { if (!Tests::spawnKitty(win)) { - NLog::log("{}Failed to spawn kitty with win class `{}`", Colors::RED, win); - ++TESTS_FAILED; - ret = 1; - return; + FAIL_TEST("Could not spawn kitty with win class `{}`", win); } } - OK(getFromSocket("/dispatch focuswindow class:master")); - OK(getFromSocket("/dispatch fullscreen 1")); + OK(getFromSocket("/dispatch hl.dsp.focus({ window = 'class:master' })")); + OK(getFromSocket("/dispatch hl.dsp.window.fullscreen({ mode = 'maximized' })")); { auto str = getFromSocket("/activewindow"); @@ -120,7 +107,7 @@ static void testFsBehavior() { EXPECT_CONTAINS(str, "class: master"); } - OK(getFromSocket("/keyword misc:on_focus_under_fullscreen 1")); + OK(getFromSocket("/eval hl.config({ misc = { on_focus_under_fullscreen = 1 } })")); Tests::spawnKitty("new_master"); @@ -132,7 +119,7 @@ static void testFsBehavior() { EXPECT_CONTAINS(str, "fullscreen: 1"); } - OK(getFromSocket("/keyword misc:on_focus_under_fullscreen 0")); + OK(getFromSocket("/eval hl.config({ misc = { on_focus_under_fullscreen = 0 } })")); Tests::spawnKitty("ignored"); @@ -144,7 +131,7 @@ static void testFsBehavior() { EXPECT_CONTAINS(str, "fullscreen: 1"); } - OK(getFromSocket("/keyword misc:on_focus_under_fullscreen 2")); + OK(getFromSocket("/eval hl.config({ misc = { on_focus_under_fullscreen = 2 } })")); Tests::spawnKitty("vaxwashere"); @@ -153,31 +140,4 @@ static void testFsBehavior() { EXPECT_CONTAINS(str, "class: vaxwashere"); EXPECT_CONTAINS(str, "fullscreen: 0"); } - - NLog::log("{}Killing all windows", Colors::YELLOW); - Tests::killAllWindows(); } - -static bool test() { - NLog::log("{}Testing Master layout", Colors::GREEN); - - // setup - OK(getFromSocket("/dispatch workspace name:master")); - OK(getFromSocket("/keyword general:layout master")); - - // test - NLog::log("{}Testing `focusmaster previous` layoutmsg", Colors::GREEN); - focusMasterPrevious(); - - NLog::log("{}Testing fs behavior", Colors::GREEN); - testFsBehavior(); - - // clean up - NLog::log("Cleaning up", Colors::YELLOW); - OK(getFromSocket("/dispatch workspace 1")); - OK(getFromSocket("/reload")); - - return !ret; -} - -REGISTER_TEST_FN(test); diff --git a/hyprtester/src/tests/main/misc.cpp b/hyprtester/src/tests/main/misc.cpp index 0c7c9ec13..24cc40974 100644 --- a/hyprtester/src/tests/main/misc.cpp +++ b/hyprtester/src/tests/main/misc.cpp @@ -10,8 +10,6 @@ #include #include "../shared.hpp" -static int ret = 0; - using namespace Hyprutils::OS; using namespace Hyprutils::Memory; @@ -22,12 +20,12 @@ using namespace Hyprutils::Memory; // static void testAnrDialogs() { // NLog::log("{}Testing ANR dialogs", Colors::YELLOW); // -// OK(getFromSocket("/keyword misc:enable_anr_dialog true")); -// OK(getFromSocket("/keyword misc:anr_missed_pings 1")); +// OK(getFromSocket("/eval hl.config({ misc = { enable_anr_dialog = true } })")); +// OK(getFromSocket("/eval hl.config({ misc = { anr_missed_pings = 1 } })")); // // NLog::log("{}ANR dialog: regular workspaces", Colors::YELLOW); // { -// OK(getFromSocket("/dispatch workspace 2")); +// OK(getFromSocket("/dispatch hl.dsp.focus({ workspace = '2' })")); // // auto kitty = Tests::spawnKitty("bad_kitty"); // @@ -38,23 +36,23 @@ using namespace Hyprutils::Memory; // // { // auto str = getFromSocket("/activewindow"); -// EXPECT_CONTAINS(str, "workspace: 2"); +// ASSERT_CONTAINS(str, "workspace: 2"); // } // -// OK(getFromSocket("/dispatch workspace 1")); +// OK(getFromSocket("/dispatch hl.dsp.focus({ workspace = '1' })")); // // ::kill(kitty->pid(), SIGSTOP); // Tests::waitUntilWindowsN(2); // // { // auto str = getFromSocket("/activeworkspace"); -// EXPECT_CONTAINS(str, "windows: 0"); +// ASSERT_CONTAINS(str, "windows: 0"); // } // // { -// OK(getFromSocket("/dispatch focuswindow class:hyprland-dialog")) +// OK(getFromSocket("/dispatch hl.dsp.focus({ window = 'class:hyprland-dialog' })")) // auto str = getFromSocket("/activewindow"); -// EXPECT_CONTAINS(str, "workspace: 2"); +// ASSERT_CONTAINS(str, "workspace: 2"); // } // } // @@ -62,7 +60,7 @@ using namespace Hyprutils::Memory; // // NLog::log("{}ANR dialog: named workspaces", Colors::YELLOW); // { -// OK(getFromSocket("/dispatch workspace name:yummy")); +// OK(getFromSocket("/dispatch hl.dsp.focus({ workspace = 'name:yummy' })")); // // auto kitty = Tests::spawnKitty("bad_kitty"); // @@ -73,23 +71,23 @@ using namespace Hyprutils::Memory; // // { // auto str = getFromSocket("/activewindow"); -// EXPECT_CONTAINS(str, "yummy"); +// ASSERT_CONTAINS(str, "yummy"); // } // -// OK(getFromSocket("/dispatch workspace 1")); +// OK(getFromSocket("/dispatch hl.dsp.focus({ workspace = '1' })")); // // ::kill(kitty->pid(), SIGSTOP); // Tests::waitUntilWindowsN(2); // // { // auto str = getFromSocket("/activeworkspace"); -// EXPECT_CONTAINS(str, "windows: 0"); +// ASSERT_CONTAINS(str, "windows: 0"); // } // // { -// OK(getFromSocket("/dispatch focuswindow class:hyprland-dialog")) +// OK(getFromSocket("/dispatch hl.dsp.focus({ window = 'class:hyprland-dialog' })")) // auto str = getFromSocket("/activewindow"); -// EXPECT_CONTAINS(str, "yummy"); +// ASSERT_CONTAINS(str, "yummy"); // } // } // @@ -97,7 +95,7 @@ using namespace Hyprutils::Memory; // // NLog::log("{}ANR dialog: special workspaces", Colors::YELLOW); // { -// OK(getFromSocket("/dispatch workspace special:apple")); +// OK(getFromSocket("/dispatch hl.dsp.focus({ workspace = 'special:apple' })")); // // auto kitty = Tests::spawnKitty("bad_kitty"); // @@ -108,24 +106,24 @@ using namespace Hyprutils::Memory; // // { // auto str = getFromSocket("/activewindow"); -// EXPECT_CONTAINS(str, "special:apple"); +// ASSERT_CONTAINS(str, "special:apple"); // } // -// OK(getFromSocket("/dispatch togglespecialworkspace apple")); -// OK(getFromSocket("/dispatch workspace 1")); +// OK(getFromSocket("/dispatch hl.dsp.workspace.toggle_special('apple')")); +// OK(getFromSocket("/dispatch hl.dsp.focus({ workspace = '1' })")); // // ::kill(kitty->pid(), SIGSTOP); // Tests::waitUntilWindowsN(2); // // { // auto str = getFromSocket("/activeworkspace"); -// EXPECT_CONTAINS(str, "windows: 0"); +// ASSERT_CONTAINS(str, "windows: 0"); // } // // { -// OK(getFromSocket("/dispatch focuswindow class:hyprland-dialog")) +// OK(getFromSocket("/dispatch hl.dsp.focus({ window = 'class:hyprland-dialog' })")) // auto str = getFromSocket("/activewindow"); -// EXPECT_CONTAINS(str, "special:apple"); +// ASSERT_CONTAINS(str, "special:apple"); // } // } // @@ -133,46 +131,45 @@ using namespace Hyprutils::Memory; // Tests::killAllWindows(); // } -static bool test() { - NLog::log("{}Testing config: misc:", Colors::GREEN); - +// TODO: decompose this into multiple test cases +TEST_CASE(misc) { NLog::log("{}Testing close_special_on_empty", Colors::YELLOW); - OK(getFromSocket("/keyword misc:close_special_on_empty false")); - OK(getFromSocket("/dispatch workspace special:test")); + OK(getFromSocket("/eval hl.config({ misc = { close_special_on_empty = false } })")); + OK(getFromSocket("/dispatch hl.dsp.focus({ workspace = 'special:test' })")); Tests::spawnKitty(); { auto str = getFromSocket("/monitors"); - EXPECT_CONTAINS(str, "special workspace: -"); + ASSERT_CONTAINS(str, "special workspace: -"); } Tests::killAllWindows(); { auto str = getFromSocket("/monitors"); - EXPECT_CONTAINS(str, "special workspace: -"); + ASSERT_CONTAINS(str, "special workspace: -"); } Tests::spawnKitty(); - OK(getFromSocket("/keyword misc:close_special_on_empty true")); + OK(getFromSocket("/eval hl.config({ misc = { close_special_on_empty = true } })")); Tests::killAllWindows(); { auto str = getFromSocket("/monitors"); - EXPECT_NOT_CONTAINS(str, "special workspace: -"); + ASSERT_NOT_CONTAINS(str, "special workspace: -"); } NLog::log("{}Testing new_window_takes_over_fullscreen", Colors::YELLOW); - OK(getFromSocket("/keyword misc:on_focus_under_fullscreen 0")); + OK(getFromSocket("/eval hl.config({ misc = { on_focus_under_fullscreen = 0 } })")); Tests::spawnKitty("kitty_A"); - OK(getFromSocket("/dispatch fullscreen 0")); + OK(getFromSocket("/dispatch hl.dsp.window.fullscreen({ mode = 'fullscreen' })")); { auto str = getFromSocket("/activewindow"); @@ -190,7 +187,7 @@ static bool test() { EXPECT_CONTAINS(str, "kitty_A"); } - OK(getFromSocket("/dispatch focuswindow class:kitty_B")); + OK(getFromSocket("/dispatch hl.dsp.focus({ window = 'class:kitty_B' })")); { // should be ignored as per focus_under_fullscreen 0 @@ -200,7 +197,7 @@ static bool test() { EXPECT_CONTAINS(str, "kitty_A"); } - OK(getFromSocket("/keyword misc:on_focus_under_fullscreen 1")); + OK(getFromSocket("/eval hl.config({ misc = { on_focus_under_fullscreen = 1 } })")); Tests::spawnKitty("kitty_C"); @@ -211,7 +208,7 @@ static bool test() { EXPECT_CONTAINS(str, "kitty_C"); } - OK(getFromSocket("/keyword misc:on_focus_under_fullscreen 2")); + OK(getFromSocket("/eval hl.config({ misc = { on_focus_under_fullscreen = 2 } })")); Tests::spawnKitty("kitty_D"); @@ -222,18 +219,18 @@ static bool test() { EXPECT_CONTAINS(str, "kitty_D"); } - OK(getFromSocket("/keyword misc:on_focus_under_fullscreen 0")); + OK(getFromSocket("/eval hl.config({ misc = { on_focus_under_fullscreen = 0 } })")); Tests::killAllWindows(); NLog::log("{}Testing exit_window_retains_fullscreen", Colors::YELLOW); - OK(getFromSocket("/keyword misc:exit_window_retains_fullscreen false")); + OK(getFromSocket("/eval hl.config({ misc = { exit_window_retains_fullscreen = false } })")); Tests::spawnKitty("kitty_A"); Tests::spawnKitty("kitty_B"); - OK(getFromSocket("/dispatch fullscreen 0")); + OK(getFromSocket("/dispatch hl.dsp.window.fullscreen({ mode = 'fullscreen' })")); { auto str = getFromSocket("/activewindow"); @@ -241,7 +238,7 @@ static bool test() { EXPECT_CONTAINS(str, "fullscreenClient: 2"); } - OK(getFromSocket("/dispatch killwindow activewindow")); + OK(getFromSocket("/dispatch hl.dsp.window.kill({ window = 'activewindow' })")); Tests::waitUntilWindowsN(1); { @@ -251,10 +248,10 @@ static bool test() { } Tests::spawnKitty("kitty_B"); - OK(getFromSocket("/dispatch fullscreen 0")); - OK(getFromSocket("/keyword misc:exit_window_retains_fullscreen true")); + OK(getFromSocket("/dispatch hl.dsp.window.fullscreen({ mode = 'fullscreen' })")); + OK(getFromSocket("/eval hl.config({ misc = { exit_window_retains_fullscreen = true } })")); - OK(getFromSocket("/dispatch killwindow activewindow")); + OK(getFromSocket("/dispatch hl.dsp.window.kill({ window = 'activewindow' })")); Tests::waitUntilWindowsN(1); { @@ -270,8 +267,8 @@ static bool test() { Tests::spawnKitty("kitty_A"); Tests::spawnKitty("kitty_B"); - OK(getFromSocket("/dispatch focuswindow class:kitty_A")); - OK(getFromSocket("/dispatch fullscreen 0 set")); + OK(getFromSocket("/dispatch hl.dsp.focus({ window = 'class:kitty_A' })")); + OK(getFromSocket("/dispatch hl.dsp.window.fullscreen({ mode = 'fullscreen', action = 'set' })")); { auto str = getFromSocket("/activewindow"); @@ -279,7 +276,7 @@ static bool test() { EXPECT_CONTAINS(str, "fullscreenClient: 2"); } - OK(getFromSocket("/dispatch fullscreen 0 unset")); + OK(getFromSocket("/dispatch hl.dsp.window.fullscreen({ mode = 'fullscreen', action = 'unset' })")); { auto str = getFromSocket("/activewindow"); @@ -287,7 +284,7 @@ static bool test() { EXPECT_CONTAINS(str, "fullscreenClient: 0"); } - OK(getFromSocket("/dispatch fullscreen 1 toggle")); + OK(getFromSocket("/dispatch hl.dsp.window.fullscreen({ mode = 'maximized', action = 'toggle' })")); { auto str = getFromSocket("/activewindow"); @@ -295,7 +292,7 @@ static bool test() { EXPECT_CONTAINS(str, "fullscreenClient: 1"); } - OK(getFromSocket("/dispatch fullscreen 1 toggle")); + OK(getFromSocket("/dispatch hl.dsp.window.fullscreen({ mode = 'maximized', action = 'toggle' })")); { auto str = getFromSocket("/activewindow"); @@ -303,7 +300,7 @@ static bool test() { EXPECT_CONTAINS(str, "fullscreenClient: 0"); } - OK(getFromSocket("/dispatch fullscreenstate 3 3 set")); + OK(getFromSocket("/dispatch hl.dsp.window.fullscreen_state({ internal = 3, client = 3, action = 'set' })")); { auto str = getFromSocket("/activewindow"); @@ -311,7 +308,7 @@ static bool test() { EXPECT_CONTAINS(str, "fullscreenClient: 3"); } - OK(getFromSocket("/dispatch fullscreenstate 3 3 set")); + OK(getFromSocket("/dispatch hl.dsp.window.fullscreen_state({ internal = 3, client = 3, action = 'set' })")); { auto str = getFromSocket("/activewindow"); @@ -319,7 +316,7 @@ static bool test() { EXPECT_CONTAINS(str, "fullscreenClient: 3"); } - OK(getFromSocket("/dispatch fullscreenstate 2 2 set")); + OK(getFromSocket("/dispatch hl.dsp.window.fullscreen_state({ internal = 2, client = 2, action = 'set' })")); { auto str = getFromSocket("/activewindow"); @@ -327,7 +324,7 @@ static bool test() { EXPECT_CONTAINS(str, "fullscreenClient: 2"); } - OK(getFromSocket("/dispatch fullscreenstate 2 2 set")); + OK(getFromSocket("/dispatch hl.dsp.window.fullscreen_state({ internal = 2, client = 2, action = 'set' })")); { auto str = getFromSocket("/activewindow"); @@ -335,7 +332,7 @@ static bool test() { EXPECT_CONTAINS(str, "fullscreenClient: 2"); } - OK(getFromSocket("/dispatch fullscreenstate 2 2 toggle")); + OK(getFromSocket("/dispatch hl.dsp.window.fullscreen_state({ internal = 2, client = 2, action = 'toggle' })")); { auto str = getFromSocket("/activewindow"); @@ -343,26 +340,17 @@ static bool test() { EXPECT_CONTAINS(str, "fullscreenClient: 0"); } - OK(getFromSocket("/dispatch fullscreenstate 2 2 toggle")); + OK(getFromSocket("/dispatch hl.dsp.window.fullscreen_state({ internal = 2, client = 2, action = 'toggle' })")); { auto str = getFromSocket("/activewindow"); EXPECT_CONTAINS(str, "fullscreen: 2"); EXPECT_CONTAINS(str, "fullscreenClient: 2"); } - - // Ensure that the process autostarted in the config does not - // become a zombie even if it terminates very quickly. - EXPECT(Tests::execAndGet("pgrep -f 'sleep 0'").empty(), true); - - // kill all - NLog::log("{}Killing all windows", Colors::YELLOW); - Tests::killAllWindows(); - - NLog::log("{}Expecting 0 windows", Colors::YELLOW); - EXPECT(Tests::windowCount(), 0); - - return !ret; } -REGISTER_TEST_FN(test); +TEST_CASE(processesThatDieEarlyAreReaped) { + // Ensure that the process autostarted in the config does not + // become a zombie even if it terminates very quickly. + ASSERT(Tests::execAndGet("pgrep -f 'sleep 0'").empty(), true); +} diff --git a/hyprtester/src/tests/main/moveintoorcreategroup.cpp b/hyprtester/src/tests/main/moveintoorcreategroup.cpp index ecb491bc2..8c2f3aae0 100644 --- a/hyprtester/src/tests/main/moveintoorcreategroup.cpp +++ b/hyprtester/src/tests/main/moveintoorcreategroup.cpp @@ -10,48 +10,42 @@ #include #include "../shared.hpp" -static int ret = 0; - using namespace Hyprutils::OS; using namespace Hyprutils::Memory; #define UP CUniquePointer #define SP CSharedPointer -static bool test() { - NLog::log("{}Testing moveintoorcreategroup", Colors::GREEN); - +TEST_CASE(moveIntoOrCreateGroup) { NLog::log("{}Dispatching workspace `moveintoorcreategroup`", Colors::YELLOW); - getFromSocket("/dispatch workspace name:moveintoorcreategroup"); + getFromSocket("/dispatch hl.dsp.focus({ workspace = 'name:moveintoorcreategroup' })"); - OK(getFromSocket("/keyword group:auto_group false")); + OK(getFromSocket("/eval hl.config({ group = { auto_group = false } })")); NLog::log("{}Spawning kittyA", Colors::YELLOW); auto kittyA = Tests::spawnKitty("kitty_A"); if (!kittyA) { - NLog::log("{}Error: kittyA did not spawn", Colors::RED); - return false; + FAIL_TEST("Could not spawn kitty_A"); } NLog::log("{}Spawning kittyB", Colors::YELLOW); auto kittyB = Tests::spawnKitty("kitty_B"); if (!kittyB) { - NLog::log("{}Error: kittyB did not spawn", Colors::RED); - return false; + FAIL_TEST("Could not spawn kitty_B"); } NLog::log("{}Expecting 2 windows", Colors::YELLOW); - EXPECT(Tests::windowCount(), 2); + ASSERT(Tests::windowCount(), 2); { auto str = getFromSocket("/clients"); EXPECT_CONTAINS(str, "grouped: 0"); } - OK(getFromSocket("/dispatch focuswindow class:kitty_A")); + OK(getFromSocket("/dispatch hl.dsp.focus({ window = 'class:kitty_A' })")); NLog::log("{}Move kittyA into group with kittyB (creates group)", Colors::YELLOW); - OK(getFromSocket("/dispatch moveintoorcreategroup r")); + OK(getFromSocket("/dispatch hl.dsp.window.move({ into_or_create_group = 'right' })")); { auto str = getFromSocket("/clients"); @@ -68,7 +62,7 @@ static bool test() { Tests::killAllWindows(); NLog::log("{}Expecting 0 windows", Colors::YELLOW); - EXPECT(Tests::windowCount(), 0); + ASSERT(Tests::windowCount(), 0); NLog::log("{}Testing moveintoorcreategroup into existing group", Colors::YELLOW); @@ -80,15 +74,15 @@ static bool test() { auto kittyE = Tests::spawnKitty("kitty_E"); NLog::log("{}Expecting 3 windows", Colors::YELLOW); - EXPECT(Tests::windowCount(), 3); + ASSERT(Tests::windowCount(), 3); - OK(getFromSocket("/dispatch focuswindow class:kitty_D")); - OK(getFromSocket("/dispatch togglegroup")); + OK(getFromSocket("/dispatch hl.dsp.focus({ window = 'class:kitty_D' })")); + OK(getFromSocket("/dispatch hl.dsp.group.toggle()")); - OK(getFromSocket("/dispatch focuswindow class:kitty_E")); + OK(getFromSocket("/dispatch hl.dsp.focus({ window = 'class:kitty_E' })")); NLog::log("{}Move kittyE into existing group with kittyD", Colors::YELLOW); - OK(getFromSocket("/dispatch moveintoorcreategroup l")); + OK(getFromSocket("/dispatch hl.dsp.window.move({ into_or_create_group = 'left' })")); { auto str = getFromSocket("/clients"); @@ -100,12 +94,4 @@ static bool test() { auto str = getFromSocket("/activewindow"); EXPECT_CONTAINS(str, "kitty_E"); } - - NLog::log("{}Kill windows", Colors::YELLOW); - Tests::killAllWindows(); - OK(getFromSocket("/keyword group:auto_group true")); - - return !ret; } - -REGISTER_TEST_FN(test) diff --git a/hyprtester/src/tests/main/persistent.cpp b/hyprtester/src/tests/main/persistent.cpp index 5e2fdc5ae..a203b1f21 100644 --- a/hyprtester/src/tests/main/persistent.cpp +++ b/hyprtester/src/tests/main/persistent.cpp @@ -10,27 +10,21 @@ #include #include "../shared.hpp" -static int ret = 0; - using namespace Hyprutils::OS; using namespace Hyprutils::Memory; #define UP CUniquePointer #define SP CSharedPointer -static bool test() { - NLog::log("{}Testing persistent workspaces", Colors::GREEN); - - EXPECT(Tests::windowCount(), 0); - +TEST_CASE(persistentWorkspaces) { // test on workspace "window" NLog::log("{}Switching to workspace 1", Colors::YELLOW); - getFromSocket("/dispatch workspace 1"); // no OK: we might be on 1 already + getFromSocket("/dispatch hl.dsp.focus({ workspace = '1' })"); // no OK: we might be on 1 already - OK(getFromSocket("/keyword workspace 5, monitor:HEADLESS-2, persistent:1")); - OK(getFromSocket("/keyword workspace 6, monitor:HEADLESS-PERSISTENT-TEST, persistent:1")); - OK(getFromSocket("/keyword workspace name:PERSIST, monitor:HEADLESS-PERSISTENT-TEST, persistent:1")); - OK(getFromSocket("/keyword workspace name:PERSIST-2, monitor:HEADLESS-PERSISTENT-TEST, persistent:1")); + OK(getFromSocket("/eval hl.workspace_rule({ workspace = '5', monitor = 'HEADLESS-2', persistent = true })")); + OK(getFromSocket("/eval hl.workspace_rule({ workspace = '6', monitor = 'HEADLESS-PERSISTENT-TEST', persistent = true })")); + OK(getFromSocket("/eval hl.workspace_rule({ workspace = 'name:PERSIST', monitor = 'HEADLESS-PERSISTENT-TEST', persistent = true })")); + OK(getFromSocket("/eval hl.workspace_rule({ workspace = 'name:PERSIST-2', monitor = 'HEADLESS-PERSISTENT-TEST', persistent = true })")); { auto str = getFromSocket("/workspaces"); @@ -42,10 +36,10 @@ static bool test() { { auto str = getFromSocket("/monitors"); - EXPECT_CONTAINS(str, "HEADLESS-PERSISTENT-TEST"); + ASSERT_CONTAINS(str, "HEADLESS-PERSISTENT-TEST"); } - OK(getFromSocket("/dispatch focusmonitor HEADLESS-PERSISTENT-TEST")); + OK(getFromSocket("/dispatch hl.dsp.focus({ monitor = 'HEADLESS-PERSISTENT-TEST' })")); { auto str = getFromSocket("/workspaces"); @@ -68,18 +62,4 @@ static bool test() { } OK(getFromSocket("/output remove HEADLESS-PERSISTENT-TEST")); - - // kill all - NLog::log("{}Killing all windows", Colors::YELLOW); - Tests::killAllWindows(); - - NLog::log("{}Expecting 0 windows", Colors::YELLOW); - EXPECT(Tests::windowCount(), 0); - - // reload cfg - OK(getFromSocket("/reload")); - - return !ret; } - -REGISTER_TEST_FN(test) diff --git a/hyprtester/src/tests/main/scroll.cpp b/hyprtester/src/tests/main/scroll.cpp index 3662f9658..bd33e345e 100644 --- a/hyprtester/src/tests/main/scroll.cpp +++ b/hyprtester/src/tests/main/scroll.cpp @@ -3,136 +3,123 @@ #include "../../hyprctlCompat.hpp" #include "tests.hpp" -static int ret = 0; +TEST_CASE(scrollFocusCycling) { + OK(getFromSocket("r/eval hl.config({ general = { layout = 'scrolling' } })")); -static void testFocusCycling() { for (auto const& win : {"a", "b", "c", "d"}) { if (!Tests::spawnKitty(win)) { - NLog::log("{}Failed to spawn kitty with win class `{}`", Colors::RED, win); - ++TESTS_FAILED; - ret = 1; - return; + FAIL_TEST("Could not spawn kitty with win class `{}`", win); } } - OK(getFromSocket("/dispatch focuswindow class:a")); + OK(getFromSocket("/dispatch hl.dsp.focus({ window = 'class:a' })")); - OK(getFromSocket("/dispatch movefocus r")); + OK(getFromSocket("/dispatch hl.dsp.focus({ direction = 'right' })")); { auto str = getFromSocket("/activewindow"); - EXPECT_CONTAINS(str, "class: b"); + ASSERT_CONTAINS(str, "class: b"); } - OK(getFromSocket("/dispatch movefocus r")); + OK(getFromSocket("/dispatch hl.dsp.focus({ direction = 'right' })")); { auto str = getFromSocket("/activewindow"); - EXPECT_CONTAINS(str, "class: c"); + ASSERT_CONTAINS(str, "class: c"); } - OK(getFromSocket("/dispatch movefocus r")); + OK(getFromSocket("/dispatch hl.dsp.focus({ direction = 'right' })")); { auto str = getFromSocket("/activewindow"); - EXPECT_CONTAINS(str, "class: d"); + ASSERT_CONTAINS(str, "class: d"); } - OK(getFromSocket("/dispatch movewindow l")); + OK(getFromSocket("/dispatch hl.dsp.window.move({ direction = 'left' })")); { auto str = getFromSocket("/activewindow"); - EXPECT_CONTAINS(str, "class: d"); + ASSERT_CONTAINS(str, "class: d"); } - OK(getFromSocket("/dispatch movefocus u")); + OK(getFromSocket("/dispatch hl.dsp.focus({ direction = 'up' })")); { auto str = getFromSocket("/activewindow"); - EXPECT_CONTAINS(str, "class: c"); + ASSERT_CONTAINS(str, "class: c"); } - - // clean up - NLog::log("{}Killing all windows", Colors::YELLOW); - Tests::killAllWindows(); } -static void testFocusWrapping() { +TEST_CASE(scrollFocusWrapping) { + OK(getFromSocket("r/eval hl.config({ general = { layout = 'scrolling' } })")); + for (auto const& win : {"a", "b", "c", "d"}) { if (!Tests::spawnKitty(win)) { - NLog::log("{}Failed to spawn kitty with win class `{}`", Colors::RED, win); - ++TESTS_FAILED; - ret = 1; - return; + FAIL_TEST("Could not spawn kitty with win class `{}`", win); } } // set wrap_focus to true - OK(getFromSocket("/keyword scrolling:wrap_focus true")); + OK(getFromSocket("/eval hl.config({ scrolling = { wrap_focus = true } })")); - OK(getFromSocket("/dispatch focuswindow class:a")); + OK(getFromSocket("/dispatch hl.dsp.focus({ window = 'class:a' })")); - OK(getFromSocket("/dispatch layoutmsg focus l")); + OK(getFromSocket("/dispatch hl.dsp.layout('focus l')")); { auto str = getFromSocket("/activewindow"); - EXPECT_CONTAINS(str, "class: d"); + ASSERT_CONTAINS(str, "class: d"); } - OK(getFromSocket("/dispatch layoutmsg focus r")); + OK(getFromSocket("/dispatch hl.dsp.layout('focus r')")); { auto str = getFromSocket("/activewindow"); - EXPECT_CONTAINS(str, "class: a"); + ASSERT_CONTAINS(str, "class: a"); } // set wrap_focus to false - OK(getFromSocket("/keyword scrolling:wrap_focus false")); + OK(getFromSocket("/eval hl.config({ scrolling = { wrap_focus = false } })")); - OK(getFromSocket("/dispatch focuswindow class:a")); + OK(getFromSocket("/dispatch hl.dsp.focus({ window = 'class:a' })")); - OK(getFromSocket("/dispatch layoutmsg focus l")); + OK(getFromSocket("/dispatch hl.dsp.layout('focus l')")); { auto str = getFromSocket("/activewindow"); - EXPECT_CONTAINS(str, "class: a"); + ASSERT_CONTAINS(str, "class: a"); } - OK(getFromSocket("/dispatch focuswindow class:d")); + OK(getFromSocket("/dispatch hl.dsp.focus({ window = 'class:d' })")); - OK(getFromSocket("/dispatch layoutmsg focus r")); + OK(getFromSocket("/dispatch hl.dsp.layout('focus r')")); { auto str = getFromSocket("/activewindow"); - EXPECT_CONTAINS(str, "class: d"); + ASSERT_CONTAINS(str, "class: d"); } - - // clean up - NLog::log("{}Killing all windows", Colors::YELLOW); - Tests::killAllWindows(); } -static void testSwapcolWrapping() { +TEST_CASE(scrollSwapcolWrapping) { + OK(getFromSocket("r/eval hl.config({ general = { layout = 'scrolling' } })")); + for (auto const& win : {"a", "b", "c", "d"}) { if (!Tests::spawnKitty(win)) { - NLog::log("{}Failed to spawn kitty with win class `{}`", Colors::RED, win); - ++TESTS_FAILED; - ret = 1; - return; + FAIL_TEST("Could not spawn kitty with win class `{}`", win); } } // set wrap_swapcol to true - OK(getFromSocket("/keyword scrolling:wrap_swapcol true")); + OK(getFromSocket("/eval hl.config({ scrolling = { wrap_swapcol = true } })")); - OK(getFromSocket("/dispatch focuswindow class:a")); + OK(getFromSocket("/dispatch hl.dsp.focus({ window = 'class:a' })")); - OK(getFromSocket("/dispatch layoutmsg swapcol l")); - OK(getFromSocket("/dispatch layoutmsg focus l")); + OK(getFromSocket("/dispatch hl.dsp.layout('swapcol l')")); + OK(getFromSocket("/dispatch hl.dsp.layout('focus l')")); { auto str = getFromSocket("/activewindow"); - EXPECT_CONTAINS(str, "class: c"); + ASSERT_CONTAINS(str, "class: c"); } // clean up @@ -141,20 +128,17 @@ static void testSwapcolWrapping() { for (auto const& win : {"a", "b", "c", "d"}) { if (!Tests::spawnKitty(win)) { - NLog::log("{}Failed to spawn kitty with win class `{}`", Colors::RED, win); - ++TESTS_FAILED; - ret = 1; - return; + FAIL_TEST("Could not spawn kitty with win class `{}`", win); } } - OK(getFromSocket("/dispatch focuswindow class:d")); - OK(getFromSocket("/dispatch layoutmsg swapcol r")); - OK(getFromSocket("/dispatch layoutmsg focus r")); + OK(getFromSocket("/dispatch hl.dsp.focus({ window = 'class:d' })")); + OK(getFromSocket("/dispatch hl.dsp.layout('swapcol r')")); + OK(getFromSocket("/dispatch hl.dsp.layout('focus r')")); { auto str = getFromSocket("/activewindow"); - EXPECT_CONTAINS(str, "class: b"); + ASSERT_CONTAINS(str, "class: b"); } // clean up @@ -163,96 +147,53 @@ static void testSwapcolWrapping() { for (auto const& win : {"a", "b", "c", "d"}) { if (!Tests::spawnKitty(win)) { - NLog::log("{}Failed to spawn kitty with win class `{}`", Colors::RED, win); - ++TESTS_FAILED; - ret = 1; - return; + FAIL_TEST("Could not spawn kitty with win class `{}`", win); } } // set wrap_swapcol to false - OK(getFromSocket("/keyword scrolling:wrap_swapcol false")); + OK(getFromSocket("/eval hl.config({ scrolling = { wrap_swapcol = false } })")); - OK(getFromSocket("/dispatch focuswindow class:a")); + OK(getFromSocket("/dispatch hl.dsp.focus({ window = 'class:a' })")); - OK(getFromSocket("/dispatch layoutmsg swapcol l")); - OK(getFromSocket("/dispatch layoutmsg focus r")); + OK(getFromSocket("/dispatch hl.dsp.layout('swapcol l')")); + OK(getFromSocket("/dispatch hl.dsp.layout('focus r')")); { auto str = getFromSocket("/activewindow"); - EXPECT_CONTAINS(str, "class: b"); + ASSERT_CONTAINS(str, "class: b"); } - OK(getFromSocket("/dispatch focuswindow class:d")); + OK(getFromSocket("/dispatch hl.dsp.focus({ window = 'class:d' })")); - OK(getFromSocket("/dispatch layoutmsg swapcol r")); - OK(getFromSocket("/dispatch layoutmsg focus l")); + OK(getFromSocket("/dispatch hl.dsp.layout('swapcol r')")); + OK(getFromSocket("/dispatch hl.dsp.layout('focus l')")); { auto str = getFromSocket("/activewindow"); - EXPECT_CONTAINS(str, "class: c"); + ASSERT_CONTAINS(str, "class: c"); } - - // clean up - NLog::log("{}Killing all windows", Colors::YELLOW); - Tests::killAllWindows(); } -static bool testWindowRule() { +TEST_CASE(scrollWindowRule) { + OK(getFromSocket("r/eval hl.config({ general = { layout = 'scrolling' } })")); + NLog::log("{}Testing Scrolling Width", Colors::GREEN); // inject a new rule. - OK(getFromSocket("/keyword windowrule[scrolling-width]:match:class kitty_scroll")); - OK(getFromSocket("/keyword windowrule[scrolling-width]:scrolling_width 0.1")); + OK(getFromSocket("/eval hl.window_rule({ name = 'scrolling-width', match = { class = 'kitty_scroll' } })")); + OK(getFromSocket("/eval hl.window_rule({ name = 'scrolling-width', scrolling_width = 0.1 })")); if (!Tests::spawnKitty("kitty_scroll")) { - NLog::log("{}Failed to spawn kitty with win class `kitty_scroll`", Colors::RED); - return false; + FAIL_TEST("Could not spawn kitty with win class `kitty_scroll`"); } if (!Tests::spawnKitty("kitty_scroll")) { - NLog::log("{}Failed to spawn kitty with win class `kitty_scroll`", Colors::RED); - return false; + FAIL_TEST("Could not spawn kitty with win class `kitty_scroll`"); } - EXPECT(Tests::windowCount(), 2); + ASSERT(Tests::windowCount(), 2); // not the greatest test, but as long as res and gaps don't change, we good. EXPECT_CONTAINS(getFromSocket("/activewindow"), "size: 174,1036"); - - NLog::log("{}Killing all windows", Colors::YELLOW); - Tests::killAllWindows(); - EXPECT(Tests::windowCount(), 0); - return true; } - -static bool test() { - NLog::log("{}Testing Scroll layout", Colors::GREEN); - - // setup - OK(getFromSocket("/dispatch workspace name:scroll")); - OK(getFromSocket("/keyword general:layout scrolling")); - - // test - NLog::log("{}Testing focus cycling", Colors::GREEN); - testFocusCycling(); - - // test - NLog::log("{}Testing focus wrap", Colors::GREEN); - testFocusWrapping(); - - // test - NLog::log("{}Testing swapcol wrap", Colors::GREEN); - testSwapcolWrapping(); - - testWindowRule(); - - // clean up - NLog::log("Cleaning up", Colors::YELLOW); - OK(getFromSocket("/dispatch workspace 1")); - OK(getFromSocket("/reload")); - - return !ret; -} - -REGISTER_TEST_FN(test); diff --git a/hyprtester/src/tests/main/snap.cpp b/hyprtester/src/tests/main/snap.cpp index 9c25de112..0e0c4604b 100644 --- a/hyprtester/src/tests/main/snap.cpp +++ b/hyprtester/src/tests/main/snap.cpp @@ -9,43 +9,35 @@ using Hyprutils::Math::Vector2D; -static int ret = 0; - static bool spawnFloatingKitty() { if (!Tests::spawnKitty()) { NLog::log("{}Error: kitty did not spawn", Colors::RED); return false; } - OK(getFromSocket("/dispatch setfloating active")); - OK(getFromSocket("/dispatch resizeactive exact 100 100")); - return true; + bool ok = true; + ok &= getFromSocket("/dispatch hl.dsp.window.float({ action = 'set' })") == "ok"; + ok &= getFromSocket("/dispatch hl.dsp.window.resize({ x = 100, y = 100 })") == "ok"; + return ok; } -static void expectSocket(const std::string& CMD) { - if (const auto RESULT = getFromSocket(CMD); RESULT != "ok") { - NLog::log("{}Failed: {}getFromSocket({}), expected ok, got {}. Source: {}@{}.", Colors::RED, Colors::RESET, CMD, RESULT, __FILE__, __LINE__); - ret = 1; - TESTS_FAILED++; - } else { - NLog::log("{}Passed: {}getFromSocket({}). Got ok", Colors::GREEN, Colors::RESET, CMD); - TESTS_PASSED++; - } -} - -static void expectSnapMove(const Vector2D FROM, const Vector2D* TO) { - const Vector2D& A = FROM; - const Vector2D& B = TO ? *TO : FROM; - if (TO) - NLog::log("{}Expecting snap to ({},{}) when window is moved to ({},{})", Colors::YELLOW, B.x, B.y, A.x, A.y); +SUBTEST(expectSnapMove, double fromX, double fromY, double toX, double toY) { + const Vector2D FROM = {fromX, fromY}; + const Vector2D TO = {toX, toY}; + if (FROM == TO) + NLog::log("{}Expecting no snap when window is moved to ({},{})", Colors::YELLOW, FROM.x, FROM.y); else - NLog::log("{}Expecting no snap when window is moved to ({},{})", Colors::YELLOW, A.x, A.y); + NLog::log("{}Expecting snap to ({},{}) when window is moved to ({},{})", Colors::YELLOW, TO.x, TO.y, FROM.x, FROM.y); - expectSocket(std::format("/dispatch moveactive exact {} {}", A.x, A.y)); - expectSocket("/dispatch plugin:test:snapmove"); - EXPECT_CONTAINS(getFromSocket("/activewindow"), std::format("at: {},{}", B.x, B.y)); + OK(getFromSocket(std::format("/dispatch hl.dsp.window.move({{ x = {}, y = {} }})", FROM.x, FROM.y))); + OK(getFromSocket("/eval hl.plugin.test.snapmove()")); + EXPECT_CONTAINS(getFromSocket("/activewindow"), std::format("at: {},{}", TO.x, TO.y)); } -static void testWindowSnap(const bool RESPECTGAPS) { +SUBTEST(expectNoSnapMove, double x, double y) { + CALL_SUBTEST(expectSnapMove, x, y, x, y); +} + +SUBTEST(testWindowSnap, const bool RESPECTGAPS) { const int BORDERSIZE = 2; const int WINDOWSIZE = 100; @@ -55,22 +47,19 @@ static void testWindowSnap(const bool RESPECTGAPS) { const int GAP = (RESPECTGAPS ? 2 * GAPSIN : 0) + (2 * BORDERSIZE); const int END = GAP + WINDOWSIZE; - int x; - Vector2D predict; - - x = WINDOWGAP + END; - expectSnapMove({OTHER + x, OTHER}, nullptr); - expectSnapMove({OTHER - x, OTHER}, nullptr); - expectSnapMove({OTHER, OTHER + x}, nullptr); - expectSnapMove({OTHER, OTHER - x}, nullptr); + int x = WINDOWGAP + END; + CALL_SUBTEST(expectNoSnapMove, OTHER + x, OTHER); + CALL_SUBTEST(expectNoSnapMove, OTHER - x, OTHER); + CALL_SUBTEST(expectNoSnapMove, OTHER, OTHER + x); + CALL_SUBTEST(expectNoSnapMove, OTHER, OTHER - x); x -= 1; - expectSnapMove({OTHER + x, OTHER}, &(predict = {OTHER + END, OTHER})); - expectSnapMove({OTHER - x, OTHER}, &(predict = {OTHER - END, OTHER})); - expectSnapMove({OTHER, OTHER + x}, &(predict = {OTHER, OTHER + END})); - expectSnapMove({OTHER, OTHER - x}, &(predict = {OTHER, OTHER - END})); + CALL_SUBTEST(expectSnapMove, OTHER + x, OTHER, OTHER + END, OTHER); + CALL_SUBTEST(expectSnapMove, OTHER - x, OTHER, OTHER - END, OTHER); + CALL_SUBTEST(expectSnapMove, OTHER, OTHER + x, OTHER, OTHER + END); + CALL_SUBTEST(expectSnapMove, OTHER, OTHER - x, OTHER, OTHER - END); } -static void testMonitorSnap(const bool RESPECTGAPS, const bool OVERLAP) { +SUBTEST(testMonitorSnap, const bool RESPECTGAPS, const bool OVERLAP) { const int BORDERSIZE = 2; const int WINDOWSIZE = 100; @@ -84,14 +73,14 @@ static void testMonitorSnap(const bool RESPECTGAPS, const bool OVERLAP) { Vector2D predict; x = MONITORGAP + GAP; - expectSnapMove({x, x}, nullptr); + CALL_SUBTEST(expectNoSnapMove, x, x); x -= 1; - expectSnapMove({x, x}, &(predict = {GAP, GAP})); + CALL_SUBTEST(expectSnapMove, x, x, GAP, GAP); x = MONITORGAP + END; - expectSnapMove({1920 - x, 1080 - x}, nullptr); + CALL_SUBTEST(expectNoSnapMove, 1920 - x, 1080 - x); x -= 1; - expectSnapMove({1920 - x, 1080 - x}, &(predict = {1920 - END, 1080 - END})); + CALL_SUBTEST(expectSnapMove, 1920 - x, 1080 - x, 1920 - END, 1080 - END); // test reserved area const int RESERVED = 200; @@ -99,78 +88,64 @@ static void testMonitorSnap(const bool RESPECTGAPS, const bool OVERLAP) { const int REND = RGAP + WINDOWSIZE; x = MONITORGAP + RGAP; - expectSnapMove({x, x}, nullptr); + CALL_SUBTEST(expectNoSnapMove, x, x); x -= 1; - expectSnapMove({x, x}, &(predict = {RGAP, RGAP})); + CALL_SUBTEST(expectSnapMove, x, x, RGAP, RGAP); x = MONITORGAP + REND; - expectSnapMove({1920 - x, 1080 - x}, nullptr); + CALL_SUBTEST(expectNoSnapMove, 1920 - x, 1080 - x); x -= 1; - expectSnapMove({1920 - x, 1080 - x}, &(predict = {1920 - REND, 1080 - REND})); + CALL_SUBTEST(expectSnapMove, 1920 - x, 1080 - x, 1920 - REND, 1080 - REND); } -static bool test() { +// TODO: decompose this into multiple test cases +TEST_CASE(snap) { NLog::log("{}Testing snap", Colors::GREEN); // move to monitor HEADLESS-2 NLog::log("{}Moving to monitor HEADLESS-2", Colors::YELLOW); - OK(getFromSocket("/dispatch focusmonitor HEADLESS-2")); + OK(getFromSocket("/dispatch hl.dsp.focus({ monitor = 'HEADLESS-2' })")); NLog::log("{}Adding reserved monitor area to HEADLESS-2", Colors::YELLOW); - OK(getFromSocket("/keyword monitor HEADLESS-2,addreserved,200,200,200,200")); + OK(getFromSocket("/eval hl.monitor({ output = 'HEADLESS-2', reserved = { top = 200, right = 200, bottom = 200, left = 200 } })")); // test on workspace "snap" NLog::log("{}Dispatching workspace `snap`", Colors::YELLOW); - OK(getFromSocket("/dispatch workspace name:snap")); + OK(getFromSocket("/dispatch hl.dsp.focus({ workspace = 'name:snap' })")); // spawn a kitty terminal and move to (500,500) NLog::log("{}Spawning kittyProcA", Colors::YELLOW); if (!spawnFloatingKitty()) - return false; + FAIL_TEST("Could not spawn kitty"); NLog::log("{}Expecting 1 window", Colors::YELLOW); - EXPECT(Tests::windowCount(), 1); + ASSERT(Tests::windowCount(), 1); NLog::log("{}Move the kitty window to (500,500)", Colors::YELLOW); - OK(getFromSocket("/dispatch moveactive exact 500 500")); + OK(getFromSocket("/dispatch hl.dsp.window.move({ x = 500, y = 500 })")); // spawn a second kitty terminal NLog::log("{}Spawning kittyProcB", Colors::YELLOW); if (!spawnFloatingKitty()) - return false; + FAIL_TEST("Could not spawn kitty"); NLog::log("{}Expecting 2 windows", Colors::YELLOW); - EXPECT(Tests::windowCount(), 2); + ASSERT(Tests::windowCount(), 2); NLog::log(""); - testWindowSnap(false); - testMonitorSnap(false, false); + CALL_SUBTEST(testWindowSnap, false); + CALL_SUBTEST(testMonitorSnap, false, false); NLog::log("\n{}Turning on respect_gaps", Colors::YELLOW); - OK(getFromSocket("/keyword general:snap:respect_gaps true")); - testWindowSnap(true); - testMonitorSnap(true, false); + OK(getFromSocket("/eval hl.config({ general = { snap = { respect_gaps = true } } })")); + CALL_SUBTEST(testWindowSnap, true); + CALL_SUBTEST(testMonitorSnap, true, false); NLog::log("\n{}Turning on border_overlap", Colors::YELLOW); - OK(getFromSocket("/keyword general:snap:respect_gaps false")); - OK(getFromSocket("/keyword general:snap:border_overlap true")); - testMonitorSnap(false, true); + OK(getFromSocket("/eval hl.config({ general = { snap = { respect_gaps = false } } })")); + OK(getFromSocket("/eval hl.config({ general = { snap = { border_overlap = true } } })")); + CALL_SUBTEST(testMonitorSnap, false, true); NLog::log("\n{}Turning on both border_overlap and respect_gaps", Colors::YELLOW); - OK(getFromSocket("/keyword general:snap:respect_gaps true")); - testMonitorSnap(true, true); - - // kill all - NLog::log("\n{}Killing all windows", Colors::YELLOW); - Tests::killAllWindows(); - - NLog::log("{}Expecting 0 windows", Colors::YELLOW); - EXPECT(Tests::windowCount(), 0); - - NLog::log("{}Reloading the config", Colors::YELLOW); - OK(getFromSocket("/reload")); - OK(getFromSocket("/dispatch workspace 1")); - - return !ret; + OK(getFromSocket("/eval hl.config({ general = { snap = { respect_gaps = true } } })")); + CALL_SUBTEST(testMonitorSnap, true, true); } - -REGISTER_TEST_FN(test) diff --git a/hyprtester/src/tests/main/solitary.cpp b/hyprtester/src/tests/main/solitary.cpp index 6f4d13732..522b7968f 100644 --- a/hyprtester/src/tests/main/solitary.cpp +++ b/hyprtester/src/tests/main/solitary.cpp @@ -1,35 +1,42 @@ #include "tests.hpp" #include "../../shared.hpp" #include "../../hyprctlCompat.hpp" -#include +#include #include +#include +#include +#include #include #include #include "../shared.hpp" -static int ret = 0; - using namespace Hyprutils::OS; using namespace Hyprutils::Memory; #define UP CUniquePointer #define SP CSharedPointer -static bool test() { - NLog::log("{}Testing solitary clients", Colors::GREEN); +SUBTEST(expectBlockedByAll, const std::string& blockedByLine, const std::set& expectedBlockedBy) { + const std::set blockedBy = blockedByLine | std::ranges::views::split(',') | std::ranges::to>(); + NLog::log("blockedBy = {}", blockedBy); + NLog::log("expectedBlockedBy = {}", expectedBlockedBy); + ASSERT(std::ranges::includes(blockedBy, expectedBlockedBy), true); +} - OK(getFromSocket("/keyword general:allow_tearing false")); - OK(getFromSocket("/keyword render:direct_scanout 0")); - OK(getFromSocket("/keyword cursor:no_hardware_cursors 1")); +TEST_CASE(solitaryClients) { + OK(getFromSocket("/eval hl.config({ general = { allow_tearing = false } })")); + OK(getFromSocket("/eval hl.config({ render = { direct_scanout = 0 } })")); + OK(getFromSocket("/eval hl.config({ cursor = { no_hardware_cursors = 1 } })")); NLog::log("{}Expecting blocked solitary/DS/tearing", Colors::YELLOW); { auto str = getFromSocket("/monitors"); EXPECT_CONTAINS(str, "solitary: 0\n"); - EXPECT_CONTAINS(str, "solitaryBlockedBy: windowed mode,missing candidate"); + CALL_SUBTEST(expectBlockedByAll, Tests::getAttribute(str, "solitaryBlockedBy"), {"windowed mode", "missing candidate"}); EXPECT_CONTAINS(str, "activelyTearing: false"); - EXPECT_CONTAINS(str, "tearingBlockedBy: next frame is not torn,user settings,not supported by monitor,missing candidate"); + CALL_SUBTEST(expectBlockedByAll, Tests::getAttribute(str, "tearingBlockedBy"), + {"next frame is not torn", "user settings", "not supported by monitor", "missing candidate"}); EXPECT_CONTAINS(str, "directScanoutTo: 0\n"); - EXPECT_CONTAINS(str, "directScanoutBlockedBy: user settings,software renders/cursors,missing candidate"); + CALL_SUBTEST(expectBlockedByAll, Tests::getAttribute(str, "directScanoutBlockedBy"), {"user settings", "software renders/cursors", "missing candidate"}); } // FIXME: need a reliable client with solitary opaque surface in fullscreen. kitty doesn't work all the time @@ -41,36 +48,25 @@ static bool test() { // return false; // } - // OK(getFromSocket("/keyword general:allow_tearing true")); - // OK(getFromSocket("/keyword render:direct_scanout 1")); + // OK(getFromSocket("/eval hl.config({ general = { allow_tearing = true } })")); + // OK(getFromSocket("/eval hl.config({ render = { direct_scanout = 1 } })")); // NLog::log("{}", getFromSocket("/clients")); - // OK(getFromSocket("/dispatch fullscreen")); + // OK(getFromSocket("/dispatch hl.dsp.window.fullscreen()")); // NLog::log("{}", getFromSocket("/clients")); // std::this_thread::sleep_for(std::chrono::milliseconds(100)); // NLog::log("{}Expecting kitty to almost pass for solitary/DS/tearing", Colors::YELLOW); // { // auto str = getFromSocket("/monitors"); - // EXPECT_NOT_CONTAINS(str, "solitary: 0\n"); - // EXPECT_CONTAINS(str, "solitaryBlockedBy: null"); - // EXPECT_CONTAINS(str, "activelyTearing: false"); - // EXPECT_CONTAINS(str, "tearingBlockedBy: next frame is not torn,not supported by monitor,window settings"); + // ASSERT_NOT_CONTAINS(str, "solitary: 0\n"); + // ASSERT_CONTAINS(str, "solitaryBlockedBy: null"); + // ASSERT_CONTAINS(str, "activelyTearing: false"); + // ASSERT_CONTAINS(str, "tearingBlockedBy: next frame is not torn,not supported by monitor,window settings"); // } - // OK(getFromSocket("/dispatch setprop active immediate 1")); + // OK(getFromSocket("/dispatch hl.dsp.window.set_prop({ window = 'active', prop = 'immediate', value = '1' })")); // NLog::log("{}Expecting kitty to almost pass for tearing", Colors::YELLOW); // { // auto str = getFromSocket("/monitors"); - // EXPECT_CONTAINS(str, "tearingBlockedBy: next frame is not torn,not supported by monitor\n"); + // ASSERT_CONTAINS(str, "tearingBlockedBy: next frame is not torn,not supported by monitor\n"); // } - - // // kill all - // NLog::log("{}Killing all windows", Colors::YELLOW); - // Tests::killAllWindows(); - - NLog::log("{}Reloading the config", Colors::YELLOW); - OK(getFromSocket("/reload")); - - return !ret; } - -REGISTER_TEST_FN(test) diff --git a/hyprtester/src/tests/main/tags.cpp b/hyprtester/src/tests/main/tags.cpp index c345fe718..d83f00fab 100644 --- a/hyprtester/src/tests/main/tags.cpp +++ b/hyprtester/src/tests/main/tags.cpp @@ -3,49 +3,33 @@ #include "../shared.hpp" #include "tests.hpp" -static int ret = 0; - -static bool testTags() { - NLog::log("{}Testing tags", Colors::GREEN); - - EXPECT(Tests::windowCount(), 0); - +TEST_CASE(tags) { NLog::log("{}Spawning kittyProcA&B on ws 1", Colors::YELLOW); auto kittyProcA = Tests::spawnKitty("tagged"); auto kittyProcB = Tests::spawnKitty("untagged"); if (!kittyProcA || !kittyProcB) { - NLog::log("{}Error: kitty did not spawn", Colors::RED); - return false; + FAIL_TEST("Could not spawn kitty"); } NLog::log("{}Testing testTag tags", Colors::YELLOW); - OK(getFromSocket("/keyword windowrule[tag-test-1]:tag +testTag")); - OK(getFromSocket("/keyword windowrule[tag-test-1]:match:class tagged")); - OK(getFromSocket("/keyword windowrule[tag-test-2]:match:tag negative:testTag")); - OK(getFromSocket("/keyword windowrule[tag-test-2]:no_shadow true")); - OK(getFromSocket("/keyword windowrule[tag-test-3]:match:tag testTag")); - OK(getFromSocket("/keyword windowrule[tag-test-3]:no_dim true")); + OK(getFromSocket("/eval hl.window_rule({ name = 'tag-test-1', tag = '+testTag' })")); + OK(getFromSocket("/eval hl.window_rule({ name = 'tag-test-1', match = { class = 'tagged' } })")); + OK(getFromSocket("/eval hl.window_rule({ name = 'tag-test-2', match = { tag = 'negative:testTag' } })")); + OK(getFromSocket("/eval hl.window_rule({ name = 'tag-test-2', no_shadow = true })")); + OK(getFromSocket("/eval hl.window_rule({ name = 'tag-test-3', match = { tag = 'testTag' } })")); + OK(getFromSocket("/eval hl.window_rule({ name = 'tag-test-3', no_dim = true })")); - EXPECT(Tests::windowCount(), 2); - OK(getFromSocket("/dispatch focuswindow class:tagged")); + ASSERT(Tests::windowCount(), 2); + OK(getFromSocket("/dispatch hl.dsp.focus({ window = 'class:tagged' })")); NLog::log("{}Testing tagged window for no_dim 0 & no_shadow", Colors::YELLOW); EXPECT_CONTAINS(getFromSocket("/activewindow"), "testTag"); EXPECT_CONTAINS(getFromSocket("/getprop activewindow no_dim"), "true"); EXPECT_CONTAINS(getFromSocket("/getprop activewindow no_shadow"), "false"); NLog::log("{}Testing untagged window for no_dim & no_shadow", Colors::YELLOW); - OK(getFromSocket("/dispatch focuswindow class:untagged")); + OK(getFromSocket("/dispatch hl.dsp.focus({ window = 'class:untagged' })")); EXPECT_NOT_CONTAINS(getFromSocket("/activewindow"), "testTag"); EXPECT_CONTAINS(getFromSocket("/getprop activewindow no_shadow"), "true"); EXPECT_CONTAINS(getFromSocket("/getprop activewindow no_dim"), "false"); - - Tests::killAllWindows(); - EXPECT(Tests::windowCount(), 0); - - OK(getFromSocket("/reload")); - - return ret == 0; } - -REGISTER_TEST_FN(testTags) diff --git a/hyprtester/src/tests/main/tests.hpp b/hyprtester/src/tests/main/tests.hpp index 757052b60..7ee4fd4a4 100644 --- a/hyprtester/src/tests/main/tests.hpp +++ b/hyprtester/src/tests/main/tests.hpp @@ -1,12 +1,9 @@ #pragma once +#include -#include -#include +#include "../shared.hpp" -inline std::vector> testFns; +inline std::map mainTestCases; -#define REGISTER_TEST_FN(fn) \ - static auto _register_fn = [] { \ - testFns.emplace_back(fn); \ - return 1; \ - }(); +// Where `TEST_CASE` macros will store generated test cases: +#define TEST_CASES_STORAGE mainTestCases diff --git a/hyprtester/src/tests/main/window.cpp b/hyprtester/src/tests/main/window.cpp index 32673d280..4a4da0ccb 100644 --- a/hyprtester/src/tests/main/window.cpp +++ b/hyprtester/src/tests/main/window.cpp @@ -14,8 +14,7 @@ #include "../shared.hpp" #include "tests.hpp" -static int ret = 0; - +// TODO: seems redundant, can just use `Tests::spawnKitty`? static bool spawnKitty(const std::string& class_, const std::vector& args = {}) { NLog::log("{}Spawning {}", Colors::YELLOW, class_); if (!Tests::spawnKitty(class_, args)) { @@ -52,113 +51,98 @@ static std::string getWindowAddress(const std::string& winInfo) { auto pos2 = winInfo.find(" -> "); if (pos == std::string::npos || pos2 == std::string::npos) { NLog::log("{}Wrong window info", Colors::RED); - ret = 1; return "Wrong window info"; } return winInfo.substr(pos + 7, pos2 - pos - 7); } -static void testSwapWindow() { - NLog::log("{}Testing swapwindow", Colors::GREEN); - +TEST_CASE(swapWindow) { // test on workspace "swapwindow" NLog::log("{}Switching to workspace \"swapwindow\"", Colors::YELLOW); - getFromSocket("/dispatch workspace name:swapwindow"); + getFromSocket("/dispatch hl.dsp.focus({ workspace = 'name:swapwindow' })"); if (!Tests::spawnKitty("kitty_A")) { - ret = 1; - return; + FAIL_TEST("Could not spawn kitty"); } if (!Tests::spawnKitty("kitty_B")) { - ret = 1; - return; + FAIL_TEST("Could not spawn kitty"); } NLog::log("{}Expecting 2 windows", Colors::YELLOW); - EXPECT(Tests::windowCount(), 2); + ASSERT(Tests::windowCount(), 2); // Test swapwindow by direction { - getFromSocket("/dispatch focuswindow class:kitty_A"); - auto pos = Tests::getWindowAttribute(getFromSocket("/activewindow"), "at:"); + getFromSocket("/dispatch hl.dsp.focus({ window = 'class:kitty_A' })"); + auto pos = "at: " + Tests::getAttribute(getFromSocket("/activewindow"), "at"); NLog::log("{}Testing kitty_A {}, swapwindow with direction 'r'", Colors::YELLOW, pos); - OK(getFromSocket("/dispatch swapwindow r")); - OK(getFromSocket("/dispatch focuswindow class:kitty_B")); + OK(getFromSocket("/dispatch hl.dsp.window.swap({ direction = 'right' })")); + OK(getFromSocket("/dispatch hl.dsp.focus({ window = 'class:kitty_B' })")); EXPECT_CONTAINS(getFromSocket("/activewindow"), std::format("{}", pos)); } // Test swapwindow by class { - getFromSocket("/dispatch focuswindow class:kitty_A"); - auto pos = Tests::getWindowAttribute(getFromSocket("/activewindow"), "at:"); + getFromSocket("/dispatch hl.dsp.focus({ window = 'class:kitty_A' })"); + auto pos = "at: " + Tests::getAttribute(getFromSocket("/activewindow"), "at"); NLog::log("{}Testing kitty_A {}, swapwindow with class:kitty_B", Colors::YELLOW, pos); - OK(getFromSocket("/dispatch swapwindow class:kitty_B")); - OK(getFromSocket("/dispatch focuswindow class:kitty_B")); + OK(getFromSocket("/dispatch hl.dsp.window.swap({ target = 'class:kitty_B' })")); + OK(getFromSocket("/dispatch hl.dsp.focus({ window = 'class:kitty_B' })")); EXPECT_CONTAINS(getFromSocket("/activewindow"), std::format("{}", pos)); } // Test swapwindow by address { - getFromSocket("/dispatch focuswindow class:kitty_B"); + getFromSocket("/dispatch hl.dsp.focus({ window = 'class:kitty_B' })"); auto addr = getWindowAddress(getFromSocket("/activewindow")); - getFromSocket("/dispatch focuswindow class:kitty_A"); - auto pos = Tests::getWindowAttribute(getFromSocket("/activewindow"), "at:"); + getFromSocket("/dispatch hl.dsp.focus({ window = 'class:kitty_A' })"); + auto pos = "at: " + Tests::getAttribute(getFromSocket("/activewindow"), "at"); NLog::log("{}Testing kitty_A {}, swapwindow with address:0x{}(kitty_B)", Colors::YELLOW, pos, addr); - OK(getFromSocket(std::format("/dispatch swapwindow address:0x{}", addr))); - OK(getFromSocket(std::format("/dispatch focuswindow address:0x{}", addr))); + OK(getFromSocket(std::format("/dispatch hl.dsp.window.swap({{ target = 'address:0x{}' }})", addr))); + OK(getFromSocket(std::format("/dispatch hl.dsp.focus({{ window = 'address:0x{}' }})", addr))); EXPECT_CONTAINS(getFromSocket("/activewindow"), std::format("{}", pos)); } NLog::log("{}Testing swapwindow with fullscreen. Expecting to fail", Colors::YELLOW); { - OK(getFromSocket("/dispatch fullscreen")); + OK(getFromSocket("/dispatch hl.dsp.window.fullscreen()")); - auto str = getFromSocket("/dispatch swapwindow l"); + auto str = getFromSocket("/dispatch hl.dsp.window.swap({ direction = 'left' })"); EXPECT_CONTAINS(str, "Can't swap fullscreen window"); - OK(getFromSocket("/dispatch fullscreen")); + OK(getFromSocket("/dispatch hl.dsp.window.fullscreen()")); } NLog::log("{}Testing swapwindow with different workspace", Colors::YELLOW); { - getFromSocket("/dispatch focuswindow class:kitty_B"); + getFromSocket("/dispatch hl.dsp.focus({ window = 'class:kitty_B' })"); auto addr = getWindowAddress(getFromSocket("/activewindow")); - auto ws = Tests::getWindowAttribute(getFromSocket("/activewindow"), "workspace:"); + auto ws = "workspace: " + Tests::getAttribute(getFromSocket("/activewindow"), "workspace"); NLog::log("{}Sending address:0x{}(kitty_B) to workspace \"swapwindow2\"", Colors::YELLOW, addr); - OK(getFromSocket("/dispatch movetoworkspacesilent name:swapwindow2")); - OK(getFromSocket(std::format("/dispatch swapwindow address:0x{}", addr))); - getFromSocket("/dispatch focuswindow class:kitty_B"); + OK(getFromSocket("/dispatch hl.dsp.window.move({ workspace = 'name:swapwindow2', follow = false })")); + OK(getFromSocket(std::format("/dispatch hl.dsp.window.swap({{ target = 'address:0x{}' }})", addr))); + getFromSocket("/dispatch hl.dsp.focus({ window = 'class:kitty_B' })"); EXPECT_CONTAINS(getFromSocket("/activewindow"), std::format("{}", ws)); } - - // kill all - NLog::log("{}Killing all windows", Colors::YELLOW); - Tests::killAllWindows(); - - NLog::log("{}Expecting 0 windows", Colors::YELLOW); - EXPECT(Tests::windowCount(), 0); } -static void testGroupRules() { - NLog::log("{}Testing group window rules", Colors::YELLOW); - - OK(getFromSocket("/keyword general:border_size 8")); - OK(getFromSocket("/keyword workspace w[tv1], bordersize:0")); - OK(getFromSocket("/keyword workspace f[1], bordersize:0")); - OK(getFromSocket("/keyword windowrule match:workspace w[tv1], border_size 0")); - OK(getFromSocket("/keyword windowrule match:workspace f[1], border_size 0")); +TEST_CASE(windowGroupRules) { + OK(getFromSocket("/eval hl.config({ general = { border_size = 8 } })")); + OK(getFromSocket("/eval hl.workspace_rule({ workspace = 'w[tv1]', border_size = 0 })")); + OK(getFromSocket("/eval hl.workspace_rule({ workspace = 'f[1]', border_size = 0 })")); + OK(getFromSocket("/eval hl.window_rule({ match = { workspace = 'w[tv1]' }, border_size = 0 })")); + OK(getFromSocket("/eval hl.window_rule({ match = { workspace = 'f[1]' }, border_size = 0 })")); if (!Tests::spawnKitty("kitty_A")) { - ret = 1; - return; + FAIL_TEST("Could not spawn kitty"); } { @@ -167,8 +151,7 @@ static void testGroupRules() { } if (!Tests::spawnKitty("kitty_B")) { - ret = 1; - return; + FAIL_TEST("Could not spawn kitty"); } { @@ -176,17 +159,17 @@ static void testGroupRules() { EXPECT_CONTAINS(str, "8"); } - OK(getFromSocket("/dispatch focuswindow class:kitty_A")); - OK(getFromSocket("/dispatch togglegroup")); - OK(getFromSocket("/dispatch focuswindow class:kitty_B")); - OK(getFromSocket("/dispatch moveintogroup l")); + OK(getFromSocket("/dispatch hl.dsp.focus({ window = 'class:kitty_A' })")); + OK(getFromSocket("/dispatch hl.dsp.group.toggle()")); + OK(getFromSocket("/dispatch hl.dsp.focus({ window = 'class:kitty_B' })")); + OK(getFromSocket("/dispatch hl.dsp.window.move({ into_group = 'left' })")); { auto str = getFromSocket("/getprop active border_size"); EXPECT_CONTAINS(str, "0"); } - OK(getFromSocket("/dispatch changegroupactive f")); + OK(getFromSocket("/dispatch hl.dsp.group.next()")); { auto str = getFromSocket("/getprop active border_size"); @@ -194,26 +177,22 @@ static void testGroupRules() { } if (!Tests::spawnKitty("kitty_C")) { - ret = 1; - return; + FAIL_TEST("Could not spawn kitty"); } - OK(getFromSocket("/dispatch moveoutofgroup r")); + OK(getFromSocket("/dispatch hl.dsp.window.move({ out_of_group = 'right' })")); { auto str = getFromSocket("/getprop active border_size"); EXPECT_CONTAINS(str, "8"); } - - OK(getFromSocket("/reload")); - Tests::killAllWindows(); } static bool isActiveWindow(const std::string& class_, char fullscreen = '0', bool log = true) { std::string activeWin = getFromSocket("/activewindow"); - auto winClass = Tests::getWindowAttribute(activeWin, "class:"); - auto winFullscreen = Tests::getWindowAttribute(activeWin, "fullscreen:").back(); - if (winClass.substr(strlen("class: ")) == class_ && winFullscreen == fullscreen) + auto winClass = Tests::getAttribute(activeWin, "class"); + auto winFullscreen = Tests::getAttribute(activeWin, "fullscreen").back(); + if (winClass == class_ && winFullscreen == fullscreen) return true; else { if (log) @@ -236,112 +215,99 @@ static bool waitForActiveWindow(const std::string& class_, char fullscreen = '0' /// Tests behavior of a window being focused when on that window's workspace /// another fullscreen window exists. -static bool testWindowFocusOnFullscreenConflict() { +TEST_CASE(windowFocusOnFullscreenConflict) { if (!spawnKitty("kitty_A")) - return false; + FAIL_TEST("Could not spawn kitty"); if (!spawnKitty("kitty_B")) - return false; + FAIL_TEST("Could not spawn kitty"); - OK(getFromSocket("/keyword misc:focus_on_activate true")); + OK(getFromSocket("/eval hl.config({ misc = { focus_on_activate = true } })")); // Unfullscreen on conflict { - OK(getFromSocket("/keyword misc:on_focus_under_fullscreen 2")); + OK(getFromSocket("/eval hl.config({ misc = { on_focus_under_fullscreen = 2 } })")); - OK(getFromSocket("/dispatch focuswindow class:kitty_A")); - OK(getFromSocket("/dispatch fullscreen 0 set")); + OK(getFromSocket("/dispatch hl.dsp.focus({ window = 'class:kitty_A' })")); + OK(getFromSocket("/dispatch hl.dsp.window.fullscreen({ mode = 'fullscreen', action = 'set' })")); EXPECT(isActiveWindow("kitty_A", '2'), true); // Dispatch-focus the same window - OK(getFromSocket("/dispatch focuswindow class:kitty_A")); + OK(getFromSocket("/dispatch hl.dsp.focus({ window = 'class:kitty_A' })")); EXPECT(isActiveWindow("kitty_A", '2'), true); // Dispatch-focus a different window - OK(getFromSocket("/dispatch focuswindow class:kitty_B")); + OK(getFromSocket("/dispatch hl.dsp.focus({ window = 'class:kitty_B' })")); EXPECT(isActiveWindow("kitty_B", '0'), true); // Make a window that will request focus const std::string removeToActivate = spawnKittyActivating(); if (removeToActivate.empty()) - return false; - OK(getFromSocket("/dispatch focuswindow class:kitty_A")); - OK(getFromSocket("/dispatch fullscreen 0 set")); + FAIL_TEST("Could not spawn kitty_activating"); + OK(getFromSocket("/dispatch hl.dsp.focus({ window = 'class:kitty_A' })")); + OK(getFromSocket("/dispatch hl.dsp.window.fullscreen({ mode = 'fullscreen', action = 'set' })")); EXPECT(isActiveWindow("kitty_A", '2'), true); std::filesystem::remove(removeToActivate); EXPECT(waitForActiveWindow("kitty_activating", '0'), true); - OK(getFromSocket("/dispatch forcekillactive")); + OK(getFromSocket("/dispatch hl.dsp.window.kill()")); Tests::waitUntilWindowsN(2); } // Take over on conflict { - OK(getFromSocket("/keyword misc:on_focus_under_fullscreen 1")); + OK(getFromSocket("/eval hl.config({ misc = { on_focus_under_fullscreen = 1 } })")); - OK(getFromSocket("/dispatch focuswindow class:kitty_A")); - OK(getFromSocket("/dispatch fullscreen 0 set")); + OK(getFromSocket("/dispatch hl.dsp.focus({ window = 'class:kitty_A' })")); + OK(getFromSocket("/dispatch hl.dsp.window.fullscreen({ mode = 'fullscreen', action = 'set' })")); EXPECT(isActiveWindow("kitty_A", '2'), true); // Dispatch-focus the same window - OK(getFromSocket("/dispatch focuswindow class:kitty_A")); + OK(getFromSocket("/dispatch hl.dsp.focus({ window = 'class:kitty_A' })")); EXPECT(isActiveWindow("kitty_A", '2'), true); // Dispatch-focus a different window - OK(getFromSocket("/dispatch focuswindow class:kitty_B")); + OK(getFromSocket("/dispatch hl.dsp.focus({ window = 'class:kitty_B' })")); EXPECT(isActiveWindow("kitty_B", '2'), true); - OK(getFromSocket("/dispatch fullscreenstate 0 0")); + OK(getFromSocket("/dispatch hl.dsp.window.fullscreen_state({ internal = 0, client = 0, action = 'set' })")); // Make a window that will request focus const std::string removeToActivate = spawnKittyActivating(); if (removeToActivate.empty()) - return false; - OK(getFromSocket("/dispatch focuswindow class:kitty_A")); - OK(getFromSocket("/dispatch fullscreen 0 set")); + FAIL_TEST("Could not spawn kitty_activating"); + OK(getFromSocket("/dispatch hl.dsp.focus({ window = 'class:kitty_A' })")); + OK(getFromSocket("/dispatch hl.dsp.window.fullscreen({ mode = 'fullscreen', action = 'set' })")); EXPECT(isActiveWindow("kitty_A", '2'), true); std::filesystem::remove(removeToActivate); EXPECT(waitForActiveWindow("kitty_activating", '2'), true); - OK(getFromSocket("/dispatch forcekillactive")); + OK(getFromSocket("/dispatch hl.dsp.window.kill()")); Tests::waitUntilWindowsN(2); } // Keep the old focus on conflict { - OK(getFromSocket("/keyword misc:on_focus_under_fullscreen 0")); + OK(getFromSocket("/eval hl.config({ misc = { on_focus_under_fullscreen = 0 } })")); - OK(getFromSocket("/dispatch focuswindow class:kitty_A")); - OK(getFromSocket("/dispatch fullscreen 0 set")); + OK(getFromSocket("/dispatch hl.dsp.focus({ window = 'class:kitty_A' })")); + OK(getFromSocket("/dispatch hl.dsp.window.fullscreen({ mode = 'fullscreen', action = 'set' })")); EXPECT(isActiveWindow("kitty_A", '2'), true); // Dispatch-focus the same window - OK(getFromSocket("/dispatch focuswindow class:kitty_A")); + OK(getFromSocket("/dispatch hl.dsp.focus({ window = 'class:kitty_A' })")); EXPECT(isActiveWindow("kitty_A", '2'), true); // Make a window that will request focus - the setting is treated normally const std::string removeToActivate = spawnKittyActivating(); if (removeToActivate.empty()) - return false; - OK(getFromSocket("/dispatch focuswindow class:kitty_A")); - OK(getFromSocket("/dispatch fullscreen 0 set")); + FAIL_TEST("Could not spawn kitty_activating"); + OK(getFromSocket("/dispatch hl.dsp.focus({ window = 'class:kitty_A' })")); + OK(getFromSocket("/dispatch hl.dsp.window.fullscreen({ mode = 'fullscreen', action = 'set' })")); EXPECT(isActiveWindow("kitty_A", '2'), true); std::filesystem::remove(removeToActivate); EXPECT(waitForActiveWindow("kitty_A", '2'), true); } - - NLog::log("{}Reloading config", Colors::YELLOW); - OK(getFromSocket("/reload")); - - NLog::log("{}Killing all windows", Colors::YELLOW); - Tests::killAllWindows(); - - NLog::log("{}Expecting 0 windows", Colors::YELLOW); - EXPECT(Tests::windowCount(), 0); - - return true; } -static void testMaximizeSize() { - NLog::log("{}Testing maximize size", Colors::GREEN); - - EXPECT(spawnKitty("kitty_A"), true); +TEST_CASE(windowMaximizeSize) { + ASSERT(spawnKitty("kitty_A"), true); // check kitty properties. Maximizing shouldnt change its size { @@ -351,7 +317,7 @@ static void testMaximizeSize() { EXPECT(str.contains("fullscreen: 0"), true); } - OK(getFromSocket("/dispatch fullscreen 1")); + OK(getFromSocket("/dispatch hl.dsp.window.fullscreen({ mode = 'maximized' })")); { auto str = getFromSocket("/clients"); @@ -359,53 +325,37 @@ static void testMaximizeSize() { EXPECT(str.contains("size: 1876,1036"), true); EXPECT(str.contains("fullscreen: 1"), true); } - - NLog::log("{}Killing all windows", Colors::YELLOW); - Tests::killAllWindows(); - - NLog::log("{}Expecting 0 windows", Colors::YELLOW); - EXPECT(Tests::windowCount(), 0); } -static void testFloatingFocusOnFullscreen() { - NLog::log("{}Testing floating focus on fullscreen", Colors::GREEN); +TEST_CASE(floatingFocusOnFullscreen) { + ASSERT(spawnKitty("kitty_A"), true); + OK(getFromSocket("/dispatch hl.dsp.window.float({ action = 'toggle' })")); - EXPECT(spawnKitty("kitty_A"), true); - OK(getFromSocket("/dispatch togglefloating")); + ASSERT(spawnKitty("kitty_B"), true); + OK(getFromSocket("/dispatch hl.dsp.window.fullscreen({ mode = 'maximized' })")); - EXPECT(spawnKitty("kitty_B"), true); - OK(getFromSocket("/dispatch fullscreen 1")); + OK(getFromSocket("/dispatch hl.dsp.window.cycle_next()")); - OK(getFromSocket("/dispatch cyclenext")); - - OK(getFromSocket("/dispatch plugin:test:floating_focus_on_fullscreen")); - - NLog::log("{}Killing all windows", Colors::YELLOW); - Tests::killAllWindows(); - - NLog::log("{}Expecting 0 windows", Colors::YELLOW); - EXPECT(Tests::windowCount(), 0); + OK(getFromSocket("/eval hl.plugin.test.floating_focus_on_fullscreen()")); } -static void testGroupFallbackFocus() { - NLog::log("{}Testing group fallback focus", Colors::GREEN); +TEST_CASE(groupFallbackFocus) { + ASSERT(spawnKitty("kitty_A"), true); - EXPECT(spawnKitty("kitty_A"), true); + OK(getFromSocket("/dispatch hl.dsp.group.toggle()")); - OK(getFromSocket("/dispatch togglegroup")); - - EXPECT(spawnKitty("kitty_B"), true); - EXPECT(spawnKitty("kitty_C"), true); - EXPECT(spawnKitty("kitty_D"), true); + ASSERT(spawnKitty("kitty_B"), true); + ASSERT(spawnKitty("kitty_C"), true); + ASSERT(spawnKitty("kitty_D"), true); { auto str = getFromSocket("/activewindow"); EXPECT(str.contains("class: kitty_D"), true); } - OK(getFromSocket("/dispatch focuswindow class:kitty_B")); - OK(getFromSocket("/dispatch focuswindow class:kitty_D")); - OK(getFromSocket("/dispatch killactive")); + OK(getFromSocket("/dispatch hl.dsp.focus({ window = 'class:kitty_B' })")); + OK(getFromSocket("/dispatch hl.dsp.focus({ window = 'class:kitty_D' })")); + OK(getFromSocket("/dispatch hl.dsp.window.kill()")); Tests::waitUntilWindowsN(3); @@ -414,61 +364,47 @@ static void testGroupFallbackFocus() { auto str = getFromSocket("/activewindow"); EXPECT(str.contains("class: kitty_B"), true); } - - NLog::log("{}Killing all windows", Colors::YELLOW); - Tests::killAllWindows(); - - NLog::log("{}Expecting 0 windows", Colors::YELLOW); - EXPECT(Tests::windowCount(), 0); } -static void testBringActiveToTopMouseMovement() { - NLog::log("{}Testing bringactivetotop mouse movement", Colors::GREEN); +TEST_CASE(bringActiveToTopMouseMovement) { + OK(getFromSocket("/eval hl.config({ input = { follow_mouse = 2 } })")); + OK(getFromSocket("/eval hl.config({ input = { float_switch_override_focus = 0 } })")); - Tests::killAllWindows(); - OK(getFromSocket("/keyword input:follow_mouse 2")); - OK(getFromSocket("/keyword input:float_switch_override_focus 0")); + ASSERT(spawnKitty("a"), true); + OK(getFromSocket("/dispatch hl.dsp.window.float({ action = 'set' })")); + OK(getFromSocket("/dispatch hl.dsp.window.move({ x = 500, y = 300 })")); + OK(getFromSocket("/dispatch hl.dsp.window.resize({ x = 400, y = 400 })")); - EXPECT(spawnKitty("a"), true); - OK(getFromSocket("/dispatch setfloating")); - OK(getFromSocket("/dispatch movewindowpixel exact 500 300,activewindow")); - OK(getFromSocket("/dispatch resizewindowpixel exact 400 400,activewindow")); - - EXPECT(spawnKitty("b"), true); - OK(getFromSocket("/dispatch setfloating")); - OK(getFromSocket("/dispatch movewindowpixel exact 500 300,activewindow")); - OK(getFromSocket("/dispatch resizewindowpixel exact 400 400,activewindow")); + ASSERT(spawnKitty("b"), true); + OK(getFromSocket("/dispatch hl.dsp.window.float({ action = 'set' })")); + OK(getFromSocket("/dispatch hl.dsp.window.move({ x = 500, y = 300 })")); + OK(getFromSocket("/dispatch hl.dsp.window.resize({ x = 400, y = 400 })")); auto getTopWindow = []() -> std::string { auto clients = getFromSocket("/clients"); return (clients.rfind("class: a") > clients.rfind("class: b")) ? "a" : "b"; }; - EXPECT(getTopWindow(), std::string("b")); - OK(getFromSocket("/dispatch movecursor 700 500")); + ASSERT(getTopWindow(), std::string("b")); + OK(getFromSocket("/dispatch hl.dsp.cursor.move({ x = 700, y = 500 })")); - OK(getFromSocket("/dispatch focuswindow class:a")); - EXPECT_CONTAINS(getFromSocket("/activewindow"), "class: a"); + OK(getFromSocket("/dispatch hl.dsp.focus({ window = 'class:a' })")); + ASSERT_CONTAINS(getFromSocket("/activewindow"), "class: a"); - OK(getFromSocket("/dispatch bringactivetotop")); - EXPECT(getTopWindow(), std::string("a")); + OK(getFromSocket("/dispatch hl.dsp.window.bring_to_top()")); + ASSERT(getTopWindow(), std::string("a")); - OK(getFromSocket("/dispatch plugin:test:click 272,1")); - OK(getFromSocket("/dispatch plugin:test:click 272,0")); + OK(getFromSocket("/eval hl.plugin.test.click(272, 1)")); + OK(getFromSocket("/eval hl.plugin.test.click(272, 0)")); - EXPECT(getTopWindow(), std::string("a")); - - Tests::killAllWindows(); + ASSERT(getTopWindow(), std::string("a")); } -static void testInitialFloatSize() { - NLog::log("{}Testing initial float size", Colors::GREEN); +TEST_CASE(initialFloatSize) { + OK(getFromSocket("/eval hl.window_rule({ match = { class = 'kitty' }, float = true })")); + OK(getFromSocket("/eval hl.config({ input = { float_switch_override_focus = 0 } })")); - Tests::killAllWindows(); - OK(getFromSocket("/keyword windowrule match:class kitty, float yes")); - OK(getFromSocket("/keyword input:float_switch_override_focus 0")); - - EXPECT(spawnKitty("kitty"), true); + ASSERT(spawnKitty("kitty"), true); { // Kitty by default opens as 640x400, if this changes this test will break @@ -480,7 +416,7 @@ static void testInitialFloatSize() { Tests::killAllWindows(); - OK(getFromSocket("/dispatch exec [float yes]kitty")); + OK(getFromSocket("/dispatch hl.dsp.exec_cmd('kitty', { float = true })")); Tests::waitUntilWindowsN(1); @@ -490,83 +426,66 @@ static void testInitialFloatSize() { EXPECT(str.contains("size: 640,400"), true); EXPECT(str.contains("floating: 1"), true); } - - Tests::killAllWindows(); } /// Tests that the `focus_on_activate` effect of window rules always overrides /// the `misc:focus_on_activate` variable. -static bool testWindowRuleFocusOnActivate() { - OK(getFromSocket("/reload")); - +TEST_CASE(windowRuleFocusOnActivate) { if (!spawnKitty("kitty_default")) { - NLog::log("{}Error: failed to spawn kitty", Colors::RED); - return false; + FAIL_TEST("Could not spawn kitty"); } // Do not focus anyone automatically - ///////////OK(getFromSocket("/keyword windowrule match:class .*, no_initial_focus true")); + // TODO: this looks like a bug: the following line should not be commented out + ///////////OK(getFromSocket("/eval hl.window_rule({ match = { class = '.*' }, no_initial_focus = true })")); // `focus_on_activate off` takes over { - OK(getFromSocket("/keyword misc:focus_on_activate true")); - OK(getFromSocket("/keyword windowrule match:class kitty_antifocus, focus_on_activate off")); + OK(getFromSocket("/eval hl.config({ misc = { focus_on_activate = true } })")); + OK(getFromSocket("/eval hl.window_rule({ match = { class = 'kitty_antifocus' }, focus_on_activate = false })")); const std::string removeToActivate = spawnKittyActivating("kitty_antifocus"); if (removeToActivate.empty()) { - return false; + FAIL_TEST("Could not spawn kitty_antifocus"); } - EXPECT(waitForActiveWindow("kitty_antifocus"), true); - OK(getFromSocket("/dispatch focuswindow class:kitty_default")); - EXPECT(isActiveWindow("kitty_default"), true); + ASSERT(waitForActiveWindow("kitty_antifocus"), true); + OK(getFromSocket("/dispatch hl.dsp.focus({ window = 'class:kitty_default' })")); + ASSERT(isActiveWindow("kitty_default"), true); std::filesystem::remove(removeToActivate); // The focus should NOT transition, since the window rule explicitly forbids that - EXPECT(waitForActiveWindow("kitty_antifocus", '0', false), false); + ASSERT(waitForActiveWindow("kitty_antifocus", '0', false), false); } // `focus_on_activate on` takes over { - OK(getFromSocket("/keyword misc:focus_on_activate false")); - OK(getFromSocket("/keyword windowrule match:class kitty_superfocus, focus_on_activate on")); + OK(getFromSocket("/eval hl.config({ misc = { focus_on_activate = false } })")); + OK(getFromSocket("/eval hl.window_rule({ match = { class = 'kitty_superfocus' }, focus_on_activate = true })")); const std::string removeToActivate = spawnKittyActivating("kitty_superfocus"); if (removeToActivate.empty()) { - return false; + FAIL_TEST("Could not spawn kitty_superfocus"); } - EXPECT(waitForActiveWindow("kitty_superfocus"), true); - OK(getFromSocket("/dispatch focuswindow class:kitty_default")); - EXPECT(isActiveWindow("kitty_default"), true); + ASSERT(waitForActiveWindow("kitty_superfocus"), true); + OK(getFromSocket("/dispatch hl.dsp.focus({ window = 'class:kitty_default' })")); + ASSERT(isActiveWindow("kitty_default"), true); std::filesystem::remove(removeToActivate); // Now that we requested activation, the focus SHOULD transition to kitty_superfocus, according to the window rule - EXPECT(waitForActiveWindow("kitty_superfocus"), true); + ASSERT(waitForActiveWindow("kitty_superfocus"), true); } - - NLog::log("{}Reloading config", Colors::YELLOW); - OK(getFromSocket("/reload")); - - NLog::log("{}Killing all windows", Colors::YELLOW); - Tests::killAllWindows(); - - NLog::log("{}Expecting 0 windows", Colors::YELLOW); - EXPECT(Tests::windowCount(), 0); - - return true; } // tests if a pinned window contains the valid workspace after change -static bool testPinnedWorkspacesValid() { - OK(getFromSocket("/reload")); - getFromSocket("/dispatch workspace 1337"); +TEST_CASE(pinnedWorkspacesValid) { + getFromSocket("/dispatch hl.dsp.focus({ workspace = '1337' })"); if (!spawnKitty("kitty")) { - NLog::log("{}Error: failed to spawn kitty", Colors::RED); - return false; + FAIL_TEST("Could not spawn kitty"); } - OK(getFromSocket("/dispatch setfloating class:kitty")); - OK(getFromSocket("/dispatch pin class:kitty")); + OK(getFromSocket("/dispatch hl.dsp.window.float({ action = 'set', window = 'class:kitty' })")); + OK(getFromSocket("/dispatch hl.dsp.window.pin({ action = 'toggle', window = 'class:kitty' })")); { auto str = getFromSocket("/activewindow"); @@ -574,7 +493,7 @@ static bool testPinnedWorkspacesValid() { EXPECT(str.contains("pinned: 1"), true); } - getFromSocket("/dispatch workspace 1338"); + getFromSocket("/dispatch hl.dsp.focus({ workspace = '1338' })"); { auto str = getFromSocket("/activewindow"); @@ -582,38 +501,23 @@ static bool testPinnedWorkspacesValid() { EXPECT(str.contains("pinned: 1"), true); } - OK(getFromSocket("/dispatch settiled class:kitty")) + OK(getFromSocket("/dispatch hl.dsp.window.float({ action = 'unset', window = 'class:kitty' })")); { auto str = getFromSocket("/activewindow"); EXPECT(str.contains("workspace: 1338"), true); EXPECT(str.contains("pinned: 0"), true); } - - NLog::log("{}Reloading config", Colors::YELLOW); - OK(getFromSocket("/reload")); - - NLog::log("{}Killing all windows", Colors::YELLOW); - Tests::killAllWindows(); - - NLog::log("{}Expecting 0 windows", Colors::YELLOW); - EXPECT(Tests::windowCount(), 0); - - return true; } -static bool testWindowRuleWorkspaceEmpty() { - NLog::log("{}Testing windowrule workspace empty", Colors::YELLOW); - OK(getFromSocket("/reload")); +TEST_CASE(windowruleWorkspaceEmpty) { + OK(getFromSocket("/eval hl.window_rule({ match = { class = 'kitty_A' }, workspace = 'empty' })")); + OK(getFromSocket("/eval hl.window_rule({ match = { class = 'kitty_B' }, workspace = 'emptyn' })")); - OK(getFromSocket("/keyword windowrule match:class kitty_A, workspace empty")); - OK(getFromSocket("/keyword windowrule match:class kitty_B, workspace emptyn")); - - getFromSocket("/dispatch workspace 3"); + getFromSocket("/dispatch hl.dsp.focus({ workspace = '3' })"); if (!spawnKitty("kitty")) { - NLog::log("{}Error: failed to spawn kitty", Colors::RED); - return false; + FAIL_TEST("Could not spawn kitty"); } { @@ -622,8 +526,7 @@ static bool testWindowRuleWorkspaceEmpty() { } if (!spawnKitty("kitty_A")) { - NLog::log("{}Error: failed to spawn kitty", Colors::RED); - return false; + FAIL_TEST("Could not spawn kitty"); } { @@ -631,81 +534,68 @@ static bool testWindowRuleWorkspaceEmpty() { EXPECT(str.contains("workspace: 1"), true); } - getFromSocket("/dispatch workspace 3"); + getFromSocket("/dispatch hl.dsp.focus({ workspace = '3' })"); if (!spawnKitty("kitty_B")) { - NLog::log("{}Error: failed to spawn kitty", Colors::RED); - return false; + FAIL_TEST("Could not spawn kitty"); } { auto str = getFromSocket("/activewindow"); EXPECT(str.contains("workspace: 4"), true); } - - Tests::killAllWindows(); - - return true; } -static bool testContentRules() { - NLog::log("{}Testing content window rules", Colors::YELLOW); - +TEST_CASE(contentWindowRules) { // kill me PLEASE - OK(getFromSocket("/keyword windowrule match:class kitty_content_string, content game")); - OK(getFromSocket("/keyword windowrule match:class kitty_content_numbers, content 3")); - OK(getFromSocket("/keyword windowrule match:content game, border_size 10")); - OK(getFromSocket("/keyword windowrule match:content 3, opacity 0.5")); + OK(getFromSocket("/eval hl.window_rule({ match = { class = 'kitty_content_string' }, content = 'game' })")); + OK(getFromSocket("/eval hl.window_rule({ match = { class = 'kitty_content_numbers' }, content = '3' })")); + OK(getFromSocket("/eval hl.window_rule({ match = { content = 'game' }, border_size = 10 })")); + OK(getFromSocket("/eval hl.window_rule({ match = { content = '3' }, opacity = '0.5' })")); + +#define TEST_PROPS() \ + EXPECT_CONTAINS(getFromSocket("/getprop active border_size"), "10"); \ + EXPECT_CONTAINS(getFromSocket("/getprop active opacity"), "0.5"); - const auto testProps = []() { - EXPECT_CONTAINS(getFromSocket("/getprop active border_size"), "10"); - EXPECT_CONTAINS(getFromSocket("/getprop active opacity"), "0.5"); - }; if (!spawnKitty("kitty_content_string")) - return false; + FAIL_TEST("Could not spawn kitty_content_string"); waitForActiveWindow("kitty_content_string"); - testProps(); + TEST_PROPS(); Tests::killAllWindows(); - EXPECT(Tests::windowCount(), 0); + ASSERT(Tests::windowCount(), 0); if (!spawnKitty("kitty_content_numbers")) - return false; + FAIL_TEST("Could not spawn kitty_content_numbers"); waitForActiveWindow("kitty_content_numbers"); - testProps(); + TEST_PROPS(); Tests::killAllWindows(); - EXPECT(Tests::windowCount(), 0); - return true; + ASSERT(Tests::windowCount(), 0); + +#undef TEST_PROPS } -static bool test14038() { - NLog::log("{}Testing #14038 crash", Colors::YELLOW); - +TEST_CASE(issue14038) { if (!spawnKitty("kitty_14038")) - return false; + FAIL_TEST("Could not spawn kitty"); - OK(getFromSocket("/dispatch movetoworkspacesilent special:a,class:kitty_14038")); - OK(getFromSocket("/dispatch togglefloating class:kitty_14038")); - OK(getFromSocket("/dispatch pin class:kitty_14038")); - OK(getFromSocket("/dispatch togglefloating class:kitty_14038")); + OK(getFromSocket("/dispatch hl.dsp.window.move({ workspace = 'special:a', follow = false, window = 'class:kitty_14038' })")); + OK(getFromSocket("/dispatch hl.dsp.window.float({ action = 'toggle', window = 'class:kitty_14038' })")); + OK(getFromSocket("/dispatch hl.dsp.window.pin({ action = 'toggle', window = 'class:kitty_14038' })")); + OK(getFromSocket("/dispatch hl.dsp.window.float({ action = 'toggle', window = 'class:kitty_14038' })")); // this should not crash hyprland. If we are alive, we good. - - Tests::killAllWindows(); - EXPECT(Tests::windowCount(), 0); - return true; } -static bool test() { - NLog::log("{}Testing windows", Colors::GREEN); - +// TODO: decompose this into multiple test cases +TEST_CASE(windows) { // test on workspace "window" NLog::log("{}Switching to workspace `window`", Colors::YELLOW); - getFromSocket("/dispatch workspace name:window"); + getFromSocket("/dispatch hl.dsp.focus({ workspace = 'name:window' })"); if (!spawnKitty("kitty_A")) - return false; + FAIL_TEST("Could not spawn kitty"); // check kitty properties. One kitty should take the entire screen, as this is smart gaps NLog::log("{}Expecting kitty_A to take up the whole screen", Colors::YELLOW); @@ -746,22 +636,22 @@ static bool test() { const int WIDTH2 = calculateFinalWidth(geomBoxWidthB_R2, false); const int WIDTH_A_FINAL = calculateFinalWidth(geomBoxWidthA_R2, true); - OK(getFromSocket("/keyword dwindle:default_split_ratio 1.25")); + OK(getFromSocket("/eval hl.config({ dwindle = { default_split_ratio = 1.25 } })")); if (!spawnKitty("kitty_B")) - return false; + FAIL_TEST("Could not spawn kitty"); NLog::log("{}Expecting kitty_B size: {},{}", Colors::YELLOW, WIDTH1, HEIGHT); EXPECT_CONTAINS(getFromSocket("/activewindow"), std::format("size: {},{}", WIDTH1, HEIGHT)); - OK(getFromSocket("/dispatch killwindow activewindow")); + OK(getFromSocket("/dispatch hl.dsp.window.kill({ window = 'activewindow' })")); Tests::waitUntilWindowsN(1); NLog::log("{}Inverting the split ratio", Colors::YELLOW); - OK(getFromSocket("/keyword dwindle:default_split_ratio 0.75")); + OK(getFromSocket("/eval hl.config({ dwindle = { default_split_ratio = 0.75 } })")); if (!spawnKitty("kitty_B")) - return false; + FAIL_TEST("Could not spawn kitty"); try { NLog::log("{}Expecting kitty_B size: {},{}", Colors::YELLOW, WIDTH2, HEIGHT); @@ -777,7 +667,7 @@ static bool test() { EXPECT_MAX_DELTA(std::stoi(std::string{sizes[1]}), HEIGHT, 2); } - OK(getFromSocket("/dispatch focuswindow class:kitty_A")); + OK(getFromSocket("/dispatch hl.dsp.focus({ window = 'class:kitty_A' })")); NLog::log("{}Expecting kitty_A size: {},{}", Colors::YELLOW, WIDTH_A_FINAL, HEIGHT); { @@ -791,30 +681,27 @@ static bool test() { EXPECT_MAX_DELTA(std::stoi(std::string{sizes[1]}), HEIGHT, 2); } - } catch (...) { - NLog::log("{}Exception thrown", Colors::RED); - EXPECT(false, true); - } + } catch (...) { FAIL_TEST("Exception thrown"); } - OK(getFromSocket("/keyword dwindle:default_split_ratio 1")); + OK(getFromSocket("/eval hl.config({ dwindle = { default_split_ratio = 1 } })")); } // open xeyes NLog::log("{}Spawning xeyes", Colors::YELLOW); - getFromSocket("/dispatch exec xeyes"); + getFromSocket("/dispatch hl.dsp.exec_cmd('xeyes')"); NLog::log("{}Keep checking if xeyes spawned", Colors::YELLOW); Tests::waitUntilWindowsN(3); NLog::log("{}Expecting 3 windows", Colors::YELLOW); - EXPECT(Tests::windowCount(), 3); + ASSERT(Tests::windowCount(), 3); NLog::log("{}Checking props of xeyes", Colors::YELLOW); // check some window props of xeyes, try to float it { auto str = getFromSocket("/clients"); EXPECT_NOT_CONTAINS(str, "floating: 1"); - getFromSocket("/dispatch setfloating class:XEyes"); + getFromSocket("/dispatch hl.dsp.window.float({ action = 'set', window = 'class:XEyes' })"); std::this_thread::sleep_for(std::chrono::milliseconds(200)); str = getFromSocket("/clients"); EXPECT_CONTAINS(str, "floating: 1"); @@ -825,32 +712,25 @@ static bool test() { Tests::killAllWindows(); NLog::log("{}Expecting 0 windows", Colors::YELLOW); - EXPECT(Tests::windowCount(), 0); + ASSERT(Tests::windowCount(), 0); - testSwapWindow(); - - getFromSocket("/dispatch workspace 1"); - - if (!testWindowFocusOnFullscreenConflict()) { - ret = 1; - return false; - } + getFromSocket("/dispatch hl.dsp.focus({ workspace = '1' })"); NLog::log("{}Testing spawning a floating window over a fullscreen window", Colors::YELLOW); { if (!spawnKitty("kitty_A")) - return false; - OK(getFromSocket("/dispatch fullscreen 0 set")); - EXPECT(Tests::windowCount(), 1); + FAIL_TEST("Could not spawn kitty"); + OK(getFromSocket("/dispatch hl.dsp.window.fullscreen({ mode = 'fullscreen', action = 'set' })")); + ASSERT(Tests::windowCount(), 1); - OK(getFromSocket("/dispatch exec [float] kitty")); + OK(getFromSocket("/dispatch hl.dsp.exec_cmd('kitty', { float = true })")); Tests::waitUntilWindowsN(2); - OK(getFromSocket("/dispatch focuswindow class:^kitty$")); + OK(getFromSocket("/dispatch hl.dsp.focus({ window = 'class:^kitty$' })")); const auto focused1 = getFromSocket("/activewindow"); EXPECT_CONTAINS(focused1, "class: kitty\n"); - OK(getFromSocket("/dispatch killwindow activewindow")); + OK(getFromSocket("/dispatch hl.dsp.window.kill({ window = 'activewindow' })")); Tests::waitUntilWindowsN(1); // The old window should be focused again @@ -864,12 +744,12 @@ static bool test() { NLog::log("{}Testing minsize/maxsize rules for tiled windows", Colors::YELLOW); { // Enable the config for testing, test max/minsize for tiled windows and centering - OK(getFromSocket("/keyword misc:size_limits_tiled 1")); - OK(getFromSocket("/keyword windowrule[kitty-max-rule]:match:class kitty_maxsize")); - OK(getFromSocket("/keyword windowrule[kitty-max-rule]:max_size 1500 500")); - OK(getFromSocket("r/keyword windowrule[kitty-max-rule]:min_size 1200 500")); + OK(getFromSocket("/eval hl.config({ misc = { size_limits_tiled = 1 } })")); + OK(getFromSocket("/eval hl.window_rule({ name = 'kitty-max-rule', match = { class = 'kitty_maxsize' } })")); + OK(getFromSocket("/eval hl.window_rule({ name = 'kitty-max-rule', max_size = '1500 500' })")); + OK(getFromSocket("r/eval hl.window_rule({ name = 'kitty-max-rule', min_size = '1200 500' })")); if (!spawnKitty("kitty_maxsize")) - return false; + FAIL_TEST("Could not spawn kitty"); auto dwindle = getFromSocket("/activewindow"); EXPECT_CONTAINS(dwindle, "size: 1500,500"); @@ -881,39 +761,39 @@ static bool test() { // EXPECT_CONTAINS(getFromSocket("/activewindow"), "size: 1200,500"); Tests::killAllWindows(); - EXPECT(Tests::windowCount(), 0); + ASSERT(Tests::windowCount(), 0); - OK(getFromSocket("/keyword general:layout master")); + OK(getFromSocket("r/eval hl.config({ general = { layout = 'master' } })")); if (!spawnKitty("kitty_maxsize")) - return false; + FAIL_TEST("Could not spawn kitty"); auto master = getFromSocket("/activewindow"); EXPECT_CONTAINS(master, "size: 1500,500"); EXPECT_CONTAINS(master, "at: 210,290"); if (!spawnKitty("kitty_maxsize")) - return false; + FAIL_TEST("Could not spawn kitty"); // FIXME: I can't be arsed. - OK(getFromSocket("/dispatch focuswindow class:kitty_maxsize")); + OK(getFromSocket("/dispatch hl.dsp.focus({ window = 'class:kitty_maxsize' })")); // EXPECT_CONTAINS(getFromSocket("/activewindow"), "size: 1200,500") NLog::log("{}Reloading config", Colors::YELLOW); OK(getFromSocket("/reload")); Tests::killAllWindows(); - EXPECT(Tests::windowCount(), 0); + ASSERT(Tests::windowCount(), 0); } NLog::log("{}Testing minsize/maxsize rules", Colors::YELLOW); { // Disable size limits tiled and check if props are working and not getting skipped - OK(getFromSocket("/keyword misc:size_limits_tiled 0")); - OK(getFromSocket("/keyword windowrule[kitty-max-rule]:match:class kitty_maxsize")); - OK(getFromSocket("/keyword windowrule[kitty-max-rule]:max_size 1500 500")); - OK(getFromSocket("r/keyword windowrule[kitty-max-rule]:min_size 1200 500")); + OK(getFromSocket("/eval hl.config({ misc = { size_limits_tiled = 0 } })")); + OK(getFromSocket("/eval hl.window_rule({ name = 'kitty-max-rule', match = { class = 'kitty_maxsize' } })")); + OK(getFromSocket("/eval hl.window_rule({ name = 'kitty-max-rule', max_size = '1500 500' })")); + OK(getFromSocket("r/eval hl.window_rule({ name = 'kitty-max-rule', min_size = '1200 500' })")); if (!spawnKitty("kitty_maxsize")) - return false; + FAIL_TEST("Could not spawn kitty"); { auto res = getFromSocket("/getprop active max_size"); @@ -930,17 +810,17 @@ static bool test() { NLog::log("{}Reloading config", Colors::YELLOW); OK(getFromSocket("/reload")); Tests::killAllWindows(); - EXPECT(Tests::windowCount(), 0); + ASSERT(Tests::windowCount(), 0); } { // Set float - OK(getFromSocket("/keyword windowrule[kitty-max-rule]:match:class kitty_maxsize")); - OK(getFromSocket("/keyword windowrule[kitty-max-rule]:max_size 1200 500")); - OK(getFromSocket("r/keyword windowrule[kitty-max-rule]:min_size 1200 500")); - OK(getFromSocket("r/keyword windowrule[kitty-max-rule]:float yes")); + OK(getFromSocket("/eval hl.window_rule({ name = 'kitty-max-rule', match = { class = 'kitty_maxsize' } })")); + OK(getFromSocket("/eval hl.window_rule({ name = 'kitty-max-rule', max_size = '1200 500' })")); + OK(getFromSocket("r/eval hl.window_rule({ name = 'kitty-max-rule', min_size = '1200 500' })")); + OK(getFromSocket("r/eval hl.window_rule({ name = 'kitty-max-rule', float = true })")); if (!spawnKitty("kitty_maxsize")) - return false; + FAIL_TEST("Could not spawn kitty"); { auto res = getFromSocket("/getprop active max_size"); @@ -962,12 +842,12 @@ static bool test() { NLog::log("{}Reloading config", Colors::YELLOW); OK(getFromSocket("/reload")); Tests::killAllWindows(); - EXPECT(Tests::windowCount(), 0); + ASSERT(Tests::windowCount(), 0); } NLog::log("{}Testing window rules", Colors::YELLOW); if (!spawnKitty("wr_kitty")) - return false; + FAIL_TEST("Could not spawn kitty"); { auto str = getFromSocket("/activewindow"); const int SIZE = 200; @@ -976,18 +856,18 @@ static bool test() { EXPECT_NOT_CONTAINS(str, "pinned: 1"); } - OK(getFromSocket("/keyword windowrule[wr-kitty-stuff]:opacity 0.5 0.5 override")); + OK(getFromSocket("/eval hl.window_rule({ name = 'wr-kitty-stuff', opacity = '0.5 0.5 override' })")); { auto str = getFromSocket("/getprop active opacity"); EXPECT_CONTAINS(str, "0.5"); } - OK(getFromSocket("/keyword windowrule[special-magic-kitty]:match:class magic_kitty")); - OK(getFromSocket("/keyword windowrule[special-magic-kitty]:workspace special:magic")); + OK(getFromSocket("/eval hl.window_rule({ name = 'special-magic-kitty', match = { class = 'magic_kitty' } })")); + OK(getFromSocket("/eval hl.window_rule({ name = 'special-magic-kitty', workspace = 'special:magic' })")); if (!spawnKitty("magic_kitty")) - return false; + FAIL_TEST("Could not spawn kitty"); { auto str = getFromSocket("/activewindow"); @@ -996,18 +876,18 @@ static bool test() { } if (auto str = getFromSocket("/monitors"); str.contains("magic)")) { - OK(getFromSocket("/dispatch togglespecialworkspace magic")); + OK(getFromSocket("/dispatch hl.dsp.workspace.toggle_special('magic')")); } Tests::killAllWindows(); - OK(getFromSocket("/keyword windowrule[border-magic-kitty]:match:class border_kitty")); - OK(getFromSocket("/keyword windowrule[border-magic-kitty]:border_color rgba(c6ff00ff) rgba(ff0000ee) 45deg")); + OK(getFromSocket("/eval hl.window_rule({ name = 'border-magic-kitty', match = { class = 'border_kitty' } })")); + OK(getFromSocket("/eval hl.window_rule({ name = 'border-magic-kitty', border_color = 'rgba(c6ff00ff) rgba(ff0000ee) 45deg' })")); if (!spawnKitty("border_kitty")) - return false; + FAIL_TEST("Could not spawn kitty"); - OK(getFromSocket("/dispatch focuswindow class:border_kitty")); + OK(getFromSocket("/dispatch hl.dsp.focus({ window = 'class:border_kitty' })")); { auto str = getFromSocket("/getprop active active_border_color"); @@ -1019,7 +899,7 @@ static bool test() { Tests::killAllWindows(); if (!spawnKitty("tag_kitty")) - return false; + FAIL_TEST("Could not spawn kitty"); { auto str = getFromSocket("/activewindow"); @@ -1030,11 +910,11 @@ static bool test() { Tests::killAllWindows(); // test rules that overlap effects but don't overlap props - OK(getFromSocket("/keyword windowrule match:class overlap_kitty, border_size 0")); - OK(getFromSocket("/keyword windowrule match:fullscreen false, border_size 10")); + OK(getFromSocket("/eval hl.window_rule({ match = { class = 'overlap_kitty' }, border_size = 0 })")); + OK(getFromSocket("/eval hl.window_rule({ match = { fullscreen = false }, border_size = 10 })")); if (!spawnKitty("overlap_kitty")) - return false; + FAIL_TEST("Could not spawn kitty"); { auto str = getFromSocket("/getprop active border_size"); @@ -1045,12 +925,12 @@ static bool test() { Tests::killAllWindows(); // test persistent_size between floating window launches - OK(getFromSocket("/keyword windowrule match:class persistent_size_kitty, persistent_size true, float true")); + OK(getFromSocket("/eval hl.window_rule({ match = { class = 'persistent_size_kitty' }, persistent_size = true, float = true })")); if (!spawnKitty("persistent_size_kitty")) - return false; + FAIL_TEST("Could not spawn kitty"); - OK(getFromSocket("/dispatch resizeactive exact 600 400")) + OK(getFromSocket("/dispatch hl.dsp.window.resize({ x = 600, y = 400 })")); { auto str = getFromSocket("/activewindow"); @@ -1061,7 +941,7 @@ static bool test() { Tests::killAllWindows(); if (!spawnKitty("persistent_size_kitty")) - return false; + FAIL_TEST("Could not spawn kitty"); { auto str = getFromSocket("/activewindow"); @@ -1072,25 +952,25 @@ static bool test() { OK(getFromSocket("/reload")); Tests::killAllWindows(); - OK(getFromSocket("/keyword general:border_size 0")); - OK(getFromSocket("/keyword windowrule match:float true, border_size 10")); + OK(getFromSocket("/eval hl.config({ general = { border_size = 0 } })")); + OK(getFromSocket("/eval hl.window_rule({ match = { float = true }, border_size = 10 })")); if (!spawnKitty("border_kitty")) - return false; + FAIL_TEST("Could not spawn kitty"); { auto str = getFromSocket("/getprop active border_size"); EXPECT_CONTAINS(str, "0"); } - OK(getFromSocket("/dispatch togglefloating")); + OK(getFromSocket("/dispatch hl.dsp.window.float({ action = 'toggle' })")); { auto str = getFromSocket("/getprop active border_size"); EXPECT_CONTAINS(str, "10"); } - OK(getFromSocket("/dispatch togglefloating")); + OK(getFromSocket("/dispatch hl.dsp.window.float({ action = 'toggle' })")); { auto str = getFromSocket("/getprop active border_size"); @@ -1101,11 +981,12 @@ static bool test() { Tests::killAllWindows(); // test expression rules - OK(getFromSocket("/keyword windowrule match:class expr_kitty, float yes, size monitor_w*0.5 monitor_h*0.5, min_size monitor_w*0.25 monitor_h*0.25, " - "max_size monitor_w*0.75 monitor_h*0.75, move 20+(monitor_w*0.1) monitor_h*0.5")); + OK(getFromSocket("/eval hl.window_rule({ match = { class = 'expr_kitty' }, float = true, size = {'monitor_w * 0.5', 'monitor_h * 0.5'}, " + "min_size = {'monitor_w * 0.25', 'monitor_h * 0.25'}, max_size = {'monitor_w * 0.75', 'monitor_h * 0.75'}, " + "move = {'20 + (monitor_w * 0.1)', 'monitor_h * 0.5'} })")); if (!spawnKitty("expr_kitty")) - return false; + FAIL_TEST("Could not spawn kitty"); { auto str = getFromSocket("/activewindow"); @@ -1125,55 +1006,27 @@ static bool test() { OK(getFromSocket("/reload")); Tests::killAllWindows(); - OK(getFromSocket("/dispatch plugin:test:add_window_rule")); + OK(getFromSocket("/eval hl.plugin.test.add_window_rule()")); OK(getFromSocket("/reload")); - OK(getFromSocket("/keyword windowrule match:class plugin_kitty, plugin_rule effect")); + OK(getFromSocket("/eval hl.window_rule({ match = { class = 'plugin_kitty' }, plugin_rule = 'effect' })")); if (!spawnKitty("plugin_kitty")) - return false; + FAIL_TEST("Could not spawn kitty"); - OK(getFromSocket("/dispatch plugin:test:check_window_rule")); + OK(getFromSocket("/eval hl.plugin.test.check_window_rule()")); OK(getFromSocket("/reload")); Tests::killAllWindows(); - OK(getFromSocket("/dispatch plugin:test:add_window_rule")); + OK(getFromSocket("/eval hl.plugin.test.add_window_rule()")); OK(getFromSocket("/reload")); - OK(getFromSocket("/keyword windowrule[test-plugin-rule]:match:class plugin_kitty")); - OK(getFromSocket("/keyword windowrule[test-plugin-rule]:plugin_rule effect")); + OK(getFromSocket("/eval hl.window_rule({ name = 'test-plugin-rule', match = { class = 'plugin_kitty' } })")); + OK(getFromSocket("/eval hl.window_rule({ name = 'test-plugin-rule', plugin_rule = 'effect' })")); if (!spawnKitty("plugin_kitty")) - return false; + FAIL_TEST("Could not spawn kitty"); - OK(getFromSocket("/dispatch plugin:test:check_window_rule")); - - OK(getFromSocket("/reload")); - Tests::killAllWindows(); - - testGroupRules(); - testMaximizeSize(); - testFloatingFocusOnFullscreen(); - testBringActiveToTopMouseMovement(); - testGroupFallbackFocus(); - testInitialFloatSize(); - testWindowRuleFocusOnActivate(); - testPinnedWorkspacesValid(); - testWindowRuleWorkspaceEmpty(); - testContentRules(); - test14038(); - - NLog::log("{}Reloading config", Colors::YELLOW); - OK(getFromSocket("/reload")); - - NLog::log("{}Killing all windows", Colors::YELLOW); - Tests::killAllWindows(); - - NLog::log("{}Expecting 0 windows", Colors::YELLOW); - EXPECT(Tests::windowCount(), 0); - - return !ret; + OK(getFromSocket("/eval hl.plugin.test.check_window_rule()")); } - -REGISTER_TEST_FN(test) diff --git a/hyprtester/src/tests/main/workspaces.cpp b/hyprtester/src/tests/main/workspaces.cpp index f7c948f26..8cde4914b 100644 --- a/hyprtester/src/tests/main/workspaces.cpp +++ b/hyprtester/src/tests/main/workspaces.cpp @@ -11,8 +11,6 @@ #include #include "../shared.hpp" -static int ret = 0; - using namespace Hyprutils::OS; using namespace Hyprutils::Memory; using namespace Hyprutils::Utils; @@ -20,7 +18,11 @@ using namespace Hyprutils::Utils; #define UP CUniquePointer #define SP CSharedPointer -static bool testSpecialWorkspaceFullscreen() { +// All the `SUBTEST`s below are supposed to be independent `TEST_CASE`s. +// But if isolated trivially, some of them fail. +// TODO: investigate and isolate tests by turning `SUBTEST`s into `TEST_CASE`s. + +SUBTEST(specialWorkspaceFullscreen) { NLog::log("{}Testing special workspace fullscreen detection", Colors::YELLOW); CScopeGuard guard = {[&]() { @@ -28,20 +30,20 @@ static bool testSpecialWorkspaceFullscreen() { // Close special workspace if open auto monitors = getFromSocket("/monitors"); if (monitors.contains("(special:test_fs_special)") && !monitors.contains("special workspace: 0 ()")) - getFromSocket("/dispatch togglespecialworkspace test_fs_special"); + getFromSocket("/dispatch hl.dsp.workspace.toggle_special('test_fs_special')"); Tests::killAllWindows(); OK(getFromSocket("/reload")); }}; - getFromSocket("/dispatch workspace 1"); - EXPECT(Tests::windowCount(), 0); + getFromSocket("/dispatch hl.dsp.focus({ workspace = '1' })"); + ASSERT(Tests::windowCount(), 0); NLog::log("{}Test 1: Fullscreen detection on special workspace", Colors::YELLOW); - OK(getFromSocket("/dispatch workspace special:test_fs_special")); + OK(getFromSocket("/dispatch hl.dsp.focus({ workspace = 'special:test_fs_special' })")); if (!Tests::spawnKitty("kitty_special")) - return false; + FAIL_TEST("Could not spawn kitty"); { auto str = getFromSocket("/activewindow"); @@ -49,7 +51,7 @@ static bool testSpecialWorkspaceFullscreen() { EXPECT_CONTAINS(str, "(special:test_fs_special)"); } - OK(getFromSocket("/dispatch fullscreen 0")); + OK(getFromSocket("/dispatch hl.dsp.window.fullscreen({ mode = 'fullscreen' })")); { auto str = getFromSocket("/activewindow"); @@ -64,13 +66,13 @@ static bool testSpecialWorkspaceFullscreen() { NLog::log("{}Test 2: Special workspace fullscreen precedence", Colors::YELLOW); // Close special workspace before spawning on regular workspace - OK(getFromSocket("/dispatch togglespecialworkspace test_fs_special")); - getFromSocket("/dispatch workspace 1"); + OK(getFromSocket("/dispatch hl.dsp.workspace.toggle_special('test_fs_special')")); + getFromSocket("/dispatch hl.dsp.focus({ workspace = '1' })"); if (!Tests::spawnKitty("kitty_regular")) - return false; + FAIL_TEST("Could not spawn kitty"); - OK(getFromSocket("/dispatch fullscreen 0")); + OK(getFromSocket("/dispatch hl.dsp.window.fullscreen({ mode = 'fullscreen' })")); { auto str = getFromSocket("/activewindow"); @@ -78,8 +80,8 @@ static bool testSpecialWorkspaceFullscreen() { EXPECT_CONTAINS(str, "fullscreen: 2"); } - OK(getFromSocket("/dispatch togglespecialworkspace test_fs_special")); - OK(getFromSocket("/dispatch focuswindow class:kitty_special")); + OK(getFromSocket("/dispatch hl.dsp.workspace.toggle_special('test_fs_special')")); + OK(getFromSocket("/dispatch hl.dsp.focus({ window = 'class:kitty_special' })")); { auto str = getFromSocket("/activewindow"); @@ -88,8 +90,8 @@ static bool testSpecialWorkspaceFullscreen() { NLog::log("{}Test 3: Toggle special workspace hides it", Colors::YELLOW); - OK(getFromSocket("/dispatch togglespecialworkspace test_fs_special")); - OK(getFromSocket("/dispatch focuswindow class:kitty_regular")); + OK(getFromSocket("/dispatch hl.dsp.workspace.toggle_special('test_fs_special')")); + OK(getFromSocket("/dispatch hl.dsp.focus({ window = 'class:kitty_regular' })")); { auto str = getFromSocket("/activewindow"); @@ -101,118 +103,110 @@ static bool testSpecialWorkspaceFullscreen() { auto str = getFromSocket("/monitors"); EXPECT_CONTAINS(str, "special workspace: 0 ()"); } - - return true; } -static bool testAsymmetricGaps() { +SUBTEST(asymmetricGaps) { NLog::log("{}Testing asymmetric gap splits", Colors::YELLOW); - { - CScopeGuard guard = {[&]() { - NLog::log("{}Cleaning up asymmetric gap test", Colors::YELLOW); - Tests::killAllWindows(); - OK(getFromSocket("/reload")); - }}; + ASSERT(Tests::windowCount(), 0); + getFromSocket("/dispatch workspace 1"); + getFromSocket("/reload"); - OK(getFromSocket("/dispatch workspace name:gap_split_test")); - OK(getFromSocket("r/keyword general:gaps_in 0")); - OK(getFromSocket("r/keyword general:border_size 0")); - OK(getFromSocket("r/keyword dwindle:split_width_multiplier 1.0")); - OK(getFromSocket("r/keyword workspace name:gap_split_test,gapsout:0 1000 0 0")); + OK(getFromSocket("/dispatch hl.dsp.focus({ workspace = 'name:gap_split_test' })")); + OK(getFromSocket("r/eval hl.config({ general = { gaps_in = 0 } })")); + OK(getFromSocket("r/eval hl.config({ general = { border_size = 0 } })")); + OK(getFromSocket("r/eval hl.config({ dwindle = { split_width_multiplier = 1.0 } })")); + OK(getFromSocket("r/eval hl.workspace_rule({ workspace = 'name:gap_split_test', gaps_out = { top = 0, right = 1000, bottom = 0, left = 0 } })")); - NLog::log("{}Testing default split (force_split = 0)", Colors::YELLOW); - OK(getFromSocket("r/keyword dwindle:force_split 0")); + NLog::log("{}Testing default split (force_split = 0)", Colors::YELLOW); + OK(getFromSocket("r/eval hl.config({ dwindle = { force_split = 0 } })")); - if (!Tests::spawnKitty("gaps_kitty_A") || !Tests::spawnKitty("gaps_kitty_B")) - return false; + if (!Tests::spawnKitty("gaps_kitty_A") || !Tests::spawnKitty("gaps_kitty_B")) + FAIL_TEST("Could not spawn kitty"); - NLog::log("{}Expecting vertical split (B below A)", Colors::YELLOW); - OK(getFromSocket("/dispatch focuswindow class:gaps_kitty_A")); - EXPECT_CONTAINS(getFromSocket("/activewindow"), "at: 0,0"); - OK(getFromSocket("/dispatch focuswindow class:gaps_kitty_B")); - EXPECT_CONTAINS(getFromSocket("/activewindow"), "at: 0,540"); + NLog::log("{}Expecting vertical split (B below A)", Colors::YELLOW); + OK(getFromSocket("/dispatch hl.dsp.focus({ window = 'class:gaps_kitty_A' })")); + EXPECT_CONTAINS(getFromSocket("/activewindow"), "at: 0,0"); + OK(getFromSocket("/dispatch hl.dsp.focus({ window = 'class:gaps_kitty_B' })")); + EXPECT_CONTAINS(getFromSocket("/activewindow"), "at: 0,540"); - Tests::killAllWindows(); - EXPECT(Tests::windowCount(), 0); + Tests::killAllWindows(); + ASSERT(Tests::windowCount(), 0); - NLog::log("{}Testing force_split = 1", Colors::YELLOW); - OK(getFromSocket("r/keyword dwindle:force_split 1")); + NLog::log("{}Testing force_split = 1", Colors::YELLOW); + OK(getFromSocket("r/eval hl.config({ dwindle = { force_split = 1 } })")); - if (!Tests::spawnKitty("gaps_kitty_A") || !Tests::spawnKitty("gaps_kitty_B")) - return false; + if (!Tests::spawnKitty("gaps_kitty_A") || !Tests::spawnKitty("gaps_kitty_B")) + FAIL_TEST("Could not spawn kitty"); - NLog::log("{}Expecting vertical split (B above A)", Colors::YELLOW); - OK(getFromSocket("/dispatch focuswindow class:gaps_kitty_B")); - EXPECT_CONTAINS(getFromSocket("/activewindow"), "at: 0,0"); - OK(getFromSocket("/dispatch focuswindow class:gaps_kitty_A")); - EXPECT_CONTAINS(getFromSocket("/activewindow"), "at: 0,540"); + NLog::log("{}Expecting vertical split (B above A)", Colors::YELLOW); + OK(getFromSocket("/dispatch hl.dsp.focus({ window = 'class:gaps_kitty_B' })")); + EXPECT_CONTAINS(getFromSocket("/activewindow"), "at: 0,0"); + OK(getFromSocket("/dispatch hl.dsp.focus({ window = 'class:gaps_kitty_A' })")); + EXPECT_CONTAINS(getFromSocket("/activewindow"), "at: 0,540"); - NLog::log("{}Expecting horizontal split (C left of B)", Colors::YELLOW); - OK(getFromSocket("/dispatch focuswindow class:gaps_kitty_B")); + NLog::log("{}Expecting horizontal split (C left of B)", Colors::YELLOW); + OK(getFromSocket("/dispatch hl.dsp.focus({ window = 'class:gaps_kitty_B' })")); - if (!Tests::spawnKitty("gaps_kitty_C")) - return false; + if (!Tests::spawnKitty("gaps_kitty_C")) + FAIL_TEST("Could not spawn kitty"); - OK(getFromSocket("/dispatch focuswindow class:gaps_kitty_C")); - EXPECT_CONTAINS(getFromSocket("/activewindow"), "at: 0,0"); - OK(getFromSocket("/dispatch focuswindow class:gaps_kitty_B")); - EXPECT_CONTAINS(getFromSocket("/activewindow"), "at: 460,0"); + OK(getFromSocket("/dispatch hl.dsp.focus({ window = 'class:gaps_kitty_C' })")); + EXPECT_CONTAINS(getFromSocket("/activewindow"), "at: 0,0"); + OK(getFromSocket("/dispatch hl.dsp.focus({ window = 'class:gaps_kitty_B' })")); + EXPECT_CONTAINS(getFromSocket("/activewindow"), "at: 460,0"); - Tests::killAllWindows(); - EXPECT(Tests::windowCount(), 0); + Tests::killAllWindows(); + ASSERT(Tests::windowCount(), 0); - NLog::log("{}Testing force_split = 2", Colors::YELLOW); - OK(getFromSocket("r/keyword dwindle:force_split 2")); + NLog::log("{}Testing force_split = 2", Colors::YELLOW); + OK(getFromSocket("r/eval hl.config({ dwindle = { force_split = 2 } })")); - if (!Tests::spawnKitty("gaps_kitty_A") || !Tests::spawnKitty("gaps_kitty_B")) - return false; + if (!Tests::spawnKitty("gaps_kitty_A") || !Tests::spawnKitty("gaps_kitty_B")) + FAIL_TEST("Could not spawn kitty"); - NLog::log("{}Expecting vertical split (B below A)", Colors::YELLOW); - OK(getFromSocket("/dispatch focuswindow class:gaps_kitty_A")); - EXPECT_CONTAINS(getFromSocket("/activewindow"), "at: 0,0"); - OK(getFromSocket("/dispatch focuswindow class:gaps_kitty_B")); - EXPECT_CONTAINS(getFromSocket("/activewindow"), "at: 0,540"); + NLog::log("{}Expecting vertical split (B below A)", Colors::YELLOW); + OK(getFromSocket("/dispatch hl.dsp.focus({ window = 'class:gaps_kitty_A' })")); + EXPECT_CONTAINS(getFromSocket("/activewindow"), "at: 0,0"); + OK(getFromSocket("/dispatch hl.dsp.focus({ window = 'class:gaps_kitty_B' })")); + EXPECT_CONTAINS(getFromSocket("/activewindow"), "at: 0,540"); - NLog::log("{}Expecting horizontal split (C right of A)", Colors::YELLOW); - OK(getFromSocket("/dispatch focuswindow class:gaps_kitty_A")); + NLog::log("{}Expecting horizontal split (C right of A)", Colors::YELLOW); + OK(getFromSocket("/dispatch hl.dsp.focus({ window = 'class:gaps_kitty_A' })")); - if (!Tests::spawnKitty("gaps_kitty_C")) - return false; + if (!Tests::spawnKitty("gaps_kitty_C")) + FAIL_TEST("Could not spawn kitty"); - OK(getFromSocket("/dispatch focuswindow class:gaps_kitty_A")); - EXPECT_CONTAINS(getFromSocket("/activewindow"), "at: 0,0"); - OK(getFromSocket("/dispatch focuswindow class:gaps_kitty_C")); - EXPECT_CONTAINS(getFromSocket("/activewindow"), "at: 460,0"); - } + OK(getFromSocket("/dispatch hl.dsp.focus({ window = 'class:gaps_kitty_A' })")); + EXPECT_CONTAINS(getFromSocket("/activewindow"), "at: 0,0"); + OK(getFromSocket("/dispatch hl.dsp.focus({ window = 'class:gaps_kitty_C' })")); + EXPECT_CONTAINS(getFromSocket("/activewindow"), "at: 460,0"); // kill all NLog::log("{}Killing all windows", Colors::YELLOW); Tests::killAllWindows(); - - return true; } -static void testWorkspaceHistoryMultiMon() { +SUBTEST(workspaceHistoryMultiMon) { NLog::log("{}Testing multimon workspace history tracker", Colors::YELLOW); // Initial state: - OK(getFromSocket("/dispatch focusmonitor HEADLESS-2")); - OK(getFromSocket("/dispatch workspace 10")); + OK(getFromSocket("/dispatch hl.dsp.focus({ monitor = 'HEADLESS-2' })")); + OK(getFromSocket("/dispatch hl.dsp.focus({ workspace = '10' })")); Tests::spawnKitty(); - OK(getFromSocket("/dispatch workspace 11")); + OK(getFromSocket("/dispatch hl.dsp.focus({ workspace = '11' })")); Tests::spawnKitty(); - OK(getFromSocket("/dispatch focusmonitor HEADLESS-3")); - OK(getFromSocket("/dispatch workspace 12")); + OK(getFromSocket("/dispatch hl.dsp.focus({ monitor = 'HEADLESS-3' })")); + OK(getFromSocket("/dispatch hl.dsp.focus({ workspace = '12' })")); Tests::spawnKitty(); - OK(getFromSocket("/dispatch focusmonitor HEADLESS-2")); + OK(getFromSocket("/dispatch hl.dsp.focus({ monitor = 'HEADLESS-2' })")); { auto str = getFromSocket("/activeworkspace"); EXPECT_CONTAINS(str, "workspace ID 11"); } - OK(getFromSocket("/dispatch workspace previous_per_monitor")); + OK(getFromSocket("/dispatch hl.dsp.focus({ workspace = 'previous_per_monitor' })")); { auto str = getFromSocket("/activeworkspace"); EXPECT_CONTAINS(str, "workspace ID 10"); @@ -222,59 +216,59 @@ static void testWorkspaceHistoryMultiMon() { Tests::killAllWindows(); } -static void testMultimonBAF() { +SUBTEST(multimonBAF) { NLog::log("{}Testing multimon back and forth", Colors::YELLOW); - OK(getFromSocket("/keyword binds:workspace_back_and_forth 1")); + OK(getFromSocket("/eval hl.config({ binds = { workspace_back_and_forth = 1 } })")); - OK(getFromSocket("/dispatch focusmonitor HEADLESS-2")); - OK(getFromSocket("/dispatch workspace 1")); + OK(getFromSocket("/dispatch hl.dsp.focus({ monitor = 'HEADLESS-2' })")); + OK(getFromSocket("/dispatch hl.dsp.focus({ workspace = '1' })")); Tests::spawnKitty(); - OK(getFromSocket("/dispatch workspace 2")); + OK(getFromSocket("/dispatch hl.dsp.focus({ workspace = '2' })")); Tests::spawnKitty(); - OK(getFromSocket("/dispatch focusmonitor HEADLESS-3")); - OK(getFromSocket("/dispatch workspace 3")); + OK(getFromSocket("/dispatch hl.dsp.focus({ monitor = 'HEADLESS-3' })")); + OK(getFromSocket("/dispatch hl.dsp.focus({ workspace = '3' })")); Tests::spawnKitty(); - OK(getFromSocket("/dispatch workspace 3")); + OK(getFromSocket("/dispatch hl.dsp.focus({ workspace = '3' })")); { auto str = getFromSocket("/activeworkspace"); EXPECT_CONTAINS(str, "workspace ID 2 "); } - OK(getFromSocket("/dispatch workspace 4")); - OK(getFromSocket("/dispatch workspace 4")); + OK(getFromSocket("/dispatch hl.dsp.focus({ workspace = '4' })")); + OK(getFromSocket("/dispatch hl.dsp.focus({ workspace = '4' })")); { auto str = getFromSocket("/activeworkspace"); EXPECT_CONTAINS(str, "workspace ID 2 "); } - OK(getFromSocket("/dispatch workspace 2")); + OK(getFromSocket("/dispatch hl.dsp.focus({ workspace = '2' })")); { auto str = getFromSocket("/activeworkspace"); EXPECT_CONTAINS(str, "workspace ID 4 "); } - OK(getFromSocket("/dispatch workspace 3")); - OK(getFromSocket("/dispatch workspace 3")); + OK(getFromSocket("/dispatch hl.dsp.focus({ workspace = '3' })")); + OK(getFromSocket("/dispatch hl.dsp.focus({ workspace = '3' })")); { auto str = getFromSocket("/activeworkspace"); EXPECT_CONTAINS(str, "workspace ID 4 "); } - OK(getFromSocket("/dispatch workspace 2")); - OK(getFromSocket("/dispatch workspace 3")); - OK(getFromSocket("/dispatch workspace 1")); - OK(getFromSocket("/dispatch workspace 1")); + OK(getFromSocket("/dispatch hl.dsp.focus({ workspace = '2' })")); + OK(getFromSocket("/dispatch hl.dsp.focus({ workspace = '3' })")); + OK(getFromSocket("/dispatch hl.dsp.focus({ workspace = '1' })")); + OK(getFromSocket("/dispatch hl.dsp.focus({ workspace = '1' })")); { auto str = getFromSocket("/activeworkspace"); @@ -284,26 +278,23 @@ static void testMultimonBAF() { Tests::killAllWindows(); } -static void testMultimonFocus() { +SUBTEST(multimonFocus) { NLog::log("{}Testing multimon focus and move", Colors::YELLOW); - OK(getFromSocket("/keyword input:follow_mouse 0")); - OK(getFromSocket("/dispatch focusmonitor HEADLESS-3")); - OK(getFromSocket("/dispatch workspace 8")); - OK(getFromSocket("/dispatch focusmonitor HEADLESS-2")); - OK(getFromSocket("/dispatch workspace 7")); + OK(getFromSocket("/eval hl.config({ input = { follow_mouse = 0 } })")); + OK(getFromSocket("/dispatch hl.dsp.focus({ monitor = 'HEADLESS-3' })")); + OK(getFromSocket("/dispatch hl.dsp.focus({ workspace = '8' })")); + OK(getFromSocket("/dispatch hl.dsp.focus({ monitor = 'HEADLESS-2' })")); + OK(getFromSocket("/dispatch hl.dsp.focus({ workspace = '7' })")); for (auto const& win : {"a", "b"}) { if (!Tests::spawnKitty(win)) { - NLog::log("{}Failed to spawn kitty with win class `{}`", Colors::RED, win); - ++TESTS_FAILED; - ret = 1; - return; + FAIL_TEST("Could not spawn kitty with win class `{}`", win); } } - OK(getFromSocket("/dispatch focuswindow class:a")); - OK(getFromSocket("/dispatch movefocus r")); + OK(getFromSocket("/dispatch hl.dsp.focus({ window = 'class:a' })")); + OK(getFromSocket("/dispatch hl.dsp.focus({ direction = 'right' })")); { auto str = getFromSocket("/activeworkspace"); @@ -315,7 +306,7 @@ static void testMultimonFocus() { EXPECT_CONTAINS(str, "class: b"); } - OK(getFromSocket("/dispatch movefocus r")); + OK(getFromSocket("/dispatch hl.dsp.focus({ direction = 'right' })")); { auto str = getFromSocket("/activeworkspace"); @@ -334,7 +325,7 @@ static void testMultimonFocus() { EXPECT_CONTAINS(str, "workspace ID 8 "); } - OK(getFromSocket("/dispatch movefocus l")); + OK(getFromSocket("/dispatch hl.dsp.focus({ direction = 'left' })")); { auto str = getFromSocket("/activewindow"); @@ -346,57 +337,57 @@ static void testMultimonFocus() { EXPECT_CONTAINS(str, "workspace ID 7 "); } - OK(getFromSocket("/dispatch movewindow r")); + OK(getFromSocket("/dispatch hl.dsp.window.move({ direction = 'right' })")); { auto str = getFromSocket("/activewindow"); - EXPECT_CONTAINS(str, "class: b"); + ASSERT_CONTAINS(str, "class: b"); } { auto str = getFromSocket("/activeworkspace"); - EXPECT_CONTAINS(str, "workspace ID 8 "); + ASSERT_CONTAINS(str, "workspace ID 8 "); } - OK(getFromSocket("/dispatch movefocus r")); + OK(getFromSocket("/dispatch hl.dsp.focus({ direction = 'right' })")); { auto str = getFromSocket("/activewindow"); - EXPECT_CONTAINS(str, "class: c"); + ASSERT_CONTAINS(str, "class: c"); } { auto str = getFromSocket("/activeworkspace"); - EXPECT_CONTAINS(str, "workspace ID 8 "); + ASSERT_CONTAINS(str, "workspace ID 8 "); } - OK(getFromSocket("/dispatch movefocus l")); + OK(getFromSocket("/dispatch hl.dsp.focus({ direction = 'left' })")); - OK(getFromSocket("/keyword general:no_focus_fallback true")); - OK(getFromSocket("/keyword binds:window_direction_monitor_fallback false")); + OK(getFromSocket("/eval hl.config({ general = { no_focus_fallback = true } })")); + OK(getFromSocket("/eval hl.config({ binds = { window_direction_monitor_fallback = false } })")); - EXPECT_NOT(getFromSocket("/dispatch movefocus l"), "ok"); + ASSERT_NOT(getFromSocket("/dispatch hl.dsp.focus({ direction = 'left' })"), "ok"); { auto str = getFromSocket("/activewindow"); - EXPECT_CONTAINS(str, "class: b"); + ASSERT_CONTAINS(str, "class: b"); } { auto str = getFromSocket("/activeworkspace"); - EXPECT_CONTAINS(str, "workspace ID 8 "); + ASSERT_CONTAINS(str, "workspace ID 8 "); } - OK(getFromSocket("/dispatch movewindow l")); + OK(getFromSocket("/dispatch hl.dsp.window.move({ direction = 'left' })")); { auto str = getFromSocket("/activewindow"); - EXPECT_CONTAINS(str, "class: b"); + ASSERT_CONTAINS(str, "class: b"); } { auto str = getFromSocket("/activeworkspace"); - EXPECT_CONTAINS(str, "workspace ID 8 "); + ASSERT_CONTAINS(str, "workspace ID 8 "); } OK(getFromSocket("/reload")); @@ -404,39 +395,32 @@ static void testMultimonFocus() { Tests::killAllWindows(); } -static void testDynamicWsEffects() { +SUBTEST(dynamicWsEffects) { // test dynamic workspace effects, they shouldn't lag - OK(getFromSocket("/dispatch workspace 69")); + OK(getFromSocket("/dispatch hl.dsp.focus({ workspace = '69' })")); Tests::spawnKitty("bitch"); - OK(getFromSocket("r/keyword workspace 69,bordersize:20")); - OK(getFromSocket("r/keyword workspace 69,rounding:false")); + OK(getFromSocket("r/eval hl.workspace_rule({ workspace = '69', border_size = 20 })")); + OK(getFromSocket("r/eval hl.workspace_rule({ workspace = '69', no_rounding = true })")); - EXPECT(getFromSocket("/getprop class:bitch border_size"), "20"); - EXPECT(getFromSocket("/getprop class:bitch rounding"), "0"); + ASSERT(getFromSocket("/getprop class:bitch border_size"), "20"); + ASSERT(getFromSocket("/getprop class:bitch rounding"), "0"); OK(getFromSocket("/reload")); Tests::killAllWindows(); } -static bool test() { - NLog::log("{}Testing workspaces", Colors::GREEN); - - EXPECT(Tests::windowCount(), 0); - - // test on workspace "window" - NLog::log("{}Switching to workspace 1", Colors::YELLOW); - getFromSocket("/dispatch workspace 1"); - +// TODO: decompose this into multiple test cases +TEST_CASE(workspacesCombined) { NLog::log("{}Checking persistent no-mon", Colors::YELLOW); - OK(getFromSocket("r/keyword workspace 966,persistent:1")); + OK(getFromSocket("r/eval hl.workspace_rule({ workspace = '966', persistent = true })")); { auto str = getFromSocket("/workspaces"); - EXPECT_CONTAINS(str, "workspace ID 966 (966)"); + ASSERT_CONTAINS(str, "workspace ID 966 (966)"); } OK(getFromSocket("/reload")); @@ -444,337 +428,329 @@ static bool test() { NLog::log("{}Spawning kittyProc on ws 1", Colors::YELLOW); auto kittyProcA = Tests::spawnKitty(); - if (!kittyProcA) { - NLog::log("{}Error: kitty did not spawn", Colors::RED); - return false; - } + if (!kittyProcA) + FAIL_TEST("Could not spawn kitty"); NLog::log("{}Switching to workspace 3", Colors::YELLOW); - OK(getFromSocket("/dispatch workspace 3")); + OK(getFromSocket("/dispatch hl.dsp.focus({ workspace = '3' })")); NLog::log("{}Spawning kittyProc on ws 3", Colors::YELLOW); auto kittyProcB = Tests::spawnKitty(); - if (!kittyProcB) { - NLog::log("{}Error: kitty did not spawn", Colors::RED); - return false; - } + if (!kittyProcB) + FAIL_TEST("Could not spawn kitty"); NLog::log("{}Switching to workspace 1", Colors::YELLOW); - OK(getFromSocket("/dispatch workspace 1")); + OK(getFromSocket("/dispatch hl.dsp.focus({ workspace = '1' })")); NLog::log("{}Switching to workspace +1", Colors::YELLOW); - OK(getFromSocket("/dispatch workspace +1")); + OK(getFromSocket("/dispatch hl.dsp.focus({ workspace = '+1' })")); { auto str = getFromSocket("/activeworkspace"); - EXPECT_STARTS_WITH(str, "workspace ID 2 (2)"); + ASSERT_STARTS_WITH(str, "workspace ID 2 (2)"); } // check if the other workspaces are alive { auto str = getFromSocket("/workspaces"); - EXPECT_CONTAINS(str, "workspace ID 3 (3)"); - EXPECT_CONTAINS(str, "workspace ID 1 (1)"); + ASSERT_CONTAINS(str, "workspace ID 3 (3)"); + ASSERT_CONTAINS(str, "workspace ID 1 (1)"); } NLog::log("{}Switching to workspace 1", Colors::YELLOW); - OK(getFromSocket("/dispatch workspace 1")); + OK(getFromSocket("/dispatch hl.dsp.focus({ workspace = '1' })")); { auto str = getFromSocket("/workspaces"); - EXPECT_NOT_CONTAINS(str, "workspace ID 2 (2)"); + ASSERT_NOT_CONTAINS(str, "workspace ID 2 (2)"); } NLog::log("{}Switching to workspace m+1", Colors::YELLOW); - OK(getFromSocket("/dispatch workspace m+1")); + OK(getFromSocket("/dispatch hl.dsp.focus({ workspace = 'm+1' })")); { auto str = getFromSocket("/activeworkspace"); - EXPECT_STARTS_WITH(str, "workspace ID 3 (3)"); + ASSERT_STARTS_WITH(str, "workspace ID 3 (3)"); } NLog::log("{}Switching to workspace -1", Colors::YELLOW); - OK(getFromSocket("/dispatch workspace -1")); + OK(getFromSocket("/dispatch hl.dsp.focus({ workspace = '-1' })")); { auto str = getFromSocket("/activeworkspace"); - EXPECT_STARTS_WITH(str, "workspace ID 2 (2)"); + ASSERT_STARTS_WITH(str, "workspace ID 2 (2)"); } NLog::log("{}Switching to workspace 1", Colors::YELLOW); - OK(getFromSocket("/dispatch workspace 1")); + OK(getFromSocket("/dispatch hl.dsp.focus({ workspace = '1' })")); NLog::log("{}Switching to workspace r+1", Colors::YELLOW); - OK(getFromSocket("/dispatch workspace r+1")); + OK(getFromSocket("/dispatch hl.dsp.focus({ workspace = 'r+1' })")); { auto str = getFromSocket("/activeworkspace"); - EXPECT_STARTS_WITH(str, "workspace ID 2 (2)"); + ASSERT_STARTS_WITH(str, "workspace ID 2 (2)"); } NLog::log("{}Switching to workspace r+1", Colors::YELLOW); - OK(getFromSocket("/dispatch workspace r+1")); + OK(getFromSocket("/dispatch hl.dsp.focus({ workspace = 'r+1' })")); { auto str = getFromSocket("/activeworkspace"); - EXPECT_STARTS_WITH(str, "workspace ID 3 (3)"); + ASSERT_STARTS_WITH(str, "workspace ID 3 (3)"); } NLog::log("{}Switching to workspace r~1", Colors::YELLOW); - OK(getFromSocket("/dispatch workspace r~1")); + OK(getFromSocket("/dispatch hl.dsp.focus({ workspace = 'r~1' })")); { auto str = getFromSocket("/activeworkspace"); - EXPECT_STARTS_WITH(str, "workspace ID 1 (1)"); + ASSERT_STARTS_WITH(str, "workspace ID 1 (1)"); } NLog::log("{}Switching to workspace empty", Colors::YELLOW); - OK(getFromSocket("/dispatch workspace empty")); + OK(getFromSocket("/dispatch hl.dsp.focus({ workspace = 'empty' })")); { auto str = getFromSocket("/activeworkspace"); - EXPECT_STARTS_WITH(str, "workspace ID 2 (2)"); + ASSERT_STARTS_WITH(str, "workspace ID 2 (2)"); } NLog::log("{}Switching to workspace previous", Colors::YELLOW); - OK(getFromSocket("/dispatch workspace previous")); + OK(getFromSocket("/dispatch hl.dsp.focus({ workspace = 'previous' })")); { auto str = getFromSocket("/activeworkspace"); - EXPECT_STARTS_WITH(str, "workspace ID 1 (1)"); + ASSERT_STARTS_WITH(str, "workspace ID 1 (1)"); } NLog::log("{}Switching to workspace name:TEST_WORKSPACE_NULL", Colors::YELLOW); - OK(getFromSocket("/dispatch workspace name:TEST_WORKSPACE_NULL")); + OK(getFromSocket("/dispatch hl.dsp.focus({ workspace = 'name:TEST_WORKSPACE_NULL' })")); { auto str = getFromSocket("/activeworkspace"); - EXPECT_STARTS_WITH(str, "workspace ID -1337 (TEST_WORKSPACE_NULL)"); + ASSERT_STARTS_WITH(str, "workspace ID -1337 (TEST_WORKSPACE_NULL)"); } NLog::log("{}Switching to workspace 1", Colors::YELLOW); - OK(getFromSocket("/dispatch workspace 1")); + OK(getFromSocket("/dispatch hl.dsp.focus({ workspace = '1' })")); // add a new monitor NLog::log("{}Adding a new monitor", Colors::YELLOW); - EXPECT(getFromSocket("/output create headless"), "ok") + ASSERT(getFromSocket("/output create headless"), "ok"); // should take workspace 2 { auto str = getFromSocket("/monitors"); - EXPECT_CONTAINS(str, "active workspace: 2 (2)"); - EXPECT_CONTAINS(str, "active workspace: 1 (1)"); - EXPECT_CONTAINS(str, "HEADLESS-3"); + ASSERT_CONTAINS(str, "active workspace: 2 (2)"); + ASSERT_CONTAINS(str, "active workspace: 1 (1)"); + ASSERT_CONTAINS(str, "HEADLESS-3"); } // focus the first monitor - OK(getFromSocket("/dispatch focusmonitor 0")); + OK(getFromSocket("/dispatch hl.dsp.focus({ monitor = 'HEADLESS-2' })")); { auto str = getFromSocket("/activeworkspace"); - EXPECT_STARTS_WITH(str, "workspace ID 1 (1)"); + ASSERT_STARTS_WITH(str, "workspace ID 1 (1)"); } NLog::log("{}Switching to workspace r+1", Colors::YELLOW); - OK(getFromSocket("/dispatch workspace r+1")); + OK(getFromSocket("/dispatch hl.dsp.focus({ workspace = 'r+1' })")); { auto str = getFromSocket("/activeworkspace"); - EXPECT_STARTS_WITH(str, "workspace ID 3 (3)"); + ASSERT_STARTS_WITH(str, "workspace ID 3 (3)"); } NLog::log("{}Switching to workspace r~2", Colors::YELLOW); - OK(getFromSocket("/dispatch workspace 1")); - OK(getFromSocket("/dispatch workspace r~2")); + OK(getFromSocket("/dispatch hl.dsp.focus({ workspace = '1' })")); + OK(getFromSocket("/dispatch hl.dsp.focus({ workspace = 'r~2' })")); { auto str = getFromSocket("/activeworkspace"); - EXPECT_STARTS_WITH(str, "workspace ID 3 (3)"); + ASSERT_STARTS_WITH(str, "workspace ID 3 (3)"); } NLog::log("{}Switching to workspace m+1", Colors::YELLOW); - OK(getFromSocket("/dispatch workspace m+1")); + OK(getFromSocket("/dispatch hl.dsp.focus({ workspace = 'm+1' })")); { auto str = getFromSocket("/activeworkspace"); - EXPECT_STARTS_WITH(str, "workspace ID 1 (1)"); + ASSERT_STARTS_WITH(str, "workspace ID 1 (1)"); } NLog::log("{}Switching to workspace 1", Colors::YELLOW); // no OK: this will throw an error as it should - getFromSocket("/dispatch workspace 1"); + getFromSocket("/dispatch hl.dsp.focus({ workspace = '1' })"); { auto str = getFromSocket("/activeworkspace"); - EXPECT_STARTS_WITH(str, "workspace ID 1 (1)"); + ASSERT_STARTS_WITH(str, "workspace ID 1 (1)"); } NLog::log("{}Testing back_and_forth", Colors::YELLOW); - OK(getFromSocket("/keyword binds:workspace_back_and_forth true")); - OK(getFromSocket("/dispatch workspace 1")); + OK(getFromSocket("/eval hl.config({ binds = { workspace_back_and_forth = true } })")); + OK(getFromSocket("/dispatch hl.dsp.focus({ workspace = '1' })")); { auto str = getFromSocket("/activeworkspace"); - EXPECT_STARTS_WITH(str, "workspace ID 3 (3)"); + ASSERT_STARTS_WITH(str, "workspace ID 3 (3)"); } - OK(getFromSocket("/keyword binds:workspace_back_and_forth false")); + OK(getFromSocket("/eval hl.config({ binds = { workspace_back_and_forth = false } })")); NLog::log("{}Testing hide_special_on_workspace_change", Colors::YELLOW); - OK(getFromSocket("/keyword binds:hide_special_on_workspace_change true")); - OK(getFromSocket("/dispatch workspace special:HELLO")); + OK(getFromSocket("/eval hl.config({ binds = { hide_special_on_workspace_change = true } })")); + OK(getFromSocket("/dispatch hl.dsp.focus({ workspace = 'special:HELLO' })")); { auto str = getFromSocket("/monitors"); - EXPECT_CONTAINS(str, "special workspace: -"); - EXPECT_CONTAINS(str, "special:HELLO"); + ASSERT_CONTAINS(str, "special workspace: -"); + ASSERT_CONTAINS(str, "special:HELLO"); } // no OK: will err (it shouldn't prolly but oh well) - getFromSocket("/dispatch workspace 3"); + getFromSocket("/dispatch hl.dsp.focus({ workspace = '3' })"); { auto str = getFromSocket("/monitors"); - EXPECT_COUNT_STRING(str, "special workspace: 0 ()", 2); + ASSERT_COUNT_STRING(str, "special workspace: 0 ()", 2); } - OK(getFromSocket("/keyword binds:hide_special_on_workspace_change false")); + OK(getFromSocket("/eval hl.config({ binds = { hide_special_on_workspace_change = false } })")); NLog::log("{}Testing allow_workspace_cycles", Colors::YELLOW); - OK(getFromSocket("/keyword binds:allow_workspace_cycles true")); + OK(getFromSocket("/eval hl.config({ binds = { allow_workspace_cycles = true } })")); - OK(getFromSocket("/dispatch workspace 1")); - OK(getFromSocket("/dispatch workspace 3")); - OK(getFromSocket("/dispatch workspace 1")); + OK(getFromSocket("/dispatch hl.dsp.focus({ workspace = '1' })")); + OK(getFromSocket("/dispatch hl.dsp.focus({ workspace = '3' })")); + OK(getFromSocket("/dispatch hl.dsp.focus({ workspace = '1' })")); - OK(getFromSocket("/dispatch workspace previous")); + OK(getFromSocket("/dispatch hl.dsp.focus({ workspace = 'previous' })")); { auto str = getFromSocket("/activeworkspace"); - EXPECT_STARTS_WITH(str, "workspace ID 3 (3)"); + ASSERT_STARTS_WITH(str, "workspace ID 3 (3)"); } - OK(getFromSocket("/dispatch workspace previous")); + OK(getFromSocket("/dispatch hl.dsp.focus({ workspace = 'previous' })")); { auto str = getFromSocket("/activeworkspace"); - EXPECT_STARTS_WITH(str, "workspace ID 1 (1)"); + ASSERT_STARTS_WITH(str, "workspace ID 1 (1)"); } - OK(getFromSocket("/dispatch workspace previous")); + OK(getFromSocket("/dispatch hl.dsp.focus({ workspace = 'previous' })")); { auto str = getFromSocket("/activeworkspace"); - EXPECT_STARTS_WITH(str, "workspace ID 3 (3)"); + ASSERT_STARTS_WITH(str, "workspace ID 3 (3)"); } - OK(getFromSocket("/keyword binds:allow_workspace_cycles false")); + OK(getFromSocket("/eval hl.config({ binds = { allow_workspace_cycles = false } })")); - OK(getFromSocket("/dispatch workspace 1")); + OK(getFromSocket("/dispatch hl.dsp.focus({ workspace = '1' })")); NLog::log("{}Killing all windows", Colors::YELLOW); Tests::killAllWindows(); // spawn 3 kitties NLog::log("{}Testing focus_preferred_method", Colors::YELLOW); - OK(getFromSocket("/keyword dwindle:force_split 2")); + OK(getFromSocket("/eval hl.config({ dwindle = { force_split = 2 } })")); Tests::spawnKitty("kitty_A"); Tests::spawnKitty("kitty_B"); Tests::spawnKitty("kitty_C"); - OK(getFromSocket("/keyword dwindle:force_split 0")); + OK(getFromSocket("/eval hl.config({ dwindle = { force_split = 0 } })")); // focus kitty 2: will be top right (dwindle) - OK(getFromSocket("/dispatch focuswindow class:kitty_B")); + OK(getFromSocket("/dispatch hl.dsp.focus({ window = 'class:kitty_B' })")); // resize it to be a bit taller - OK(getFromSocket("/dispatch resizeactive +20 +20")); + OK(getFromSocket("/dispatch hl.dsp.window.resize({ x = 20, y = 20, relative = true })")); // now we test focus methods. - OK(getFromSocket("/keyword binds:focus_preferred_method 0")); + OK(getFromSocket("/eval hl.config({ binds = { focus_preferred_method = 0 } })")); - OK(getFromSocket("/dispatch focuswindow class:kitty_C")); - OK(getFromSocket("/dispatch focuswindow class:kitty_A")); + OK(getFromSocket("/dispatch hl.dsp.focus({ window = 'class:kitty_C' })")); + OK(getFromSocket("/dispatch hl.dsp.focus({ window = 'class:kitty_A' })")); - OK(getFromSocket("/dispatch movefocus r")); + OK(getFromSocket("/dispatch hl.dsp.focus({ direction = 'right' })")); { auto str = getFromSocket("/activewindow"); - EXPECT_CONTAINS(str, "class: kitty_C"); + ASSERT_CONTAINS(str, "class: kitty_C"); } - OK(getFromSocket("/dispatch focuswindow class:kitty_A")); + OK(getFromSocket("/dispatch hl.dsp.focus({ window = 'class:kitty_A' })")); - OK(getFromSocket("/keyword binds:focus_preferred_method 1")); + OK(getFromSocket("/eval hl.config({ binds = { focus_preferred_method = 1 } })")); - OK(getFromSocket("/dispatch movefocus r")); + OK(getFromSocket("/dispatch hl.dsp.focus({ direction = 'right' })")); { auto str = getFromSocket("/activewindow"); - EXPECT_CONTAINS(str, "class: kitty_B"); + ASSERT_CONTAINS(str, "class: kitty_B"); } NLog::log("{}Testing movefocus_cycles_fullscreen", Colors::YELLOW); - OK(getFromSocket("/dispatch focuswindow class:kitty_A")); - OK(getFromSocket("/dispatch focusmonitor HEADLESS-3")); + OK(getFromSocket("/dispatch hl.dsp.focus({ window = 'class:kitty_A' })")); + OK(getFromSocket("/dispatch hl.dsp.focus({ monitor = 'HEADLESS-3' })")); Tests::spawnKitty("kitty_D"); { auto str = getFromSocket("/activewindow"); - EXPECT_CONTAINS(str, "class: kitty_D"); + ASSERT_CONTAINS(str, "class: kitty_D"); } - OK(getFromSocket("/dispatch focusmonitor l")); + OK(getFromSocket("/dispatch hl.dsp.focus({ monitor = 'l' })")); { auto str = getFromSocket("/activewindow"); - EXPECT_CONTAINS(str, "class: kitty_A"); + ASSERT_CONTAINS(str, "class: kitty_A"); } - OK(getFromSocket("/keyword binds:movefocus_cycles_fullscreen false")); - OK(getFromSocket("/dispatch fullscreen 0")); + OK(getFromSocket("/eval hl.config({ binds = { movefocus_cycles_fullscreen = false } })")); + OK(getFromSocket("/dispatch hl.dsp.window.fullscreen({ mode = 'fullscreen' })")); - OK(getFromSocket("/dispatch movefocus r")); + OK(getFromSocket("/dispatch hl.dsp.focus({ direction = 'right' })")); { auto str = getFromSocket("/activewindow"); - EXPECT_CONTAINS(str, "class: kitty_D"); + ASSERT_CONTAINS(str, "class: kitty_D"); } - OK(getFromSocket("/dispatch focusmonitor l")); + OK(getFromSocket("/dispatch hl.dsp.focus({ monitor = 'l' })")); { auto str = getFromSocket("/activewindow"); - EXPECT_CONTAINS(str, "class: kitty_A"); + ASSERT_CONTAINS(str, "class: kitty_A"); } - OK(getFromSocket("/keyword binds:movefocus_cycles_fullscreen true")); + OK(getFromSocket("/eval hl.config({ binds = { movefocus_cycles_fullscreen = true } })")); - OK(getFromSocket("/dispatch movefocus r")); + OK(getFromSocket("/dispatch hl.dsp.focus({ direction = 'right' })")); { auto str = getFromSocket("/activewindow"); - EXPECT_CONTAINS(str, "class: kitty_B"); + ASSERT_CONTAINS(str, "class: kitty_B"); } // kill all NLog::log("{}Killing all windows", Colors::YELLOW); Tests::killAllWindows(); - testMultimonBAF(); - testMultimonFocus(); - testWorkspaceHistoryMultiMon(); + CALL_SUBTEST(multimonBAF); + CALL_SUBTEST(multimonFocus); + CALL_SUBTEST(workspaceHistoryMultiMon); // destroy the headless output OK(getFromSocket("/output remove HEADLESS-3")); - testSpecialWorkspaceFullscreen(); - testAsymmetricGaps(); - testDynamicWsEffects(); + CALL_SUBTEST(specialWorkspaceFullscreen); + CALL_SUBTEST(asymmetricGaps); + CALL_SUBTEST(dynamicWsEffects); NLog::log("{}Expecting 0 windows", Colors::YELLOW); - EXPECT(Tests::windowCount(), 0); - - return !ret; + ASSERT(Tests::windowCount(), 0); } - -REGISTER_TEST_FN(test) diff --git a/hyprtester/src/tests/plugin/plugin.cpp b/hyprtester/src/tests/plugin/plugin.cpp index ffcc351ac..fbc809ae5 100644 --- a/hyprtester/src/tests/plugin/plugin.cpp +++ b/hyprtester/src/tests/plugin/plugin.cpp @@ -11,7 +11,7 @@ #include "../shared.hpp" bool testPlugin() { - const auto RESPONSE = getFromSocket("/dispatch plugin:test:test"); + const auto RESPONSE = getFromSocket("/eval hl.plugin.test.test()"); if (RESPONSE != "ok") { NLog::log("{}Plugin tests failed, plugin returned:\n{}{}", Colors::RED, Colors::RESET, RESPONSE); @@ -21,7 +21,7 @@ bool testPlugin() { } bool testVkb() { - const auto RESPONSE = getFromSocket("/dispatch plugin:test:vkb"); + const auto RESPONSE = getFromSocket("/eval hl.plugin.test.vkb()"); if (RESPONSE != "ok") { NLog::log("{}Vkb tests failed, tests returned:\n{}{}", Colors::RED, Colors::RESET, RESPONSE); diff --git a/hyprtester/src/tests/shared.cpp b/hyprtester/src/tests/shared.cpp index 32824f3b6..d08f4ea23 100644 --- a/hyprtester/src/tests/shared.cpp +++ b/hyprtester/src/tests/shared.cpp @@ -1,4 +1,5 @@ #include "shared.hpp" +#include #include #include #include @@ -10,6 +11,10 @@ using namespace Hyprutils::OS; using namespace Hyprutils::Memory; +// Almost everywhere `Tests::spawnKitty` is used, the return value is immediately tested against `nullptr` +// and the test fails if it is. +// TODO: add a test macro for that. + CUniquePointer Tests::spawnKitty(const std::string& class_, const std::vector args) { const auto COUNT_BEFORE = windowCount(); @@ -98,7 +103,7 @@ bool Tests::killAllWindows() { auto pos = str.find("Window "); while (pos != std::string::npos) { auto pos2 = str.find(" -> ", pos); - getFromSocket("/dispatch killwindow address:0x" + str.substr(pos + 7, pos2 - pos - 7)); + getFromSocket("/dispatch hl.dsp.window.kill({ window = 'address:0x" + str.substr(pos + 7, pos2 - pos - 7) + "' })"); pos = str.find("Window ", pos + 5); } @@ -182,12 +187,14 @@ bool Tests::writeFile(const std::string& name, const std::string& contents) { return true; } -std::string Tests::getWindowAttribute(const std::string& winInfo, const std::string& attr) { - auto pos = winInfo.find(attr); +std::string Tests::getAttribute(const std::string& hyprlandResponse, std::string attr) { + attr += ": "; + auto pos = hyprlandResponse.find(attr); if (pos == std::string::npos) { NLog::log("{}Window attribute not found: '{}'", Colors::RED, attr); return "Wrong window attribute"; } - auto pos2 = winInfo.find('\n', pos); - return winInfo.substr(pos, pos2 - pos); + pos += attr.size(); + auto pos2 = hyprlandResponse.find('\n', pos); + return hyprlandResponse.substr(pos, pos2 - pos); } diff --git a/hyprtester/src/tests/shared.hpp b/hyprtester/src/tests/shared.hpp index 95058e040..fbef8a0e1 100644 --- a/hyprtester/src/tests/shared.hpp +++ b/hyprtester/src/tests/shared.hpp @@ -19,5 +19,11 @@ namespace Tests { bool killAllLayers(); std::string execAndGet(const std::string& cmd); bool writeFile(const std::string& name, const std::string& contents); - std::string getWindowAttribute(const std::string& winInfo, const std::string& attr); + /** + * Extracts the given attribute from Hyprland's response to requests such as `/clients`, `/workspaces`, etc. + * Automatically appends `: ` to `attr`. + * + * For example, `Tests::getAttribute(getFromSocket("/activewindow"), "at")` returns the active window's coordinates, e.g., `"2,32"` + */ + std::string getAttribute(const std::string& hyprlandResponse, std::string attr); }; diff --git a/hyprtester/test.conf b/hyprtester/test.conf deleted file mode 100644 index ab4f8ee36..000000000 --- a/hyprtester/test.conf +++ /dev/null @@ -1,413 +0,0 @@ -# This is an example Hyprland config file. -# Refer to the wiki for more information. -# https://wiki.hyprland.org/Configuring/ - -# Please note not all available settings / options are set here. -# For a full list, see the wiki - -# You can split this configuration into multiple files -# Create your files separately and then link them to this file like this: -# source = ~/.config/hypr/myColors.conf - - -################ -### MONITORS ### -################ - -# See https://wiki.hyprland.org/Configuring/Monitors/ - -monitor=HEADLESS-1,1920x1080@60,auto-right,1 -monitor=HEADLESS-2,1920x1080@60,auto-right,1 -monitor=HEADLESS-3,1920x1080@60,auto-right,1 -monitor=HEADLESS-4,1920x1080@60,auto-right,1 -monitor=HEADLESS-5,1920x1080@60,auto-right,1 -monitor=HEADLESS-6,1920x1080@60,auto-right,1 -monitor=HEADLESS-PERSISTENT-TEST,1920x1080@60,auto-right,1 - -monitor=,disabled - - -################### -### MY PROGRAMS ### -################### - -# See https://wiki.hyprland.org/Configuring/Keywords/ - -# Set programs that you use -$terminal = kitty -$fileManager = dolphin -$menu = wofi --show drun - - -################# -### AUTOSTART ### -################# - -# Autostart necessary processes (like notifications daemons, status bars, etc.) -# Or execute your favorite apps at launch like this: - -exec-once = sleep 0 # Terminates very quickly -# exec-once = $terminal -# exec-once = nm-applet & -# exec-once = waybar & hyprpaper & firefox - - -############################# -### ENVIRONMENT VARIABLES ### -############################# - -# See https://wiki.hyprland.org/Configuring/Environment-variables/ - -env = XCURSOR_SIZE,24 -env = HYPRCURSOR_SIZE,24 - - -##################### -### LOOK AND FEEL ### -##################### - -# Refer to https://wiki.hyprland.org/Configuring/Variables/ - -# https://wiki.hyprland.org/Configuring/Variables/#general -general { - gaps_in = 5 - gaps_out = 20 - - border_size = 2 - - snap { - enabled = true - window_gap = 8 - monitor_gap = 10 - respect_gaps = false - border_overlap = false - } - - # https://wiki.hyprland.org/Configuring/Variables/#variable-types for info about colors - col.active_border = rgba(33ccffee) rgba(00ff99ee) 45deg - col.inactive_border = rgba(595959aa) - - # Set to true enable resizing windows by clicking and dragging on borders and gaps - resize_on_border = false - - # Please see https://wiki.hyprland.org/Configuring/Tearing/ before you turn this on - allow_tearing = false - - layout = dwindle -} - -# https://wiki.hyprland.org/Configuring/Variables/#decoration -decoration { - rounding = 10 - rounding_power = 2 - - # Change transparency of focused and unfocused windows - active_opacity = 1.0 - inactive_opacity = 1.0 - - shadow { - enabled = true - range = 4 - render_power = 3 - color = rgba(1a1a1aee) - } - - # https://wiki.hyprland.org/Configuring/Variables/#blur - blur { - enabled = true - size = 3 - passes = 1 - - vibrancy = 0.1696 - } -} - -# https://wiki.hyprland.org/Configuring/Variables/#animations -animations { - enabled = 0 - - # Default animations, see https://wiki.hyprland.org/Configuring/Animations/ for more - - bezier = easeOutQuint,0.23,1,0.32,1 - bezier = easeInOutCubic,0.65,0.05,0.36,1 - bezier = linear,0,0,1,1 - bezier = almostLinear,0.5,0.5,0.75,1.0 - bezier = quick,0.15,0,0.1,1 - - animation = global, 1, 10, default - animation = border, 1, 5.39, easeOutQuint - animation = windows, 1, 4.79, easeOutQuint - animation = windowsIn, 1, 4.1, easeOutQuint, popin 87% - animation = windowsOut, 1, 1.49, linear, popin 87% - animation = fadeIn, 1, 1.73, almostLinear - animation = fadeOut, 1, 1.46, almostLinear - animation = fade, 1, 3.03, quick - animation = layers, 1, 3.81, easeOutQuint - animation = layersIn, 1, 4, easeOutQuint, fade - animation = layersOut, 1, 1.5, linear, fade - animation = fadeLayersIn, 1, 1.79, almostLinear - animation = fadeLayersOut, 1, 1.39, almostLinear - animation = workspaces, 1, 1.94, almostLinear, fade - animation = workspacesIn, 1, 1.21, almostLinear, fade - animation = workspacesOut, 1, 1.94, almostLinear, fade -} - -device { - name = test-mouse-1 - enabled = true -} - -# Ref https://wiki.hyprland.org/Configuring/Workspace-Rules/ -# "Smart gaps" / "No gaps when only" -# uncomment all if you wish to use that. -# workspace = w[tv1], gapsout:0, gapsin:0 -# workspace = f[1], gapsout:0, gapsin:0 -# windowrulev2 = bordersize 0, floating:0, onworkspace:w[tv1] -# windowrulev2 = rounding 0, floating:0, onworkspace:w[tv1] -# windowrulev2 = bordersize 0, floating:0, onworkspace:f[1] -# windowrulev2 = rounding 0, floating:0, onworkspace:f[1] - -# See https://wiki.hyprland.org/Configuring/Dwindle-Layout/ for more -dwindle { - pseudotile = true # Master switch for pseudotiling. Enabling is bound to mainMod + P in the keybinds section below - preserve_split = true # You probably want this - split_bias = 1 -} - -# See https://wiki.hyprland.org/Configuring/Master-Layout/ for more -master { - new_status = master -} - -scrolling { - fullscreen_on_one_column = true - column_width = 0.5 - focus_fit_method = 1 - follow_focus = true - follow_min_visible = 1 - explicit_column_widths = 0.25, 0.333, 0.5, 0.667, 0.75, 1.0 - wrap_focus = true - wrap_swapcol = true -} - -# https://wiki.hyprland.org/Configuring/Variables/#misc -misc { - force_default_wallpaper = -1 # Set to 0 or 1 to disable the anime mascot wallpapers - disable_hyprland_logo = false # If true disables the random hyprland logo / anime girl background. :( -} - - -############# -### INPUT ### -############# - -# https://wiki.hyprland.org/Configuring/Variables/#input -input { - kb_layout = us - kb_variant = - kb_model = - kb_options = - kb_rules = - - follow_mouse = 1 - - sensitivity = 0 # -1.0 - 1.0, 0 means no modification. - - touchpad { - natural_scroll = false - } -} - -# https://wiki.hyprland.org/Configuring/Variables/#gestures -gestures { - -} - -# Example per-device config -# See https://wiki.hyprland.org/Configuring/Keywords/#per-device-input-configs for more -device { - name = epic-mouse-v1 - sensitivity = -0.5 -} - -debug { - disable_logs = false -} - - -################### -### KEYBINDINGS ### -################### - -# See https://wiki.hyprland.org/Configuring/Keywords/ -$mainMod = SUPER # Sets "Windows" key as main modifier - -# Example binds, see https://wiki.hyprland.org/Configuring/Binds/ for more -bind = $mainMod, Q, exec, $terminal -bind = $mainMod, C, killactive, -bind = $mainMod, M, exit, -bind = $mainMod, E, exec, $fileManager -bind = $mainMod, V, togglefloating, -bind = $mainMod, R, exec, $menu -bind = $mainMod, P, pseudo, # dwindle -bind = $mainMod, J, layoutmsg, togglesplit, # dwindle - -# Move focus with mainMod + arrow keys -bind = $mainMod, left, movefocus, l -bind = $mainMod, right, movefocus, r -bind = $mainMod, up, movefocus, u -bind = $mainMod, down, movefocus, d - -# Switch workspaces with mainMod + [0-9] -bind = $mainMod, 1, workspace, 1 -bind = $mainMod, 2, workspace, 2 -bind = $mainMod, 3, workspace, 3 -bind = $mainMod, 4, workspace, 4 -bind = $mainMod, 5, workspace, 5 -bind = $mainMod, 6, workspace, 6 -bind = $mainMod, 7, workspace, 7 -bind = $mainMod, 8, workspace, 8 -bind = $mainMod, 9, workspace, 9 -bind = $mainMod, 0, workspace, 10 - -# Move active window to a workspace with mainMod + SHIFT + [0-9] -bind = $mainMod SHIFT, 1, movetoworkspace, 1 -bind = $mainMod SHIFT, 2, movetoworkspace, 2 -bind = $mainMod SHIFT, 3, movetoworkspace, 3 -bind = $mainMod SHIFT, 4, movetoworkspace, 4 -bind = $mainMod SHIFT, 5, movetoworkspace, 5 -bind = $mainMod SHIFT, 6, movetoworkspace, 6 -bind = $mainMod SHIFT, 7, movetoworkspace, 7 -bind = $mainMod SHIFT, 8, movetoworkspace, 8 -bind = $mainMod SHIFT, 9, movetoworkspace, 9 -bind = $mainMod SHIFT, 0, movetoworkspace, 10 - -# Example special workspace (scratchpad) -bind = $mainMod, S, togglespecialworkspace, magic -bind = $mainMod SHIFT, S, movetoworkspace, special:magic - -# Scroll through existing workspaces with mainMod + scroll -bind = $mainMod, mouse_down, workspace, e+1 -bind = $mainMod, mouse_up, workspace, e-1 - -# Move/resize windows with mainMod + LMB/RMB and dragging -bindm = $mainMod, mouse:272, movewindow -bindm = $mainMod, mouse:273, resizewindow - -# Laptop multimedia keys for volume and LCD brightness -bindel = ,XF86AudioRaiseVolume, exec, wpctl set-volume -l 1 @DEFAULT_AUDIO_SINK@ 5%+ -bindel = ,XF86AudioLowerVolume, exec, wpctl set-volume @DEFAULT_AUDIO_SINK@ 5%- -bindel = ,XF86AudioMute, exec, wpctl set-mute @DEFAULT_AUDIO_SINK@ toggle -bindel = ,XF86AudioMicMute, exec, wpctl set-mute @DEFAULT_AUDIO_SOURCE@ toggle -bindel = ,XF86MonBrightnessUp, exec, brightnessctl s 10%+ -bindel = ,XF86MonBrightnessDown, exec, brightnessctl s 10%- - -# Requires playerctl -bindl = , XF86AudioNext, exec, playerctl next -bindl = , XF86AudioPause, exec, playerctl play-pause -bindl = , XF86AudioPlay, exec, playerctl play-pause -bindl = , XF86AudioPrev, exec, playerctl previous - -bind = $mainMod, u, submap, submap1 - -submap = submap1 -bind = , u, submap, submap2 -bind = , i, submap, submap3 -bind = , o, exec, $terminal -bind = , p, submap, reset - -submap = submap2, submap1 -bind = , o, exec, $terminal - -submap = submap3, reset -bind = , o, exec, $terminal - -submap = reset - - -############################## -### WINDOWS AND WORKSPACES ### -############################## - -windowrule { - # Ignore maximize requests from apps. You'll probably like this. - name = suppress-maximize-events - match:class = .* - - suppress_event = maximize -} - -windowrule { - # Fix some dragging issues with XWayland - name = fix-xwayland-drags - match:class = ^$ - match:title = ^$ - match:xwayland = true - match:float = true - match:fullscreen = false - match:pin = false - - no_focus = true -} - -workspace = n[s:window] w[tv1], gapsout:0, gapsin:0 -workspace = n[s:window] f[1], gapsout:0, gapsin:0 - -windowrule { - name = smart-gaps-1 - match:float = false - match:workspace = n[s:window] w[tv1] - - border_size = 0 - rounding = 0 -} - -windowrule { - name = smart-gaps-2 - match:float = false - match:workspace = n[s:window] f[1] - - border_size = 0 - rounding = 0 -} - -windowrule { - name = wr-kitty-stuff - match:class = wr_kitty - - float = true - size = 200 200 - pin = false -} - -windowrule { - name = tagged-kitty-floats - match:tag = tag_kitty - - float = true -} - -windowrule { - name = static-kitty-tag - match:class = tag_kitty - - tag = +tag_kitty -} - -gesture = 3, left, dispatcher, exec, kitty -gesture = 3, right, float -gesture = 3, up, close -gesture = 3, down, fullscreen - -gesture = 3, down, mod:ALT, float - -gesture = 3, horizontal, mod:ALT, workspace - -gesture = 5, up, dispatcher, sendshortcut, , e, activewindow -gesture = 5, down, dispatcher, sendshortcut, , x, activewindow -gesture = 5, left, dispatcher, sendshortcut, , i, activewindow -gesture = 5, right, dispatcher, sendshortcut, , t, activewindow -gesture = 4, right, dispatcher, sendshortcut, , return, activewindow -gesture = 4, left, dispatcher, movecursortocorner, 1 - -gesturep = 2, right, float diff --git a/hyprtester/test.lua b/hyprtester/test.lua new file mode 100644 index 000000000..6f39b34a3 --- /dev/null +++ b/hyprtester/test.lua @@ -0,0 +1,294 @@ +-- Hyprtester Lua config + +hl.monitor({ output = "HEADLESS-1", mode = "1920x1080@60", position = "auto-right", scale = "1" }) +hl.monitor({ output = "HEADLESS-2", mode = "1920x1080@60", position = "auto-right", scale = "1" }) +hl.monitor({ output = "HEADLESS-3", mode = "1920x1080@60", position = "auto-right", scale = "1" }) +hl.monitor({ output = "HEADLESS-4", mode = "1920x1080@60", position = "auto-right", scale = "1" }) +hl.monitor({ output = "HEADLESS-5", mode = "1920x1080@60", position = "auto-right", scale = "1" }) +hl.monitor({ output = "HEADLESS-6", mode = "1920x1080@60", position = "auto-right", scale = "1" }) +hl.monitor({ output = "HEADLESS-PERSISTENT-TEST", mode = "1920x1080@60", position = "auto-right", scale = "1" }) +hl.monitor({ output = "", disabled = true }) + +local terminal = "kitty" +local fileManager = "dolphin" +local menu = "wofi --show drun" + +hl.on("hyprland.start", function() + hl.dispatch(hl.dsp.exec_cmd("sleep 0")) +end) + +hl.env("XCURSOR_SIZE", "24") +hl.env("HYPRCURSOR_SIZE", "24") + +hl.config({ + general = { + gaps_in = 5, + gaps_out = 20, + border_size = 2, + snap = { + enabled = true, + window_gap = 8, + monitor_gap = 10, + respect_gaps = false, + border_overlap = false, + }, + col = { + active_border = { colors = { "rgba(33ccffee)", "rgba(00ff99ee)" }, angle = 45 }, + inactive_border = "rgba(595959aa)", + }, + resize_on_border = false, + allow_tearing = false, + layout = "dwindle", + }, +}) + +hl.config({ + decoration = { + rounding = 10, + rounding_power = 2, + active_opacity = 1.0, + inactive_opacity = 1.0, + shadow = { + enabled = true, + range = 4, + render_power = 3, + color = "rgba(1a1a1aee)", + }, + blur = { + enabled = true, + size = 3, + passes = 1, + vibrancy = 0.1696, + }, + }, +}) + +hl.config({ + animations = { + enabled = false, + }, +}) + +hl.curve("easeOutQuint", { type = "bezier", points = { {0.23, 1}, {0.32, 1} } }) +hl.curve("easeInOutCubic", { type = "bezier", points = { {0.65, 0.05}, {0.36, 1} } }) +hl.curve("linear", { type = "bezier", points = { {0, 0}, {1, 1} } }) +hl.curve("almostLinear", { type = "bezier", points = { {0.5, 0.5}, {0.75, 1.0} } }) +hl.curve("quick", { type = "bezier", points = { {0.15, 0}, {0.1, 1} } }) + +hl.animation({ leaf = "global", enabled = true, speed = 10, bezier = "default" }) +hl.animation({ leaf = "border", enabled = true, speed = 5.39, bezier = "easeOutQuint" }) +hl.animation({ leaf = "windows", enabled = true, speed = 4.79, bezier = "easeOutQuint" }) +hl.animation({ leaf = "windowsIn", enabled = true, speed = 4.1, bezier = "easeOutQuint", style = "popin 87%" }) +hl.animation({ leaf = "windowsOut", enabled = true, speed = 1.49, bezier = "linear", style = "popin 87%" }) +hl.animation({ leaf = "fadeIn", enabled = true, speed = 1.73, bezier = "almostLinear" }) +hl.animation({ leaf = "fadeOut", enabled = true, speed = 1.46, bezier = "almostLinear" }) +hl.animation({ leaf = "fade", enabled = true, speed = 3.03, bezier = "quick" }) +hl.animation({ leaf = "layers", enabled = true, speed = 3.81, bezier = "easeOutQuint" }) +hl.animation({ leaf = "layersIn", enabled = true, speed = 4, bezier = "easeOutQuint", style = "fade" }) +hl.animation({ leaf = "layersOut", enabled = true, speed = 1.5, bezier = "linear", style = "fade" }) +hl.animation({ leaf = "fadeLayersIn", enabled = true, speed = 1.79, bezier = "almostLinear" }) +hl.animation({ leaf = "fadeLayersOut", enabled = true, speed = 1.39, bezier = "almostLinear" }) +hl.animation({ leaf = "workspaces", enabled = true, speed = 1.94, bezier = "almostLinear", style = "fade" }) +hl.animation({ leaf = "workspacesIn", enabled = true, speed = 1.21, bezier = "almostLinear", style = "fade" }) +hl.animation({ leaf = "workspacesOut", enabled = true, speed = 1.94, bezier = "almostLinear", style = "fade" }) + +hl.device({ name = "test-mouse-1", enabled = true }) + +hl.config({ + dwindle = { + preserve_split = true, + split_bias = 1, + }, +}) + +hl.config({ + master = { + new_status = "master", + }, +}) + +hl.config({ + scrolling = { + fullscreen_on_one_column = true, + column_width = 0.5, + focus_fit_method = 1, + follow_focus = true, + follow_min_visible = 1, + explicit_column_widths = "0.25, 0.333, 0.5, 0.667, 0.75, 1.0", + wrap_focus = true, + wrap_swapcol = true, + }, +}) + +hl.config({ + misc = { + force_default_wallpaper = -1, + disable_hyprland_logo = false, + }, +}) + +hl.config({ + input = { + kb_layout = "us", + kb_variant = "", + kb_model = "", + kb_options = "", + kb_rules = "", + follow_mouse = 1, + sensitivity = 0, + touchpad = { + natural_scroll = false, + }, + }, +}) + +hl.device({ + name = "epic-mouse-v1", + sensitivity = -0.5, +}) + +hl.config({ + debug = { + disable_logs = false, + }, +}) + +local mainMod = "SUPER" + +hl.bind(mainMod .. " + Q", hl.dsp.exec_cmd(terminal)) +hl.bind(mainMod .. " + C", hl.dsp.window.close()) +hl.bind(mainMod .. " + M", hl.dsp.exit()) +hl.bind(mainMod .. " + E", hl.dsp.exec_cmd(fileManager)) +hl.bind(mainMod .. " + V", hl.dsp.window.float({ action = "toggle" })) +hl.bind(mainMod .. " + R", hl.dsp.exec_cmd(menu)) +hl.bind(mainMod .. " + P", hl.dsp.window.pseudo()) +hl.bind(mainMod .. " + J", hl.dsp.layout("togglesplit")) + +hl.bind(mainMod .. " + left", hl.dsp.focus({ direction = "left" })) +hl.bind(mainMod .. " + right", hl.dsp.focus({ direction = "right" })) +hl.bind(mainMod .. " + up", hl.dsp.focus({ direction = "up" })) +hl.bind(mainMod .. " + down", hl.dsp.focus({ direction = "down" })) + +for i = 1, 10 do + local key = i % 10 + hl.bind(mainMod .. " + " .. key, hl.dsp.focus({ workspace = tostring(i) })) + hl.bind(mainMod .. " + SHIFT + " .. key, hl.dsp.window.move({ workspace = tostring(i) })) +end + +hl.bind(mainMod .. " + S", hl.dsp.workspace.toggle_special("magic")) +hl.bind(mainMod .. " + SHIFT + S", hl.dsp.window.move({ workspace = "special:magic" })) + +hl.bind(mainMod .. " + mouse_down", hl.dsp.focus({ workspace = "e+1" })) +hl.bind(mainMod .. " + mouse_up", hl.dsp.focus({ workspace = "e-1" })) + +hl.bind(mainMod .. " + mouse:272", hl.dsp.window.drag(), { mouse = true }) +hl.bind(mainMod .. " + mouse:273", hl.dsp.window.resize(), { mouse = true }) + +hl.bind("XF86AudioRaiseVolume", hl.dsp.exec_cmd("wpctl set-volume -l 1 @DEFAULT_AUDIO_SINK@ 5%+"), { locked = true, repeating = true }) +hl.bind("XF86AudioLowerVolume", hl.dsp.exec_cmd("wpctl set-volume @DEFAULT_AUDIO_SINK@ 5%-"), { locked = true, repeating = true }) +hl.bind("XF86AudioMute", hl.dsp.exec_cmd("wpctl set-mute @DEFAULT_AUDIO_SINK@ toggle"), { locked = true, repeating = true }) +hl.bind("XF86AudioMicMute", hl.dsp.exec_cmd("wpctl set-mute @DEFAULT_AUDIO_SOURCE@ toggle"), { locked = true, repeating = true }) +hl.bind("XF86MonBrightnessUp", hl.dsp.exec_cmd("brightnessctl s 10%+"), { locked = true, repeating = true }) +hl.bind("XF86MonBrightnessDown", hl.dsp.exec_cmd("brightnessctl s 10%-"), { locked = true, repeating = true }) + +hl.bind("XF86AudioNext", hl.dsp.exec_cmd("playerctl next"), { locked = true }) +hl.bind("XF86AudioPause", hl.dsp.exec_cmd("playerctl play-pause"), { locked = true }) +hl.bind("XF86AudioPlay", hl.dsp.exec_cmd("playerctl play-pause"), { locked = true }) +hl.bind("XF86AudioPrev", hl.dsp.exec_cmd("playerctl previous"), { locked = true }) + +hl.bind(mainMod .. " + u", hl.dsp.submap("submap1")) + +hl.define_submap("submap1", function() + hl.bind("u", hl.dsp.submap("submap2")) + hl.bind("i", hl.dsp.submap("submap3")) + hl.bind("o", hl.dsp.exec_cmd(terminal)) + hl.bind("p", hl.dsp.submap("reset")) +end) + +hl.define_submap("submap2", "submap1", function() + hl.bind("o", hl.dsp.exec_cmd(terminal)) +end) + +hl.define_submap("submap3", "reset", function() + hl.bind("o", hl.dsp.exec_cmd(terminal)) +end) + +hl.window_rule({ + name = "suppress-maximize-events", + match = { class = ".*" }, + suppress_event = "maximize", +}) + +hl.window_rule({ + name = "fix-xwayland-drags", + match = { + class = "^$", + title = "^$", + xwayland = true, + float = true, + fullscreen = false, + pin = false, + }, + no_focus = true, +}) + +hl.workspace_rule({ workspace = "n[s:window] w[tv1]", gaps_out = { top = 0, right = 0, bottom = 0, left = 0 }, gaps_in = { top = 0, right = 0, bottom = 0, left = 0 } }) +hl.workspace_rule({ workspace = "n[s:window] f[1]", gaps_out = { top = 0, right = 0, bottom = 0, left = 0 }, gaps_in = { top = 0, right = 0, bottom = 0, left = 0 } }) + +hl.window_rule({ + name = "smart-gaps-1", + match = { float = false, workspace = "n[s:window] w[tv1]" }, + border_size = 0, + rounding = 0, +}) + +hl.window_rule({ + name = "smart-gaps-2", + match = { float = false, workspace = "n[s:window] f[1]" }, + border_size = 0, + rounding = 0, +}) + +hl.window_rule({ + name = "wr-kitty-stuff", + match = { class = "wr_kitty" }, + float = true, + size = "200 200", + pin = false, +}) + +hl.window_rule({ + name = "tagged-kitty-floats", + match = { tag = "tag_kitty" }, + float = true, +}) + +hl.window_rule({ + name = "static-kitty-tag", + match = { class = "tag_kitty" }, + tag = "+tag_kitty", +}) + +hl.gesture({ + fingers = 3, + direction = "left", + action = function() + hl.dispatch(hl.dsp.exec_cmd("kitty")) + end, +}) + +hl.gesture({ fingers = 3, direction = "right", action = "float" }) +hl.gesture({ fingers = 3, direction = "up", action = "close" }) +hl.gesture({ fingers = 3, direction = "down", action = "fullscreen" }) + +hl.gesture({ fingers = 3, direction = "down", mods = "ALT", action = "float" }) +hl.gesture({ fingers = 3, direction = "horizontal", mods = "ALT", action = "workspace" }) + +hl.gesture({ fingers = 5, direction = "up", action = function() hl.dispatch(hl.dsp.send_shortcut({ mods = "", key = "e", window = "activewindow" })) end }) +hl.gesture({ fingers = 5, direction = "down", action = function() hl.dispatch(hl.dsp.send_shortcut({ mods = "", key = "x", window = "activewindow" })) end }) +hl.gesture({ fingers = 5, direction = "left", action = function() hl.dispatch(hl.dsp.send_shortcut({ mods = "", key = "i", window = "activewindow" })) end }) +hl.gesture({ fingers = 5, direction = "right", action = function() hl.dispatch(hl.dsp.send_shortcut({ mods = "", key = "t", window = "activewindow" })) end }) +hl.gesture({ fingers = 4, direction = "right", action = function() hl.dispatch(hl.dsp.send_shortcut({ mods = "", key = "return", window = "activewindow" })) end }) +hl.gesture({ fingers = 4, direction = "left", action = function() hl.dispatch(hl.dsp.cursor.move_to_corner({ corner = 1, window = "activewindow" })) end }) + +hl.gesture({ fingers = 2, direction = "right", action = "float", disable_inhibit = true }) diff --git a/meta/generateLuaStubs.py b/meta/generateLuaStubs.py new file mode 100644 index 000000000..a523496df --- /dev/null +++ b/meta/generateLuaStubs.py @@ -0,0 +1,746 @@ +#!/usr/bin/env python3 + +from __future__ import annotations + +import argparse +import re +import sys +from dataclasses import dataclass, field +from pathlib import Path +from typing import Iterable + + +@dataclass +class ApiNode: + methods: set[str] = field(default_factory=set) + children: dict[str, "ApiNode"] = field(default_factory=dict) + + +@dataclass +class ObjectClass: + name: str + methods: set[str] = field(default_factory=set) + fields: dict[str, str] = field(default_factory=dict) + + +def read_text(path: Path) -> str: + return path.read_text(encoding="utf-8") + + +def find_matching_brace(text: str, open_brace_idx: int) -> int: + depth = 0 + in_string = False + string_char = "" + escaped = False + + for i in range(open_brace_idx, len(text)): + c = text[i] + + if in_string: + if escaped: + escaped = False + continue + + if c == "\\": + escaped = True + continue + + if c == string_char: + in_string = False + continue + + if c in ('"', "'"): + in_string = True + string_char = c + continue + + if c == "{": + depth += 1 + elif c == "}": + depth -= 1 + if depth == 0: + return i + + raise ValueError("Unbalanced braces while parsing C++ source") + + +def extract_function_bodies(source: str, header_pattern: re.Pattern[str]) -> list[tuple[re.Match[str], str]]: + out: list[tuple[re.Match[str], str]] = [] + for match in header_pattern.finditer(source): + open_idx = source.find("{", match.end() - 1) + if open_idx < 0: + continue + + close_idx = find_matching_brace(source, open_idx) + out.append((match, source[open_idx + 1 : close_idx])) + + return out + + +def merge_node(dst: ApiNode, src: ApiNode) -> None: + dst.methods |= src.methods + for key, child in src.children.items(): + if key not in dst.children: + dst.children[key] = child + else: + merge_node(dst.children[key], child) + + +def parse_binding_tree(root: Path) -> tuple[ApiNode, set[str]]: + bindings_dir = root / "src/config/lua/bindings" + + root_node = ApiNode() + callable_namespaces: set[str] = set() + + register_header = re.compile( + r"void\s+Internal::register\w+Bindings\s*\([^)]*\)\s*\{", re.MULTILINE + ) + set_fn = re.compile(r'Internal::set(?:Mgr)?Fn\(\s*L\s*,(?:\s*mgr\s*,)?\s*"([^"]+)"\s*,') + set_field = re.compile(r'lua_setfield\(L,\s*-2,\s*"([^"]+)"\s*\);') + + for cpp in sorted(bindings_dir.glob("*.cpp")): + source = read_text(cpp) + for _, body in extract_function_bodies(source, register_header): + local_root = ApiNode() + stack: list[ApiNode] = [local_root] + + if re.search( + r'lua_setfield\(L,\s*-2,\s*"__call"\s*\);.*?lua_setfield\(L,\s*-2,\s*"([^"]+)"\s*\);', + body, + flags=re.DOTALL, + ): + for ns in re.findall( + r'lua_setfield\(L,\s*-2,\s*"__call"\s*\);.*?lua_setfield\(L,\s*-2,\s*"([^"]+)"\s*\);', + body, + flags=re.DOTALL, + ): + callable_namespaces.add(ns) + + for raw_line in body.splitlines(): + line = raw_line.strip() + if not line: + continue + + if "lua_newtable(L)" in line: + stack.append(ApiNode()) + continue + + m = set_fn.search(line) + if m: + stack[-1].methods.add(m.group(1)) + continue + + if "lua_setmetatable(L" in line: + if len(stack) > 1: + stack.pop() + continue + + m = set_field.search(line) + if m: + field_name = m.group(1) + if field_name == "__call": + continue + + if len(stack) > 1: + node = stack.pop() + if field_name in stack[-1].children: + merge_node(stack[-1].children[field_name], node) + else: + stack[-1].children[field_name] = node + + merge_node(root_node, local_root) + + return root_node, callable_namespaces + + +def parse_object_classes(root: Path) -> dict[str, ObjectClass]: + objects_dir = root / "src/config/lua/objects" + mt_regex = re.compile(r'static constexpr const char\* MT = "([^"]+)";') + index_header = re.compile(r"static int\s+\w*Index\s*\(lua_State\* L\)\s*\{", re.MULTILINE) + cond_regex = re.compile(r"(?:if|else\s+if)\s*\(([^)]*\bkey\b[^)]*)\)") + push_class_regex = re.compile(r"Objects::CLua([A-Za-z0-9_]+)::push") + + out: dict[str, ObjectClass] = {} + + for cpp in sorted(objects_dir.glob("*.cpp")): + source = read_text(cpp) + mt_match = mt_regex.search(source) + if not mt_match: + continue + + mt_name = mt_match.group(1) + if not mt_name.startswith("HL."): + continue + + class_name = mt_name + obj = ObjectClass(name=class_name) + + bodies = extract_function_bodies(source, index_header) + if not bodies: + out[class_name] = obj + continue + + body = bodies[0][1] + cond_matches = list(cond_regex.finditer(body)) + + for i, cond in enumerate(cond_matches): + start = cond.start() + end = cond_matches[i + 1].start() if i + 1 < len(cond_matches) else len(body) + segment = body[start:end] + + keys = re.findall(r'"([^"]+)"', cond.group(1)) + if not keys: + continue + + is_method = "lua_pushcfunction" in segment + + if is_method: + for key in keys: + obj.methods.add(key) + continue + + inferred_types: set[str] = set() + if "lua_pushboolean" in segment: + inferred_types.add("boolean") + if "lua_pushstring" in segment or "lua_pushfstring" in segment: + inferred_types.add("string") + if "lua_pushinteger" in segment: + inferred_types.add("integer") + if "lua_pushnumber" in segment: + inferred_types.add("number") + if "lua_newtable" in segment: + inferred_types.add("table") + if "lua_pushnil" in segment: + inferred_types.add("nil") + + for pushed in push_class_regex.findall(segment): + inferred_types.add(f"HL.{pushed}") + + if not inferred_types: + type_str = "any" + else: + ordered = sorted(inferred_types, key=lambda t: (t == "nil", t)) + type_str = "|".join(ordered) + + for key in keys: + if key in obj.fields: + existing = set(obj.fields[key].split("|")) + merged = existing | set(type_str.split("|")) + obj.fields[key] = "|".join(sorted(merged, key=lambda t: (t == "nil", t))) + else: + obj.fields[key] = type_str + + out[class_name] = obj + + return out + + +def lua_type_from_config_ctor(ctor: str) -> str: + mapping = { + "CLuaConfigBool": "boolean", + "CLuaConfigInt": "integer|boolean", + "CLuaConfigFloat": "number|boolean", + "CLuaConfigString": "string", + "CLuaConfigColor": "string", + "CLuaConfigVec2": "HL.Vec2Like", + "CLuaConfigCssGap": "integer|HL.CssGap", + "CLuaConfigFontWeight": "integer|string", + "CLuaConfigGradient": "string|HL.Gradient", + } + return mapping.get(ctor, "any") + + +def parse_config_values(root: Path) -> dict[str, str]: + cfg = root / "src/config/values/ConfigValues.cpp" + source = read_text(cfg) + pattern = re.compile(r'MS<([A-Za-z0-9_]+)>\("([^"]+)"') + + type_map = { + "Bool": "boolean", + "Int": "integer|boolean", + "Float": "number|boolean", + "String": "string", + "Color": "string", + "Vec2": "HL.Vec2Like", + "CssGap": "integer|HL.CssGap", + "FontWeight": "integer|string", + "Gradient": "string|HL.Gradient", + } + + out: dict[str, str] = {} + for vtype, key in pattern.findall(source): + out[key.replace(":", ".").replace("-", "_")] = type_map.get(vtype, "any") + + return out + + +def extract_initializer_body(source: str, array_name: str) -> str: + marker = f"{array_name}[]" + idx = source.find(marker) + if idx < 0: + return "" + + eq_idx = source.find("=", idx) + if eq_idx < 0: + return "" + + open_idx = source.find("{", eq_idx) + if open_idx < 0: + return "" + + close_idx = find_matching_brace(source, open_idx) + return source[open_idx + 1 : close_idx] + + +def parse_descriptor_fields(root: Path) -> dict[str, dict[str, str]]: + source = read_text(root / "src/config/lua/bindings/LuaBindingsConfigRules.cpp") + arrays = { + "MONITOR_FIELDS": "HL.MonitorSpec", + "DEVICE_FIELDS": "HL.DeviceSpec", + "WORKSPACE_RULE_FIELDS": "HL.WorkspaceRuleSpec", + "WINDOW_RULE_EFFECT_DESCS": "HL.WindowRuleSpec", + "LAYER_RULE_EFFECT_DESCS": "HL.LayerRuleSpec", + } + + entry_regex = re.compile( + r'\{\s*"([^"]+)"\s*,\s*\[\]\(\)\s*->\s*ILuaConfigValue\*\s*\{\s*return\s+new\s+([A-Za-z0-9_]+)\((.*?)\);\s*\}', + re.DOTALL, + ) + + out: dict[str, dict[str, str]] = {class_name: {} for class_name in arrays.values()} + + for array_name, class_name in arrays.items(): + body = extract_initializer_body(source, array_name) + if not body: + continue + + for name, ctor, _ in entry_regex.findall(body): + out[class_name][name] = lua_type_from_config_ctor(ctor) + + # required / conventional fields not included in descriptor arrays + out["HL.MonitorSpec"]["output"] = "string" + out["HL.DeviceSpec"]["name"] = "string" + out["HL.WorkspaceRuleSpec"]["workspace"] = "string" + out["HL.WorkspaceRuleSpec"]["enabled"] = "boolean" + out["HL.WorkspaceRuleSpec"]["layout_opts"] = "table" + out["HL.WindowRuleSpec"]["name"] = "string" + out["HL.WindowRuleSpec"]["enabled"] = "boolean" + out["HL.WindowRuleSpec"]["match"] = "table" + out["HL.LayerRuleSpec"]["name"] = "string" + out["HL.LayerRuleSpec"]["enabled"] = "boolean" + out["HL.LayerRuleSpec"]["match"] = "table" + + return out + + +def parse_known_events(root: Path) -> list[str]: + source = read_text(root / "src/config/lua/LuaEventHandler.cpp") + block_match = re.search( + r"static const std::unordered_set EVENTS = \{(.*?)\};", + source, + flags=re.DOTALL, + ) + if not block_match: + return [] + + events = sorted(set(re.findall(r'"([^"]+)"', block_match.group(1)))) + return events + + +def helper_to_lua_type(helper: str) -> str: + mapping = { + "Str": "string", + "Num": "number", + "Bool": "boolean", + "Monitor": "HL.MonitorSelector", + "Workspace": "HL.WorkspaceSelector", + "Window": "HL.WindowSelector", + "MonitorSelector": "string", + "WorkspaceSelector": "string", + "WindowSelector": "string", + } + return mapping.get(helper, "any") + + +def query_struct_to_type(struct_name: str) -> str: + name = struct_name + if name.startswith("S") and len(name) > 1: + name = name[1:] + if name.endswith("Query"): + name = name + "Filter" + return f"HL.{name}" + + +def parse_query_filter_types(root: Path) -> tuple[dict[str, dict[str, str]], dict[str, str]]: + source = read_text(root / "src/config/lua/bindings/LuaBindingsQuery.cpp") + + parse_header = re.compile( + r"static void\s+(\w+)\s*\(\s*lua_State\* L,\s*int idx,\s*const char\* fnName,\s*(\w+)&\s*\w+\s*\)\s*\{" + ) + + query_types: dict[str, dict[str, str]] = {} + parse_fn_to_type: dict[str, str] = {} + + for m, body in extract_function_bodies(source, parse_header): + parse_fn = m.group(1) + struct_name = m.group(2) + type_name = query_struct_to_type(struct_name) + parse_fn_to_type[parse_fn] = type_name + query_types.setdefault(type_name, {}) + + direct_assign = re.finditer( + r'query\.([A-Za-z_][A-Za-z0-9_]*)\s*=\s*Internal::tableOpt([A-Za-z_]+)\(L,\s*idx,\s*"([^"]+)"', + body, + ) + for dm in direct_assign: + field_name = dm.group(3) + helper = dm.group(2) + query_types[type_name][field_name] = helper_to_lua_type(helper) + + helper_calls = re.finditer(r'Internal::tableOpt([A-Za-z_]+)\(L,\s*idx,\s*"([^"]+)"', body) + for hm in helper_calls: + helper = hm.group(1) + field_name = hm.group(2) + query_types[type_name].setdefault(field_name, helper_to_lua_type(helper)) + + api_overrides: dict[str, str] = {} + for parse_fn, type_name in parse_fn_to_type.items(): + for api in re.findall(rf'{parse_fn}\(L,\s*1,\s*"([^"]+)"\s*,', source): + if api == "hl.get_windows": + api_overrides[api] = f"fun(filters?: {type_name}): HL.Window[]" + elif api == "hl.get_layers": + api_overrides[api] = f"fun(filters?: {type_name}): HL.LayerSurface[]" + else: + api_overrides[api] = f"fun(filters?: {type_name}): any" + + return query_types, api_overrides + + +def namespace_class_name(path: list[str]) -> str: + if not path: + return "HL.API" + parts = [p[:1].upper() + p[1:] for p in path] + return f"HL.{''.join(parts)}Namespace" + + +def format_union_alias(name: str, values: Iterable[str]) -> list[str]: + values = list(values) + if not values: + return [] + + lines = [f"---@alias {name}"] + for value in values: + lines.append(f'---| "{value}"') + return lines + + +def emit_class_block(class_name: str, fields: list[tuple[str, str, bool]], operator_call: str | None = None) -> list[str]: + lines = [f"---@class {class_name}"] + if operator_call: + lines.append(f"---@operator call:{operator_call}") + + for field_name, type_name, optional in fields: + if field_name.startswith("[") and field_name.endswith("]"): + # preformatted index field, e.g. [string] + lines.append(f"---@field {field_name} {type_name}") + continue + + if re.match(r"^[A-Za-z_][A-Za-z0-9_]*$", field_name): + suffix = "?" if optional else "" + lines.append(f"---@field {field_name}{suffix} {type_name}") + continue + + quoted = field_name.replace("'", "\\'") + type_with_optional = f"{type_name}|nil" if optional else type_name + lines.append(f"---@field ['{quoted}'] {type_with_optional}") + + local_name = "__" + class_name.replace(".", "_") + lines.append(f"local {local_name} = {{}}") + return lines + + +def generate_stub(root: Path) -> str: + api_tree, callable_namespaces = parse_binding_tree(root) + object_classes = parse_object_classes(root) + config_values = parse_config_values(root) + descriptor_classes = parse_descriptor_fields(root) + events = parse_known_events(root) + query_types, query_overrides = parse_query_filter_types(root) + + 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.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", + "hl.get_config": "fun(key: HL.ConfigKey|string): any, string?", + "hl.device": "fun(spec: HL.DeviceSpec): nil", + "hl.monitor": "fun(spec: HL.MonitorSpec): nil", + "hl.window_rule": "fun(spec: HL.WindowRuleSpec): HL.WindowRule", + "hl.layer_rule": "fun(spec: HL.LayerRuleSpec): HL.LayerRule", + "hl.workspace_rule": "fun(spec: HL.WorkspaceRuleSpec): nil", + "hl.permission": "fun(spec: HL.PermissionSpec): nil", + "hl.gesture": "fun(spec: HL.GestureSpec): nil", + "hl.get_windows": "fun(filters?: HL.WindowQueryFilter): HL.Window[]", + "hl.get_window": "fun(selector: HL.WindowSelector): HL.Window|nil", + "hl.get_active_window": "fun(): HL.Window|nil", + "hl.get_urgent_window": "fun(): HL.Window|nil", + "hl.get_workspaces": "fun(): HL.Workspace[]", + "hl.get_workspace": "fun(selector: HL.WorkspaceSelector): HL.Workspace|nil", + "hl.get_active_workspace": "fun(monitor?: HL.MonitorSelector): HL.Workspace|nil", + "hl.get_active_special_workspace": "fun(monitor?: HL.MonitorSelector): HL.Workspace|nil", + "hl.get_monitors": "fun(): HL.Monitor[]", + "hl.get_monitor": "fun(selector: HL.MonitorSelector): HL.Monitor|nil", + "hl.get_active_monitor": "fun(): HL.Monitor|nil", + "hl.get_monitor_at": "fun(x: number|HL.Vec2, y?: number): HL.Monitor|nil", + "hl.get_monitor_at_cursor": "fun(): HL.Monitor|nil", + "hl.get_layers": "fun(filters?: HL.LayerQueryFilter): HL.LayerSurface[]", + "hl.get_workspace_windows": "fun(workspace: HL.WorkspaceSelector): HL.Window[]", + "hl.get_cursor_pos": "fun(): HL.Vec2|nil", + "hl.get_last_window": "fun(): HL.Window|nil", + "hl.get_last_workspace": "fun(monitor?: HL.MonitorSelector): HL.Workspace|nil", + "hl.get_current_submap": "fun(): string", + "hl.notification.create": "fun(opts?: HL.NotificationOptions): HL.Notification", + "hl.notification.get": "fun(): HL.Notification[]", + "hl.exec_cmd": "fun(cmd: string, rules?: table): nil", + } + api_signatures.update(query_overrides) + + lines: list[str] = [] + lines.append("-- This file is autogenerated. Do not edit by hand.") + lines.append("-- Generator: scripts/generateLuaStubs.py") + lines.append("---@meta") + lines.append("") + + lines.extend(format_union_alias("HL.EventName", events)) + lines.append("") + lines.extend(format_union_alias("HL.ConfigKey", sorted(config_values.keys()))) + lines.append("") + lines.append("---@alias HL.MonitorSelector string|integer|HL.Monitor") + lines.append("---@alias HL.WorkspaceSelector string|integer|HL.Workspace") + lines.append("---@alias HL.WindowSelector string|integer|HL.Window") + lines.append("---@alias HL.Vec2Like HL.Vec2|{x:number, y:number}|{number, number}|string") + 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.extend( + emit_class_block( + "HL.Vec2", + [ + ("x", "number", False), + ("y", "number", False), + ], + ) + ) + lines.append("") + + lines.extend( + emit_class_block( + "HL.BindOptions", + [ + ("repeating", "boolean", True), + ("locked", "boolean", True), + ("release", "boolean", True), + ("non_consuming", "boolean", True), + ("transparent", "boolean", True), + ("ignore_mods", "boolean", True), + ("dont_inhibit", "boolean", True), + ("long_press", "boolean", True), + ("submap_universal", "boolean", True), + ("click", "boolean", True), + ("drag", "boolean", True), + ("description", "string", True), + ("desc", "string", True), + ("device", "{inclusive?: boolean, list?: string[]}", True), + ], + ) + ) + lines.append("") + + lines.extend( + emit_class_block( + "HL.TimerOptions", + [ + ("timeout", "integer", False), + ("type", '"repeat"|"oneshot"', False), + ], + ) + ) + lines.append("") + + lines.extend( + emit_class_block( + "HL.GestureSpec", + [ + ("fingers", "integer", False), + ("direction", "string", False), + ("action", "string", False), + ("mods", "string", True), + ("scale", "number", True), + ("mode", "string", True), + ("zoom_level", "number", True), + ("workspace_name", "string", True), + ("disable_inhibit", "boolean", True), + ], + ) + ) + lines.append("") + + lines.extend( + emit_class_block( + "HL.PermissionSpec", + [ + ("binary", "string", False), + ("type", "string", False), + ("allow", "string", False), + ], + ) + ) + lines.append("") + + lines.extend( + emit_class_block( + "HL.NotificationOptions", + [ + ("color", "string", True), + ("timeout", "number", True), + ("icon", "integer", True), + ("font_size", "number", True), + ], + ) + ) + lines.append("") + + for class_name in sorted(query_types.keys()): + fields = [(name, typ, True) for name, typ in sorted(query_types[class_name].items())] + lines.extend(emit_class_block(class_name, fields)) + lines.append("") + + for class_name in sorted(descriptor_classes.keys()): + required_fields = { + ("HL.MonitorSpec", "output"), + ("HL.DeviceSpec", "name"), + ("HL.WorkspaceRuleSpec", "workspace"), + } + fields: list[tuple[str, str, bool]] = [] + for key, typ in sorted(descriptor_classes[class_name].items()): + optional = (class_name, key) not in required_fields + fields.append((key, typ, optional)) + lines.extend(emit_class_block(class_name, fields)) + lines.append("") + + for class_name in sorted(object_classes.keys()): + obj = object_classes[class_name] + fields: list[tuple[str, str, bool]] = [] + for key in sorted(obj.methods): + fields.append((key, f"fun(self: {class_name}, ...): any", False)) + for key, typ in sorted(obj.fields.items()): + if key in obj.methods: + continue + fields.append((key, typ, False)) + + lines.extend(emit_class_block(class_name, fields)) + lines.append("") + + def emit_namespace(node: ApiNode, path: list[str]) -> None: + class_name = namespace_class_name(path) + fields: list[tuple[str, str, bool]] = [] + + full_prefix = "hl" + ("." + ".".join(path) if path else "") + + for method in sorted(node.methods): + full_name = f"{full_prefix}.{method}" + method_type = api_signatures.get(full_name, "fun(...): any") + fields.append((method, method_type, False)) + + for child_name in sorted(node.children.keys()): + fields.append((child_name, namespace_class_name(path + [child_name]), False)) + + if path == ["plugin"]: + fields.append(("[string]", "any", False)) + + operator_call = None + if path and path[-1] in callable_namespaces: + operator_call = "fun(...): any" + + lines.extend(emit_class_block(class_name, fields, operator_call=operator_call)) + lines.append("") + + for child_name in sorted(node.children.keys()): + emit_namespace(node.children[child_name], path + [child_name]) + + emit_namespace(api_tree, []) + + lines.append("---@type HL.API") + lines.append("hl = {}") + lines.append("") + + # include a tiny map of config key value types for users who query values dynamically + lines.append("---@class HL.ConfigValueTypes") + for key, typ in sorted(config_values.items()): + lines.append(f"---@field ['{key}'] {typ}") + lines.append("local __HL_ConfigValueTypes = {}") + lines.append("") + + return "\n".join(lines) + + +def write_if_changed(path: Path, content: str) -> bool: + if path.exists(): + existing = read_text(path) + if existing == content: + return False + + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text(content, encoding="utf-8") + return True + + +def main() -> int: + parser = argparse.ArgumentParser(description="Generate LuaLS stubs for Hyprland Lua config API") + parser.add_argument( + "--root", + type=Path, + default=Path(__file__).resolve().parents[1], + help="Repository root", + ) + parser.add_argument( + "--output", + type=Path, + default=None, + help="Output .lua stub path (defaults to ./meta/hl.meta.lua)", + ) + parser.add_argument( + "--check", + action="store_true", + help="Check mode: fail if output differs from generated content", + ) + args = parser.parse_args() + + root = args.root.resolve() + output = args.output.resolve() if args.output else root / "meta/hl.meta.lua" + + content = generate_stub(root) + + if args.check: + if not output.exists(): + print(f"[lua-stubs] missing generated file: {output}", file=sys.stderr) + return 1 + + existing = read_text(output) + if existing != content: + print(f"[lua-stubs] generated stubs are out of date: {output}", file=sys.stderr) + return 1 + + print(f"[lua-stubs] up to date: {output}") + return 0 + + changed = write_if_changed(output, content) + state = "updated" if changed else "unchanged" + print(f"[lua-stubs] {state}: {output}") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/meta/hl.meta.lua b/meta/hl.meta.lua new file mode 100644 index 000000000..96092e91e --- /dev/null +++ b/meta/hl.meta.lua @@ -0,0 +1,1202 @@ +-- This file is autogenerated. Do not edit by hand. +-- Generator: scripts/generateLuaStubs.py +---@meta + +---@alias HL.EventName +---| "config.reloaded" +---| "hyprland.shutdown" +---| "hyprland.start" +---| "keybinds.submap" +---| "layer.closed" +---| "layer.opened" +---| "monitor.added" +---| "monitor.focused" +---| "monitor.layout_changed" +---| "monitor.removed" +---| "screenshare.state" +---| "window.active" +---| "window.class" +---| "window.close" +---| "window.destroy" +---| "window.fullscreen" +---| "window.kill" +---| "window.move_to_workspace" +---| "window.open" +---| "window.open_early" +---| "window.pin" +---| "window.title" +---| "window.update_rules" +---| "window.urgent" +---| "workspace.active" +---| "workspace.created" +---| "workspace.move_to_monitor" +---| "workspace.removed" + +---@alias HL.ConfigKey +---| "animations.enabled" +---| "animations.workspace_wraparound" +---| "binds.allow_pin_fullscreen" +---| "binds.allow_workspace_cycles" +---| "binds.disable_keybind_grabbing" +---| "binds.drag_threshold" +---| "binds.focus_preferred_method" +---| "binds.hide_special_on_workspace_change" +---| "binds.ignore_group_lock" +---| "binds.movefocus_cycles_fullscreen" +---| "binds.movefocus_cycles_groupfirst" +---| "binds.pass_mouse_when_bound" +---| "binds.scroll_event_delay" +---| "binds.window_direction_monitor_fallback" +---| "binds.workspace_back_and_forth" +---| "binds.workspace_center_on" +---| "cursor.default_monitor" +---| "cursor.enable_hyprcursor" +---| "cursor.hide_on_key_press" +---| "cursor.hide_on_tablet" +---| "cursor.hide_on_touch" +---| "cursor.hotspot_padding" +---| "cursor.inactive_timeout" +---| "cursor.invisible" +---| "cursor.min_refresh_rate" +---| "cursor.no_break_fs_vrr" +---| "cursor.no_hardware_cursors" +---| "cursor.no_warps" +---| "cursor.persistent_warps" +---| "cursor.sync_gsettings_theme" +---| "cursor.use_cpu_buffer" +---| "cursor.warp_back_after_non_mouse_input" +---| "cursor.warp_on_change_workspace" +---| "cursor.warp_on_toggle_special" +---| "cursor.zoom_detached_camera" +---| "cursor.zoom_disable_aa" +---| "cursor.zoom_factor" +---| "cursor.zoom_rigid" +---| "debug.colored_stdout_logs" +---| "debug.damage_blink" +---| "debug.damage_tracking" +---| "debug.disable_logs" +---| "debug.disable_scale_checks" +---| "debug.disable_time" +---| "debug.ds_handle_same_buffer" +---| "debug.ds_handle_same_buffer_fifo" +---| "debug.enable_stdout_logs" +---| "debug.error_limit" +---| "debug.error_position" +---| "debug.fifo_pending_workaround" +---| "debug.full_cm_proto" +---| "debug.gl_debugging" +---| "debug.invalidate_fp16" +---| "debug.log_damage" +---| "debug.manual_crash" +---| "debug.overlay" +---| "debug.pass" +---| "debug.render_solitary_wo_damage" +---| "debug.suppress_errors" +---| "debug.vfr" +---| "decoration.active_opacity" +---| "decoration.blur.brightness" +---| "decoration.blur.contrast" +---| "decoration.blur.enabled" +---| "decoration.blur.ignore_opacity" +---| "decoration.blur.input_methods" +---| "decoration.blur.input_methods_ignorealpha" +---| "decoration.blur.new_optimizations" +---| "decoration.blur.noise" +---| "decoration.blur.passes" +---| "decoration.blur.popups" +---| "decoration.blur.popups_ignorealpha" +---| "decoration.blur.size" +---| "decoration.blur.special" +---| "decoration.blur.vibrancy" +---| "decoration.blur.vibrancy_darkness" +---| "decoration.blur.xray" +---| "decoration.border_part_of_window" +---| "decoration.dim_around" +---| "decoration.dim_inactive" +---| "decoration.dim_modal" +---| "decoration.dim_special" +---| "decoration.dim_strength" +---| "decoration.fullscreen_opacity" +---| "decoration.glow.color" +---| "decoration.glow.color_inactive" +---| "decoration.glow.enabled" +---| "decoration.glow.range" +---| "decoration.glow.render_power" +---| "decoration.inactive_opacity" +---| "decoration.rounding" +---| "decoration.rounding_power" +---| "decoration.screen_shader" +---| "decoration.shadow.color" +---| "decoration.shadow.color_inactive" +---| "decoration.shadow.enabled" +---| "decoration.shadow.offset" +---| "decoration.shadow.range" +---| "decoration.shadow.render_power" +---| "decoration.shadow.scale" +---| "decoration.shadow.sharp" +---| "dwindle.default_split_ratio" +---| "dwindle.force_split" +---| "dwindle.permanent_direction_override" +---| "dwindle.precise_mouse_move" +---| "dwindle.preserve_split" +---| "dwindle.smart_resizing" +---| "dwindle.smart_split" +---| "dwindle.special_scale_factor" +---| "dwindle.split_bias" +---| "dwindle.split_width_multiplier" +---| "dwindle.use_active_for_splits" +---| "ecosystem.enforce_permissions" +---| "ecosystem.no_donation_nag" +---| "ecosystem.no_update_news" +---| "experimental.wp_cm_1_2" +---| "general.allow_tearing" +---| "general.border_size" +---| "general.col.active_border" +---| "general.col.inactive_border" +---| "general.col.nogroup_border" +---| "general.col.nogroup_border_active" +---| "general.extend_border_grab_area" +---| "general.float_gaps" +---| "general.gaps_in" +---| "general.gaps_out" +---| "general.gaps_workspaces" +---| "general.hover_icon_on_border" +---| "general.layout" +---| "general.locale" +---| "general.modal_parent_blocking" +---| "general.no_focus_fallback" +---| "general.resize_corner" +---| "general.resize_on_border" +---| "general.snap.border_overlap" +---| "general.snap.enabled" +---| "general.snap.monitor_gap" +---| "general.snap.respect_gaps" +---| "general.snap.window_gap" +---| "gestures.close_max_timeout" +---| "gestures.workspace_swipe_cancel_ratio" +---| "gestures.workspace_swipe_create_new" +---| "gestures.workspace_swipe_direction_lock" +---| "gestures.workspace_swipe_direction_lock_threshold" +---| "gestures.workspace_swipe_distance" +---| "gestures.workspace_swipe_forever" +---| "gestures.workspace_swipe_invert" +---| "gestures.workspace_swipe_min_speed_to_force" +---| "gestures.workspace_swipe_touch" +---| "gestures.workspace_swipe_touch_invert" +---| "gestures.workspace_swipe_use_r" +---| "group.auto_group" +---| "group.col.border_active" +---| "group.col.border_inactive" +---| "group.col.border_locked_active" +---| "group.col.border_locked_inactive" +---| "group.drag_into_group" +---| "group.focus_removed_window" +---| "group.group_on_movetoworkspace" +---| "group.groupbar.blur" +---| "group.groupbar.col.active" +---| "group.groupbar.col.inactive" +---| "group.groupbar.col.locked_active" +---| "group.groupbar.col.locked_inactive" +---| "group.groupbar.enabled" +---| "group.groupbar.font_family" +---| "group.groupbar.font_size" +---| "group.groupbar.font_weight_active" +---| "group.groupbar.font_weight_inactive" +---| "group.groupbar.gaps_in" +---| "group.groupbar.gaps_out" +---| "group.groupbar.gradient_round_only_edges" +---| "group.groupbar.gradient_rounding" +---| "group.groupbar.gradient_rounding_power" +---| "group.groupbar.gradients" +---| "group.groupbar.height" +---| "group.groupbar.indicator_gap" +---| "group.groupbar.indicator_height" +---| "group.groupbar.keep_upper_gap" +---| "group.groupbar.priority" +---| "group.groupbar.render_titles" +---| "group.groupbar.round_only_edges" +---| "group.groupbar.rounding" +---| "group.groupbar.rounding_power" +---| "group.groupbar.scrolling" +---| "group.groupbar.stacked" +---| "group.groupbar.text_color" +---| "group.groupbar.text_color_inactive" +---| "group.groupbar.text_color_locked_active" +---| "group.groupbar.text_color_locked_inactive" +---| "group.groupbar.text_offset" +---| "group.groupbar.text_padding" +---| "group.insert_after_current" +---| "group.merge_floated_into_tiled_on_groupbar" +---| "group.merge_groups_on_drag" +---| "group.merge_groups_on_groupbar" +---| "input.accel_profile" +---| "input.emulate_discrete_scroll" +---| "input.float_switch_override_focus" +---| "input.focus_on_close" +---| "input.follow_mouse" +---| "input.follow_mouse_shrink" +---| "input.follow_mouse_threshold" +---| "input.force_no_accel" +---| "input.kb_file" +---| "input.kb_layout" +---| "input.kb_model" +---| "input.kb_options" +---| "input.kb_rules" +---| "input.kb_variant" +---| "input.left_handed" +---| "input.mouse_refocus" +---| "input.natural_scroll" +---| "input.numlock_by_default" +---| "input.off_window_axis_events" +---| "input.repeat_delay" +---| "input.repeat_rate" +---| "input.resolve_binds_by_sym" +---| "input.rotation" +---| "input.scroll_button" +---| "input.scroll_button_lock" +---| "input.scroll_factor" +---| "input.scroll_method" +---| "input.scroll_points" +---| "input.sensitivity" +---| "input.special_fallthrough" +---| "input.tablet.absolute_region_position" +---| "input.tablet.active_area_position" +---| "input.tablet.active_area_size" +---| "input.tablet.left_handed" +---| "input.tablet.output" +---| "input.tablet.region_position" +---| "input.tablet.region_size" +---| "input.tablet.relative_input" +---| "input.tablet.transform" +---| "input.touchdevice.enabled" +---| "input.touchdevice.output" +---| "input.touchdevice.transform" +---| "input.touchpad.clickfinger_behavior" +---| "input.touchpad.disable_while_typing" +---| "input.touchpad.drag_3fg" +---| "input.touchpad.drag_lock" +---| "input.touchpad.flip_x" +---| "input.touchpad.flip_y" +---| "input.touchpad.middle_button_emulation" +---| "input.touchpad.natural_scroll" +---| "input.touchpad.scroll_factor" +---| "input.touchpad.tap_and_drag" +---| "input.touchpad.tap_button_map" +---| "input.touchpad.tap_to_click" +---| "input.virtualkeyboard.release_pressed_on_close" +---| "input.virtualkeyboard.share_states" +---| "layout.single_window_aspect_ratio" +---| "layout.single_window_aspect_ratio_tolerance" +---| "master.allow_small_split" +---| "master.always_keep_position" +---| "master.center_ignores_reserved" +---| "master.center_master_fallback" +---| "master.drop_at_cursor" +---| "master.mfact" +---| "master.new_on_active" +---| "master.new_on_top" +---| "master.new_status" +---| "master.orientation" +---| "master.slave_count_for_center_master" +---| "master.smart_resizing" +---| "master.special_scale_factor" +---| "misc.allow_session_lock_restore" +---| "misc.always_follow_on_dnd" +---| "misc.animate_manual_resizes" +---| "misc.animate_mouse_windowdragging" +---| "misc.anr_missed_pings" +---| "misc.background_color" +---| "misc.close_special_on_empty" +---| "misc.col.splash" +---| "misc.disable_autoreload" +---| "misc.disable_hyprland_guiutils_check" +---| "misc.disable_hyprland_logo" +---| "misc.disable_scale_notification" +---| "misc.disable_splash_rendering" +---| "misc.disable_watchdog_warning" +---| "misc.disable_xdg_env_checks" +---| "misc.enable_anr_dialog" +---| "misc.enable_swallow" +---| "misc.exit_window_retains_fullscreen" +---| "misc.focus_on_activate" +---| "misc.font_family" +---| "misc.force_default_wallpaper" +---| "misc.initial_workspace_tracking" +---| "misc.key_press_enables_dpms" +---| "misc.layers_hog_keyboard_focus" +---| "misc.lockdead_screen_delay" +---| "misc.middle_click_paste" +---| "misc.mouse_move_enables_dpms" +---| "misc.mouse_move_focuses_monitor" +---| "misc.name_vk_after_proc" +---| "misc.on_focus_under_fullscreen" +---| "misc.render_unfocused_fps" +---| "misc.screencopy_force_8b" +---| "misc.session_lock_xray" +---| "misc.size_limits_tiled" +---| "misc.splash_font_family" +---| "misc.swallow_exception_regex" +---| "misc.swallow_regex" +---| "misc.vrr" +---| "opengl.nvidia_anti_flicker" +---| "quirks.prefer_hdr" +---| "quirks.skip_non_kms_dmabuf_formats" +---| "render.cm_auto_hdr" +---| "render.cm_enabled" +---| "render.cm_sdr_eotf" +---| "render.commit_timing_enabled" +---| "render.ctm_animation" +---| "render.direct_scanout" +---| "render.expand_undersized_textures" +---| "render.icc_vcgt_enabled" +---| "render.keep_unmodified_copy" +---| "render.new_render_scheduling" +---| "render.non_shader_cm" +---| "render.non_shader_cm_interop" +---| "render.send_content_type" +---| "render.use_fp16" +---| "render.use_shader_blur_blend" +---| "render.xp_mode" +---| "scrolling.column_width" +---| "scrolling.direction" +---| "scrolling.explicit_column_widths" +---| "scrolling.focus_fit_method" +---| "scrolling.follow_focus" +---| "scrolling.follow_min_visible" +---| "scrolling.fullscreen_on_one_column" +---| "scrolling.wrap_focus" +---| "scrolling.wrap_swapcol" +---| "xwayland.create_abstract_socket" +---| "xwayland.enabled" +---| "xwayland.force_zero_scaling" +---| "xwayland.use_nearest_neighbor" + +---@alias HL.MonitorSelector string|integer|HL.Monitor +---@alias HL.WorkspaceSelector string|integer|HL.Workspace +---@alias HL.WindowSelector string|integer|HL.Window +---@alias HL.Vec2Like HL.Vec2|{x:number, y:number}|{number, number}|string +---@alias HL.CssGap integer|{top?:integer, right?:integer, bottom?:integer, left?:integer} +---@alias HL.Gradient string|{colors:string[], angle?:number} + +---@class HL.Vec2 +---@field x number +---@field y number +local __HL_Vec2 = {} + +---@class HL.BindOptions +---@field repeating? boolean +---@field locked? boolean +---@field release? boolean +---@field non_consuming? boolean +---@field transparent? boolean +---@field ignore_mods? boolean +---@field dont_inhibit? boolean +---@field long_press? boolean +---@field submap_universal? boolean +---@field click? boolean +---@field drag? boolean +---@field description? string +---@field desc? string +---@field device? {inclusive?: boolean, list?: string[]} +local __HL_BindOptions = {} + +---@class HL.TimerOptions +---@field timeout integer +---@field type "repeat"|"oneshot" +local __HL_TimerOptions = {} + +---@class HL.GestureSpec +---@field fingers integer +---@field direction string +---@field action string +---@field mods? string +---@field scale? number +---@field mode? string +---@field zoom_level? number +---@field workspace_name? string +---@field disable_inhibit? boolean +local __HL_GestureSpec = {} + +---@class HL.PermissionSpec +---@field binary string +---@field type string +---@field allow string +local __HL_PermissionSpec = {} + +---@class HL.NotificationOptions +---@field color? string +---@field timeout? number +---@field icon? integer +---@field font_size? number +local __HL_NotificationOptions = {} + +---@class HL.LayerQueryFilter +---@field monitor? HL.MonitorSelector +---@field namespace? string +local __HL_LayerQueryFilter = {} + +---@class HL.WindowQueryFilter +---@field class? string +---@field floating? boolean +---@field mapped? boolean +---@field monitor? HL.MonitorSelector +---@field tag? string +---@field title? string +---@field workspace? HL.WorkspaceSelector +local __HL_WindowQueryFilter = {} + +---@class HL.DeviceSpec +---@field absolute_region_position? boolean +---@field accel_profile? string +---@field active_area_position? HL.Vec2Like +---@field active_area_size? HL.Vec2Like +---@field clickfinger_behavior? boolean +---@field disable_while_typing? boolean +---@field drag_3fg? integer|boolean +---@field drag_lock? integer|boolean +---@field enabled? boolean +---@field flip_x? boolean +---@field flip_y? boolean +---@field kb_file? string +---@field kb_layout? string +---@field kb_model? string +---@field kb_options? string +---@field kb_rules? string +---@field kb_variant? string +---@field keybinds? boolean +---@field left_handed? boolean +---@field middle_button_emulation? boolean +---@field name string +---@field natural_scroll? boolean +---@field numlock_by_default? boolean +---@field output? string +---@field region_position? HL.Vec2Like +---@field region_size? HL.Vec2Like +---@field relative_input? boolean +---@field release_pressed_on_close? boolean +---@field repeat_delay? integer|boolean +---@field repeat_rate? integer|boolean +---@field resolve_binds_by_sym? boolean +---@field rotation? integer|boolean +---@field scroll_button? integer|boolean +---@field scroll_button_lock? boolean +---@field scroll_factor? number|boolean +---@field scroll_method? string +---@field scroll_points? string +---@field sensitivity? number|boolean +---@field share_states? integer|boolean +---@field tap_and_drag? boolean +---@field tap_button_map? string +---@field tap_to_click? boolean +---@field transform? integer|boolean +local __HL_DeviceSpec = {} + +---@class HL.LayerRuleSpec +---@field above_lock? integer|boolean +---@field animation? string +---@field blur? boolean +---@field blur_popups? boolean +---@field dim_around? boolean +---@field enabled? boolean +---@field ignore_alpha? number|boolean +---@field match? table +---@field name? string +---@field no_anim? boolean +---@field no_screen_share? boolean +---@field order? integer|boolean +---@field xray? boolean +local __HL_LayerRuleSpec = {} + +---@class HL.MonitorSpec +---@field bitdepth? integer|boolean +---@field cm? string +---@field disabled? boolean +---@field icc? string +---@field max_avg_luminance? integer|boolean +---@field max_luminance? integer|boolean +---@field min_luminance? number|boolean +---@field mirror? string +---@field mode? string +---@field output string +---@field position? string +---@field reserved? integer|HL.CssGap +---@field reserved_area? integer|HL.CssGap +---@field scale? string +---@field sdr_eotf? string +---@field sdr_max_luminance? integer|boolean +---@field sdr_min_luminance? number|boolean +---@field sdrbrightness? number|boolean +---@field sdrsaturation? number|boolean +---@field supports_hdr? integer|boolean +---@field supports_wide_color? integer|boolean +---@field transform? integer|boolean +---@field vrr? integer|boolean +local __HL_MonitorSpec = {} + +---@class HL.WindowRuleSpec +---@field enabled? boolean +---@field match? table +---@field name? string +local __HL_WindowRuleSpec = {} + +---@class HL.WorkspaceRuleSpec +---@field animation? string +---@field border_size? integer|boolean +---@field decorate? boolean +---@field default? boolean +---@field default_name? string +---@field enabled? boolean +---@field float_gaps? integer|HL.CssGap +---@field gaps_in? integer|HL.CssGap +---@field gaps_out? integer|HL.CssGap +---@field layout? string +---@field layout_opts? table +---@field monitor? string +---@field no_border? boolean +---@field no_rounding? boolean +---@field no_shadow? boolean +---@field on_created_empty? string +---@field persistent? boolean +---@field workspace string +local __HL_WorkspaceRuleSpec = {} + +---@class HL.EventSubscription +---@field is_active fun(self: HL.EventSubscription, ...): any +---@field remove fun(self: HL.EventSubscription, ...): any +local __HL_EventSubscription = {} + +---@class HL.Group +---@field current HL.Window|nil +---@field current_index integer +---@field denied boolean +---@field locked boolean +---@field members HL.Window|table|nil +---@field size integer +local __HL_Group = {} + +---@class HL.Keybind +---@field is_enabled fun(self: HL.Keybind, ...): any +---@field remove fun(self: HL.Keybind, ...): any +---@field set_enabled fun(self: HL.Keybind, ...): any +---@field unbind fun(self: HL.Keybind, ...): any +---@field arg string +---@field auto_consuming boolean +---@field catchall boolean +---@field click boolean +---@field description any +---@field device_inclusive boolean +---@field devices nil +---@field display_key string +---@field dont_inhibit boolean +---@field drag boolean +---@field enabled boolean +---@field handler string +---@field has_description boolean +---@field ignore_mods boolean +---@field key string +---@field keycode integer +---@field locked boolean +---@field long_press boolean +---@field modmask integer +---@field mouse boolean +---@field non_consuming boolean +---@field release boolean +---@field repeating boolean +---@field submap string +---@field submap_universal boolean +---@field transparent boolean +local __HL_Keybind = {} + +---@class HL.LayerRule +---@field is_enabled fun(self: HL.LayerRule, ...): any +---@field set_enabled fun(self: HL.LayerRule, ...): any +local __HL_LayerRule = {} + +---@class HL.LayerSurface +---@field above_fullscreen boolean|nil +---@field address string +---@field h integer +---@field interactivity integer +---@field layer integer +---@field mapped boolean +---@field monitor HL.Monitor|nil +---@field namespace string +---@field pid integer +---@field w integer +---@field x integer +---@field y integer +local __HL_LayerSurface = {} + +---@class HL.Monitor +---@field active_special_workspace HL.Workspace|nil +---@field active_workspace HL.Workspace|nil +---@field description string +---@field dpms_status boolean +---@field focused boolean|nil +---@field height integer +---@field id integer +---@field is_mirror boolean +---@field mirrors HL.Monitor|table +---@field name string +---@field position integer|table +---@field refresh_rate number +---@field scale number +---@field size integer|table +---@field transform integer +---@field vrr_active boolean +---@field width integer +---@field x integer +---@field y integer +local __HL_Monitor = {} + +---@class HL.Notification +---@field dismiss fun(self: HL.Notification, ...): any +---@field get_color fun(self: HL.Notification, ...): any +---@field get_elapsed fun(self: HL.Notification, ...): any +---@field get_elapsed_since_creation fun(self: HL.Notification, ...): any +---@field get_font_size fun(self: HL.Notification, ...): any +---@field get_icon fun(self: HL.Notification, ...): any +---@field get_text fun(self: HL.Notification, ...): any +---@field get_timeout fun(self: HL.Notification, ...): any +---@field is_alive fun(self: HL.Notification, ...): any +---@field is_paused fun(self: HL.Notification, ...): any +---@field pause fun(self: HL.Notification, ...): any +---@field resume fun(self: HL.Notification, ...): any +---@field set_color fun(self: HL.Notification, ...): any +---@field set_font_size fun(self: HL.Notification, ...): any +---@field set_icon fun(self: HL.Notification, ...): any +---@field set_paused fun(self: HL.Notification, ...): any +---@field set_text fun(self: HL.Notification, ...): any +---@field set_timeout fun(self: HL.Notification, ...): any +local __HL_Notification = {} + +---@class HL.Timer +---@field is_enabled fun(self: HL.Timer, ...): any +---@field set_enabled fun(self: HL.Timer, ...): any +---@field set_timeout fun(self: HL.Timer, ...): any +local __HL_Timer = {} + +---@class HL.Window +---@field accepts_input boolean +---@field active boolean|nil +---@field address string +---@field at integer|table +---@field class string +---@field content_type string +---@field floating boolean +---@field focus_history_id integer +---@field fullscreen integer +---@field fullscreen_client integer +---@field group HL.Group|nil +---@field hidden boolean +---@field inhibiting_idle boolean +---@field initial_class string +---@field initial_title string +---@field layout HL.Window|boolean|integer|number|string|table|nil +---@field mapped boolean +---@field monitor HL.Monitor|nil +---@field over_fullscreen boolean +---@field pid integer +---@field pinned boolean +---@field size integer|table +---@field stable_id integer +---@field swallowing HL.Window|nil +---@field tags string|table +---@field title string +---@field visible boolean +---@field workspace HL.Workspace|nil +---@field xdg_description string|nil +---@field xdg_tag string|nil +---@field xwayland boolean +local __HL_Window = {} + +---@class HL.WindowRule +---@field is_enabled fun(self: HL.WindowRule, ...): any +---@field set_enabled fun(self: HL.WindowRule, ...): any +local __HL_WindowRule = {} + +---@class HL.Workspace +---@field get_groups fun(self: HL.Workspace, ...): any +---@field get_windows fun(self: HL.Workspace, ...): any +---@field active boolean +---@field config_name string +---@field fullscreen_mode integer +---@field fullscreen_window HL.Window|nil +---@field groups integer|nil +---@field has_fullscreen boolean +---@field has_urgent boolean +---@field id integer +---@field is_empty boolean +---@field is_persistent boolean +---@field last_window HL.Window|nil +---@field monitor HL.Monitor|nil +---@field name string +---@field special boolean +---@field tiled_layout string +---@field visible boolean +---@field windows integer +local __HL_Workspace = {} + +---@class HL.API +---@field animation fun(...): any +---@field bind fun(keys: string, 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 env fun(...): any +---@field exec_cmd fun(cmd: string, rules?: table): nil +---@field gesture fun(spec: HL.GestureSpec): nil +---@field get_active_monitor fun(): HL.Monitor|nil +---@field get_active_special_workspace fun(monitor?: HL.MonitorSelector): HL.Workspace|nil +---@field get_active_window fun(): HL.Window|nil +---@field get_active_workspace fun(monitor?: HL.MonitorSelector): HL.Workspace|nil +---@field get_config fun(key: HL.ConfigKey|string): any, string? +---@field get_current_submap fun(): string +---@field get_cursor_pos fun(): HL.Vec2|nil +---@field get_last_window fun(): HL.Window|nil +---@field get_last_workspace fun(monitor?: HL.MonitorSelector): HL.Workspace|nil +---@field get_layers fun(filters?: HL.LayerQueryFilter): HL.LayerSurface[] +---@field get_monitor fun(selector: HL.MonitorSelector): HL.Monitor|nil +---@field get_monitor_at fun(x: number|HL.Vec2, y?: number): HL.Monitor|nil +---@field get_monitor_at_cursor fun(): HL.Monitor|nil +---@field get_monitors fun(): HL.Monitor[] +---@field get_urgent_window fun(): HL.Window|nil +---@field get_window fun(selector: HL.WindowSelector): HL.Window|nil +---@field get_windows fun(filters?: HL.WindowQueryFilter): HL.Window[] +---@field get_workspace fun(selector: HL.WorkspaceSelector): HL.Workspace|nil +---@field get_workspace_windows fun(workspace: HL.WorkspaceSelector): HL.Window[] +---@field get_workspaces fun(): HL.Workspace[] +---@field layer_rule fun(spec: HL.LayerRuleSpec): HL.LayerRule +---@field monitor fun(spec: HL.MonitorSpec): nil +---@field on fun(event: HL.EventName, cb: fun(...)): HL.EventSubscription +---@field permission fun(spec: HL.PermissionSpec): nil +---@field timer fun(callback: function, opts: HL.TimerOptions): HL.Timer +---@field unbind fun(...): any +---@field version fun(...): any +---@field window_rule fun(spec: HL.WindowRuleSpec): HL.WindowRule +---@field workspace_rule fun(spec: HL.WorkspaceRuleSpec): nil +---@field dsp HL.DspNamespace +---@field notification HL.NotificationNamespace +---@field plugin HL.PluginNamespace +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 pass fun(...): any +---@field send_key_state fun(...): any +---@field send_shortcut fun(...): any +---@field submap fun(...): any +---@field cursor HL.DspCursorNamespace +---@field group HL.DspGroupNamespace +---@field window HL.DspWindowNamespace +---@field workspace HL.DspWorkspaceNamespace +local __HL_DspNamespace = {} + +---@class HL.DspCursorNamespace +---@field move fun(...): any +---@field move_to_corner fun(...): any +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 +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 +local __HL_DspWindowNamespace = {} + +---@class HL.DspWorkspaceNamespace +---@field move fun(...): any +---@field rename fun(...): any +---@field swap_monitors fun(...): any +---@field toggle_special fun(...): any +local __HL_DspWorkspaceNamespace = {} + +---@class HL.NotificationNamespace +---@field create fun(opts?: HL.NotificationOptions): HL.Notification +---@field get fun(): HL.Notification[] +local __HL_NotificationNamespace = {} + +---@class HL.PluginNamespace +---@field load fun(...): any +---@field [string] any +local __HL_PluginNamespace = {} + +---@type HL.API +hl = {} + +---@class HL.ConfigValueTypes +---@field ['animations.enabled'] boolean +---@field ['animations.workspace_wraparound'] boolean +---@field ['binds.allow_pin_fullscreen'] boolean +---@field ['binds.allow_workspace_cycles'] boolean +---@field ['binds.disable_keybind_grabbing'] boolean +---@field ['binds.drag_threshold'] integer|boolean +---@field ['binds.focus_preferred_method'] integer|boolean +---@field ['binds.hide_special_on_workspace_change'] boolean +---@field ['binds.ignore_group_lock'] boolean +---@field ['binds.movefocus_cycles_fullscreen'] boolean +---@field ['binds.movefocus_cycles_groupfirst'] boolean +---@field ['binds.pass_mouse_when_bound'] boolean +---@field ['binds.scroll_event_delay'] integer|boolean +---@field ['binds.window_direction_monitor_fallback'] boolean +---@field ['binds.workspace_back_and_forth'] boolean +---@field ['binds.workspace_center_on'] integer|boolean +---@field ['cursor.default_monitor'] string +---@field ['cursor.enable_hyprcursor'] boolean +---@field ['cursor.hide_on_key_press'] boolean +---@field ['cursor.hide_on_tablet'] boolean +---@field ['cursor.hide_on_touch'] boolean +---@field ['cursor.hotspot_padding'] integer|boolean +---@field ['cursor.inactive_timeout'] number|boolean +---@field ['cursor.invisible'] boolean +---@field ['cursor.min_refresh_rate'] integer|boolean +---@field ['cursor.no_break_fs_vrr'] integer|boolean +---@field ['cursor.no_hardware_cursors'] integer|boolean +---@field ['cursor.no_warps'] boolean +---@field ['cursor.persistent_warps'] boolean +---@field ['cursor.sync_gsettings_theme'] boolean +---@field ['cursor.use_cpu_buffer'] integer|boolean +---@field ['cursor.warp_back_after_non_mouse_input'] boolean +---@field ['cursor.warp_on_change_workspace'] integer|boolean +---@field ['cursor.warp_on_toggle_special'] integer|boolean +---@field ['cursor.zoom_detached_camera'] boolean +---@field ['cursor.zoom_disable_aa'] boolean +---@field ['cursor.zoom_factor'] number|boolean +---@field ['cursor.zoom_rigid'] boolean +---@field ['debug.colored_stdout_logs'] boolean +---@field ['debug.damage_blink'] boolean +---@field ['debug.damage_tracking'] integer|boolean +---@field ['debug.disable_logs'] boolean +---@field ['debug.disable_scale_checks'] boolean +---@field ['debug.disable_time'] boolean +---@field ['debug.ds_handle_same_buffer'] boolean +---@field ['debug.ds_handle_same_buffer_fifo'] boolean +---@field ['debug.enable_stdout_logs'] boolean +---@field ['debug.error_limit'] integer|boolean +---@field ['debug.error_position'] integer|boolean +---@field ['debug.fifo_pending_workaround'] boolean +---@field ['debug.full_cm_proto'] boolean +---@field ['debug.gl_debugging'] boolean +---@field ['debug.invalidate_fp16'] integer|boolean +---@field ['debug.log_damage'] boolean +---@field ['debug.manual_crash'] integer|boolean +---@field ['debug.overlay'] boolean +---@field ['debug.pass'] boolean +---@field ['debug.render_solitary_wo_damage'] boolean +---@field ['debug.suppress_errors'] boolean +---@field ['debug.vfr'] boolean +---@field ['decoration.active_opacity'] number|boolean +---@field ['decoration.blur.brightness'] number|boolean +---@field ['decoration.blur.contrast'] number|boolean +---@field ['decoration.blur.enabled'] boolean +---@field ['decoration.blur.ignore_opacity'] boolean +---@field ['decoration.blur.input_methods'] boolean +---@field ['decoration.blur.input_methods_ignorealpha'] number|boolean +---@field ['decoration.blur.new_optimizations'] boolean +---@field ['decoration.blur.noise'] number|boolean +---@field ['decoration.blur.passes'] integer|boolean +---@field ['decoration.blur.popups'] boolean +---@field ['decoration.blur.popups_ignorealpha'] number|boolean +---@field ['decoration.blur.size'] integer|boolean +---@field ['decoration.blur.special'] boolean +---@field ['decoration.blur.vibrancy'] number|boolean +---@field ['decoration.blur.vibrancy_darkness'] number|boolean +---@field ['decoration.blur.xray'] boolean +---@field ['decoration.border_part_of_window'] boolean +---@field ['decoration.dim_around'] number|boolean +---@field ['decoration.dim_inactive'] boolean +---@field ['decoration.dim_modal'] boolean +---@field ['decoration.dim_special'] number|boolean +---@field ['decoration.dim_strength'] number|boolean +---@field ['decoration.fullscreen_opacity'] number|boolean +---@field ['decoration.glow.color'] string +---@field ['decoration.glow.color_inactive'] string +---@field ['decoration.glow.enabled'] boolean +---@field ['decoration.glow.range'] integer|boolean +---@field ['decoration.glow.render_power'] integer|boolean +---@field ['decoration.inactive_opacity'] number|boolean +---@field ['decoration.rounding'] integer|boolean +---@field ['decoration.rounding_power'] number|boolean +---@field ['decoration.screen_shader'] string +---@field ['decoration.shadow.color'] string +---@field ['decoration.shadow.color_inactive'] string +---@field ['decoration.shadow.enabled'] boolean +---@field ['decoration.shadow.offset'] HL.Vec2Like +---@field ['decoration.shadow.range'] integer|boolean +---@field ['decoration.shadow.render_power'] integer|boolean +---@field ['decoration.shadow.scale'] number|boolean +---@field ['decoration.shadow.sharp'] boolean +---@field ['dwindle.default_split_ratio'] number|boolean +---@field ['dwindle.force_split'] integer|boolean +---@field ['dwindle.permanent_direction_override'] boolean +---@field ['dwindle.precise_mouse_move'] boolean +---@field ['dwindle.preserve_split'] boolean +---@field ['dwindle.smart_resizing'] boolean +---@field ['dwindle.smart_split'] boolean +---@field ['dwindle.special_scale_factor'] number|boolean +---@field ['dwindle.split_bias'] integer|boolean +---@field ['dwindle.split_width_multiplier'] number|boolean +---@field ['dwindle.use_active_for_splits'] boolean +---@field ['ecosystem.enforce_permissions'] boolean +---@field ['ecosystem.no_donation_nag'] boolean +---@field ['ecosystem.no_update_news'] boolean +---@field ['experimental.wp_cm_1_2'] boolean +---@field ['general.allow_tearing'] boolean +---@field ['general.border_size'] integer|boolean +---@field ['general.col.active_border'] string|HL.Gradient +---@field ['general.col.inactive_border'] string|HL.Gradient +---@field ['general.col.nogroup_border'] string|HL.Gradient +---@field ['general.col.nogroup_border_active'] string|HL.Gradient +---@field ['general.extend_border_grab_area'] integer|boolean +---@field ['general.float_gaps'] integer|HL.CssGap +---@field ['general.gaps_in'] integer|HL.CssGap +---@field ['general.gaps_out'] integer|HL.CssGap +---@field ['general.gaps_workspaces'] integer|boolean +---@field ['general.hover_icon_on_border'] boolean +---@field ['general.layout'] string +---@field ['general.locale'] string +---@field ['general.modal_parent_blocking'] boolean +---@field ['general.no_focus_fallback'] boolean +---@field ['general.resize_corner'] integer|boolean +---@field ['general.resize_on_border'] boolean +---@field ['general.snap.border_overlap'] boolean +---@field ['general.snap.enabled'] boolean +---@field ['general.snap.monitor_gap'] integer|boolean +---@field ['general.snap.respect_gaps'] boolean +---@field ['general.snap.window_gap'] integer|boolean +---@field ['gestures.close_max_timeout'] integer|boolean +---@field ['gestures.workspace_swipe_cancel_ratio'] number|boolean +---@field ['gestures.workspace_swipe_create_new'] boolean +---@field ['gestures.workspace_swipe_direction_lock'] boolean +---@field ['gestures.workspace_swipe_direction_lock_threshold'] integer|boolean +---@field ['gestures.workspace_swipe_distance'] integer|boolean +---@field ['gestures.workspace_swipe_forever'] boolean +---@field ['gestures.workspace_swipe_invert'] boolean +---@field ['gestures.workspace_swipe_min_speed_to_force'] integer|boolean +---@field ['gestures.workspace_swipe_touch'] boolean +---@field ['gestures.workspace_swipe_touch_invert'] boolean +---@field ['gestures.workspace_swipe_use_r'] boolean +---@field ['group.auto_group'] boolean +---@field ['group.col.border_active'] string|HL.Gradient +---@field ['group.col.border_inactive'] string|HL.Gradient +---@field ['group.col.border_locked_active'] string|HL.Gradient +---@field ['group.col.border_locked_inactive'] string|HL.Gradient +---@field ['group.drag_into_group'] integer|boolean +---@field ['group.focus_removed_window'] boolean +---@field ['group.group_on_movetoworkspace'] boolean +---@field ['group.groupbar.blur'] boolean +---@field ['group.groupbar.col.active'] string|HL.Gradient +---@field ['group.groupbar.col.inactive'] string|HL.Gradient +---@field ['group.groupbar.col.locked_active'] string|HL.Gradient +---@field ['group.groupbar.col.locked_inactive'] string|HL.Gradient +---@field ['group.groupbar.enabled'] boolean +---@field ['group.groupbar.font_family'] string +---@field ['group.groupbar.font_size'] integer|boolean +---@field ['group.groupbar.font_weight_active'] integer|string +---@field ['group.groupbar.font_weight_inactive'] integer|string +---@field ['group.groupbar.gaps_in'] integer|boolean +---@field ['group.groupbar.gaps_out'] integer|boolean +---@field ['group.groupbar.gradient_round_only_edges'] boolean +---@field ['group.groupbar.gradient_rounding'] integer|boolean +---@field ['group.groupbar.gradient_rounding_power'] number|boolean +---@field ['group.groupbar.gradients'] boolean +---@field ['group.groupbar.height'] integer|boolean +---@field ['group.groupbar.indicator_gap'] integer|boolean +---@field ['group.groupbar.indicator_height'] integer|boolean +---@field ['group.groupbar.keep_upper_gap'] boolean +---@field ['group.groupbar.priority'] integer|boolean +---@field ['group.groupbar.render_titles'] boolean +---@field ['group.groupbar.round_only_edges'] boolean +---@field ['group.groupbar.rounding'] integer|boolean +---@field ['group.groupbar.rounding_power'] number|boolean +---@field ['group.groupbar.scrolling'] boolean +---@field ['group.groupbar.stacked'] boolean +---@field ['group.groupbar.text_color'] string +---@field ['group.groupbar.text_color_inactive'] string +---@field ['group.groupbar.text_color_locked_active'] string +---@field ['group.groupbar.text_color_locked_inactive'] string +---@field ['group.groupbar.text_offset'] integer|boolean +---@field ['group.groupbar.text_padding'] integer|boolean +---@field ['group.insert_after_current'] boolean +---@field ['group.merge_floated_into_tiled_on_groupbar'] boolean +---@field ['group.merge_groups_on_drag'] boolean +---@field ['group.merge_groups_on_groupbar'] boolean +---@field ['input.accel_profile'] string +---@field ['input.emulate_discrete_scroll'] integer|boolean +---@field ['input.float_switch_override_focus'] integer|boolean +---@field ['input.focus_on_close'] integer|boolean +---@field ['input.follow_mouse'] integer|boolean +---@field ['input.follow_mouse_shrink'] integer|boolean +---@field ['input.follow_mouse_threshold'] number|boolean +---@field ['input.force_no_accel'] boolean +---@field ['input.kb_file'] string +---@field ['input.kb_layout'] string +---@field ['input.kb_model'] string +---@field ['input.kb_options'] string +---@field ['input.kb_rules'] string +---@field ['input.kb_variant'] string +---@field ['input.left_handed'] boolean +---@field ['input.mouse_refocus'] boolean +---@field ['input.natural_scroll'] boolean +---@field ['input.numlock_by_default'] boolean +---@field ['input.off_window_axis_events'] integer|boolean +---@field ['input.repeat_delay'] integer|boolean +---@field ['input.repeat_rate'] integer|boolean +---@field ['input.resolve_binds_by_sym'] boolean +---@field ['input.rotation'] integer|boolean +---@field ['input.scroll_button'] integer|boolean +---@field ['input.scroll_button_lock'] boolean +---@field ['input.scroll_factor'] number|boolean +---@field ['input.scroll_method'] string +---@field ['input.scroll_points'] string +---@field ['input.sensitivity'] number|boolean +---@field ['input.special_fallthrough'] boolean +---@field ['input.tablet.absolute_region_position'] boolean +---@field ['input.tablet.active_area_position'] HL.Vec2Like +---@field ['input.tablet.active_area_size'] HL.Vec2Like +---@field ['input.tablet.left_handed'] boolean +---@field ['input.tablet.output'] string +---@field ['input.tablet.region_position'] HL.Vec2Like +---@field ['input.tablet.region_size'] HL.Vec2Like +---@field ['input.tablet.relative_input'] boolean +---@field ['input.tablet.transform'] integer|boolean +---@field ['input.touchdevice.enabled'] boolean +---@field ['input.touchdevice.output'] string +---@field ['input.touchdevice.transform'] integer|boolean +---@field ['input.touchpad.clickfinger_behavior'] boolean +---@field ['input.touchpad.disable_while_typing'] boolean +---@field ['input.touchpad.drag_3fg'] integer|boolean +---@field ['input.touchpad.drag_lock'] integer|boolean +---@field ['input.touchpad.flip_x'] boolean +---@field ['input.touchpad.flip_y'] boolean +---@field ['input.touchpad.middle_button_emulation'] boolean +---@field ['input.touchpad.natural_scroll'] boolean +---@field ['input.touchpad.scroll_factor'] number|boolean +---@field ['input.touchpad.tap_and_drag'] boolean +---@field ['input.touchpad.tap_button_map'] string +---@field ['input.touchpad.tap_to_click'] boolean +---@field ['input.virtualkeyboard.release_pressed_on_close'] boolean +---@field ['input.virtualkeyboard.share_states'] integer|boolean +---@field ['layout.single_window_aspect_ratio'] HL.Vec2Like +---@field ['layout.single_window_aspect_ratio_tolerance'] number|boolean +---@field ['master.allow_small_split'] boolean +---@field ['master.always_keep_position'] boolean +---@field ['master.center_ignores_reserved'] boolean +---@field ['master.center_master_fallback'] string +---@field ['master.drop_at_cursor'] boolean +---@field ['master.mfact'] number|boolean +---@field ['master.new_on_active'] string +---@field ['master.new_on_top'] boolean +---@field ['master.new_status'] string +---@field ['master.orientation'] string +---@field ['master.slave_count_for_center_master'] integer|boolean +---@field ['master.smart_resizing'] boolean +---@field ['master.special_scale_factor'] number|boolean +---@field ['misc.allow_session_lock_restore'] boolean +---@field ['misc.always_follow_on_dnd'] boolean +---@field ['misc.animate_manual_resizes'] boolean +---@field ['misc.animate_mouse_windowdragging'] boolean +---@field ['misc.anr_missed_pings'] integer|boolean +---@field ['misc.background_color'] string +---@field ['misc.close_special_on_empty'] boolean +---@field ['misc.col.splash'] string +---@field ['misc.disable_autoreload'] boolean +---@field ['misc.disable_hyprland_guiutils_check'] boolean +---@field ['misc.disable_hyprland_logo'] boolean +---@field ['misc.disable_scale_notification'] boolean +---@field ['misc.disable_splash_rendering'] boolean +---@field ['misc.disable_watchdog_warning'] boolean +---@field ['misc.disable_xdg_env_checks'] boolean +---@field ['misc.enable_anr_dialog'] boolean +---@field ['misc.enable_swallow'] boolean +---@field ['misc.exit_window_retains_fullscreen'] boolean +---@field ['misc.focus_on_activate'] boolean +---@field ['misc.font_family'] string +---@field ['misc.force_default_wallpaper'] integer|boolean +---@field ['misc.initial_workspace_tracking'] integer|boolean +---@field ['misc.key_press_enables_dpms'] boolean +---@field ['misc.layers_hog_keyboard_focus'] boolean +---@field ['misc.lockdead_screen_delay'] integer|boolean +---@field ['misc.middle_click_paste'] boolean +---@field ['misc.mouse_move_enables_dpms'] boolean +---@field ['misc.mouse_move_focuses_monitor'] boolean +---@field ['misc.name_vk_after_proc'] boolean +---@field ['misc.on_focus_under_fullscreen'] integer|boolean +---@field ['misc.render_unfocused_fps'] integer|boolean +---@field ['misc.screencopy_force_8b'] boolean +---@field ['misc.session_lock_xray'] boolean +---@field ['misc.size_limits_tiled'] boolean +---@field ['misc.splash_font_family'] string +---@field ['misc.swallow_exception_regex'] string +---@field ['misc.swallow_regex'] string +---@field ['misc.vrr'] integer|boolean +---@field ['opengl.nvidia_anti_flicker'] boolean +---@field ['quirks.prefer_hdr'] integer|boolean +---@field ['quirks.skip_non_kms_dmabuf_formats'] boolean +---@field ['render.cm_auto_hdr'] integer|boolean +---@field ['render.cm_enabled'] boolean +---@field ['render.cm_sdr_eotf'] string +---@field ['render.commit_timing_enabled'] boolean +---@field ['render.ctm_animation'] integer|boolean +---@field ['render.direct_scanout'] integer|boolean +---@field ['render.expand_undersized_textures'] boolean +---@field ['render.icc_vcgt_enabled'] boolean +---@field ['render.keep_unmodified_copy'] integer|boolean +---@field ['render.new_render_scheduling'] boolean +---@field ['render.non_shader_cm'] integer|boolean +---@field ['render.non_shader_cm_interop'] integer|boolean +---@field ['render.send_content_type'] boolean +---@field ['render.use_fp16'] integer|boolean +---@field ['render.use_shader_blur_blend'] boolean +---@field ['render.xp_mode'] boolean +---@field ['scrolling.column_width'] number|boolean +---@field ['scrolling.direction'] string +---@field ['scrolling.explicit_column_widths'] string +---@field ['scrolling.focus_fit_method'] integer|boolean +---@field ['scrolling.follow_focus'] boolean +---@field ['scrolling.follow_min_visible'] number|boolean +---@field ['scrolling.fullscreen_on_one_column'] boolean +---@field ['scrolling.wrap_focus'] boolean +---@field ['scrolling.wrap_swapcol'] boolean +---@field ['xwayland.create_abstract_socket'] boolean +---@field ['xwayland.enabled'] boolean +---@field ['xwayland.force_zero_scaling'] boolean +---@field ['xwayland.use_nearest_neighbor'] boolean +local __HL_ConfigValueTypes = {} diff --git a/nix/default.nix b/nix/default.nix index 51bb58fef..a8cb52803 100644 --- a/nix/default.nix +++ b/nix/default.nix @@ -36,9 +36,11 @@ libxkbcommon, libuuid, libgbm, + lua5_5, muparser, pango, pciutils, + python3, re2, systemd, tomlplusplus, @@ -116,13 +118,14 @@ customStdenv.mkDerivation (finalAttrs: { ../hyprland.pc.in ../hyprpm ../LICENSE + ../meta ../protocols ../src ../start ../systemd ../VERSION (fs.fileFilter (file: file.hasExt "1") ../docs) - (fs.fileFilter (file: file.hasExt "conf" || file.hasExt "in") ../example) + (fs.fileFilter (file: file.hasExt "conf" || file.hasExt "in" || file.hasExt "lua" ) ../example) (fs.fileFilter (file: file.hasExt "sh") ../scripts) (fs.fileFilter (file: file.name == "CMakeLists.txt") ../.) (optional withTests [ @@ -160,6 +163,7 @@ customStdenv.mkDerivation (finalAttrs: { makeWrapper cmake pkg-config + python3 ]; outputs = [ @@ -190,6 +194,7 @@ customStdenv.mkDerivation (finalAttrs: { libuuid libxcursor libxkbcommon + lua5_5 muparser pango pciutils diff --git a/nix/tests/default.nix b/nix/tests/default.nix index c17379925..6c8e54090 100644 --- a/nix/tests/default.nix +++ b/nix/tests/default.nix @@ -43,7 +43,7 @@ in }; # Test configuration - environment.etc."test.conf".source = "${hyprland}/share/hypr/test.conf"; + environment.etc."test.lua".source = "${hyprland}/share/hypr/test.lua"; # Disable portals xdg.portal.enable = pkgs.lib.mkForce false; @@ -87,7 +87,7 @@ in # Run hyprtester testing framework/suite print("Running hyprtester") - exit_status, _out = machine.execute("su - alice -c 'hyprtester -b ${hyprland}/bin/Hyprland -c /etc/test.conf -p ${hyprland}/lib/hyprtestplugin.so 2>&1 | tee /tmp/testerlog; exit ''${PIPESTATUS[0]}'") + exit_status, _out = machine.execute("su - alice -c 'hyprtester -b ${hyprland}/bin/Hyprland -c /etc/test.lua -p ${hyprland}/lib/hyprtestplugin.so 2>&1 | tee /tmp/testerlog; exit ''${PIPESTATUS[0]}'") print(f"Hyprtester exited with {exit_status}") # Print logs for visibility in CI @@ -101,6 +101,7 @@ in machine.copy_from_vm("/tmp/testerlog") machine.copy_from_vm("/tmp/hyprlog") machine.copy_from_vm("/tmp/exit_status") + machine.copy_from_vm("/tmp/exit_status_gtests") # Finally - shutdown machine.shutdown() diff --git a/src/Compositor.cpp b/src/Compositor.cpp index f823b62cb..14a7843ec 100644 --- a/src/Compositor.cpp +++ b/src/Compositor.cpp @@ -2,6 +2,7 @@ #include #include "Compositor.hpp" +#include "config/supplementary/executor/Executor.hpp" #include "debug/log/Logger.hpp" #include "desktop/DesktopTypes.hpp" #include "desktop/state/FocusState.hpp" @@ -98,6 +99,7 @@ using namespace Hyprutils::String; using namespace Aquamarine; using enum NContentType::eContentType; using namespace NColorManagement; +using namespace Desktop::View; using namespace Render::GL; static int handleCritSignal(int signo, void* data) { @@ -534,7 +536,7 @@ void CCompositor::cleanEnvironment() { "dbus-update-activation-environment 2>/dev/null && " #endif "dbus-update-activation-environment --systemd WAYLAND_DISPLAY XDG_CURRENT_DESKTOP HYPRLAND_INSTANCE_SIGNATURE QT_QPA_PLATFORMTHEME PATH XDG_DATA_DIRS"; - CKeybindManager::spawn(CMD); + Config::Supplementary::executor()->spawn(CMD); } } @@ -652,6 +654,9 @@ void CCompositor::initManagers(eManagersInitStage stage) { Log::logger->log(Log::DEBUG, "Creating the TokenManager!"); g_pTokenManager = makeUnique(); + // create executor + Config::Supplementary::executor(); + Config::mgr()->init(); Log::logger->log(Log::DEBUG, "Creating the PointerManager!"); @@ -781,7 +786,7 @@ void CCompositor::startCompositor() { "dbus-update-activation-environment 2>/dev/null && " #endif "dbus-update-activation-environment --systemd WAYLAND_DISPLAY XDG_CURRENT_DESKTOP HYPRLAND_INSTANCE_SIGNATURE QT_QPA_PLATFORMTHEME PATH XDG_DATA_DIRS"; - CKeybindManager::spawn(CMD); + Config::Supplementary::executor()->spawn(CMD); } Log::logger->log(Log::DEBUG, "Running on WAYLAND_DISPLAY: {}", m_wlDisplaySocket); @@ -900,12 +905,12 @@ PHLWINDOW CCompositor::vectorToWindowUnified(const Vector2D& pos, uint16_t prope if (!PMONITOR) return nullptr; - static auto PRESIZEONBORDER = CConfigValue("general:resize_on_border"); - static auto PBORDERSIZE = CConfigValue("general:border_size"); - static auto PBORDERGRABEXTEND = CConfigValue("general:extend_border_grab_area"); - static auto PSPECIALFALLTHRU = CConfigValue("input:special_fallthrough"); - static auto PMODALPARENTBLOCKING = CConfigValue("general:modal_parent_blocking"); - static auto PFOLLOWMOUSESHRINK = CConfigValue("input:follow_mouse_shrink"); + static auto PRESIZEONBORDER = CConfigValue("general:resize_on_border"); + static auto PBORDERSIZE = CConfigValue("general:border_size"); + static auto PBORDERGRABEXTEND = CConfigValue("general:extend_border_grab_area"); + static auto PSPECIALFALLTHRU = CConfigValue("input:special_fallthrough"); + static auto PMODALPARENTBLOCKING = CConfigValue("general:modal_parent_blocking"); + static auto PFOLLOWMOUSESHRINK = CConfigValue("input:follow_mouse_shrink"); const auto BORDER_GRAB_AREA = *PRESIZEONBORDER ? *PBORDERSIZE + *PBORDERGRABEXTEND : 0; const bool ONLY_PRIORITY = properties & Desktop::View::FOCUS_PRIORITY; const bool FOLLOW_MOUSE_CHECK = properties & Desktop::View::FOLLOW_MOUSE_CHECK; @@ -922,7 +927,7 @@ PHLWINDOW CCompositor::vectorToWindowUnified(const Vector2D& pos, uint16_t prope if (ONLY_PRIORITY && !w->priorityFocus()) continue; - if (w->m_isFloating && w->m_isMapped && !w->isHidden() && !w->m_X11ShouldntFocus && w->m_pinned && !w->m_ruleApplicator->noFocus().valueOrDefault() && + if (w->m_isFloating && w->m_isMapped && w->acceptsInput() && !w->m_X11ShouldntFocus && w->m_pinned && !w->m_ruleApplicator->noFocus().valueOrDefault() && w != pIgnoreWindow && !isShadowedByModal(w)) { const auto BB = w->getWindowBoxUnified(properties); CBox box = BB.copy().expand(!w->isX11OverrideRedirect() ? BORDER_GRAB_AREA : 0); @@ -962,8 +967,8 @@ PHLWINDOW CCompositor::vectorToWindowUnified(const Vector2D& pos, uint16_t prope continue; } - if (w->m_isFloating && w->m_isMapped && w->m_workspace->isVisible() && !w->isHidden() && !w->m_pinned && !w->m_ruleApplicator->noFocus().valueOrDefault() && - w != pIgnoreWindow && (!aboveFullscreen || w->m_createdOverFullscreen) && !isShadowedByModal(w)) { + if (w->m_isFloating && w->m_isMapped && w->m_workspace->isVisible() && w->acceptsInput() && !w->m_pinned && !w->m_ruleApplicator->noFocus().valueOrDefault() && + w != pIgnoreWindow && (!aboveFullscreen || w->isAllowedOverFullscreen()) && !isShadowedByModal(w)) { // OR windows should add focus to parent if (w->m_X11ShouldntFocus && !w->isX11OverrideRedirect()) continue; @@ -1034,7 +1039,7 @@ PHLWINDOW CCompositor::vectorToWindowUnified(const Vector2D& pos, uint16_t prope if (!w->m_workspace) continue; - if (!w->m_isX11 && !w->m_isFloating && w->m_isMapped && w->workspaceID() == WSPID && !w->isHidden() && !w->m_X11ShouldntFocus && + if (!w->m_isX11 && !w->m_isFloating && w->m_isMapped && w->workspaceID() == WSPID && w->acceptsInput() && !w->m_X11ShouldntFocus && !w->m_ruleApplicator->noFocus().valueOrDefault() && w != pIgnoreWindow && !isShadowedByModal(w)) { if (w->hasPopupAt(pos)) return w; @@ -1051,7 +1056,7 @@ PHLWINDOW CCompositor::vectorToWindowUnified(const Vector2D& pos, uint16_t prope if (!w->m_workspace) continue; - if (!w->m_isFloating && w->m_isMapped && w->workspaceID() == WSPID && !w->isHidden() && !w->m_X11ShouldntFocus && !w->m_ruleApplicator->noFocus().valueOrDefault() && + if (!w->m_isFloating && w->m_isMapped && w->workspaceID() == WSPID && w->acceptsInput() && !w->m_X11ShouldntFocus && !w->m_ruleApplicator->noFocus().valueOrDefault() && w != pIgnoreWindow && !isShadowedByModal(w)) { CBox box = (properties & Desktop::View::USE_PROP_TILED) ? w->getWindowBoxUnified(properties) : CBox{w->m_position, w->m_size}; if ((properties & Desktop::View::INPUT_EXTENTS) && BORDER_GRAB_AREA > 0 && !w->isX11OverrideRedirect()) { @@ -1305,6 +1310,9 @@ void CCompositor::changeWindowZOrder(PHLWINDOW pWindow, bool top) { else pWindow->m_createdOverFullscreen = false; + pWindow->updateFullscreenInputState(); + *pWindow->alpha(WINDOW_ALPHA_FULLSCREEN) = pWindow->isBlockedByFullscreen() ? 0.F : 1.F; + if (pWindow == (top ? m_windows.back() : m_windows.front())) return; @@ -1364,7 +1372,7 @@ void CCompositor::cleanupFadingOut(const MONITORID& monid) { if (w->monitorID() != monid && w->m_monitor) continue; - if (!w->m_fadingOut || w->m_alpha->value() == 0.f) { + if (!w->m_fadingOut || w->alphaValue(WINDOW_ALPHA_FADE) == 0.f) { w->m_fadingOut = false; @@ -1470,8 +1478,8 @@ PHLWINDOW CCompositor::getWindowInDirection(const CBox& box, PHLWORKSPACE pWorks return nullptr; // 0 -> history, 1 -> shared length - static auto PMETHOD = CConfigValue("binds:focus_preferred_method"); - static auto PMONITORFALLBACK = CConfigValue("binds:window_direction_monitor_fallback"); + static auto PMETHOD = CConfigValue("binds:focus_preferred_method"); + static auto PMONITORFALLBACK = CConfigValue("binds:window_direction_monitor_fallback"); const auto POSA = box.pos(); const auto SIZEA = box.size(); @@ -1510,13 +1518,13 @@ PHLWINDOW CCompositor::getWindowInDirection(const CBox& box, PHLWORKSPACE pWorks }; for (auto const& w : m_windows) { - if (w == ignoreWindow || !w->m_workspace || !w->m_isMapped || w->isHidden() || (!w->isFullscreen() && w->m_isFloating) || !w->m_workspace->isVisible()) + if (w == ignoreWindow || !w->m_workspace || !w->m_isMapped || !w->acceptsInput() || (!w->isFullscreen() && w->m_isFloating) || !w->m_workspace->isVisible()) continue; if (pWorkspace->m_monitor == w->m_monitor && pWorkspace != w->m_workspace) continue; - if (pWorkspace->m_hasFullscreenWindow && !w->isFullscreen() && !w->m_createdOverFullscreen) + if (pWorkspace->m_hasFullscreenWindow && !w->isAllowedOverFullscreen()) continue; if (!*PMONITORFALLBACK && pWorkspace->m_monitor != w->m_monitor) @@ -1589,13 +1597,13 @@ PHLWINDOW CCompositor::getWindowInDirection(const CBox& box, PHLWORKSPACE pWorks constexpr float THRESHOLD = 0.3 * M_PI; for (auto const& w : m_windows) { - if (w == ignoreWindow || !w->m_isMapped || !w->m_workspace || w->isHidden() || (!w->isFullscreen() && !w->m_isFloating) || !w->m_workspace->isVisible()) + if (w == ignoreWindow || !w->m_isMapped || !w->m_workspace || !w->acceptsInput() || (!w->isFullscreen() && !w->m_isFloating) || !w->m_workspace->isVisible()) continue; if (pWorkspace->m_monitor == w->m_monitor && pWorkspace != w->m_workspace) continue; - if (pWorkspace->m_hasFullscreenWindow && !w->isFullscreen() && !w->m_createdOverFullscreen) + if (pWorkspace->m_hasFullscreenWindow && !w->isAllowedOverFullscreen()) continue; if (!*PMONITORFALLBACK && pWorkspace->m_monitor != w->m_monitor) @@ -1635,9 +1643,19 @@ static bool isFloatingMatches(WINDOWPTR w, std::optional floating) { } template -static bool isWindowAvailableForCycle(WINDOWPTR pWindow, WINDOWPTR w, bool focusableOnly, std::optional floating, bool anyWorkspace = false) { +static bool acceptsInputForCycle(WINDOWPTR w, bool allowFullscreenBlocked) { + if (w->acceptsInput()) + return true; + + return allowFullscreenBlocked && !w->isHidden() && w->isInputBlockedOnly(INPUT_BLOCK_BELOW_FULLSCREEN); +} + +template +static bool isWindowAvailableForCycle(WINDOWPTR pWindow, WINDOWPTR w, bool focusableOnly, std::optional floating, bool anyWorkspace = false, + bool allowFullscreenBlocked = false) { return isFloatingMatches(w, floating) && - (w != pWindow && isWorkspaceMatches(pWindow, w, anyWorkspace) && w->m_isMapped && !w->isHidden() && (!focusableOnly || !w->m_ruleApplicator->noFocus().valueOrDefault())); + (w != pWindow && isWorkspaceMatches(pWindow, w, anyWorkspace) && w->m_isMapped && acceptsInputForCycle(w, allowFullscreenBlocked) && + (!focusableOnly || !w->m_ruleApplicator->noFocus().valueOrDefault())); } template @@ -1658,16 +1676,16 @@ static PHLWINDOW getWeakWindowPred(Iterator cur, Iterator end, Iterator begin, c return IN_OTHER_SIDE->lock(); } -PHLWINDOW CCompositor::getWindowCycleHist(PHLWINDOWREF cur, bool focusableOnly, std::optional floating, bool visible, bool next) { - const auto FINDER = [&](const PHLWINDOWREF& w) { return isWindowAvailableForCycle(cur, w, focusableOnly, floating, visible); }; +PHLWINDOW CCompositor::getWindowCycleHist(PHLWINDOWREF cur, bool focusableOnly, std::optional floating, bool visible, bool next, bool allowFullscreenBlocked) { + const auto FINDER = [&](const PHLWINDOWREF& w) { return isWindowAvailableForCycle(cur, w, focusableOnly, floating, visible, allowFullscreenBlocked); }; // also m_vWindowFocusHistory has reverse order, so when it is next - we need to reverse again const auto& HISTORY = Desktop::History::windowTracker()->fullHistory(); return next ? getWeakWindowPred(std::ranges::find(HISTORY, cur), HISTORY.end(), HISTORY.begin(), FINDER) : getWeakWindowPred(std::ranges::find(HISTORY | std::views::reverse, cur), HISTORY.rend(), HISTORY.rbegin(), FINDER); } -PHLWINDOW CCompositor::getWindowCycle(PHLWINDOW cur, bool focusableOnly, std::optional floating, bool visible, bool prev) { - const auto FINDER = [&](const PHLWINDOW& w) { return isWindowAvailableForCycle(cur, w, focusableOnly, floating, visible); }; +PHLWINDOW CCompositor::getWindowCycle(PHLWINDOW cur, bool focusableOnly, std::optional floating, bool visible, bool prev, bool allowFullscreenBlocked) { + const auto FINDER = [&](const PHLWINDOW& w) { return isWindowAvailableForCycle(cur, w, focusableOnly, floating, visible, allowFullscreenBlocked); }; return prev ? getWindowPred(std::ranges::find(m_windows | std::views::reverse, cur), m_windows.rend(), m_windows.rbegin(), FINDER) : getWindowPred(std::ranges::find(m_windows, cur), m_windows.end(), m_windows.begin(), FINDER); } @@ -1729,7 +1747,7 @@ bool CCompositor::isPointOnReservedArea(const Vector2D& point, const PHLMONITOR } std::optional CCompositor::calculateX11WorkArea() { - static auto PXWLFORCESCALEZERO = CConfigValue("xwayland:force_zero_scaling"); + static auto PXWLFORCESCALEZERO = CConfigValue("xwayland:force_zero_scaling"); // We more than likely won't be able to calculate one // and even if we could this is minor if (m_monitors.size() > 1 || m_monitors.empty()) @@ -2007,7 +2025,7 @@ PHLMONITOR CCompositor::getMonitorFromString(const std::string& name) { } void CCompositor::moveWorkspaceToMonitor(PHLWORKSPACE pWorkspace, PHLMONITOR pMonitor, bool noWarpCursor) { - static auto PHIDESPECIALONWORKSPACECHANGE = CConfigValue("binds:hide_special_on_workspace_change"); + static auto PHIDESPECIALONWORKSPACECHANGE = CConfigValue("binds:hide_special_on_workspace_change"); if (!pWorkspace || !pMonitor) return; @@ -2181,8 +2199,8 @@ void CCompositor::setWindowFullscreenClient(const PHLWINDOW PWINDOW, const eFull } void CCompositor::setWindowFullscreenState(const PHLWINDOW PWINDOW, Desktop::View::SFullscreenState state) { - static auto PDIRECTSCANOUT = CConfigValue("render:direct_scanout"); - static auto PALLOWPINFULLSCREEN = CConfigValue("binds:allow_pin_fullscreen"); + static auto PDIRECTSCANOUT = CConfigValue("render:direct_scanout"); + static auto PALLOWPINFULLSCREEN = CConfigValue("binds:allow_pin_fullscreen"); if (!validMapped(PWINDOW) || g_pCompositor->m_unsafeState) return; @@ -2253,8 +2271,12 @@ void CCompositor::setWindowFullscreenState(const PHLWINDOW PWINDOW, Desktop::Vie // make all windows and layers on the same workspace under the fullscreen window for (auto const& w : m_windows) { - if (w->m_workspace == PWORKSPACE && !w->isFullscreen() && !w->m_fadingOut && !w->m_pinned) - w->m_createdOverFullscreen = false; + if (w->m_workspace == PWORKSPACE) { + if (!w->isFullscreen() && !w->m_fadingOut && !w->m_pinned) + w->m_createdOverFullscreen = false; + + w->updateFullscreenInputState(); + } } for (auto const& ls : m_layers) { if (ls->m_monitor == PMONITOR) @@ -2326,7 +2348,7 @@ PHLWINDOW CCompositor::getWindowByRegex(const std::string& regexp_) { const bool FLOAT = regexp.starts_with("floating"); for (auto const& w : m_windows) { - if (!w->m_isMapped || w->m_isFloating != FLOAT || w->m_workspace != Desktop::focusState()->window()->m_workspace || w->isHidden()) + if (!w->m_isMapped || w->m_isFloating != FLOAT || w->m_workspace != Desktop::focusState()->window()->m_workspace || !w->acceptsInput()) continue; return w; @@ -2428,7 +2450,7 @@ void CCompositor::warpCursorTo(const Vector2D& pos, bool force) { // warpCursorTo should only be used for warps that // should be disabled with no_warps - static auto PNOWARPS = CConfigValue("cursor:no_warps"); + static auto PNOWARPS = CConfigValue("cursor:no_warps"); if (*PNOWARPS && !force) { const auto PMONITORNEW = getMonitorFromVector(pos); @@ -2442,11 +2464,6 @@ void CCompositor::warpCursorTo(const Vector2D& pos, bool force) { Desktop::focusState()->rawMonitorFocus(PMONITORNEW); } -void CCompositor::closeWindow(PHLWINDOW pWindow) { - if (pWindow && validMapped(pWindow)) - g_pXWaylandManager->sendCloseWindow(pWindow); -} - PHLLS CCompositor::getLayerSurfaceFromSurface(SP pSurface) { std::pair, bool> result = {pSurface, false}; @@ -2576,9 +2593,9 @@ std::vector CCompositor::getWorkspacesCopy() { } void CCompositor::performUserChecks() { - static auto PNOCHECKXDG = CConfigValue("misc:disable_xdg_env_checks"); - static auto PNOCHECKGUIUTILS = CConfigValue("misc:disable_hyprland_guiutils_check"); - static auto PNOWATCHDOG = CConfigValue("misc:disable_watchdog_warning"); + static auto PNOCHECKXDG = CConfigValue("misc:disable_xdg_env_checks"); + static auto PNOCHECKGUIUTILS = CConfigValue("misc:disable_hyprland_guiutils_check"); + static auto PNOWATCHDOG = CConfigValue("misc:disable_watchdog_warning"); if (!*PNOCHECKXDG) { const auto CURRENT_DESKTOP_ENV = getenv("XDG_CURRENT_DESKTOP"); @@ -2674,7 +2691,7 @@ void CCompositor::moveWindowToWorkspaceSafe(PHLWINDOW pWindow, PHLWORKSPACE pWor pWindow->moveToWorkspace(pWorkspace); pWindow->m_monitor = pWorkspace->m_monitor; - static auto PGROUPONMOVETOWORKSPACE = CConfigValue("group:group_on_movetoworkspace"); + static auto PGROUPONMOVETOWORKSPACE = CConfigValue("group:group_on_movetoworkspace"); if (*PGROUPONMOVETOWORKSPACE && visibleWindowsOnWorkspace == 1 && pFirstWindowOnWorkspace && pFirstWindowOnWorkspace != pWindow && pFirstWindowOnWorkspace->m_group && pWindow->canBeGroupedInto(pFirstWindowOnWorkspace->m_group)) { pFirstWindowOnWorkspace->m_group->add(pWindow); @@ -2701,14 +2718,14 @@ void CCompositor::moveWindowToWorkspaceSafe(PHLWINDOW pWindow, PHLWORKSPACE pWor g_pCompositor->updateSuspendedStates(); if (!WASVISIBLE && pWindow->m_workspace && pWindow->m_workspace->isVisible()) { - pWindow->m_movingFromWorkspaceAlpha->setValueAndWarp(0.F); - *pWindow->m_movingFromWorkspaceAlpha = 1.F; + pWindow->alpha(WINDOW_ALPHA_MOVE_FROM_WORKSPACE)->setValueAndWarp(0.F); + *pWindow->alpha(WINDOW_ALPHA_MOVE_FROM_WORKSPACE) = 1.F; } } PHLWINDOW CCompositor::getForceFocus() { for (auto const& w : m_windows) { - if (!w->m_isMapped || w->isHidden() || !w->m_workspace || !w->m_workspace->isVisible()) + if (!w->m_isMapped || !w->acceptsInput() || !w->m_workspace || !w->m_workspace->isVisible()) continue; if (!w->m_ruleApplicator->stayFocused().valueOrDefault()) @@ -2751,7 +2768,7 @@ void CCompositor::checkMonitorOverlaps() { } void CCompositor::arrangeMonitors() { - static auto PXWLFORCESCALEZERO = CConfigValue("xwayland:force_zero_scaling"); + static auto PXWLFORCESCALEZERO = CConfigValue("xwayland:force_zero_scaling"); std::vector toArrange(m_monitors.begin(), m_monitors.end()); std::vector arranged; diff --git a/src/Compositor.hpp b/src/Compositor.hpp index fe9ed2e93..7fbc7d525 100644 --- a/src/Compositor.hpp +++ b/src/Compositor.hpp @@ -117,8 +117,10 @@ class CCompositor { void cleanupFadingOut(const MONITORID& monid); PHLWINDOW getWindowInDirection(PHLWINDOW, Math::eDirection); PHLWINDOW getWindowInDirection(const CBox& box, PHLWORKSPACE pWorkspace, Math::eDirection dir, PHLWINDOW ignoreWindow = nullptr, bool useVectorAngles = false); - PHLWINDOW getWindowCycle(PHLWINDOW cur, bool focusableOnly = false, std::optional floating = std::nullopt, bool visible = false, bool prev = false); - PHLWINDOW getWindowCycleHist(PHLWINDOWREF cur, bool focusableOnly = false, std::optional floating = std::nullopt, bool visible = false, bool next = false); + PHLWINDOW getWindowCycle(PHLWINDOW cur, bool focusableOnly = false, std::optional floating = std::nullopt, bool visible = false, bool prev = false, + bool allowFullscreenBlocked = false); + PHLWINDOW getWindowCycleHist(PHLWINDOWREF cur, bool focusableOnly = false, std::optional floating = std::nullopt, bool visible = false, bool next = false, + bool allowFullscreenBlocked = false); WORKSPACEID getNextAvailableNamedWorkspace(); bool isPointOnAnyMonitor(const Vector2D&); bool isPointOnReservedArea(const Vector2D& point, const PHLMONITOR monitor = nullptr); @@ -143,7 +145,6 @@ class CCompositor { PHLWINDOW getWindowByRegex(const std::string&); void warpCursorTo(const Vector2D&, bool force = false); PHLLS getLayerSurfaceFromSurface(SP); - void closeWindow(PHLWINDOW); Vector2D parseWindowVectorArgsRelative(const std::string&, const Vector2D&); [[nodiscard]] PHLWORKSPACE createNewWorkspace(const WORKSPACEID&, const MONITORID&, const std::string& name = "", bool isEmpty = true); // will be deleted next frame if left empty and unfocused! diff --git a/src/config/ConfigManager.cpp b/src/config/ConfigManager.cpp index 5238f9dd1..aaad6dc9d 100644 --- a/src/config/ConfigManager.cpp +++ b/src/config/ConfigManager.cpp @@ -1,6 +1,7 @@ #include "ConfigManager.hpp" #include "supplementary/jeremy/Jeremy.hpp" #include "legacy/ConfigManager.hpp" +#include "lua/ConfigManager.hpp" #include "../debug/log/Logger.hpp" #include @@ -19,21 +20,49 @@ bool Config::initConfigManager() { const auto CFG_PATH = Supplementary::Jeremy::getMainConfigPath(); if (!CFG_PATH) { - Log::logger->log(Log::CRIT, "Couldn't load config: {}", CFG_PATH.error()); + Log::logger->log(Log::CRIT, "[cfg] Couldn't load config: {}", CFG_PATH.error()); return false; } - std::filesystem::path filePath = *CFG_PATH; + std::filesystem::path filePath = CFG_PATH->path; - // TODO: - // filePath.replace_extension(".lua"); + if (CFG_PATH->type == Supplementary::Jeremy::CONFIG_TYPE_REGULAR) { + Log::logger->log(Log::DEBUG, "[cfg] Regular config at {}", filePath.string()); - g_mgr = makeUnique(); + std::error_code ec; + if (std::filesystem::exists(filePath, ec) && !ec && filePath.extension() == ".lua") { + // we have lua! + Log::logger->log(Log::DEBUG, "[cfg] Using lua config found at {}", filePath.string()); + g_mgr = makeUnique(); + } else { + filePath.replace_extension(".conf"); + Log::logger->log(Log::DEBUG, "[cfg] Lua config not found, using legacy config at {}", filePath.string()); + g_mgr = makeUnique(); + } + } else { + Log::logger->log(Log::DEBUG, "[cfg] Config is either explicit or special."); + + if (filePath.extension() == ".lua" || filePath.extension() == "lua") { + Log::logger->log(Log::DEBUG, "[cfg] Config is lua, loading lua mgr"); + g_mgr = makeUnique(); + } else { + Log::logger->log(Log::DEBUG, "[cfg] Config is NOT lua, loading regular mgr"); + g_mgr = makeUnique(); + } + } + + RASSERT(g_mgr, "failed to create a suitable config manager"); std::error_code ec; if (!std::filesystem::exists(filePath, ec) || ec) { if (ec) { - Log::logger->log(Log::CRIT, "Couldn't load config: {}", ec.message()); + Log::logger->log(Log::CRIT, "[cfg] Couldn't load config: {}", ec.message()); + return false; + } + + // generate default + if (const auto v = g_mgr->generateDefaultConfig(filePath); !v) { + Log::logger->log(Log::CRIT, "[cfg] Couldn't generate default config: {}", v.error()); return false; } } @@ -43,4 +72,12 @@ bool Config::initConfigManager() { UP& Config::mgr() { return g_mgr; -} \ No newline at end of file +} + +const char* Config::typeToString(eConfigManagerType t) { + switch (t) { + case CONFIG_LUA: return "lua"; + case CONFIG_LEGACY: return "hyprlang"; + default: return "error"; + } +} diff --git a/src/config/ConfigManager.hpp b/src/config/ConfigManager.hpp index c9898e138..c5a8390a9 100644 --- a/src/config/ConfigManager.hpp +++ b/src/config/ConfigManager.hpp @@ -7,8 +7,16 @@ #include "./shared/Types.hpp" #include "../helpers/memory/Memory.hpp" +#include "values/types/IValue.hpp" + +extern "C" { +struct lua_State; +} + namespace Config { + using PLUGIN_LUA_FN = int (*)(lua_State*); + struct SConfigOptionReply { // * const* void* const* dataptr = nullptr; @@ -21,6 +29,8 @@ namespace Config { CONFIG_LUA }; + const char* typeToString(eConfigManagerType t); + class IConfigManager { protected: IConfigManager() = default; @@ -55,9 +65,12 @@ namespace Config { virtual std::expected generateDefaultConfig(const std::filesystem::path&, bool safeMode = false) = 0; virtual void handlePluginLoads() = 0; + + virtual std::expected registerPluginValue(void* handle, SP value) = 0; + virtual void onPluginUnload(void* handle) = 0; }; bool initConfigManager(); UP& mgr(); -}; \ No newline at end of file +}; diff --git a/src/config/ConfigValue.cpp b/src/config/ConfigValue.cpp index 169bf29cb..1c658b05b 100644 --- a/src/config/ConfigValue.cpp +++ b/src/config/ConfigValue.cpp @@ -1,9 +1,18 @@ #include "ConfigValue.hpp" #include "ConfigManager.hpp" -void local__configValuePopulate(void* const** p, const std::string& val) { +void local__configValuePopulate(void* const** p, void* const** hlangp, std::type_index* ti, const std::string& val) { const auto BIGP = Config::mgr()->getConfigValue(val); - *p = BIGP.dataptr; + + RASSERT(BIGP.dataptr, "Something went really fucking wrong with config values"); + + *ti = std::type_index(*BIGP.type); + + if (std::type_index(*BIGP.type) == typeid(void*) || std::type_index(*BIGP.type) == typeid(const char*)) { + // this is a special, cursed case. ew. + *hlangp = BIGP.dataptr; + } else + *p = BIGP.dataptr; } std::type_index local__configValueTypeIdx(const std::string& val) { diff --git a/src/config/ConfigValue.hpp b/src/config/ConfigValue.hpp index 1da359b60..5818d6338 100644 --- a/src/config/ConfigValue.hpp +++ b/src/config/ConfigValue.hpp @@ -2,42 +2,59 @@ #include #include +#include #include #include "../macros.hpp" +#include "../config/shared/complex/ComplexDataType.hpp" +#include "../config/shared/Types.hpp" + +// Welcome to wonky fucking pointer + type hell +// Enjoy your stay // giga hack to avoid including configManager here // NOLINTNEXTLINE -void local__configValuePopulate(void* const** p, const std::string& val); +void local__configValuePopulate(void* const** p, void* const** hlangp, std::type_index* ti, const std::string& val); std::type_index local__configValueTypeIdx(const std::string& val); template class CConfigValue { public: + // creates an empty value. Deref'ing this will be a crash + CConfigValue() = default; + CConfigValue(const std::string& val) { #ifdef HYPRLAND_DEBUG // verify type - const auto TYPE = local__configValueTypeIdx(val); + // TODO: fix this or leave it idk I'm tired. + // const auto TYPE = local__configValueTypeIdx(val); - // exceptions - const bool STRINGEX = (typeid(T) == typeid(std::string) && TYPE == typeid(Hyprlang::STRING)); - const bool CUSTOMEX = (typeid(T) == typeid(Hyprlang::CUSTOMTYPE) && (TYPE == typeid(Hyprlang::CUSTOMTYPE*) || TYPE == typeid(void*) /* dunno why it does this? */)); + // // exceptions + // const bool STRINGEX = (typeid(T) == typeid(std::string) && TYPE == typeid(Hyprlang::STRING)); + // const bool CUSTOMEX = ((typeid(T) == typeid(Hyprlang::CUSTOMTYPE) || typeid(T) == typeid(Config::IComplexConfigValue)) && + // (TYPE == typeid(Hyprlang::CUSTOMTYPE*) || TYPE == typeid(Config::IComplexConfigValue*) || TYPE == typeid(void*) /* dunno why it does this? */)); - RASSERT(typeid(T) == TYPE || STRINGEX || CUSTOMEX, "Mismatched type in CConfigValue, got {} but has {}", typeid(T).name(), TYPE.name()); + // RASSERT(typeid(T) == TYPE || STRINGEX || CUSTOMEX, "Mismatched type in CConfigValue, got {} but has {}", typeid(T).name(), TYPE.name()); #endif - local__configValuePopulate(&p_, val); + local__configValuePopulate(&m_p, &m_hlangp, &m_typeIndex, val); } T* ptr() const { - return *rc(p_); + return *rc(m_p); } T operator*() const { return *ptr(); } + bool good() const { + return m_p || m_hlangp; + } + private: - void* const* p_ = nullptr; + void* const* m_p = nullptr; + void* const* m_hlangp = nullptr; + std::type_index m_typeIndex = typeid(void); }; template <> @@ -48,26 +65,30 @@ inline std::string* CConfigValue::ptr() const { template <> inline std::string CConfigValue::operator*() const { - return std::string{*rc(p_)}; + if (m_typeIndex == typeid(std::string)) + return **rc(m_p); + else if (m_typeIndex == typeid(const char*)) + return std::string{*rc(m_hlangp)}; + else + RASSERT(false, "CConfigValue on a FUCKED type"); + return "FUCK"; } template <> -inline Hyprlang::STRING* CConfigValue::ptr() const { - return rc(*p_); +inline Config::INTEGER CConfigValue::operator*() const { + if (m_typeIndex == typeid(bool)) + return **rc(m_p); + else if (m_typeIndex == typeid(Config::INTEGER)) + return **rc(m_p); + else + RASSERT(false, "CConfigValue on a FUCKED type"); + return -1; } template <> -inline Hyprlang::STRING CConfigValue::operator*() const { - return *rc(p_); +inline Config::IComplexConfigValue* CConfigValue::ptr() const { + if (m_hlangp) + return rc((*rc(m_hlangp))->getData()); + else + return *rc(m_p); } - -template <> -inline Hyprlang::CUSTOMTYPE* CConfigValue::ptr() const { - return *rc(p_); -} - -template <> -inline Hyprlang::CUSTOMTYPE CConfigValue::operator*() const { - RASSERT(false, "Impossible to implement operator* of CConfigValue, use ptr()"); - return *ptr(); -} \ No newline at end of file diff --git a/src/config/legacy/ConfigManager.cpp b/src/config/legacy/ConfigManager.cpp index 310d27109..2705c3959 100644 --- a/src/config/legacy/ConfigManager.cpp +++ b/src/config/legacy/ConfigManager.cpp @@ -1,6 +1,8 @@ #include #include "ConfigManager.hpp" +#include "DefaultConfig.hpp" +#include "../values/ConfigValues.hpp" #include "../shared/inotify/ConfigWatcher.hpp" #include "../../managers/KeybindManager.hpp" #include "../../Compositor.hpp" @@ -26,7 +28,6 @@ #include "../../desktop/state/FocusState.hpp" #include "../../layout/space/Space.hpp" #include "../../layout/supplementary/WorkspaceAlgoMatcher.hpp" -#include "../defaultConfig.hpp" #include "../../render/Renderer.hpp" #include "../../errorOverlay/Overlay.hpp" @@ -36,6 +37,15 @@ #include "../../managers/permissions/DynamicPermissionManager.hpp" #include "../../notification/NotificationOverlay.hpp" #include "../../plugins/PluginSystem.hpp" +#include "../values/types/IntValue.hpp" +#include "../values/types/FloatValue.hpp" +#include "../values/types/BoolValue.hpp" +#include "../values/types/StringValue.hpp" +#include "../values/types/ColorValue.hpp" +#include "../values/types/Vec2Value.hpp" +#include "../values/types/CssGapValue.hpp" +#include "../values/types/FontWeightValue.hpp" +#include "../values/types/GradientValue.hpp" #include "../../managers/input/trackpad/TrackpadGestures.hpp" #include "../../managers/input/trackpad/gestures/DispatcherGesture.hpp" @@ -79,9 +89,7 @@ using namespace Config::Legacy; using enum NContentType::eContentType; //NOLINTNEXTLINE -extern "C" char** environ; - -#include "../supplementary/ConfigDescriptions.hpp" +extern "C" char** environ; WP Config::Legacy::mgr() { if (Config::mgr() && Config::mgr()->type() == CONFIG_LEGACY) @@ -469,373 +477,40 @@ void CConfigManager::registerConfigVar(const char* name, Hyprlang::CUSTOMTYPE&& CConfigManager::CConfigManager() { const auto ERR = verifyConfigExists(); - m_mainConfigPath = *Supplementary::Jeremy::getMainConfigPath(); + m_mainConfigPath = Supplementary::Jeremy::getMainConfigPath()->path; m_configPaths.emplace_back(m_mainConfigPath); m_config = makeUnique(m_configPaths.begin()->c_str(), Hyprlang::SConfigOptions{.throwAllErrors = true, .allowMissingConfig = true}); - registerConfigVar("general:border_size", Hyprlang::INT{1}); - registerConfigVar("general:gaps_in", Hyprlang::CConfigCustomValueType{configHandleGapSet, configHandleGapDestroy, "5"}); - registerConfigVar("general:gaps_out", Hyprlang::CConfigCustomValueType{configHandleGapSet, configHandleGapDestroy, "20"}); - registerConfigVar("general:float_gaps", Hyprlang::CConfigCustomValueType{configHandleGapSet, configHandleGapDestroy, "0"}); - registerConfigVar("general:gaps_workspaces", Hyprlang::INT{0}); - registerConfigVar("general:no_focus_fallback", Hyprlang::INT{0}); - registerConfigVar("general:resize_on_border", Hyprlang::INT{0}); - registerConfigVar("general:extend_border_grab_area", Hyprlang::INT{15}); - registerConfigVar("general:hover_icon_on_border", Hyprlang::INT{1}); - registerConfigVar("general:layout", {"dwindle"}); - registerConfigVar("general:allow_tearing", Hyprlang::INT{0}); - registerConfigVar("general:resize_corner", Hyprlang::INT{0}); - registerConfigVar("general:snap:enabled", Hyprlang::INT{0}); - registerConfigVar("general:snap:window_gap", Hyprlang::INT{10}); - registerConfigVar("general:snap:monitor_gap", Hyprlang::INT{10}); - registerConfigVar("general:snap:border_overlap", Hyprlang::INT{0}); - registerConfigVar("general:snap:respect_gaps", Hyprlang::INT{0}); - registerConfigVar("general:col.active_border", Hyprlang::CConfigCustomValueType{&configHandleGradientSet, configHandleGradientDestroy, "0xffffffff"}); - registerConfigVar("general:col.inactive_border", Hyprlang::CConfigCustomValueType{&configHandleGradientSet, configHandleGradientDestroy, "0xff444444"}); - registerConfigVar("general:col.nogroup_border", Hyprlang::CConfigCustomValueType{&configHandleGradientSet, configHandleGradientDestroy, "0xffffaaff"}); - registerConfigVar("general:col.nogroup_border_active", Hyprlang::CConfigCustomValueType{&configHandleGradientSet, configHandleGradientDestroy, "0xffff00ff"}); - registerConfigVar("general:modal_parent_blocking", Hyprlang::INT{1}); - registerConfigVar("general:locale", {""}); + for (const auto& v : Values::CONFIG_VALUES) { + const char* NAME = v->name(); - registerConfigVar("misc:disable_hyprland_logo", Hyprlang::INT{0}); - registerConfigVar("misc:disable_splash_rendering", Hyprlang::INT{0}); - registerConfigVar("misc:col.splash", Hyprlang::INT{0x55ffffff}); - registerConfigVar("misc:splash_font_family", {STRVAL_EMPTY}); - registerConfigVar("misc:font_family", {"Sans"}); - registerConfigVar("misc:force_default_wallpaper", Hyprlang::INT{-1}); - registerConfigVar("misc:vrr", Hyprlang::INT{0}); - registerConfigVar("misc:mouse_move_enables_dpms", Hyprlang::INT{0}); - registerConfigVar("misc:key_press_enables_dpms", Hyprlang::INT{0}); - registerConfigVar("misc:name_vk_after_proc", Hyprlang::INT{1}); - registerConfigVar("misc:always_follow_on_dnd", Hyprlang::INT{1}); - registerConfigVar("misc:layers_hog_keyboard_focus", Hyprlang::INT{1}); - registerConfigVar("misc:animate_manual_resizes", Hyprlang::INT{0}); - registerConfigVar("misc:animate_mouse_windowdragging", Hyprlang::INT{0}); - registerConfigVar("misc:disable_autoreload", Hyprlang::INT{0}); - registerConfigVar("misc:enable_swallow", Hyprlang::INT{0}); - registerConfigVar("misc:swallow_regex", {STRVAL_EMPTY}); - registerConfigVar("misc:swallow_exception_regex", {STRVAL_EMPTY}); - registerConfigVar("misc:focus_on_activate", Hyprlang::INT{0}); - registerConfigVar("misc:mouse_move_focuses_monitor", Hyprlang::INT{1}); - registerConfigVar("misc:allow_session_lock_restore", Hyprlang::INT{0}); - registerConfigVar("misc:session_lock_xray", Hyprlang::INT{0}); - registerConfigVar("misc:close_special_on_empty", Hyprlang::INT{1}); - registerConfigVar("misc:background_color", Hyprlang::INT{0xff111111}); - registerConfigVar("misc:on_focus_under_fullscreen", Hyprlang::INT{2}); - registerConfigVar("misc:exit_window_retains_fullscreen", Hyprlang::INT{0}); - registerConfigVar("misc:initial_workspace_tracking", Hyprlang::INT{1}); - registerConfigVar("misc:middle_click_paste", Hyprlang::INT{1}); - registerConfigVar("misc:render_unfocused_fps", Hyprlang::INT{15}); - registerConfigVar("misc:disable_xdg_env_checks", Hyprlang::INT{0}); - registerConfigVar("misc:disable_hyprland_guiutils_check", Hyprlang::INT{0}); - registerConfigVar("misc:disable_watchdog_warning", Hyprlang::INT{0}); - registerConfigVar("misc:lockdead_screen_delay", Hyprlang::INT{1000}); - registerConfigVar("misc:enable_anr_dialog", Hyprlang::INT{1}); - registerConfigVar("misc:anr_missed_pings", Hyprlang::INT{5}); - registerConfigVar("misc:screencopy_force_8b", Hyprlang::INT{1}); - registerConfigVar("misc:disable_scale_notification", Hyprlang::INT{0}); - registerConfigVar("misc:size_limits_tiled", Hyprlang::INT{0}); - - registerConfigVar("group:insert_after_current", Hyprlang::INT{1}); - registerConfigVar("group:focus_removed_window", Hyprlang::INT{1}); - registerConfigVar("group:merge_groups_on_drag", Hyprlang::INT{1}); - registerConfigVar("group:merge_groups_on_groupbar", Hyprlang::INT{1}); - registerConfigVar("group:merge_floated_into_tiled_on_groupbar", Hyprlang::INT{0}); - registerConfigVar("group:auto_group", Hyprlang::INT{1}); - registerConfigVar("group:drag_into_group", Hyprlang::INT{1}); - registerConfigVar("group:group_on_movetoworkspace", Hyprlang::INT{0}); - registerConfigVar("group:groupbar:enabled", Hyprlang::INT{1}); - registerConfigVar("group:groupbar:font_family", {STRVAL_EMPTY}); - registerConfigVar("group:groupbar:font_weight_active", Hyprlang::CConfigCustomValueType{&configHandleFontWeightSet, configHandleFontWeightDestroy, "normal"}); - registerConfigVar("group:groupbar:font_weight_inactive", Hyprlang::CConfigCustomValueType{&configHandleFontWeightSet, configHandleFontWeightDestroy, "normal"}); - registerConfigVar("group:groupbar:font_size", Hyprlang::INT{8}); - registerConfigVar("group:groupbar:gradients", Hyprlang::INT{0}); - registerConfigVar("group:groupbar:height", Hyprlang::INT{14}); - registerConfigVar("group:groupbar:indicator_gap", Hyprlang::INT{0}); - registerConfigVar("group:groupbar:indicator_height", Hyprlang::INT{3}); - registerConfigVar("group:groupbar:priority", Hyprlang::INT{3}); - registerConfigVar("group:groupbar:render_titles", Hyprlang::INT{1}); - registerConfigVar("group:groupbar:scrolling", Hyprlang::INT{1}); - registerConfigVar("group:groupbar:text_color", Hyprlang::INT{0xffffffff}); - registerConfigVar("group:groupbar:text_color_inactive", Hyprlang::INT{-1}); - registerConfigVar("group:groupbar:text_color_locked_active", Hyprlang::INT{-1}); - registerConfigVar("group:groupbar:text_color_locked_inactive", Hyprlang::INT{-1}); - registerConfigVar("group:groupbar:stacked", Hyprlang::INT{0}); - registerConfigVar("group:groupbar:rounding", Hyprlang::INT{1}); - registerConfigVar("group:groupbar:rounding_power", {2.F}); - registerConfigVar("group:groupbar:gradient_rounding", Hyprlang::INT{2}); - registerConfigVar("group:groupbar:gradient_rounding_power", {2.F}); - registerConfigVar("group:groupbar:round_only_edges", Hyprlang::INT{1}); - registerConfigVar("group:groupbar:gradient_round_only_edges", Hyprlang::INT{1}); - registerConfigVar("group:groupbar:gaps_out", Hyprlang::INT{2}); - registerConfigVar("group:groupbar:gaps_in", Hyprlang::INT{2}); - registerConfigVar("group:groupbar:keep_upper_gap", Hyprlang::INT{1}); - registerConfigVar("group:groupbar:text_offset", Hyprlang::INT{0}); - registerConfigVar("group:groupbar:text_padding", Hyprlang::INT{0}); - registerConfigVar("group:groupbar:blur", Hyprlang::INT{0}); - - registerConfigVar("debug:log_damage", Hyprlang::INT{0}); - registerConfigVar("debug:overlay", Hyprlang::INT{0}); - registerConfigVar("debug:damage_blink", Hyprlang::INT{0}); - registerConfigVar("debug:vfr", Hyprlang::INT{1}); - registerConfigVar("debug:pass", Hyprlang::INT{0}); - registerConfigVar("debug:gl_debugging", Hyprlang::INT{0}); - registerConfigVar("debug:disable_logs", Hyprlang::INT{1}); - registerConfigVar("debug:disable_time", Hyprlang::INT{1}); - registerConfigVar("debug:enable_stdout_logs", Hyprlang::INT{0}); - registerConfigVar("debug:damage_tracking", {sc(Render::DAMAGE_TRACKING_FULL)}); - registerConfigVar("debug:manual_crash", Hyprlang::INT{0}); - registerConfigVar("debug:suppress_errors", Hyprlang::INT{0}); - registerConfigVar("debug:error_limit", Hyprlang::INT{5}); - registerConfigVar("debug:error_position", Hyprlang::INT{0}); - registerConfigVar("debug:disable_scale_checks", Hyprlang::INT{0}); - registerConfigVar("debug:colored_stdout_logs", Hyprlang::INT{1}); - registerConfigVar("debug:full_cm_proto", Hyprlang::INT{0}); - registerConfigVar("debug:ds_handle_same_buffer", Hyprlang::INT{1}); - registerConfigVar("debug:ds_handle_same_buffer_fifo", Hyprlang::INT{1}); - registerConfigVar("debug:fifo_pending_workaround", Hyprlang::INT{0}); - registerConfigVar("debug:render_solitary_wo_damage", Hyprlang::INT{0}); - registerConfigVar("debug:invalidate_fp16", Hyprlang::INT{2}); - - registerConfigVar("decoration:rounding", Hyprlang::INT{0}); - registerConfigVar("decoration:rounding_power", {2.F}); - registerConfigVar("decoration:blur:enabled", Hyprlang::INT{1}); - registerConfigVar("decoration:blur:size", Hyprlang::INT{8}); - registerConfigVar("decoration:blur:passes", Hyprlang::INT{1}); - registerConfigVar("decoration:blur:ignore_opacity", Hyprlang::INT{1}); - registerConfigVar("decoration:blur:new_optimizations", Hyprlang::INT{1}); - registerConfigVar("decoration:blur:xray", Hyprlang::INT{0}); - registerConfigVar("decoration:blur:contrast", {0.8916F}); - registerConfigVar("decoration:blur:brightness", {1.0F}); - registerConfigVar("decoration:blur:vibrancy", {0.1696F}); - registerConfigVar("decoration:blur:vibrancy_darkness", {0.0F}); - registerConfigVar("decoration:blur:noise", {0.0117F}); - registerConfigVar("decoration:blur:special", Hyprlang::INT{0}); - registerConfigVar("decoration:blur:popups", Hyprlang::INT{0}); - registerConfigVar("decoration:blur:popups_ignorealpha", {0.2F}); - registerConfigVar("decoration:blur:input_methods", Hyprlang::INT{0}); - registerConfigVar("decoration:blur:input_methods_ignorealpha", {0.2F}); - registerConfigVar("decoration:active_opacity", {1.F}); - registerConfigVar("decoration:inactive_opacity", {1.F}); - registerConfigVar("decoration:fullscreen_opacity", {1.F}); - registerConfigVar("decoration:shadow:enabled", Hyprlang::INT{1}); - registerConfigVar("decoration:shadow:range", Hyprlang::INT{4}); - registerConfigVar("decoration:shadow:render_power", Hyprlang::INT{3}); - registerConfigVar("decoration:shadow:offset", Hyprlang::VEC2{0, 0}); - registerConfigVar("decoration:shadow:scale", {1.f}); - registerConfigVar("decoration:shadow:sharp", Hyprlang::INT{0}); - registerConfigVar("decoration:shadow:color", Hyprlang::INT{0xee1a1a1a}); - registerConfigVar("decoration:shadow:color_inactive", Hyprlang::INT{-1}); - registerConfigVar("decoration:glow:enabled", Hyprlang::INT{0}); - registerConfigVar("decoration:glow:range", Hyprlang::INT{10}); - registerConfigVar("decoration:glow:render_power", Hyprlang::INT{3}); - registerConfigVar("decoration:glow:color", Hyprlang::INT{0xee33ccff}); - registerConfigVar("decoration:glow:color_inactive", Hyprlang::INT{0x0033ccff}); - registerConfigVar("decoration:dim_inactive", Hyprlang::INT{0}); - registerConfigVar("decoration:dim_modal", Hyprlang::INT{1}); - registerConfigVar("decoration:dim_strength", {0.5f}); - registerConfigVar("decoration:dim_special", {0.2f}); - registerConfigVar("decoration:dim_around", {0.4f}); - registerConfigVar("decoration:screen_shader", {STRVAL_EMPTY}); - registerConfigVar("decoration:border_part_of_window", Hyprlang::INT{1}); - - registerConfigVar("layout:single_window_aspect_ratio", Hyprlang::VEC2{0, 0}); - registerConfigVar("layout:single_window_aspect_ratio_tolerance", {0.1f}); - - registerConfigVar("dwindle:pseudotile", Hyprlang::INT{0}); - registerConfigVar("dwindle:force_split", Hyprlang::INT{0}); - registerConfigVar("dwindle:permanent_direction_override", Hyprlang::INT{0}); - registerConfigVar("dwindle:preserve_split", Hyprlang::INT{0}); - registerConfigVar("dwindle:special_scale_factor", {1.f}); - registerConfigVar("dwindle:split_width_multiplier", {1.0f}); - registerConfigVar("dwindle:use_active_for_splits", Hyprlang::INT{1}); - registerConfigVar("dwindle:default_split_ratio", {1.f}); - registerConfigVar("dwindle:split_bias", Hyprlang::INT{0}); - registerConfigVar("dwindle:smart_split", Hyprlang::INT{0}); - registerConfigVar("dwindle:smart_resizing", Hyprlang::INT{1}); - registerConfigVar("dwindle:precise_mouse_move", Hyprlang::INT{0}); - - registerConfigVar("master:special_scale_factor", {1.f}); - registerConfigVar("master:mfact", {0.55f}); - registerConfigVar("master:new_status", {"slave"}); - registerConfigVar("master:slave_count_for_center_master", Hyprlang::INT{2}); - registerConfigVar("master:center_master_fallback", {"left"}); - registerConfigVar("master:center_ignores_reserved", Hyprlang::INT{0}); - registerConfigVar("master:new_on_active", {"none"}); - registerConfigVar("master:new_on_top", Hyprlang::INT{0}); - registerConfigVar("master:orientation", {"left"}); - registerConfigVar("master:allow_small_split", Hyprlang::INT{0}); - registerConfigVar("master:smart_resizing", Hyprlang::INT{1}); - registerConfigVar("master:drop_at_cursor", Hyprlang::INT{1}); - registerConfigVar("master:always_keep_position", Hyprlang::INT{0}); - - registerConfigVar("scrolling:fullscreen_on_one_column", Hyprlang::INT{1}); - registerConfigVar("scrolling:column_width", Hyprlang::FLOAT{0.5F}); - registerConfigVar("scrolling:focus_fit_method", Hyprlang::INT{1}); - registerConfigVar("scrolling:follow_focus", Hyprlang::INT{1}); - registerConfigVar("scrolling:follow_min_visible", Hyprlang::FLOAT{0.4}); - registerConfigVar("scrolling:explicit_column_widths", Hyprlang::STRING{"0.333, 0.5, 0.667, 1.0"}); - registerConfigVar("scrolling:direction", Hyprlang::STRING{"right"}); - registerConfigVar("scrolling:wrap_focus", Hyprlang::INT{1}); - registerConfigVar("scrolling:wrap_swapcol", Hyprlang::INT{1}); - - registerConfigVar("animations:enabled", Hyprlang::INT{1}); - registerConfigVar("animations:workspace_wraparound", Hyprlang::INT{0}); - - registerConfigVar("input:follow_mouse", Hyprlang::INT{1}); - registerConfigVar("input:follow_mouse_shrink", Hyprlang::INT{0}); - registerConfigVar("input:follow_mouse_threshold", Hyprlang::FLOAT{0}); - registerConfigVar("input:focus_on_close", Hyprlang::INT{0}); - registerConfigVar("input:mouse_refocus", Hyprlang::INT{1}); - registerConfigVar("input:special_fallthrough", Hyprlang::INT{0}); - registerConfigVar("input:off_window_axis_events", Hyprlang::INT{1}); - registerConfigVar("input:sensitivity", {0.f}); - registerConfigVar("input:accel_profile", {STRVAL_EMPTY}); - registerConfigVar("input:rotation", Hyprlang::INT{0}); - registerConfigVar("input:kb_file", {STRVAL_EMPTY}); - registerConfigVar("input:kb_layout", {"us"}); - registerConfigVar("input:kb_variant", {STRVAL_EMPTY}); - registerConfigVar("input:kb_options", {STRVAL_EMPTY}); - registerConfigVar("input:kb_rules", {STRVAL_EMPTY}); - registerConfigVar("input:kb_model", {STRVAL_EMPTY}); - registerConfigVar("input:repeat_rate", Hyprlang::INT{25}); - registerConfigVar("input:repeat_delay", Hyprlang::INT{600}); - registerConfigVar("input:natural_scroll", Hyprlang::INT{0}); - registerConfigVar("input:numlock_by_default", Hyprlang::INT{0}); - registerConfigVar("input:resolve_binds_by_sym", Hyprlang::INT{0}); - registerConfigVar("input:force_no_accel", Hyprlang::INT{0}); - registerConfigVar("input:float_switch_override_focus", Hyprlang::INT{1}); - registerConfigVar("input:left_handed", Hyprlang::INT{0}); - registerConfigVar("input:scroll_method", {STRVAL_EMPTY}); - registerConfigVar("input:scroll_button", Hyprlang::INT{0}); - registerConfigVar("input:scroll_button_lock", Hyprlang::INT{0}); - registerConfigVar("input:scroll_factor", {1.f}); - registerConfigVar("input:scroll_points", {STRVAL_EMPTY}); - registerConfigVar("input:emulate_discrete_scroll", Hyprlang::INT{1}); - registerConfigVar("input:touchpad:natural_scroll", Hyprlang::INT{0}); - registerConfigVar("input:touchpad:disable_while_typing", Hyprlang::INT{1}); - registerConfigVar("input:touchpad:clickfinger_behavior", Hyprlang::INT{0}); - registerConfigVar("input:touchpad:tap_button_map", {STRVAL_EMPTY}); - registerConfigVar("input:touchpad:middle_button_emulation", Hyprlang::INT{0}); - registerConfigVar("input:touchpad:tap-to-click", Hyprlang::INT{1}); - registerConfigVar("input:touchpad:tap-and-drag", Hyprlang::INT{1}); - registerConfigVar("input:touchpad:drag_lock", Hyprlang::INT{0}); - registerConfigVar("input:touchpad:scroll_factor", {1.f}); - registerConfigVar("input:touchpad:flip_x", Hyprlang::INT{0}); - registerConfigVar("input:touchpad:flip_y", Hyprlang::INT{0}); - registerConfigVar("input:touchpad:drag_3fg", Hyprlang::INT{0}); - registerConfigVar("input:touchdevice:transform", Hyprlang::INT{-1}); - registerConfigVar("input:touchdevice:output", {"[[Auto]]"}); - registerConfigVar("input:touchdevice:enabled", Hyprlang::INT{1}); - registerConfigVar("input:virtualkeyboard:share_states", Hyprlang::INT{2}); - registerConfigVar("input:virtualkeyboard:release_pressed_on_close", Hyprlang::INT{0}); - registerConfigVar("input:tablet:transform", Hyprlang::INT{0}); - registerConfigVar("input:tablet:output", {STRVAL_EMPTY}); - registerConfigVar("input:tablet:region_position", Hyprlang::VEC2{0, 0}); - registerConfigVar("input:tablet:absolute_region_position", Hyprlang::INT{0}); - registerConfigVar("input:tablet:region_size", Hyprlang::VEC2{0, 0}); - registerConfigVar("input:tablet:relative_input", Hyprlang::INT{0}); - registerConfigVar("input:tablet:left_handed", Hyprlang::INT{0}); - registerConfigVar("input:tablet:active_area_position", Hyprlang::VEC2{0, 0}); - registerConfigVar("input:tablet:active_area_size", Hyprlang::VEC2{0, 0}); - - registerConfigVar("binds:pass_mouse_when_bound", Hyprlang::INT{0}); - registerConfigVar("binds:scroll_event_delay", Hyprlang::INT{300}); - registerConfigVar("binds:workspace_back_and_forth", Hyprlang::INT{0}); - registerConfigVar("binds:hide_special_on_workspace_change", Hyprlang::INT{0}); - registerConfigVar("binds:allow_workspace_cycles", Hyprlang::INT{0}); - registerConfigVar("binds:workspace_center_on", Hyprlang::INT{1}); - registerConfigVar("binds:focus_preferred_method", Hyprlang::INT{0}); - registerConfigVar("binds:ignore_group_lock", Hyprlang::INT{0}); - registerConfigVar("binds:movefocus_cycles_fullscreen", Hyprlang::INT{0}); - registerConfigVar("binds:movefocus_cycles_groupfirst", Hyprlang::INT{0}); - registerConfigVar("binds:disable_keybind_grabbing", Hyprlang::INT{0}); - registerConfigVar("binds:allow_pin_fullscreen", Hyprlang::INT{0}); - registerConfigVar("binds:drag_threshold", Hyprlang::INT{0}); - registerConfigVar("binds:window_direction_monitor_fallback", Hyprlang::INT{1}); - - registerConfigVar("gestures:workspace_swipe_distance", Hyprlang::INT{300}); - registerConfigVar("gestures:workspace_swipe_invert", Hyprlang::INT{1}); - registerConfigVar("gestures:workspace_swipe_min_speed_to_force", Hyprlang::INT{30}); - registerConfigVar("gestures:workspace_swipe_cancel_ratio", {0.5f}); - registerConfigVar("gestures:workspace_swipe_create_new", Hyprlang::INT{1}); - registerConfigVar("gestures:workspace_swipe_direction_lock", Hyprlang::INT{1}); - registerConfigVar("gestures:workspace_swipe_direction_lock_threshold", Hyprlang::INT{10}); - registerConfigVar("gestures:workspace_swipe_forever", Hyprlang::INT{0}); - registerConfigVar("gestures:workspace_swipe_use_r", Hyprlang::INT{0}); - registerConfigVar("gestures:workspace_swipe_touch", Hyprlang::INT{0}); - registerConfigVar("gestures:workspace_swipe_touch_invert", Hyprlang::INT{0}); - registerConfigVar("gestures:close_max_timeout", Hyprlang::INT{1000}); - - registerConfigVar("xwayland:enabled", Hyprlang::INT{1}); - registerConfigVar("xwayland:use_nearest_neighbor", Hyprlang::INT{1}); - registerConfigVar("xwayland:force_zero_scaling", Hyprlang::INT{0}); - registerConfigVar("xwayland:create_abstract_socket", Hyprlang::INT{0}); - - registerConfigVar("opengl:nvidia_anti_flicker", Hyprlang::INT{1}); - - registerConfigVar("cursor:invisible", Hyprlang::INT{0}); - registerConfigVar("cursor:no_hardware_cursors", Hyprlang::INT{2}); - registerConfigVar("cursor:no_break_fs_vrr", Hyprlang::INT{2}); - registerConfigVar("cursor:min_refresh_rate", Hyprlang::INT{24}); - registerConfigVar("cursor:hotspot_padding", Hyprlang::INT{0}); - registerConfigVar("cursor:inactive_timeout", {0.f}); - registerConfigVar("cursor:no_warps", Hyprlang::INT{0}); - registerConfigVar("cursor:persistent_warps", Hyprlang::INT{0}); - registerConfigVar("cursor:warp_on_change_workspace", Hyprlang::INT{0}); - registerConfigVar("cursor:warp_on_toggle_special", Hyprlang::INT{0}); - registerConfigVar("cursor:default_monitor", {STRVAL_EMPTY}); - registerConfigVar("cursor:zoom_factor", {1.f}); - registerConfigVar("cursor:zoom_rigid", Hyprlang::INT{0}); - registerConfigVar("cursor:zoom_disable_aa", Hyprlang::INT{0}); - registerConfigVar("cursor:zoom_detached_camera", Hyprlang::INT{1}); - registerConfigVar("cursor:enable_hyprcursor", Hyprlang::INT{1}); - registerConfigVar("cursor:sync_gsettings_theme", Hyprlang::INT{1}); - registerConfigVar("cursor:hide_on_key_press", Hyprlang::INT{0}); - registerConfigVar("cursor:hide_on_touch", Hyprlang::INT{1}); - registerConfigVar("cursor:hide_on_tablet", Hyprlang::INT{0}); - registerConfigVar("cursor:use_cpu_buffer", Hyprlang::INT{2}); - registerConfigVar("cursor:warp_back_after_non_mouse_input", Hyprlang::INT{0}); + if (auto p = dc(v.get())) + registerConfigVar(NAME, Hyprlang::INT{p->defaultVal()}); + else if (auto p = dc(v.get())) + registerConfigVar(NAME, Hyprlang::FLOAT{p->defaultVal()}); + else if (auto p = dc(v.get())) + registerConfigVar(NAME, Hyprlang::INT{p->defaultVal() ? 1 : 0}); + else if (auto p = dc(v.get())) + registerConfigVar(NAME, Hyprlang::STRING{p->defaultVal().c_str()}); + else if (auto p = dc(v.get())) + registerConfigVar(NAME, Hyprlang::INT{p->defaultVal()}); + else if (auto p = dc(v.get())) + registerConfigVar(NAME, Hyprlang::VEC2{p->defaultVal().x, p->defaultVal().y}); + else if (auto p = dc(v.get())) + registerConfigVar(NAME, Hyprlang::CConfigCustomValueType{configHandleGapSet, configHandleGapDestroy, std::to_string(p->defaultVal().m_top).c_str()}); + else if (auto p = dc(v.get())) + registerConfigVar(NAME, + Hyprlang::CConfigCustomValueType{&configHandleFontWeightSet, configHandleFontWeightDestroy, std::format("{}", p->defaultVal().m_value).c_str()}); + else if (auto p = dc(v.get())) + registerConfigVar(NAME, + Hyprlang::CConfigCustomValueType{&configHandleGradientSet, configHandleGradientDestroy, + std::format("0x{:x}", (int64_t)p->defaultVal().m_colors.begin()->getAsHex()).c_str()}); + else + RASSERT(false, "legacy cfg: bad value {}", NAME); + } registerConfigVar("autogenerated", Hyprlang::INT{0}); - - registerConfigVar("group:col.border_active", Hyprlang::CConfigCustomValueType{&configHandleGradientSet, configHandleGradientDestroy, "0x66ffff00"}); - registerConfigVar("group:col.border_inactive", Hyprlang::CConfigCustomValueType{&configHandleGradientSet, configHandleGradientDestroy, "0x66777700"}); - registerConfigVar("group:col.border_locked_active", Hyprlang::CConfigCustomValueType{&configHandleGradientSet, configHandleGradientDestroy, "0x66ff5500"}); - registerConfigVar("group:col.border_locked_inactive", Hyprlang::CConfigCustomValueType{&configHandleGradientSet, configHandleGradientDestroy, "0x66775500"}); - - registerConfigVar("group:groupbar:col.active", Hyprlang::CConfigCustomValueType{&configHandleGradientSet, configHandleGradientDestroy, "0x66ffff00"}); - registerConfigVar("group:groupbar:col.inactive", Hyprlang::CConfigCustomValueType{&configHandleGradientSet, configHandleGradientDestroy, "0x66777700"}); - registerConfigVar("group:groupbar:col.locked_active", Hyprlang::CConfigCustomValueType{&configHandleGradientSet, configHandleGradientDestroy, "0x66ff5500"}); - registerConfigVar("group:groupbar:col.locked_inactive", Hyprlang::CConfigCustomValueType{&configHandleGradientSet, configHandleGradientDestroy, "0x66775500"}); - - registerConfigVar("render:direct_scanout", Hyprlang::INT{0}); - registerConfigVar("render:expand_undersized_textures", Hyprlang::INT{1}); - registerConfigVar("render:xp_mode", Hyprlang::INT{0}); - registerConfigVar("render:ctm_animation", Hyprlang::INT{2}); - registerConfigVar("render:cm_enabled", Hyprlang::INT{1}); - registerConfigVar("render:send_content_type", Hyprlang::INT{1}); - registerConfigVar("render:cm_auto_hdr", Hyprlang::INT{1}); - registerConfigVar("render:new_render_scheduling", Hyprlang::INT{0}); - registerConfigVar("render:non_shader_cm", Hyprlang::INT{2}); - registerConfigVar("render:non_shader_cm_interop", Hyprlang::INT{2}); - registerConfigVar("render:cm_sdr_eotf", {"default"}); - registerConfigVar("render:commit_timing_enabled", Hyprlang::INT{1}); - registerConfigVar("render:icc_vcgt_enabled", Hyprlang::INT{1}); - registerConfigVar("render:use_shader_blur_blend", Hyprlang::INT{0}); - registerConfigVar("render:use_fp16", Hyprlang::INT{2}); - registerConfigVar("render:keep_unmodified_copy", Hyprlang::INT{2}); - - registerConfigVar("ecosystem:no_update_news", Hyprlang::INT{0}); - registerConfigVar("ecosystem:no_donation_nag", Hyprlang::INT{0}); - registerConfigVar("ecosystem:enforce_permissions", Hyprlang::INT{0}); - - registerConfigVar("experimental:wp_cm_1_2", Hyprlang::INT{0}); - - registerConfigVar("quirks:prefer_hdr", Hyprlang::INT{0}); - registerConfigVar("quirks:skip_non_kms_dmabuf_formats", Hyprlang::INT{0}); - // devices m_config->addSpecialCategory("device", {"name"}); m_config->addSpecialConfigValue("device", "sensitivity", {0.F}); @@ -946,15 +621,11 @@ CConfigManager::CConfigManager() { resetHLConfig(); - if (Config::Supplementary::CONFIG_OPTIONS.size() != m_configValueNumber - 1 /* autogenerated is special */) - Log::logger->log(Log::DEBUG, "Warning: config descriptions have {} entries, but there are {} config values. This should fail tests!!", - Config::Supplementary::CONFIG_OPTIONS.size(), m_configValueNumber); - if (!g_pCompositor->m_onlyConfigVerification) { Log::logger->log( Log::DEBUG, "!!!!HEY YOU, YES YOU!!!!: further logs to stdout / logfile are disabled by default. BEFORE SENDING THIS LOG, ENABLE THEM. Use debug:disable_logs = false to do so: " - "https://wiki.hypr.land/Configuring/Variables/#debug"); + "https://wiki.hypr.land/Configuring/Basics/Variables/#debug"); } if (g_pEventLoopManager && ERR.has_value()) @@ -992,13 +663,13 @@ std::optional CConfigManager::verifyConfigExists() { return "Broken config directory"; std::error_code ec; - const bool VALID_CFG = std::filesystem::exists(*mainConfigPath, ec) && !ec; + const bool VALID_CFG = std::filesystem::exists(mainConfigPath->path, ec) && !ec; if (!VALID_CFG && !g_pCompositor->m_explicitConfigPath.empty()) return "Invalid config file provided as explicit"; if (!VALID_CFG) { - if (const auto res = generateDefaultConfig(*mainConfigPath, g_pCompositor->m_safeMode); !res) + if (const auto res = generateDefaultConfig(mainConfigPath->path, g_pCompositor->m_safeMode); !res) return res.error(); } @@ -1053,7 +724,7 @@ void CConfigManager::reload() { auto oldConfigPath = m_mainConfigPath; - m_mainConfigPath = *Supplementary::Jeremy::getMainConfigPath(); + m_mainConfigPath = Supplementary::Jeremy::getMainConfigPath()->path; m_configCurrentPath = m_mainConfigPath; if (m_mainConfigPath != oldConfigPath) @@ -1074,7 +745,7 @@ void CConfigManager::reload() { std::string CConfigManager::verify() { Config::animationTree()->reset(); resetHLConfig(); - m_configCurrentPath = *Supplementary::Jeremy::getMainConfigPath(); + m_configCurrentPath = Supplementary::Jeremy::getMainConfigPath()->path; const auto ERR = m_config->parse(); m_lastConfigVerificationWasSuccessful = !ERR.error; if (ERR.error) @@ -1224,8 +895,11 @@ std::optional CConfigManager::addRuleFromConfigKey(const std::strin for (const auto& e : Desktop::Rule::windowEffects()->allEffectStrings()) { auto VAL = m_config->getSpecialConfigValuePtr("windowrule", e.c_str(), name.c_str()); - if (VAL && VAL->m_bSetByUser) - rule->addEffect(Desktop::Rule::windowEffects()->get(e).value_or(Desktop::Rule::WINDOW_RULE_EFFECT_NONE), std::any_cast(VAL->getValue())); + if (VAL && VAL->m_bSetByUser) { + auto res = rule->addEffect(Desktop::Rule::windowEffects()->get(e).value_or(Desktop::Rule::WINDOW_RULE_EFFECT_NONE), std::any_cast(VAL->getValue())); + if (!res) + return res.error(); + } } Desktop::Rule::ruleEngine()->registerRule(std::move(rule)); @@ -1248,8 +922,11 @@ std::optional CConfigManager::addLayerRuleFromConfigKey(const std:: for (const auto& e : Desktop::Rule::layerEffects()->allEffectStrings()) { auto VAL = m_config->getSpecialConfigValuePtr("layerrule", e.c_str(), name.c_str()); - if (VAL && VAL->m_bSetByUser) - rule->addEffect(Desktop::Rule::layerEffects()->get(e).value_or(Desktop::Rule::LAYER_RULE_EFFECT_NONE), std::any_cast(VAL->getValue())); + if (VAL && VAL->m_bSetByUser) { + auto res = rule->addEffect(Desktop::Rule::layerEffects()->get(e).value_or(Desktop::Rule::LAYER_RULE_EFFECT_NONE), std::any_cast(VAL->getValue())); + if (!res) + return res.error(); + } } Desktop::Rule::ruleEngine()->registerRule(std::move(rule)); @@ -1482,7 +1159,7 @@ SConfigOptionReply CConfigManager::getConfigValue(const std::string& val) { if (!VAL) return {}; - return {.dataptr = VAL->getDataStaticPtr(), .type = &VAL->getValue().type()}; + return {.dataptr = VAL->getDataStaticPtr(), .type = &VAL->getValue().type(), .setByUser = VAL->m_bSetByUser}; } Hyprlang::CConfigValue* CConfigManager::getHyprlangConfigValuePtr(const std::string& name, const std::string& specialCat) { @@ -1554,7 +1231,7 @@ std::optional CConfigManager::handleRawExec(const std::string& comm return {}; } - g_pKeybindManager->spawnRaw(args); + Config::Supplementary::executor()->spawnRaw(args); return {}; } @@ -1564,7 +1241,7 @@ std::optional CConfigManager::handleExec(const std::string& command return {}; } - g_pKeybindManager->spawn(args); + Config::Supplementary::executor()->spawn(args); return {}; } @@ -1584,7 +1261,7 @@ std::optional CConfigManager::handleExecRawOnce(const std::string& std::optional CConfigManager::handleExecShutdown(const std::string& command, const std::string& args) { if (g_pCompositor->m_finalRequests) { - g_pKeybindManager->spawn(args); + Config::Supplementary::executor()->spawn(args); return {}; } @@ -1801,6 +1478,7 @@ std::optional CConfigManager::handleBind(const std::string& command bool repeat = false; bool mouse = false; bool nonConsuming = false; + bool autoConsuming = false; bool transparent = false; bool ignoreMods = false; bool multiKey = false; @@ -1820,6 +1498,7 @@ std::optional CConfigManager::handleBind(const std::string& command case 'e': repeat = true; break; case 'm': mouse = true; break; case 'n': nonConsuming = true; break; + case 'a': autoConsuming = true; break; case 't': transparent = true; break; case 'i': ignoreMods = true; break; case 's': multiKey = true; break; @@ -1859,15 +1538,15 @@ std::optional CConfigManager::handleBind(const std::string& command else if ((ARGS.size() > sc(4) + DESCR_OFFSET + DEVICE_OFFSET && !mouse) || (ARGS.size() > sc(3) + DESCR_OFFSET + DEVICE_OFFSET && mouse)) return "bind: too many args"; - std::set KEYSYMS; - std::set MODS; + std::vector KEYSYMS; + std::vector MODS; if (multiKey) { for (const auto& splitKey : CVarList(ARGS[1], 8, '&')) { - KEYSYMS.insert(xkb_keysym_from_name(splitKey.c_str(), XKB_KEYSYM_CASE_INSENSITIVE)); + KEYSYMS.emplace_back(xkb_keysym_from_name(splitKey.c_str(), XKB_KEYSYM_CASE_INSENSITIVE)); } for (const auto& splitMod : CVarList(ARGS[0], 8, '&')) { - MODS.insert(xkb_keysym_from_name(splitMod.c_str(), XKB_KEYSYM_CASE_INSENSITIVE)); + MODS.emplace_back(xkb_keysym_from_name(splitMod.c_str(), XKB_KEYSYM_CASE_INSENSITIVE)); } } const auto MOD = g_pKeybindManager->stringToModMask(ARGS[0]); @@ -1919,10 +1598,33 @@ std::optional CConfigManager::handleBind(const std::string& command return "Invalid catchall, catchall keybinds are only allowed in submaps."; } - g_pKeybindManager->addKeybind(SKeybind{parsedKey.key, KEYSYMS, parsedKey.keycode, parsedKey.catchAll, MOD, MODS, HANDLER, - COMMAND, locked, m_currentSubmap, DESCRIPTION, release, repeat, longPress, - mouse, nonConsuming, transparent, ignoreMods, multiKey, hasDescription, dontInhibit, - click, drag, submapUniversal, deviceInclusive, devices}); + g_pKeybindManager->addKeybind(SKeybind{parsedKey.key, + KEYSYMS, + parsedKey.keycode, + parsedKey.catchAll, + MOD, + MODS, + HANDLER, + COMMAND, + locked, + m_currentSubmap, + DESCRIPTION, + release, + repeat, + longPress, + mouse, + nonConsuming, + autoConsuming, + transparent, + ignoreMods, + multiKey, + hasDescription, + dontInhibit, + click, + drag, + submapUniversal, + deviceInclusive, + devices}); } return {}; @@ -2310,7 +2012,9 @@ std::optional CConfigManager::handleWindowrule(const std::string& c const auto EFFECT = Desktop::Rule::windowEffects()->get(FIRST); if (!EFFECT.has_value()) return std::format("invalid effect {}", el); - rule->addEffect(*EFFECT, std::string{el.substr(spacePos + 1)}); + auto res = rule->addEffect(*EFFECT, std::string{el.substr(spacePos + 1)}); + if (!res) + return res.error(); } else return std::format("invalid field type {}", FIRST); } @@ -2349,7 +2053,9 @@ std::optional CConfigManager::handleLayerrule(const std::string& co const auto EFFECT = Desktop::Rule::layerEffects()->get(FIRST); if (!EFFECT.has_value()) return std::format("invalid effect {}", el); - rule->addEffect(*EFFECT, std::string{el.substr(spacePos + 1)}); + auto res = rule->addEffect(*EFFECT, std::string{el.substr(spacePos + 1)}); + if (!res) + return res.error(); } else return std::format("invalid field type {}", FIRST); } @@ -2412,3 +2118,40 @@ std::string CConfigManager::getMainConfigPath() { std::string CConfigManager::currentConfigPath() { return m_configCurrentPath; } + +void CConfigManager::onPluginUnload(void* handle) { + removePluginConfig(handle); +} + +std::expected CConfigManager::registerPluginValue(void* handle, SP value) { + const std::string NAME = value->name(); + + if (!NAME.starts_with("plugin:")) + return std::unexpected("name must start with plugin:"); + + if (auto p = dc(value.get())) + addPluginConfigVar(handle, NAME, Hyprlang::INT{p->defaultVal()}); + else if (auto p = dc(value.get())) + addPluginConfigVar(handle, NAME, Hyprlang::FLOAT{p->defaultVal()}); + else if (auto p = dc(value.get())) + addPluginConfigVar(handle, NAME, Hyprlang::INT{p->defaultVal() ? 1 : 0}); + else if (auto p = dc(value.get())) + addPluginConfigVar(handle, NAME, Hyprlang::STRING{p->defaultVal().c_str()}); + else if (auto p = dc(value.get())) + addPluginConfigVar(handle, NAME, Hyprlang::INT{p->defaultVal()}); + else if (auto p = dc(value.get())) + addPluginConfigVar(handle, NAME, Hyprlang::VEC2{p->defaultVal().x, p->defaultVal().y}); + else if (auto p = dc(value.get())) + addPluginConfigVar(handle, NAME, Hyprlang::CConfigCustomValueType{configHandleGapSet, configHandleGapDestroy, std::to_string(p->defaultVal().m_top).c_str()}); + else if (auto p = dc(value.get())) + addPluginConfigVar(handle, NAME, + Hyprlang::CConfigCustomValueType{&configHandleFontWeightSet, configHandleFontWeightDestroy, std::format("{}", p->defaultVal().m_value).c_str()}); + else if (auto p = dc(value.get())) + addPluginConfigVar(handle, NAME, + Hyprlang::CConfigCustomValueType{&configHandleGradientSet, configHandleGradientDestroy, + std::format("{:x}", (int64_t)p->defaultVal().m_colors.begin()->getAsHex()).c_str()}); + else + return std::unexpected("unknown value type"); + + return {}; +} diff --git a/src/config/legacy/ConfigManager.hpp b/src/config/legacy/ConfigManager.hpp index 30251a339..def48408e 100644 --- a/src/config/legacy/ConfigManager.hpp +++ b/src/config/legacy/ConfigManager.hpp @@ -66,6 +66,9 @@ namespace Config::Legacy { virtual void handlePluginLoads() override; virtual bool configVerifPassed() override; + virtual std::expected registerPluginValue(void* handle, SP value) override; + virtual void onPluginUnload(void* handle) override; + void addPluginConfigVar(HANDLE handle, const std::string& name, const Hyprlang::CConfigValue& value); void addPluginKeyword(HANDLE handle, const std::string& name, Hyprlang::PCONFIGHANDLERFUNC fun, Hyprlang::SHandlerOptions opts = {}); void removePluginConfig(HANDLE handle); diff --git a/src/config/defaultConfig.hpp b/src/config/legacy/DefaultConfig.hpp similarity index 90% rename from src/config/defaultConfig.hpp rename to src/config/legacy/DefaultConfig.hpp index 8bdfea39d..dea4ec516 100644 --- a/src/config/defaultConfig.hpp +++ b/src/config/legacy/DefaultConfig.hpp @@ -1,6 +1,6 @@ #pragma once -#include +#include inline constexpr std::string_view AUTOGENERATED_PREFIX = R"#( # ####################################################################################### @@ -12,7 +12,7 @@ autogenerated = 1 # remove this line to remove the warning )#"; inline constexpr char EXAMPLE_CONFIG_BYTES[] = { -#embed "../../example/hyprland.conf" +#embed "../../../example/hyprland.conf" }; inline constexpr std::string_view EXAMPLE_CONFIG = {EXAMPLE_CONFIG_BYTES, sizeof(EXAMPLE_CONFIG_BYTES)}; diff --git a/src/config/legacy/DispatcherTranslator.cpp b/src/config/legacy/DispatcherTranslator.cpp new file mode 100644 index 000000000..0521f1618 --- /dev/null +++ b/src/config/legacy/DispatcherTranslator.cpp @@ -0,0 +1,859 @@ +#include "DispatcherTranslator.hpp" + +#include "../shared/actions/ConfigActions.hpp" +#include "../supplementary/executor/Executor.hpp" +#include "../../Compositor.hpp" +#include "../../desktop/state/FocusState.hpp" +#include "../../helpers/Monitor.hpp" +#include "../../desktop/view/Group.hpp" +#include "../../managers/KeybindManager.hpp" +#include "../../managers/SeatManager.hpp" +#include "../../managers/input/InputManager.hpp" +#include "../../layout/LayoutManager.hpp" + +#include +#include +using namespace Hyprutils::String; + +using namespace Config; +using namespace Config::Legacy; +using namespace Config::Actions; + +UP& Legacy::translator() { + static UP p = makeUnique(); + return p; +} + +SDispatchResult CDispatcherTranslator::run(const std::string& d, const std::string& w) { + if (!m_dispMap.contains(d)) + return {.success = false, .error = "Bad dispatcher"}; + + return m_dispMap.at(d)(w); +} + +// helper: convert ActionResult to SDispatchResult +static SDispatchResult wrap(ActionResult res) { + if (!res) + return {.success = false, .error = res.error().message}; + return {.passEvent = res->passEvent}; +} + +// helper: resolve window from regex string, or focused if empty/active +static std::optional windowFromArg(const std::string& arg) { + if (arg.empty() || arg == "active") + return std::nullopt; // will use xtract(nullopt) -> focused window + return g_pCompositor->getWindowByRegex(arg); +} + +// helper: resolve workspace from string and optionally create it +static PHLWORKSPACE resolveWorkspace(const std::string& args) { + const auto& [id, name, isAutoID] = getWorkspaceIDNameFromString(args); + if (id == WORKSPACE_INVALID) + return nullptr; + auto ws = g_pCompositor->getWorkspaceByID(id); + if (!ws) { + const auto PMONITOR = Desktop::focusState()->monitor(); + if (PMONITOR) + ws = g_pCompositor->createNewWorkspace(id, PMONITOR->m_id, name, false); + } + return ws; +} + +static SDispatchResult exec(const std::string& args) { + const auto PROC = Config::Supplementary::executor()->spawn(args); + if (!PROC.has_value()) + return {.success = false, .error = std::format("Failed to start process. No closing bracket in exec rule. {}", args)}; + return {.success = PROC.value() > 0, .error = std::format("Failed to start process {}", args)}; +} + +static SDispatchResult execr(const std::string& args) { + const auto PROC = Config::Supplementary::executor()->spawnRaw(args); + return {.success = PROC && *PROC > 0, .error = std::format("Failed to start process {}", args)}; +} + +static SDispatchResult killactive(const std::string&) { + return wrap(Actions::closeWindow()); +} + +static SDispatchResult forcekillactive(const std::string&) { + return wrap(Actions::killWindow()); +} + +static SDispatchResult closewindow(const std::string& data) { + return wrap(Actions::closeWindow(g_pCompositor->getWindowByRegex(data))); +} + +static SDispatchResult killwindow(const std::string& data) { + return wrap(Actions::killWindow(g_pCompositor->getWindowByRegex(data))); +} + +static SDispatchResult signalactive(const std::string& args) { + if (!isNumber(args)) + return {.success = false, .error = "signalActive: signal has to be int"}; + try { + return wrap(Actions::signalWindow(std::stoi(args))); + } catch (...) { return {.success = false, .error = "signalActive: invalid signal format"}; } +} + +static SDispatchResult signalwindow(const std::string& args) { + const auto WINDOWREGEX = args.substr(0, args.find_first_of(',')); + const auto SIGNAL = args.substr(args.find_first_of(',') + 1); + + const auto PWINDOW = g_pCompositor->getWindowByRegex(WINDOWREGEX); + if (!PWINDOW) + return {.success = false, .error = "signalWindow: no window"}; + + if (!isNumber(SIGNAL)) + return {.success = false, .error = "signalWindow: signal has to be int"}; + + try { + return wrap(Actions::signalWindow(std::stoi(SIGNAL), PWINDOW)); + } catch (...) { return {.success = false, .error = "signalWindow: invalid signal format"}; } +} + +static SDispatchResult togglefloating(const std::string& args) { + auto w = windowFromArg(args); + return wrap(Actions::floatWindow(TOGGLE_ACTION_TOGGLE, w)); +} + +static SDispatchResult setfloating(const std::string& args) { + auto w = windowFromArg(args); + return wrap(Actions::floatWindow(TOGGLE_ACTION_ENABLE, w)); +} + +static SDispatchResult settiled(const std::string& args) { + auto w = windowFromArg(args); + return wrap(Actions::floatWindow(TOGGLE_ACTION_DISABLE, w)); +} + +static SDispatchResult pseudo(const std::string& args) { + auto w = windowFromArg(args); + return wrap(Actions::pseudoWindow(TOGGLE_ACTION_TOGGLE, w)); +} + +static SDispatchResult workspace(const std::string& args) { + return wrap(Actions::changeWorkspace(args)); +} + +static SDispatchResult renameworkspace(const std::string& args) { + try { + const auto FIRSTSPACEPOS = args.find_first_of(' '); + if (FIRSTSPACEPOS != std::string::npos) { + int wsid = std::stoi(args.substr(0, FIRSTSPACEPOS)); + std::string name = args.substr(FIRSTSPACEPOS + 1); + const auto PWS = g_pCompositor->getWorkspaceByID(wsid); + if (!PWS) + return {.success = false, .error = "No such workspace"}; + return wrap(Actions::renameWorkspace(PWS, name)); + } else { + const auto PWS = g_pCompositor->getWorkspaceByID(std::stoi(args)); + if (!PWS) + return {.success = false, .error = "No such workspace"}; + return wrap(Actions::renameWorkspace(PWS, "")); + } + } catch (std::exception& e) { return {.success = false, .error = std::format("Invalid arg in renameWorkspace: {}", e.what())}; } +} + +static SDispatchResult fullscreen(const std::string& args) { + CVarList2 ARGS(args, 2, ' '); + + const eFullscreenMode MODE = ARGS.size() > 0 && ARGS[0] == "1" ? FSMODE_MAXIMIZED : FSMODE_FULLSCREEN; + + if (ARGS.size() <= 1 || ARGS[1] == "toggle") + return wrap(Actions::fullscreenWindow(MODE)); + + // "set" means enable, "unset" means disable - but the Action toggles. + // We need to check current state ourselves. + const auto PWINDOW = Desktop::focusState()->window(); + if (!PWINDOW) + return {.success = false, .error = "Window not found"}; + + if (ARGS[1] == "set") { + if (!PWINDOW->isEffectiveInternalFSMode(MODE)) + return wrap(Actions::fullscreenWindow(MODE)); + return {}; + } else if (ARGS[1] == "unset") { + if (PWINDOW->isEffectiveInternalFSMode(MODE)) + return wrap(Actions::fullscreenWindow(MODE)); + return {}; + } + + return {}; +} + +static SDispatchResult fullscreenstate(const std::string& args) { + CVarList2 ARGS(args, 3, ' '); + + const auto PWINDOW = Desktop::focusState()->window(); + if (!PWINDOW) + return {.success = false, .error = "Window not found"}; + + int internalMode, clientMode; + try { + internalMode = std::stoi(std::string(ARGS[0])); + } catch (...) { internalMode = -1; } + try { + clientMode = std::stoi(std::string(ARGS[1])); + } catch (...) { clientMode = -1; } + + eFullscreenMode im = internalMode != -1 ? sc(internalMode) : PWINDOW->m_fullscreenState.internal; + eFullscreenMode cm = clientMode != -1 ? sc(clientMode) : PWINDOW->m_fullscreenState.client; + + return wrap(Actions::fullscreenWindow(im, cm)); +} + +static SDispatchResult movetoworkspace(const std::string& args) { + PHLWINDOW PWINDOW = Desktop::focusState()->window(); + std::string wsArgs = args; + + if (args.contains(',')) { + PWINDOW = g_pCompositor->getWindowByRegex(args.substr(args.find_last_of(',') + 1)); + wsArgs = args.substr(0, args.find_last_of(',')); + } + + auto ws = resolveWorkspace(wsArgs); + if (!ws) + return {.success = false, .error = "Invalid workspace"}; + + return wrap(Actions::moveToWorkspace(ws, false, PWINDOW)); +} + +static SDispatchResult movetoworkspacesilent(const std::string& args) { + PHLWINDOW PWINDOW = Desktop::focusState()->window(); + std::string wsArgs = args; + + if (args.contains(',')) { + PWINDOW = g_pCompositor->getWindowByRegex(args.substr(args.find_last_of(',') + 1)); + wsArgs = args.substr(0, args.find_last_of(',')); + } + + auto ws = resolveWorkspace(wsArgs); + if (!ws) + return {.success = false, .error = "Invalid workspace"}; + + return wrap(Actions::moveToWorkspace(ws, true, PWINDOW)); +} + +static SDispatchResult movefocus(const std::string& args) { + Math::eDirection dir = Math::fromChar(args[0]); + if (dir == Math::DIRECTION_DEFAULT) + return {.success = false, .error = std::format("Unsupported direction: {}", args[0])}; + return wrap(Actions::moveFocus(dir)); +} + +static SDispatchResult movewindow(const std::string& args) { + // "movewindow" dispatcher handles both "mon:" and directional moves. + // For mon: prefix, it delegates to movetoworkspace. + bool silent = args.ends_with(" silent"); + auto cleanArgs = silent ? args.substr(0, args.length() - 7) : args; + + if (cleanArgs.starts_with("mon:")) { + const auto PNEWMONITOR = g_pCompositor->getMonitorFromString(cleanArgs.substr(4)); + if (!PNEWMONITOR) + return {.success = false, .error = std::format("Monitor {} not found", cleanArgs.substr(4))}; + + auto ws = PNEWMONITOR->m_activeWorkspace; + return wrap(Actions::moveToWorkspace(ws, silent)); + } + + Math::eDirection dir = Math::fromChar(cleanArgs[0]); + if (dir == Math::DIRECTION_DEFAULT) + return {.success = false, .error = std::format("Unsupported direction: {}", cleanArgs[0])}; + + return wrap(Actions::moveInDirection(dir)); +} + +static SDispatchResult swapwindow(const std::string& args) { + if (isDirection(args)) + return wrap(Actions::swapInDirection(Math::fromChar(args[0]))); + + // regex-based swap: resolve window and use swapInDirection? No - the old code used getWindowByRegex + switchTargets. + // The new Actions don't have a "swap with specific window" variant. + // Fall through to the old swapActive logic via getWindowByRegex + layout switchTargets. + const auto PLASTWINDOW = Desktop::focusState()->window(); + if (!PLASTWINDOW) + return {.success = false, .error = "Window to swap with not found"}; + if (PLASTWINDOW->isFullscreen()) + return {.success = false, .error = "Can't swap fullscreen window"}; + + const auto PWINDOWTOCHANGETO = g_pCompositor->getWindowByRegex(args); + if (!PWINDOWTOCHANGETO || PWINDOWTOCHANGETO == PLASTWINDOW) + return {.success = false, .error = std::format("Can't swap with {}, invalid window", args)}; + + g_layoutManager->switchTargets(PLASTWINDOW->layoutTarget(), PWINDOWTOCHANGETO->layoutTarget(), true); + PLASTWINDOW->warpCursor(); + return {}; +} + +static SDispatchResult centerwindow(const std::string&) { + return wrap(Actions::center()); +} + +static SDispatchResult togglegroup(const std::string&) { + return wrap(Actions::toggleGroup()); +} + +static SDispatchResult changegroupactive(const std::string& args) { + bool forward = !(args == "b" || args == "prev"); + + // index-based change + if (isNumber(args, false)) { + const auto PWINDOW = Desktop::focusState()->window(); + if (!PWINDOW) + return {.success = false, .error = "No window found"}; + if (!PWINDOW->m_group) + return {.success = false, .error = "No group"}; + if (PWINDOW->m_group->size() == 1) + return {.success = false, .error = "Only one window in group"}; + try { + const int INDEX = std::stoi(args); + if (INDEX <= 0) + PWINDOW->m_group->setCurrent(PWINDOW->m_group->size() - 1); + else + PWINDOW->m_group->setCurrent(INDEX - 1); + } catch (...) { return {.success = false, .error = "invalid idx"}; } + return {}; + } + + return wrap(Actions::changeGroupActive(forward)); +} + +static SDispatchResult movegroupwindow(const std::string& args) { + return wrap(Actions::moveGroupWindow(!(args == "b" || args == "prev"))); +} + +static SDispatchResult focusmonitor(const std::string& args) { + const auto PMONITOR = g_pCompositor->getMonitorFromString(args); + if (!PMONITOR) + return {.success = false, .error = "Monitor not found"}; + return wrap(Actions::focusMonitor(PMONITOR)); +} + +static SDispatchResult movecursortocorner(const std::string& args) { + if (!isNumber(args)) + return {.success = false, .error = "moveCursorToCorner, arg has to be a number"}; + return wrap(Actions::moveCursorToCorner(std::stoi(args))); +} + +static SDispatchResult movecursor(const std::string& args) { + size_t i = args.find_first_of(' '); + if (i == std::string::npos) + return {.success = false, .error = "moveCursor takes 2 arguments"}; + + auto x_str = args.substr(0, i); + auto y_str = args.substr(i + 1); + + if (!isNumber(x_str) || !isNumber(y_str)) + return {.success = false, .error = "moveCursor arguments must be numbers"}; + + return wrap(Actions::moveCursor({std::stoi(x_str), std::stoi(y_str)})); +} + +static SDispatchResult workspaceopt(const std::string&) { + return {.success = false, .error = "workspaceopt is deprecated"}; +} + +static SDispatchResult exitHyprland(const std::string&) { + return wrap(Actions::exit()); +} + +static SDispatchResult movecurrentworkspacetomonitor(const std::string& args) { + const auto PMONITOR = g_pCompositor->getMonitorFromString(args); + if (!PMONITOR) + return {.success = false, .error = "Monitor not found"}; + + const auto PCURRENTWORKSPACE = Desktop::focusState()->monitor()->m_activeWorkspace; + if (!PCURRENTWORKSPACE) + return {.success = false, .error = "Invalid workspace"}; + + return wrap(Actions::moveToMonitor(PCURRENTWORKSPACE, PMONITOR)); +} + +static SDispatchResult moveworkspacetomonitor(const std::string& args) { + if (!args.contains(' ')) + return {.success = false, .error = "Expected: workspace monitor"}; + + std::string wsStr = args.substr(0, args.find_first_of(' ')); + std::string monStr = args.substr(args.find_first_of(' ') + 1); + + const auto PMONITOR = g_pCompositor->getMonitorFromString(monStr); + if (!PMONITOR) + return {.success = false, .error = "Monitor not found"}; + + const auto WORKSPACEID = getWorkspaceIDNameFromString(wsStr).id; + if (WORKSPACEID == WORKSPACE_INVALID) + return {.success = false, .error = "Invalid workspace"}; + + const auto PWORKSPACE = g_pCompositor->getWorkspaceByID(WORKSPACEID); + if (!PWORKSPACE) + return {.success = false, .error = "Workspace not found"}; + + return wrap(Actions::moveToMonitor(PWORKSPACE, PMONITOR)); +} + +static SDispatchResult focusworkspaceoncurrentmonitor(const std::string& args) { + auto ws = resolveWorkspace(args); + if (!ws) + return {.success = false, .error = "Invalid workspace"}; + return wrap(Actions::changeWorkspaceOnCurrentMonitor(ws)); +} + +static SDispatchResult togglespecialworkspace(const std::string& args) { + const auto& [workspaceID, workspaceName, isAutoID] = getWorkspaceIDNameFromString("special:" + args); + if (workspaceID == WORKSPACE_INVALID || !g_pCompositor->isWorkspaceSpecial(workspaceID)) + return {.success = false, .error = "Invalid special workspace"}; + + auto ws = g_pCompositor->getWorkspaceByID(workspaceID); + if (!ws) { + const auto PMONITOR = Desktop::focusState()->monitor(); + if (PMONITOR) + ws = g_pCompositor->createNewWorkspace(workspaceID, PMONITOR->m_id, workspaceName); + } + + if (!ws) + return {.success = false, .error = "Could not resolve special workspace"}; + + return wrap(Actions::toggleSpecial(ws)); +} + +static SDispatchResult forcerendererreload(const std::string&) { + return wrap(Actions::forceRendererReload()); +} + +static SDispatchResult resizeactive(const std::string& args) { + const auto PWINDOW = Desktop::focusState()->window(); + if (!PWINDOW) + return {.success = false, .error = "No window found"}; + + const auto SIZ = g_pCompositor->parseWindowVectorArgsRelative(args, PWINDOW->m_realSize->goal()); + if (SIZ.x < 1 || SIZ.y < 1) + return {.success = false, .error = "Invalid size"}; + + return wrap(Actions::resize(SIZ)); +} + +static SDispatchResult moveactive(const std::string& args) { + const auto PWINDOW = Desktop::focusState()->window(); + if (!PWINDOW) + return {.success = false, .error = "No window found"}; + + const auto POS = g_pCompositor->parseWindowVectorArgsRelative(args, PWINDOW->m_realPosition->goal()); + return wrap(Actions::move(POS)); +} + +static SDispatchResult movewindowpixel(const std::string& args) { + const auto WINDOWREGEX = args.substr(args.find_first_of(',') + 1); + const auto MOVECMD = args.substr(0, args.find_first_of(',')); + + const auto PWINDOW = g_pCompositor->getWindowByRegex(WINDOWREGEX); + if (!PWINDOW) + return {.success = false, .error = "moveWindow: no window"}; + + const auto POS = g_pCompositor->parseWindowVectorArgsRelative(MOVECMD, PWINDOW->m_realPosition->goal()); + return wrap(Actions::move(POS, false, PWINDOW)); +} + +static SDispatchResult resizewindowpixel(const std::string& args) { + const auto WINDOWREGEX = args.substr(args.find_first_of(',') + 1); + const auto MOVECMD = args.substr(0, args.find_first_of(',')); + + const auto PWINDOW = g_pCompositor->getWindowByRegex(WINDOWREGEX); + if (!PWINDOW) + return {.success = false, .error = "resizeWindow: no window"}; + + const auto SIZ = g_pCompositor->parseWindowVectorArgsRelative(MOVECMD, PWINDOW->m_realSize->goal()); + if (SIZ.x < 1 || SIZ.y < 1) + return {.success = false, .error = "Invalid size"}; + + return wrap(Actions::resize(SIZ, false, PWINDOW)); +} + +static SDispatchResult cyclenext(const std::string& arg) { + CVarList2 args(arg, 0, 's', true); + + const bool PREV = args.contains("prev") || args.contains("p") || args.contains("last") || args.contains("l"); + const bool NEXT = args.contains("next") || args.contains("n"); + + std::optional onlyTiled = {}; + std::optional onlyFloating = {}; + + if (args.contains("tile") || args.contains("tiled")) + onlyTiled = true; + if (args.contains("float") || args.contains("floating")) + onlyFloating = true; + + // "hist" and "visible" modes are not mapped to the new API - they remain niche. + // The new cycleNext uses a simple next/prev boolean. + // PREV is default in classic alt+tab, NEXT overrides it. + return wrap(Actions::cycleNext(NEXT || !PREV, onlyTiled, onlyFloating)); +} + +static SDispatchResult focuswindow(const std::string& regexp) { + const auto PWINDOW = g_pCompositor->getWindowByRegex(regexp); + if (!PWINDOW) + return {.success = false, .error = "No such window found"}; + return wrap(Actions::focus(PWINDOW)); +} + +static SDispatchResult tagwindow(const std::string& args) { + CVarList2 vars(args, 0, 's', true); + + PHLWINDOW PWINDOW = nullptr; + if (vars.size() == 1) + ; // use focused (nullptr) + else if (vars.size() == 2) + PWINDOW = g_pCompositor->getWindowByRegex(std::string(vars[1])); + else + return {.success = false, .error = "Invalid number of arguments, expected 1 or 2"}; + + return wrap(Actions::tag(std::string(vars[0]), PWINDOW)); +} + +static SDispatchResult toggleswallow(const std::string&) { + return wrap(Actions::toggleSwallow()); +} + +static SDispatchResult setsubmap(const std::string& submap) { + return wrap(Actions::setSubmap(submap)); +} + +static SDispatchResult passDispatcher(const std::string& regexp) { + const auto PWINDOW = g_pCompositor->getWindowByRegex(regexp); + if (!PWINDOW) + return {.success = false, .error = "pass: window not found"}; + return wrap(Actions::pass(PWINDOW)); +} + +static SDispatchResult sendshortcut(const std::string& args) { + CVarList2 ARGS(args, 3); + if (ARGS.size() != 3) + return {.success = false, .error = "sendshortcut: invalid args"}; + + const auto MOD = g_pKeybindManager->stringToModMask(std::string(ARGS[0])); + const auto KEY = std::string(ARGS[1]); + uint32_t keycode = 0; + + if (isNumber(KEY) && std::stoi(KEY) > 9) + keycode = std::stoi(KEY); + else if (KEY.starts_with("code:") && isNumber(KEY.substr(5))) + keycode = std::stoi(KEY.substr(5)); + else if (KEY.starts_with("mouse:") && isNumber(KEY.substr(6))) { + keycode = std::stoi(KEY.substr(6)); + if (keycode < 272) + return {.success = false, .error = "sendshortcut: invalid mouse button"}; + } else { + // resolve keycode from key name via xkb + const auto KEYSYM = xkb_keysym_from_name(KEY.c_str(), XKB_KEYSYM_CASE_INSENSITIVE); + keycode = 0; + + const auto KB = g_pSeatManager->m_keyboard; + if (!KB) + return {.success = false, .error = "sendshortcut: no kb"}; + + const auto KEYPAIRSTRING = std::format("{}{}", rc(KB.get()), KEY); + + if (!g_pKeybindManager->m_keyToCodeCache.contains(KEYPAIRSTRING)) { + xkb_keymap* km = KB->m_xkbKeymap; + xkb_state* ks = KB->m_xkbState; + xkb_keycode_t keycode_min = xkb_keymap_min_keycode(km); + xkb_keycode_t keycode_max = xkb_keymap_max_keycode(km); + + for (xkb_keycode_t kc = keycode_min; kc <= keycode_max; ++kc) { + xkb_keysym_t sym = xkb_state_key_get_one_sym(ks, kc); + if (sym == KEYSYM) { + keycode = kc; + g_pKeybindManager->m_keyToCodeCache[KEYPAIRSTRING] = keycode; + } + } + + if (!keycode) + return {.success = false, .error = "sendshortcut: key not found"}; + } else + keycode = g_pKeybindManager->m_keyToCodeCache[KEYPAIRSTRING]; + } + + if (!keycode) + return {.success = false, .error = "sendshortcut: invalid key"}; + + const std::string regexp = std::string(ARGS[2]); + PHLWINDOW PWINDOW = regexp.empty() ? nullptr : g_pCompositor->getWindowByRegex(regexp); + + if (!regexp.empty() && !PWINDOW) + return {.success = false, .error = "sendshortcut: window not found"}; + + return wrap(Actions::pass(MOD, keycode, PWINDOW)); +} + +static SDispatchResult sendkeystate(const std::string& args) { + CVarList2 ARGS(args, 4); + if (ARGS.size() != 4) + return {.success = false, .error = "sendkeystate: invalid args"}; + + const auto STATE = ARGS[2]; + if (STATE != "down" && STATE != "repeat" && STATE != "up") + return {.success = false, .error = "sendkeystate: invalid state, must be 'down', 'repeat', or 'up'"}; + + uint32_t keyState = 0; + if (STATE == "down") + keyState = 1; + else if (STATE == "repeat") + keyState = 2; + + // Reuse sendshortcut for keycode resolution, but wrap with state + std::string modifiedArgs = std::string(ARGS[0]) + "," + std::string(ARGS[1]) + "," + std::string(ARGS[3]); + + // We need to resolve the keycode first, so delegate to sendshortcut parsing. + // But sendkeystate overrides m_passPressed. Let's just call through sendshortcut + // with the proper state set. + const int oldPassPressed = Config::Actions::state()->m_passPressed; + + if (keyState == 1 || keyState == 2) + Config::Actions::state()->m_passPressed = 1; + else + Config::Actions::state()->m_passPressed = 0; + + auto result = sendshortcut(modifiedArgs); + + if (keyState == 2 && result.success) + result = sendshortcut(modifiedArgs); + + Config::Actions::state()->m_passPressed = oldPassPressed; + + return result; +} + +static SDispatchResult layoutmsg(const std::string& msg) { + return wrap(Actions::layoutMessage(msg)); +} + +static SDispatchResult dpmsDispatcher(const std::string& arg) { + eTogglableAction action; + if (arg.starts_with("on")) + action = TOGGLE_ACTION_ENABLE; + else if (arg.starts_with("toggle")) + action = TOGGLE_ACTION_TOGGLE; + else + action = TOGGLE_ACTION_DISABLE; + + std::optional mon = std::nullopt; + if (arg.find_first_of(' ') != std::string::npos) { + auto port = arg.substr(arg.find_first_of(' ') + 1); + auto pMon = g_pCompositor->getMonitorFromString(port); + if (pMon) + mon = pMon; + } + + return wrap(Actions::dpms(action, mon)); +} + +static SDispatchResult swapnext(const std::string& arg) { + return wrap(Actions::swapNext(arg != "l" && arg != "last" && arg != "prev" && arg != "b" && arg != "back")); +} + +static SDispatchResult swapactiveworkspaces(const std::string& args) { + const auto MON1 = args.substr(0, args.find_first_of(' ')); + const auto MON2 = args.substr(args.find_first_of(' ') + 1); + + const auto PMON1 = g_pCompositor->getMonitorFromString(MON1); + const auto PMON2 = g_pCompositor->getMonitorFromString(MON2); + + if (!PMON1 || !PMON2) + return {.success = false, .error = "Monitor not found"}; + + return wrap(Actions::swapActiveWorkspaces(PMON1, PMON2)); +} + +static SDispatchResult pin(const std::string& args) { + auto w = windowFromArg(args); + return wrap(Actions::pinWindow(TOGGLE_ACTION_TOGGLE, w)); +} + +static SDispatchResult mouseDispatcher(const std::string& args) { + return wrap(Actions::mouse(args.substr(1))); +} + +static SDispatchResult bringactivetotop(const std::string&) { + return wrap(Actions::alterZOrder("top")); +} + +static SDispatchResult alterzorder(const std::string& args) { + const auto WINDOWREGEX = args.substr(args.find_first_of(',') + 1); + const auto POSITION = args.substr(0, args.find_first_of(',')); + + auto PWINDOW = g_pCompositor->getWindowByRegex(WINDOWREGEX); + if (!PWINDOW && Desktop::focusState()->window() && Desktop::focusState()->window()->m_isFloating) + PWINDOW = Desktop::focusState()->window(); + + return wrap(Actions::alterZOrder(POSITION, PWINDOW)); +} + +static SDispatchResult focusurgentorlast(const std::string&) { + return wrap(Actions::focusUrgentOrLast()); +} + +static SDispatchResult focuscurrentorlast(const std::string&) { + return wrap(Actions::focusCurrentOrLast()); +} + +static SDispatchResult lockgroups(const std::string& args) { + eTogglableAction action; + if (args == "toggle") + action = TOGGLE_ACTION_TOGGLE; + else if (args == "lock" || args.empty() || args == "lockgroups") + action = TOGGLE_ACTION_ENABLE; + else + action = TOGGLE_ACTION_DISABLE; + return wrap(Actions::lockGroups(action)); +} + +static SDispatchResult lockactivegroup(const std::string& args) { + eTogglableAction action; + if (args == "toggle") + action = TOGGLE_ACTION_TOGGLE; + else if (args == "lock") + action = TOGGLE_ACTION_ENABLE; + else + action = TOGGLE_ACTION_DISABLE; + return wrap(Actions::lockActiveGroup(action)); +} + +static SDispatchResult moveintogroup(const std::string& args) { + Math::eDirection dir = Math::fromChar(args[0]); + if (dir == Math::DIRECTION_DEFAULT) + return {.success = false, .error = std::format("Unsupported direction: {}", args[0])}; + return wrap(Actions::moveIntoGroup(dir)); +} + +static SDispatchResult moveintoorcreategroup(const std::string& args) { + Math::eDirection dir = Math::fromChar(args[0]); + if (dir == Math::DIRECTION_DEFAULT) + return {.success = false, .error = std::format("Unsupported direction: {}", args[0])}; + return wrap(Actions::moveIntoOrCreateGroup(dir)); +} + +static SDispatchResult moveoutofgroup(const std::string& args) { + if (args != "active" && args.length() > 1) { + auto PWINDOW = g_pCompositor->getWindowByRegex(args); + return wrap(Actions::moveOutOfGroup(Math::DIRECTION_DEFAULT, PWINDOW)); + } + return wrap(Actions::moveOutOfGroup(Math::DIRECTION_DEFAULT)); +} + +static SDispatchResult movewindoworgroup(const std::string& args) { + Math::eDirection dir = Math::fromChar(args[0]); + if (dir == Math::DIRECTION_DEFAULT) + return {.success = false, .error = std::format("Unsupported direction: {}", args[0])}; + return wrap(Actions::moveWindowOrGroup(dir)); +} + +static SDispatchResult denywindowfromgroup(const std::string& args) { + eTogglableAction action; + if (args == "toggle") + action = TOGGLE_ACTION_TOGGLE; + else if (args == "on") + action = TOGGLE_ACTION_ENABLE; + else + action = TOGGLE_ACTION_DISABLE; + return wrap(Actions::denyWindowFromGroup(action)); +} + +static SDispatchResult eventDispatcher(const std::string& args) { + return wrap(Actions::event(args)); +} + +static SDispatchResult globalDispatcher(const std::string& args) { + return wrap(Actions::global(args)); +} + +static SDispatchResult setprop(const std::string& args) { + CVarList2 vars(args, 3, ' '); + if (vars.size() < 3) + return {.success = false, .error = "Not enough args"}; + + const auto PWINDOW = g_pCompositor->getWindowByRegex(std::string(vars[0])); + if (!PWINDOW) + return {.success = false, .error = "Window not found"}; + + // Reconstruct val from remaining args (for multi-arg values like colors) + return wrap(Actions::setProp(std::string(vars[1]), vars.join(" ", 2), PWINDOW)); +} + +static SDispatchResult forceidle(const std::string& args) { + std::optional duration = getPlusMinusKeywordResult(args, 0); + if (!duration.has_value()) + return {.success = false, .error = "Duration invalid in forceIdle"}; + return wrap(Actions::forceIdle(duration.value())); +} + +CDispatcherTranslator::CDispatcherTranslator() { + m_dispMap["exec"] = ::exec; + m_dispMap["execr"] = ::execr; + m_dispMap["killactive"] = ::killactive; + m_dispMap["forcekillactive"] = ::forcekillactive; + m_dispMap["closewindow"] = ::closewindow; + m_dispMap["killwindow"] = ::killwindow; + m_dispMap["signal"] = ::signalactive; + m_dispMap["signalwindow"] = ::signalwindow; + m_dispMap["togglefloating"] = ::togglefloating; + m_dispMap["setfloating"] = ::setfloating; + m_dispMap["settiled"] = ::settiled; + m_dispMap["workspace"] = ::workspace; + m_dispMap["renameworkspace"] = ::renameworkspace; + m_dispMap["fullscreen"] = ::fullscreen; + m_dispMap["fullscreenstate"] = ::fullscreenstate; + m_dispMap["movetoworkspace"] = ::movetoworkspace; + m_dispMap["movetoworkspacesilent"] = ::movetoworkspacesilent; + m_dispMap["pseudo"] = ::pseudo; + m_dispMap["movefocus"] = ::movefocus; + m_dispMap["movewindow"] = ::movewindow; + m_dispMap["swapwindow"] = ::swapwindow; + m_dispMap["centerwindow"] = ::centerwindow; + m_dispMap["togglegroup"] = ::togglegroup; + m_dispMap["changegroupactive"] = ::changegroupactive; + m_dispMap["movegroupwindow"] = ::movegroupwindow; + m_dispMap["focusmonitor"] = ::focusmonitor; + m_dispMap["movecursortocorner"] = ::movecursortocorner; + m_dispMap["movecursor"] = ::movecursor; + m_dispMap["workspaceopt"] = ::workspaceopt; + m_dispMap["exit"] = ::exitHyprland; + m_dispMap["movecurrentworkspacetomonitor"] = ::movecurrentworkspacetomonitor; + m_dispMap["focusworkspaceoncurrentmonitor"] = ::focusworkspaceoncurrentmonitor; + m_dispMap["moveworkspacetomonitor"] = ::moveworkspacetomonitor; + m_dispMap["togglespecialworkspace"] = ::togglespecialworkspace; + m_dispMap["forcerendererreload"] = ::forcerendererreload; + m_dispMap["resizeactive"] = ::resizeactive; + m_dispMap["moveactive"] = ::moveactive; + m_dispMap["cyclenext"] = ::cyclenext; + m_dispMap["focuswindowbyclass"] = ::focuswindow; + m_dispMap["focuswindow"] = ::focuswindow; + m_dispMap["tagwindow"] = ::tagwindow; + m_dispMap["toggleswallow"] = ::toggleswallow; + m_dispMap["submap"] = ::setsubmap; + m_dispMap["pass"] = ::passDispatcher; + m_dispMap["sendshortcut"] = ::sendshortcut; + m_dispMap["sendkeystate"] = ::sendkeystate; + m_dispMap["layoutmsg"] = ::layoutmsg; + m_dispMap["dpms"] = ::dpmsDispatcher; + m_dispMap["movewindowpixel"] = ::movewindowpixel; + m_dispMap["resizewindowpixel"] = ::resizewindowpixel; + m_dispMap["swapnext"] = ::swapnext; + m_dispMap["swapactiveworkspaces"] = ::swapactiveworkspaces; + m_dispMap["pin"] = ::pin; + m_dispMap["mouse"] = ::mouseDispatcher; + m_dispMap["bringactivetotop"] = ::bringactivetotop; + m_dispMap["alterzorder"] = ::alterzorder; + m_dispMap["focusurgentorlast"] = ::focusurgentorlast; + m_dispMap["focuscurrentorlast"] = ::focuscurrentorlast; + m_dispMap["lockgroups"] = ::lockgroups; + m_dispMap["lockactivegroup"] = ::lockactivegroup; + m_dispMap["moveintogroup"] = ::moveintogroup; + m_dispMap["moveintoorcreategroup"] = ::moveintoorcreategroup; + m_dispMap["moveoutofgroup"] = ::moveoutofgroup; + m_dispMap["movewindoworgroup"] = ::movewindoworgroup; + m_dispMap["setignoregrouplock"] = [](const std::string&) -> SDispatchResult { return {}; }; // deprecated + m_dispMap["denywindowfromgroup"] = ::denywindowfromgroup; + m_dispMap["event"] = ::eventDispatcher; + m_dispMap["global"] = ::globalDispatcher; + m_dispMap["setprop"] = ::setprop; + m_dispMap["forceidle"] = ::forceidle; +} diff --git a/src/config/legacy/DispatcherTranslator.hpp b/src/config/legacy/DispatcherTranslator.hpp new file mode 100644 index 000000000..18978f798 --- /dev/null +++ b/src/config/legacy/DispatcherTranslator.hpp @@ -0,0 +1,23 @@ +#pragma once + +#include "../../SharedDefs.hpp" +#include "../../helpers/memory/Memory.hpp" + +#include +#include +#include + +namespace Config::Legacy { + class CDispatcherTranslator { + public: + CDispatcherTranslator(); + ~CDispatcherTranslator() = default; + + SDispatchResult run(const std::string& dispatcher, const std::string& data); + + private: + std::unordered_map> m_dispMap; + }; + + UP& translator(); +} \ No newline at end of file diff --git a/src/config/lua/ConfigManager.cpp b/src/config/lua/ConfigManager.cpp new file mode 100644 index 000000000..7819ff2e8 --- /dev/null +++ b/src/config/lua/ConfigManager.cpp @@ -0,0 +1,1132 @@ +#include "ConfigManager.hpp" +#include "LuaBindings.hpp" +#include "DefaultConfig.hpp" +#include "Emergency.hpp" + +#include +#include +#include +#include +#include +#include +#include + +#include "types/LuaConfigUtils.hpp" +#include "types/LuaConfigBool.hpp" + +#include "../values/ConfigValues.hpp" + +#include "../supplementary/jeremy/Jeremy.hpp" +#include "../shared/workspace/WorkspaceRuleManager.hpp" +#include "../shared/monitor/MonitorRuleManager.hpp" +#include "../shared/animation/AnimationTree.hpp" +#include "../shared/inotify/ConfigWatcher.hpp" + +#include "../../desktop/rule/Engine.hpp" +#include "../../helpers/MiscFunctions.hpp" + +#include "../../event/EventBus.hpp" +#include "../../Compositor.hpp" +#include "../../managers/input/InputManager.hpp" +#include "../../managers/KeybindManager.hpp" +#include "../../helpers/Monitor.hpp" +#include "../../layout/space/Space.hpp" +#include "../../layout/supplementary/WorkspaceAlgoMatcher.hpp" +#include "../../render/Renderer.hpp" +#include "../../errorOverlay/Overlay.hpp" +#include "../../xwayland/XWayland.hpp" +#include "../../plugins/PluginSystem.hpp" +#include "../../managers/EventManager.hpp" +#include "../../managers/eventLoop/EventLoopManager.hpp" +#include "../../managers/input/trackpad/TrackpadGestures.hpp" +#include "../../notification/NotificationOverlay.hpp" +#include "../../render/decorations/CHyprGroupBarDecoration.hpp" + +using namespace Config; +using namespace Config::Lua; +using namespace Hyprutils::String; + +static uint64_t nextPluginLuaFnID = 0x10000; + +// +static bool isValidLuaIdentifier(const std::string& value) { + if (value.empty()) + return false; + + if (!std::isalpha(value[0]) && value[0] != '_') + return false; + + return std::ranges::all_of(value, [](const char& c) { return std::isalnum(c) || c == '_'; }); +} + +static int pluginLuaFunctionDispatcher(lua_State* L) { + auto* mgr = CConfigManager::fromLuaState(L); + if (!mgr) + return luaL_error(L, "hl.plugin: internal error: config manager unavailable"); + + if (!lua_isinteger(L, lua_upvalueindex(1))) + return luaL_error(L, "hl.plugin: internal error: invalid callback id"); + + const auto id = sc(lua_tointeger(L, lua_upvalueindex(1))); + return mgr->invokePluginLuaFunctionByID(id, L); +} + +static void trackRequiredLuaModulePath(lua_State* L, CConfigManager* mgr, const std::string& moduleName) { + if (!L || !mgr || moduleName.empty()) + return; + + const int stackTop = lua_gettop(L); + + lua_getglobal(L, "package"); + if (!lua_istable(L, -1)) { + lua_settop(L, stackTop); + return; + } + + lua_getfield(L, -1, "searchpath"); + if (!lua_isfunction(L, -1)) { + lua_settop(L, stackTop); + return; + } + + lua_pushstring(L, moduleName.c_str()); + lua_getfield(L, -3, "path"); + if (!lua_isstring(L, -1)) { + lua_settop(L, stackTop); + return; + } + + if (lua_pcall(L, 2, 2, 0) == LUA_OK && lua_isstring(L, -2)) { + const auto* resolvedPath = lua_tostring(L, -2); + if (resolvedPath && std::ranges::find(mgr->m_configPaths, resolvedPath) == mgr->m_configPaths.end()) + mgr->m_configPaths.emplace_back(resolvedPath); + } + + lua_settop(L, stackTop); +} + +static int safeLuaRequire(lua_State* L) { + const int nargs = lua_gettop(L); + + std::string moduleName; + if (lua_isstring(L, 1)) + moduleName = lua_tostring(L, 1); + + lua_pushvalue(L, lua_upvalueindex(1)); + lua_insert(L, 1); + + const int status = lua_pcall(L, nargs, LUA_MULTRET, 0); + if (status == LUA_OK) + return lua_gettop(L); + + std::string err; + { + size_t len = 0; + const char* str = luaL_tolstring(L, -1, &len); + if (str) + err.assign(str, len); + lua_pop(L, 1); + } + + if (auto* mgr = CConfigManager::fromLuaState(L); mgr) { + if (!moduleName.empty()) { + trackRequiredLuaModulePath(L, mgr, moduleName); + mgr->addError(std::format("require(\"{}\"): {}", moduleName, err)); + } else + mgr->addError(std::format("require: {}", err)); + } + + lua_pop(L, 1); // error object + + lua_newtable(L); // fallback module + const int fallbackIdx = lua_gettop(L); + + if (!moduleName.empty()) { + lua_getglobal(L, "package"); + if (lua_istable(L, -1)) { + lua_getfield(L, -1, "loaded"); + if (lua_istable(L, -1)) { + lua_pushstring(L, moduleName.c_str()); + lua_pushvalue(L, fallbackIdx); + lua_settable(L, -3); + } + lua_pop(L, 1); // loaded + } + lua_pop(L, 1); // package + } + + return 1; +} + +WP Lua::mgr() { + auto& mgr = Config::mgr(); + if (mgr->type() != CONFIG_LUA) + return nullptr; + + return dynamicPointerCast(WP(mgr)); +} + +CConfigManager::SDeviceConfig::SDeviceConfig() { + auto mgr = Lua::mgr(); + + for (const auto& valRaw : Values::CONFIG_DEVICE_VALUE_NAMES) { + auto generic = std::ranges::find_if(Values::CONFIG_VALUES, [&valRaw](const auto& g) { return g->name() == valRaw; }); + + RASSERT(generic != Values::CONFIG_VALUES.end(), "Lua: device value has no generic counterpart, what the fuck happened?"); + + auto valLua = mgr->luaConfigValueName(valRaw); + + auto valDevice = valLua.contains('.') ? valLua.substr(valLua.find_last_of('.') + 1) : valLua; + + if (values.contains(valDevice)) + continue; // some are duped + + values.emplace(valDevice, fromGenericValue(*generic)); + } + + // emplace the special keybinds one + values.emplace("keybinds", makeUnique(true)); +} + +CConfigManager::CConfigManager() : m_mainConfigPath(Supplementary::Jeremy::getMainConfigPath()->path) { + ; +} + +CConfigManager* CConfigManager::fromLuaState(lua_State* L) { + if (!L) + return nullptr; + + lua_getfield(L, LUA_REGISTRYINDEX, "hl_lua_manager"); + auto* mgr = sc(lua_touserdata(L, -1)); + lua_pop(L, 1); + return mgr; +} + +void CConfigManager::watchdogHook(lua_State* L, lua_Debug* /*ar*/) { + auto* mgr = fromLuaState(L); + if (!mgr || !mgr->m_watchdogActive) + return; + + if (std::chrono::steady_clock::now() <= mgr->m_watchdogDeadline) + return; + + const auto& context = mgr->m_watchdogContext; + if (context.empty()) + luaL_error(L, "[Lua] execution timed out"); + + luaL_error(L, "[Lua] execution timed out in %s", context.c_str()); +} + +int CConfigManager::guardedPCall(int nargs, int nresults, int errfunc, int timeoutMs, std::string_view context) { + if (!m_lua) + return LUA_ERRERR; + + const auto prevHook = lua_gethook(m_lua); + const int prevMask = lua_gethookmask(m_lua); + const int prevCount = lua_gethookcount(m_lua); + + const bool prevWatchdogActive = m_watchdogActive; + const std::chrono::steady_clock::time_point prevWatchdogDeadline = m_watchdogDeadline; + const std::string prevWatchdogContext = m_watchdogContext; + + m_watchdogActive = timeoutMs > 0; + m_watchdogDeadline = std::chrono::steady_clock::now() + std::chrono::milliseconds(std::max(timeoutMs, 1)); + m_watchdogContext = context; + + lua_sethook(m_lua, &CConfigManager::watchdogHook, LUA_MASKCOUNT, LUA_WATCHDOG_INSTRUCTION_INTERVAL); + const int result = lua_pcall(m_lua, nargs, nresults, errfunc); + + lua_sethook(m_lua, prevHook, prevMask, prevCount); + m_watchdogActive = prevWatchdogActive; + m_watchdogDeadline = prevWatchdogDeadline; + m_watchdogContext = prevWatchdogContext; + + return result; +} + +eConfigManagerType CConfigManager::type() { + return CONFIG_LUA; +} + +void CConfigManager::registerValue(const char* name, ILuaConfigValue* val) { + m_configValues.emplace(name, UP(val)); +} + +void CConfigManager::cleanTimers() { + for (auto& t : m_luaTimers) { + t.timer->cancel(); + g_pEventLoopManager->removeTimer(t.timer); + + if (m_lua && t.luaRef != LUA_NOREF) { + luaL_unref(m_lua, LUA_REGISTRYINDEX, t.luaRef); + t.luaRef = LUA_NOREF; + } + } + m_luaTimers.clear(); +} + +void CConfigManager::reinitLuaState() { + // Destroy the event handler first so its luaL_unref calls happen while m_lua is still valid. + m_eventHandler.reset(); + + cleanTimers(); + + if (m_lua) { + lua_close(m_lua); + m_lua = nullptr; + } + + m_lua = luaL_newstate(); + luaL_openlibs(m_lua); + + lua_getglobal(m_lua, "debug"); + if (lua_istable(m_lua, -1)) { + lua_pushnil(m_lua); + lua_setfield(m_lua, -2, "sethook"); + lua_pushnil(m_lua); + lua_setfield(m_lua, -2, "gethook"); + } + lua_pop(m_lua, 1); + + lua_pushlightuserdata(m_lua, this); + lua_setfield(m_lua, LUA_REGISTRYINDEX, "hl_lua_manager"); + + std::filesystem::path configDir = std::filesystem::path(m_mainConfigPath).parent_path(); + const std::string luaPath = (configDir / "?.lua").string() + ";" + (configDir / "?/init.lua").string(); + lua_getglobal(m_lua, "package"); + lua_pushstring(m_lua, luaPath.c_str()); + lua_setfield(m_lua, -2, "path"); + lua_pop(m_lua, 1); + + lua_getglobal(m_lua, "require"); + if (lua_isfunction(m_lua, -1)) { + lua_pushcclosure(m_lua, safeLuaRequire, 1); + lua_setglobal(m_lua, "require"); + } else + lua_pop(m_lua, 1); + + Bindings::registerBindings(m_lua, this); + + m_eventHandler = makeUnique(m_lua); + + // Hook package.searchers[2] (the Lua file searcher) to track require()'d paths. + lua_getglobal(m_lua, "package"); + lua_getfield(m_lua, -1, "searchers"); + lua_rawgeti(m_lua, -1, 2); // original file searcher + lua_pushlightuserdata(m_lua, this); + lua_pushcclosure( + m_lua, + [](lua_State* L) -> int { + // upvalue 1: original searcher, upvalue 2: CConfigManager* + lua_pushvalue(L, lua_upvalueindex(1)); + lua_pushvalue(L, 1); // module name + lua_call(L, 1, 2); // -> loader?, filename? + if (lua_isfunction(L, -2) && lua_isstring(L, -1)) { + auto* self = sc(lua_touserdata(L, lua_upvalueindex(2))); + self->m_configPaths.emplace_back(lua_tostring(L, -1)); + } + return 2; + }, + 2); + lua_rawseti(m_lua, -2, 2); // replace package.searchers[2] + lua_pop(m_lua, 2); // pop searchers, package +} + +void CConfigManager::init() { + reinitLuaState(); + + for (const auto& v : Values::CONFIG_VALUES) { + m_configValues.emplace(luaConfigValueName(v->name()), fromGenericValue(v)); + } + + m_configValues["autogenerated"] = fromGenericValue(makeShared("autogenerated", "whether the config is autogenerated or not", 0)); + + Config::watcher()->setOnChange([this](const CConfigWatcher::SConfigWatchEvent& e) { + Log::logger->log(Log::DEBUG, "[lua] file {} modified, reloading", e.file); + reload(); + }); + + reload(); +} + +void CConfigManager::reload() { + Event::bus()->m_events.config.preReload.emit(); + + Hyprutils::Utils::CScopeGuard x([this] { + m_isParsingConfig = false; + m_isFirstLaunch = false; + }); + m_isParsingConfig = true; + + m_mainConfigPath = Supplementary::Jeremy::getMainConfigPath()->path; + + // reset tracked paths; the searcher hook will re-populate them as require() runs + m_configPaths.clear(); + m_configPaths.emplace_back(m_mainConfigPath); + + // phase 1: check syntax before clearing any state, so a broken syntax + // doesn't entirely fucking nuke the config and leave the user + // with no binds. + // + // this won't help if they are launching hyprland, + // which will be handled with emergency pcall + auto phase1Load = [this]() -> bool { + // clear package.loaded for user modules so require() re-executes them and + // the searcher hook can re-track their paths. + lua_getglobal(m_lua, "package"); + lua_getfield(m_lua, -1, "loaded"); + static constexpr std::string_view STDLIB[] = {"_G", "coroutine", "debug", "io", "math", "os", "package", "string", "table", "utf8", "bit32", "jit"}; + lua_pushnil(m_lua); + while (lua_next(m_lua, -2)) { + lua_pop(m_lua, 1); // pop value, keep key + if (lua_isstring(m_lua, -1)) { + std::string_view mod = lua_tostring(m_lua, -1); + bool skip = false; + for (const auto& s : STDLIB) { + if (mod == s) { + skip = true; + break; + } + } + if (!skip) { + lua_pushvalue(m_lua, -1); // dup key + lua_pushnil(m_lua); + lua_settable(m_lua, -4); // package.loaded[key] = nil + } + } + } + lua_pop(m_lua, 2); // pop loaded, package + + if (luaL_loadfile(m_lua, m_mainConfigPath.c_str()) != LUA_OK) { + m_errors.clear(); + addError(lua_tostring(m_lua, -1)); + lua_pop(m_lua, 1); + return false; + } + + return true; + }; + + if (!phase1Load()) { + m_lastConfigVerificationWasSuccessful = false; + postConfigReload(); + return; + } + + // phase 2: syntax is valid, reset and load. + Config::animationTree()->reset(); + Config::workspaceRuleMgr()->clear(); + Config::monitorRuleMgr()->clear(); + Desktop::Rule::ruleEngine()->clearAllRules(); + g_pTrackpadGestures->clearGestures(); + cleanTimers(); + m_luaWindowRules.clear(); + m_luaLayerRules.clear(); + m_errors.clear(); + m_deviceConfigs.clear(); + m_registeredPlugins.clear(); + m_eventHandler->clearEvents(); + clearHeldLuaRefs(); + + if (g_pKeybindManager) { + for (const auto& kb : g_pKeybindManager->m_keybinds) { + if (kb->handler == "__lua") + luaL_unref(m_lua, LUA_REGISTRYINDEX, std::stoi(kb->arg)); + } + g_pKeybindManager->clearKeybinds(); + } + + // refresh lua state so we get rid of any stale state + reinitLuaState(); + + // restart phase 1 load, because our state is gone now. + if (!phase1Load()) { + m_lastConfigVerificationWasSuccessful = false; + postConfigReload(); + return; + } + + // re-register plugin functions + reregisterLuaPluginFns(); + + for (const auto& v : m_configValues) { + v.second->reset(); + } + + lua_pushcfunction(m_lua, [](lua_State* L) -> int { + luaL_traceback(L, L, lua_tostring(L, 1), 1); + return 1; + }); + lua_insert(m_lua, 1); + + if (guardedPCall(0, 0, 1, LUA_TIMEOUT_CONFIG_RELOAD_MS, "config reload") != LUA_OK) { + addError(lua_tostring(m_lua, -1)); + lua_pop(m_lua, 1); + m_lastConfigVerificationWasSuccessful = false; + } else + m_lastConfigVerificationWasSuccessful = m_errors.empty(); + + lua_remove(m_lua, 1); + + // collect stale userdata after reload so HL.Notification __gc can + // promptly release pause leases from dropped references + lua_gc(m_lua, LUA_GCCOLLECT, 0); + + postConfigReload(); +} + +void CConfigManager::postConfigReload() { + + static auto PZOOMFACTOR = CConfigValue("cursor.zoom_factor"); + static auto PSUPPRESSERRORS = CConfigValue("debug.suppress_errors"); + static auto PXWAYLAND = CConfigValue("xwayland.enabled"); + static auto PMANUALCRASH = CConfigValue("debug.manual_crash"); + static auto PENABLESTDOUT = CConfigValue("debug.enable_stdout_logs"); + static auto PAUTOGENERATED = CConfigValue("autogenerated"); + + Config::watcher()->update(); + + for (auto const& w : g_pCompositor->m_windows) { + w->uncacheWindowDecos(); + } + + for (auto const& m : g_pCompositor->m_monitors) { + *(m->m_cursorZoom) = *PZOOMFACTOR; + if (m->m_activeWorkspace) + m->m_activeWorkspace->m_space->recalculate(); + } + + // Update the keyboard layout to the cfg'd one if this is not the first launch + if (!m_isFirstLaunch) { + g_pInputManager->setKeyboardLayout(); + g_pInputManager->setPointerConfigs(); + g_pInputManager->setTouchDeviceConfigs(); + g_pInputManager->setTabletConfigs(); + + g_pHyprRenderer->m_reloadScreenShader = true; + } + + const bool emergencyModeTripped = !m_errors.empty() && g_pKeybindManager->m_keybinds.empty(); + + if (emergencyModeTripped) + luaL_dostring(m_lua, EMERGENCY_PCALL); + + // parseError will be displayed next frame + if (!m_errors.empty() && !*PSUPPRESSERRORS) { + std::string errorStr; + + if (emergencyModeTripped) { + errorStr = "⚠ Emergency mode tripped: A lua config error resulted in no binds being registered. Emergency binds active: SUPER + Q → any known terminal, SUPER + R " + "→ hyprland-run, SUPER + M → Exit\n"; + } + + errorStr += "Your config has errors:\n"; + + for (const auto& e : m_errors) { + errorStr += e + "\n"; + + if (std::ranges::count(errorStr, '\n') > 15) { + errorStr += "... more"; + break; + } + } + + if (!errorStr.empty() && errorStr.back() == '\n') + errorStr.pop_back(); + + ErrorOverlay::overlay()->queueCreate(errorStr, ErrorOverlay::Colors::ERROR); + } else if (*PAUTOGENERATED) + ErrorOverlay::overlay()->queueCreate( + "Warning: You're using an autogenerated config! Edit the config file to get rid of this message. (config file: " + getMainConfigPath() + + " )\nSUPER+Q -> kitty (if it doesn't launch, make sure it's installed or choose a different terminal in the config)\nSUPER+M -> exit Hyprland", + ErrorOverlay::Colors::WARNING); + else + ErrorOverlay::overlay()->destroy(); + + // Set the modes for all monitors as we configured them + // not on first launch because monitors might not exist yet + // and they'll be taken care of in the newMonitor event + if (!m_isFirstLaunch) { + // check + Config::monitorRuleMgr()->scheduleReload(); + Config::monitorRuleMgr()->ensureMonitorStatus(); + Config::monitorRuleMgr()->ensureVRR(); + } + +#ifndef NO_XWAYLAND + g_pCompositor->m_wantsXwayland = *PXWAYLAND; + // enable/disable xwayland usage + if (!m_isFirstLaunch && + g_pXWayland /* XWayland has to be initialized by CCompositor::initManagers for this to make sense, and it doesn't have to be (e.g. very early plugin load) */) { + bool prevEnabledXwayland = g_pXWayland->enabled(); + if (g_pCompositor->m_wantsXwayland != prevEnabledXwayland) + g_pXWayland = makeUnique(g_pCompositor->m_wantsXwayland); + } else + g_pCompositor->m_wantsXwayland = *PXWAYLAND; +#endif + + if (!m_isFirstLaunch && !g_pCompositor->m_unsafeState) + refreshGroupBarGradients(); + + for (auto const& w : g_pCompositor->getWorkspaces()) { + if (w->inert()) + continue; + w->updateWindows(); + w->updateWindowData(); + } + + g_pCompositor->updateAllWindowsAnimatedDecorationValues(); + + if (*PMANUALCRASH && !m_manualCrashInitiated) { + m_manualCrashInitiated = true; + Notification::overlay()->addNotification("Manual crash has been set up. Set debug.manual_crash back to 0 in order to crash the compositor.", CHyprColor(0), 5000, + ICON_INFO); + } else if (m_manualCrashInitiated && !*PMANUALCRASH) { + // cowabunga it is + g_pHyprRenderer->initiateManualCrash(); + } + + auto disableStdout = !*PENABLESTDOUT; + if (disableStdout && m_isFirstLaunch) + Log::logger->log(Log::DEBUG, "Disabling stdout logs! Check the log for further logs."); + + for (auto const& m : g_pCompositor->m_monitors) { + // mark blur dirty + m->m_blurFBDirty = true; + + g_pCompositor->scheduleFrameForMonitor(m); + + // Force the compositor to fully re-render all monitors + m->m_forceFullFrames = 2; + + // also force mirrors, as the aspect ratio could've changed + for (auto const& mirror : m->m_mirrors) + mirror->m_forceFullFrames = 3; + } + + handlePluginLoads(); + + if (!m_isFirstLaunch) + g_pCompositor->ensurePersistentWorkspacesPresent(); + + Layout::Supplementary::algoMatcher()->updateWorkspaceLayouts(); + + Event::bus()->m_events.config.reloaded.emit(); + if (g_pEventManager) + g_pEventManager->postEvent(SHyprIPCEvent{"configreloaded", ""}); +} + +void CConfigManager::addError(std::string&& str) { + if (m_isParsingConfig) { + m_errors.emplace_back(std::move(str)); + return; + } + + if (m_isEvaluating) { + m_errors.emplace_back(std::move(str)); + return; + } + + // pop a notification + Notification::overlay()->addNotification(std::format("Runtime error in lua:\n{}", std::move(str)), 0, 5000, ICON_WARNING); +} + +void CConfigManager::addEvalIssue(const Config::SConfigError& err) { + if (!m_isEvaluating) + return; + + if (err.level == eConfigErrorLevel::WARNING || err.level == eConfigErrorLevel::INFO) + m_evalIssues.emplace_back(err); +} + +std::optional CConfigManager::eval(const std::string& code) { + if (!m_lua) + return "error: lua state not initialized"; + + m_errors.clear(); + m_evalIssues.clear(); + m_isEvaluating = true; + + 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); + lua_pop(m_lua, 1); + return std::format("error: {}", err); + } + + if (guardedPCall(0, 0, 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); + } + + if (!m_errors.empty() || !m_evalIssues.empty()) { + std::string out; + out.reserve(256); + + for (size_t i = 0; i < m_errors.size(); ++i) { + out += "error: "; + out += m_errors.at(i); + out += "\n"; + } + + for (const auto& issue : m_evalIssues) { + out += std::format("{}: {}", Config::toString(issue.level), issue.message); + out += "\n"; + } + + out.pop_back(); + + return out; + } + + m_errors.clear(); + m_evalIssues.clear(); + + return std::nullopt; +} + +std::string CConfigManager::verify() { + if (m_errors.empty()) + return "config ok"; + + std::string fullStr = ""; + for (const auto& e : m_errors) { + fullStr += e; + fullStr += "\n"; + } + + fullStr.pop_back(); + return fullStr; +} + +static std::string normalizeDeviceName(const std::string& dev) { + auto copy = dev; + std::ranges::replace(copy, ' ', '-'); + return copy; +} + +ILuaConfigValue* CConfigManager::findDeviceValue(const std::string& dev, const std::string& field) { + const auto devIt = m_deviceConfigs.find(dev); + if (devIt == m_deviceConfigs.end()) + return nullptr; + const auto valIt = devIt->second.values.find(field); + return valIt != devIt->second.values.end() ? valIt->second.get() : nullptr; +} + +int CConfigManager::getDeviceInt(const std::string& dev, const std::string& field, const std::string& fb) { + std::string fallback = luaConfigValueName(fb); + if (auto* v = findDeviceValue(normalizeDeviceName(dev), luaConfigValueName(field)); v && v->setByUser()) + return (int)*sc(v->data()); + if (!fallback.empty() && m_configValues.contains(fallback)) + return (int)*sc(m_configValues.at(fallback)->data()); + return 0; +} + +float CConfigManager::getDeviceFloat(const std::string& dev, const std::string& field, const std::string& fb) { + std::string fallback = luaConfigValueName(fb); + if (auto* v = findDeviceValue(normalizeDeviceName(dev), luaConfigValueName(field)); v && v->setByUser()) + return *sc(v->data()); + if (!fallback.empty() && m_configValues.contains(fallback)) + return *sc(m_configValues.at(fallback)->data()); + return 0.F; +} + +Vector2D CConfigManager::getDeviceVec(const std::string& dev, const std::string& field, const std::string& fb) { + std::string fallback = luaConfigValueName(fb); + auto toVec = [](const Config::VEC2& v) -> Vector2D { return {v.x, v.y}; }; + if (auto* v = findDeviceValue(normalizeDeviceName(dev), luaConfigValueName(field)); v && v->setByUser()) + return toVec(*sc(v->data())); + if (!fallback.empty() && m_configValues.contains(fallback)) + return toVec(*sc(m_configValues.at(fallback)->data())); + return {0, 0}; +} + +std::string CConfigManager::getDeviceString(const std::string& dev, const std::string& field, const std::string& fb) { + std::string fallback = luaConfigValueName(fb); + auto clean = [](const Config::STRING& s) -> std::string { return s == STRVAL_EMPTY ? "" : s; }; + if (auto* v = findDeviceValue(normalizeDeviceName(dev), luaConfigValueName(field)); v && v->setByUser()) + return clean(*sc(v->data())); + if (!fallback.empty() && m_configValues.contains(fallback)) + return clean(*sc(m_configValues.at(fallback)->data())); + return ""; +} + +bool CConfigManager::deviceConfigExplicitlySet(const std::string& dev, const std::string& field) { + auto v = findDeviceValue(normalizeDeviceName(dev), luaConfigValueName(field)); + return v && v->setByUser(); +} + +bool CConfigManager::deviceConfigExists(const std::string& dev) { + return m_deviceConfigs.contains(normalizeDeviceName(dev)); +} + +SConfigOptionReply CConfigManager::getConfigValue(const std::string& s) { + + if (m_configValues.contains(s)) { + + auto& cv = m_configValues[s]; + + m_configPtrMap[s] = cv->data(); + + return SConfigOptionReply{.dataptr = cc(&m_configPtrMap.at(s)), .type = cv->underlying(), .setByUser = cv->setByUser()}; + } + + // try replacing all . with : unless col. + std::string s2 = s; + + for (size_t i = 0; i < s2.length(); ++i) { + if (s2[i] != ':') + continue; + + if (i <= 3) { + s2[i] = '.'; + continue; + } + + if (s2[i - 1] == 'l' && s2[i - 2] == 'o' && s2[i - 3] == 'c') + continue; + + s2[i] = '.'; + } + + if (!m_configValues.contains(s2)) + return SConfigOptionReply{.dataptr = nullptr}; + + auto& cv = m_configValues[s2]; + + m_configPtrMap[s2] = cv->data(); + + return SConfigOptionReply{.dataptr = cc(&m_configPtrMap.at(s2)), .type = cv->underlying(), .setByUser = cv->setByUser()}; +} + +std::string CConfigManager::getMainConfigPath() { + return m_mainConfigPath; +} + +std::string CConfigManager::getErrors() { + std::string errStr; + for (const auto& e : m_errors) { + errStr += e + "\n"; + } + + if (!errStr.empty()) + errStr.pop_back(); + + return errStr; +} + +std::string CConfigManager::getConfigString() { + return "Not supported under lua"; +} + +std::string CConfigManager::currentConfigPath() { + return m_mainConfigPath; +} + +const std::vector& CConfigManager::getConfigPaths() { + return m_configPaths; +} + +std::expected CConfigManager::generateDefaultConfig(const std::filesystem::path& path, bool safeMode) { + std::string parentPath = std::filesystem::path(path).parent_path(); + + if (!parentPath.empty()) { + std::error_code ec; + bool created = std::filesystem::create_directories(parentPath, ec); + if (ec) { + Log::logger->log(Log::ERR, "Couldn't create config home directory ({}): {}", ec.message(), parentPath); + return std::unexpected("Config could not be generated."); + } + if (created) + Log::logger->log(Log::WARN, "Creating config home directory"); + } + + Log::logger->log(Log::WARN, "No config file found; attempting to generate."); + std::ofstream ofs; + ofs.open(path, std::ios::trunc); + + if (!ofs.good()) + return std::unexpected("Config could not be generated."); + + if (!safeMode) { + ofs << AUTOGENERATED_PREFIX; + ofs << EXAMPLE_CONFIG; + } else { + std::string n = std::string{EXAMPLE_CONFIG}; + replaceInString(n, "\nlocal menu = \"hyprlauncher\"\n", "\nlocal menu = \"hyprland-run\"\n"); + ofs << n; + } + + ofs.close(); + + if (ofs.fail()) + return std::unexpected("Config could not be generated."); + + return {}; +} + +void CConfigManager::handlePluginLoads() { + if (!g_pPluginSystem) + return; + + bool pluginsChanged = false; + g_pPluginSystem->updateConfigPlugins(m_registeredPlugins, pluginsChanged); + + if (pluginsChanged) { + ErrorOverlay::overlay()->destroy(); + reload(); + } +} + +bool CConfigManager::configVerifPassed() { + return m_lastConfigVerificationWasSuccessful; +} + +std::string CConfigManager::luaConfigValueName(const std::string& s) { + std::string cpy = s; + std::ranges::replace(cpy, ':', '.'); + std::ranges::replace(cpy, '-', '_'); + return cpy; +} + +bool CConfigManager::isFirstLaunch() const { + return m_isFirstLaunch; +} + +std::expected CConfigManager::registerPluginValue(void* handle, SP value) { + + const auto NAME = luaConfigValueName(value->name()); + + if (m_configValues.contains(NAME)) + return std::unexpected("name collision: already registered"); + + auto val = fromGenericValue(value); + + if (!val) + return std::unexpected("unsupported value type"); + + m_configValues.emplace(NAME, std::move(val)); + + m_pluginValues[handle].emplace_back(NAME); + + return {}; +} + +std::expected CConfigManager::registerPluginLuaFunctionInState(uint64_t id, const std::string& namespace_, const std::string& name) { + if (!m_lua) + return std::unexpected("lua state not initialized"); + + lua_getglobal(m_lua, "hl"); + if (!lua_istable(m_lua, -1)) { + lua_pop(m_lua, 1); + return std::unexpected("missing global table 'hl'"); + } + + lua_getfield(m_lua, -1, "plugin"); + if (!lua_istable(m_lua, -1)) { + lua_pop(m_lua, 2); + return std::unexpected("missing global table 'hl.plugin'"); + } + + const int pluginTableIdx = lua_gettop(m_lua); + + lua_getfield(m_lua, pluginTableIdx, namespace_.c_str()); + if (lua_isnil(m_lua, -1)) { + lua_pop(m_lua, 1); + lua_newtable(m_lua); + lua_pushvalue(m_lua, -1); + lua_setfield(m_lua, pluginTableIdx, namespace_.c_str()); + } else if (!lua_istable(m_lua, -1)) { + lua_pop(m_lua, 3); + return std::unexpected(std::format("hl.plugin.{} already exists and is not a namespace table", namespace_)); + } + + const int namespaceTableIdx = lua_gettop(m_lua); + + lua_getfield(m_lua, namespaceTableIdx, name.c_str()); + const bool exists = !lua_isnil(m_lua, -1); + lua_pop(m_lua, 1); + + if (exists) { + lua_pop(m_lua, 3); + return std::unexpected(std::format("hl.plugin.{}.{} already exists", namespace_, name)); + } + + lua_pushinteger(m_lua, sc(id)); + lua_pushcclosure(m_lua, pluginLuaFunctionDispatcher, 1); + lua_setfield(m_lua, namespaceTableIdx, name.c_str()); + + lua_pop(m_lua, 3); + return {}; +} + +std::expected CConfigManager::unregisterPluginLuaFunctionInState(const std::string& namespace_, const std::string& name) { + if (!m_lua) + return std::unexpected("lua state not initialized"); + + lua_getglobal(m_lua, "hl"); + if (!lua_istable(m_lua, -1)) { + lua_pop(m_lua, 1); + return {}; + } + + lua_getfield(m_lua, -1, "plugin"); + if (!lua_istable(m_lua, -1)) { + lua_pop(m_lua, 2); + return {}; + } + + const int pluginTableIdx = lua_gettop(m_lua); + + lua_getfield(m_lua, pluginTableIdx, namespace_.c_str()); + if (!lua_istable(m_lua, -1)) { + lua_pop(m_lua, 3); + return {}; + } + + const int namespaceTableIdx = lua_gettop(m_lua); + + lua_pushnil(m_lua); + lua_setfield(m_lua, namespaceTableIdx, name.c_str()); + + bool isEmpty = true; + lua_pushnil(m_lua); + if (lua_next(m_lua, namespaceTableIdx) != 0) { + isEmpty = false; + lua_pop(m_lua, 2); + } + + if (isEmpty) { + lua_pushnil(m_lua); + lua_setfield(m_lua, pluginTableIdx, namespace_.c_str()); + } + + lua_pop(m_lua, 3); + return {}; +} + +void CConfigManager::erasePluginLuaFunction(uint64_t id) { + std::erase_if(m_pluginLuaFunctions, [&id](const SPluginLuaFunction& f) { return f.id == id; }); +} + +int CConfigManager::invokePluginLuaFunctionByID(uint64_t id, lua_State* L) { + const auto REGIT = std::ranges::find_if(m_pluginLuaFunctions, [&id](const SPluginLuaFunction& r) { return r.id == id; }); + if (REGIT == m_pluginLuaFunctions.end()) + return luaL_error(L, "hl.plugin: this function is no longer available (plugin unloaded)"); + + const auto FN = REGIT->fn; + if (!FN) + return luaL_error(L, "hl.plugin: this function is not callable"); + + return FN(L); +} + +std::expected CConfigManager::registerPluginLuaFunction(void* handle, const std::string& namespace_, const std::string& name, Config::PLUGIN_LUA_FN fn) { + if (!handle) + return std::unexpected("invalid handle"); + + if (!fn) + return std::unexpected("function pointer cannot be null"); + + if (!isValidLuaIdentifier(namespace_)) + return std::unexpected("namespace must match [A-Za-z_][A-Za-z0-9_]*"); + + if (!isValidLuaIdentifier(name)) + return std::unexpected("name must match [A-Za-z_][A-Za-z0-9_]*"); + + if (namespace_ == "load") + return std::unexpected("namespace 'load' is reserved"); + + const auto key = namespace_ + "." + name; + if (std::ranges::find_if(m_pluginLuaFunctions, [&key](const SPluginLuaFunction& r) { return r.namespace_ + "." + r.name == key; }) != m_pluginLuaFunctions.end()) + return std::unexpected("name collision: already registered"); + + const uint64_t id = nextPluginLuaFnID++; + if (const auto REGISTERED = registerPluginLuaFunctionInState(id, namespace_, name); !REGISTERED) + return REGISTERED; + + m_pluginLuaFunctions.emplace_back(SPluginLuaFunction{.id = id, .handle = handle, .namespace_ = namespace_, .name = name, .fn = fn}); + + return {}; +} + +std::expected CConfigManager::unregisterPluginLuaFunction(void* handle, const std::string& namespace_, const std::string& name) { + if (!handle) + return std::unexpected("invalid handle"); + + const auto key = namespace_ + "." + name; + auto it = std::ranges::find_if(m_pluginLuaFunctions, [&key](const SPluginLuaFunction& r) { return r.namespace_ + "." + r.name == key; }); + + if (it == m_pluginLuaFunctions.end()) + return std::unexpected("no such function"); + + if (it->handle != handle) + return std::unexpected("function belongs to a different plugin"); + + const auto removedFromState = unregisterPluginLuaFunctionInState(namespace_, name); + erasePluginLuaFunction(it->id); + + if (!removedFromState) + return removedFromState; + + return {}; +} + +void CConfigManager::onPluginUnload(void* handle) { + if (!handle) + return; + + if (const auto it = m_pluginValues.find(handle); it != m_pluginValues.end()) { + for (const auto& name : it->second) { + m_configValues.erase(name); + } + + m_pluginValues.erase(it); + } + + std::erase_if(m_pluginLuaFunctions, [&handle, this](const SPluginLuaFunction& f) { + const bool NEEDS_REMOVE = f.handle == handle; + + if (NEEDS_REMOVE) // NOLINTNEXTLINE + unregisterPluginLuaFunctionInState(f.namespace_, f.name); + + return NEEDS_REMOVE; + }); +} + +void CConfigManager::registerLuaRef(int ref) { + m_heldLuaRefs.emplace_back(ref); +} + +void CConfigManager::callLuaFn(int ref) { + lua_rawgeti(m_lua, LUA_REGISTRYINDEX, ref); + + int status = guardedPCall(0, 0, 0, CConfigManager::LUA_TIMEOUT_KEYBIND_CALLBACK_MS, "keybind callback"); + + if (status != LUA_OK) { + addError(std::format("error in callLuaFn: {}", lua_tostring(m_lua, -1))); + lua_pop(m_lua, 1); + } +} + +void CConfigManager::clearHeldLuaRefs() { + for (const auto& r : m_heldLuaRefs) { + luaL_unref(m_lua, LUA_REGISTRYINDEX, r); + } + + m_heldLuaRefs.clear(); +} + +bool CConfigManager::isDynamicParse() const { + return !m_isParsingConfig || m_isEvaluating; +} + +void CConfigManager::reregisterLuaPluginFns() { + for (auto& fn : m_pluginLuaFunctions) { + auto ret = registerPluginLuaFunctionInState(fn.id, fn.namespace_, fn.name); + if (!ret) + Log::logger->log(Log::ERR, "[lua] failed to reregister plugin fn for {}.{}: {}", fn.namespace_, fn.name, ret.error()); + } +} diff --git a/src/config/lua/ConfigManager.hpp b/src/config/lua/ConfigManager.hpp new file mode 100644 index 000000000..773877583 --- /dev/null +++ b/src/config/lua/ConfigManager.hpp @@ -0,0 +1,189 @@ +#pragma once + +#include + +#include +#include +#include +#include +#include +#include + +#include "../../helpers/memory/Memory.hpp" +#include "../ConfigManager.hpp" +#include "../../managers/eventLoop/EventLoopTimer.hpp" + +#include "./types/LuaConfigValue.hpp" +#include "./LuaEventHandler.hpp" + +#include "../../desktop/rule/windowRule/WindowRule.hpp" +#include "../../desktop/rule/layerRule/LayerRule.hpp" + +#include "../../SharedDefs.hpp" +#include "../../managers/KeybindManager.hpp" +#include "../shared/ConfigErrors.hpp" + +extern "C" { +#include +#include +#include +} + +namespace Config::Supplementary { + struct SConfigOptionDescription; +}; + +namespace Config::Lua { + class CConfigManager; + class CConfigManagerPluginLuaTestAccessor; +} + +namespace Config::Lua::Bindings { + void registerBindings(lua_State* L, CConfigManager* mgr); +} + +namespace Config::Lua { + + class CConfigManager : public Config::IConfigManager { + public: + CConfigManager(); + + virtual eConfigManagerType type() override; + + virtual void init() override; + virtual void reload() override; + virtual std::string verify() override; + + virtual int getDeviceInt(const std::string&, const std::string&, const std::string& fallback = "") override; + virtual float getDeviceFloat(const std::string&, const std::string&, const std::string& fallback = "") override; + virtual Vector2D getDeviceVec(const std::string&, const std::string&, const std::string& fallback = "") override; + virtual std::string getDeviceString(const std::string&, const std::string&, const std::string& fallback = "") override; + virtual bool deviceConfigExplicitlySet(const std::string&, const std::string&) override; + virtual bool deviceConfigExists(const std::string&) override; + + virtual SConfigOptionReply getConfigValue(const std::string&) override; + + virtual std::string getMainConfigPath() override; + virtual std::string getErrors() override; + virtual std::string getConfigString() override; + virtual std::string currentConfigPath() override; + virtual const std::vector& getConfigPaths() override; + + virtual std::expected generateDefaultConfig(const std::filesystem::path&, bool safeMode) override; + + virtual void handlePluginLoads() override; + virtual bool configVerifPassed() override; + + virtual std::expected registerPluginValue(void* handle, SP value) override; + virtual void onPluginUnload(void* handle) override; + + int invokePluginLuaFunctionByID(uint64_t id, lua_State* L); + + std::expected registerPluginLuaFunction(void* handle, const std::string& namespace_, const std::string& name, PLUGIN_LUA_FN fn); + std::expected unregisterPluginLuaFunction(void* handle, const std::string& namespace_, const std::string& name); + + void addError(std::string&& str); + void addEvalIssue(const Config::SConfigError& err); + + void registerLuaRef(int ref); + void callLuaFn(int ref); + + // execute an arbitrary lua string on the current state. + std::optional eval(const std::string& code); + + int guardedPCall(int nargs, int nresults, int errfunc, int timeoutMs, std::string_view context); + + static CConfigManager* fromLuaState(lua_State* L); + + static constexpr int LUA_WATCHDOG_INSTRUCTION_INTERVAL = 10000; + static constexpr int LUA_TIMEOUT_CONFIG_RELOAD_MS = 1500; + static constexpr int LUA_TIMEOUT_EVENT_CALLBACK_MS = 50; + static constexpr int LUA_TIMEOUT_KEYBIND_CALLBACK_MS = 100; + static constexpr int LUA_TIMEOUT_TIMER_CALLBACK_MS = 50; + static constexpr int LUA_TIMEOUT_EVAL_MS = 250; + static constexpr int LUA_TIMEOUT_DISPATCH_MS = 100; + + bool isFirstLaunch() const; + bool isDynamicParse() const; + + std::string m_currentSubmap; + std::string m_currentSubmapReset; + + UP m_eventHandler; + + struct SLuaTimer { + SP timer; + int luaRef = LUA_NOREF; // registry ref to the lua callback + }; + std::vector m_luaTimers; + + std::vector m_registeredPlugins; + + std::unordered_map> m_configValues; + + struct SDeviceConfig { + SDeviceConfig(); + + std::unordered_map> values; + }; + + std::unordered_map m_deviceConfigs; + std::vector m_errors, m_configPaths; + std::vector m_evalIssues; + + // named window/layer rules for merge-on-redeclaration + std::unordered_map> m_luaWindowRules; + std::unordered_map> m_luaLayerRules; + + private: + void reinitLuaState(); + void postConfigReload(); + void registerValue(const char* name, ILuaConfigValue* val); + void cleanTimers(); + void clearHeldLuaRefs(); + std::string luaConfigValueName(const std::string& s); + std::expected registerPluginLuaFunctionInState(uint64_t id, const std::string& namespace_, const std::string& name); + std::expected unregisterPluginLuaFunctionInState(const std::string& namespace_, const std::string& name); + void erasePluginLuaFunction(uint64_t id); + void reregisterLuaPluginFns(); + + static void watchdogHook(lua_State* L, lua_Debug* ar); + + lua_State* m_lua = nullptr; + + bool m_lastConfigVerificationWasSuccessful = true; + bool m_isFirstLaunch = true; + bool m_manualCrashInitiated = false; + bool m_watchdogActive = false; + bool m_isParsingConfig = false; + bool m_isEvaluating = false; + + std::chrono::steady_clock::time_point m_watchdogDeadline; + std::string m_watchdogContext; + + std::string m_mainConfigPath; + + std::vector m_heldLuaRefs; + + // this is here for legacy reasons. + std::unordered_map m_configPtrMap; + + // this is here for plugin reasons. + std::unordered_map> m_pluginValues; + + struct SPluginLuaFunction { + uint64_t id = 0; + void* handle = nullptr; + std::string namespace_; + std::string name; + Config::PLUGIN_LUA_FN fn = nullptr; + }; + std::vector m_pluginLuaFunctions; + + ILuaConfigValue* findDeviceValue(const std::string& dev, const std::string& field); + + friend class CConfigManagerPluginLuaTestAccessor; + }; + + WP mgr(); +} diff --git a/src/config/lua/DefaultConfig.hpp b/src/config/lua/DefaultConfig.hpp new file mode 100644 index 000000000..6fe926790 --- /dev/null +++ b/src/config/lua/DefaultConfig.hpp @@ -0,0 +1,18 @@ +#pragma once + +#include + +inline constexpr std::string_view AUTOGENERATED_PREFIX = R"#( +-- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- +-- AUTOGENERATED HYPRLAND CONFIG. -- +-- EDIT THIS CONFIG ACCORDING TO THE WIKI INSTRUCTIONS. -- +-- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- + +hl.config({ autogenerated = true }) -- remove this line to remove the warning + +)#"; +inline constexpr char EXAMPLE_CONFIG_BYTES[] = { +#embed "../../../example/hyprland.lua" +}; + +inline constexpr std::string_view EXAMPLE_CONFIG = {EXAMPLE_CONFIG_BYTES, sizeof(EXAMPLE_CONFIG_BYTES)}; diff --git a/src/config/lua/Emergency.hpp b/src/config/lua/Emergency.hpp new file mode 100644 index 000000000..ef791d450 --- /dev/null +++ b/src/config/lua/Emergency.hpp @@ -0,0 +1,47 @@ +#pragma once + +namespace Config::Lua { + constexpr const char* EMERGENCY_PCALL = R"#( +local function shell_ok(cmd) + local a, b, c = os.execute(cmd) + if type(a) == "number" then + return a == 0 + end + return a == true and b == "exit" and c == 0 +end + +local function first_installed(candidates) + for _, bin in ipairs(candidates) do + if shell_ok(("command -v %q >/dev/null 2>&1"):format(bin)) then + return bin + end + end + return nil +end + +local function launch_first_installed(candidates) + local term = first_installed(candidates) + if not term then + return false + end + hl.dispatch(hl.dsp.exec_cmd(term)) + return true +end + +hl.bind("SUPER + Q", function() + launch_first_installed({ "kitty", "alacritty", "foot", "wezterm", "gnome-terminal", "xterm" }) +end) + +hl.bind("SUPER + R", hl.dsp.exec_cmd("hyprland-run")) +hl.bind("SUPER + M", hl.dsp.exit()) + +hl.window_rule({ + name = "move-hyprland-run", + match = { class = "hyprland-run" }, + + move = "20 monitor_h-120", + float = true, +}) + +)#"; +} \ No newline at end of file diff --git a/src/config/lua/LuaBindings.cpp b/src/config/lua/LuaBindings.cpp new file mode 100644 index 000000000..2121c33ef --- /dev/null +++ b/src/config/lua/LuaBindings.cpp @@ -0,0 +1,7 @@ +#include "LuaBindings.hpp" + +#include "bindings/LuaBindingsInternal.hpp" + +void Config::Lua::Bindings::registerBindings(lua_State* L, CConfigManager* mgr) { + Internal::registerBindingsImpl(L, mgr); +} diff --git a/src/config/lua/LuaBindings.hpp b/src/config/lua/LuaBindings.hpp new file mode 100644 index 000000000..e8f307bdd --- /dev/null +++ b/src/config/lua/LuaBindings.hpp @@ -0,0 +1,13 @@ +#pragma once + +extern "C" { +#include +} + +namespace Config::Lua { + class CConfigManager; +} + +namespace Config::Lua::Bindings { + void registerBindings(lua_State* L, CConfigManager* mgr); +} diff --git a/src/config/lua/LuaEventHandler.cpp b/src/config/lua/LuaEventHandler.cpp new file mode 100644 index 000000000..eb4f6bc52 --- /dev/null +++ b/src/config/lua/LuaEventHandler.cpp @@ -0,0 +1,233 @@ +#include "LuaEventHandler.hpp" +#include "ConfigManager.hpp" +#include "objects/LuaWindow.hpp" +#include "objects/LuaWorkspace.hpp" +#include "objects/LuaGroup.hpp" +#include "objects/LuaMonitor.hpp" +#include "objects/LuaLayerSurface.hpp" + +#include "../../event/EventBus.hpp" +#include "../../desktop/state/FocusState.hpp" + +extern "C" { +#include +} + +#include + +using namespace Config::Lua; +using namespace Config::Lua::Objects; + +void CLuaEventHandler::dispatch(const std::string& name, int nargs, const std::function& pushArgs) { + auto it = m_callbacks.find(name); + if (it == m_callbacks.end() || it->second.empty()) + return; + + if (m_dispatchDepth >= MAX_DISPATCH_DEPTH) { + Log::logger->log(Log::WARN, "[LuaEvents] max dispatch depth ({}) reached while handling '{}'", MAX_DISPATCH_DEPTH, name); + return; + } + + const auto handles = it->second; + + for (const auto handle : handles) { + const auto sub = m_subscriptions.find(handle); + if (sub == m_subscriptions.end()) + continue; + + if (m_activeHandles.contains(handle)) { + if (m_reentrancyWarnedHandles.emplace(handle).second) + Log::logger->log(Log::WARN, "[LuaEvents] suppressed recursive hl.on(\"{}\") callback invocation", name); + continue; + } + + struct SDispatchScope { + CLuaEventHandler* self = nullptr; + uint64_t handle = 0; + + ~SDispatchScope() { + if (!self) + return; + + self->m_activeHandles.erase(handle); + if (self->m_dispatchDepth > 0) + --self->m_dispatchDepth; + } + } dispatchScope{.self = this, .handle = handle}; + + m_activeHandles.emplace(handle); + ++m_dispatchDepth; + + lua_rawgeti(m_lua, LUA_REGISTRYINDEX, sub->second.luaRef); + pushArgs(); + + int status = LUA_OK; + if (auto* mgr = CConfigManager::fromLuaState(m_lua); mgr) + status = mgr->guardedPCall(nargs, 0, 0, CConfigManager::LUA_TIMEOUT_EVENT_CALLBACK_MS, std::format("hl.on(\"{}\") callback", name)); + else + status = lua_pcall(m_lua, nargs, 0, 0); + + if (status != LUA_OK) { + const char* err = lua_tostring(m_lua, -1); + Config::Lua::mgr()->addError(std::format("hl.on(\"{}\") callback: {}", name, err ? err : "(unknown)")); + lua_pop(m_lua, 1); + } + } +} + +CLuaEventHandler::CLuaEventHandler(lua_State* L) : m_lua(L) { + CLuaWindow{}.setup(L); + Objects::CLuaGroup{}.setup(L); + CLuaWorkspace{}.setup(L); + CLuaMonitor{}.setup(L); + CLuaLayerSurface{}.setup(L); + + using namespace Event; + + m_listeners.push_back(bus()->m_events.window.open.listen([this](PHLWINDOW w) { dispatch("window.open", 1, [&] { CLuaWindow::push(m_lua, w); }); })); + m_listeners.push_back(bus()->m_events.window.openEarly.listen([this](PHLWINDOW w) { dispatch("window.open_early", 1, [&] { CLuaWindow::push(m_lua, w); }); })); + m_listeners.push_back(bus()->m_events.window.close.listen([this](PHLWINDOW w) { dispatch("window.close", 1, [&] { CLuaWindow::push(m_lua, w); }); })); + m_listeners.push_back(bus()->m_events.window.destroy.listen([this](PHLWINDOW w) { dispatch("window.destroy", 1, [&] { CLuaWindow::push(m_lua, w); }); })); + m_listeners.push_back(bus()->m_events.window.kill.listen([this](PHLWINDOW w) { dispatch("window.kill", 1, [&] { CLuaWindow::push(m_lua, w); }); })); + m_listeners.push_back(bus()->m_events.window.active.listen([this](PHLWINDOW w, Desktop::eFocusReason r) { + dispatch("window.active", 2, [&] { + CLuaWindow::push(m_lua, w); + lua_pushinteger(m_lua, sc(r)); + }); + })); + m_listeners.push_back(bus()->m_events.window.urgent.listen([this](PHLWINDOW w) { dispatch("window.urgent", 1, [&] { CLuaWindow::push(m_lua, w); }); })); + m_listeners.push_back(bus()->m_events.window.title.listen([this](PHLWINDOW w) { dispatch("window.title", 1, [&] { CLuaWindow::push(m_lua, w); }); })); + m_listeners.push_back(bus()->m_events.window.class_.listen([this](PHLWINDOW w) { dispatch("window.class", 1, [&] { CLuaWindow::push(m_lua, w); }); })); + m_listeners.push_back(bus()->m_events.window.pin.listen([this](PHLWINDOW w) { dispatch("window.pin", 1, [&] { CLuaWindow::push(m_lua, w); }); })); + m_listeners.push_back(bus()->m_events.window.fullscreen.listen([this](PHLWINDOW w) { dispatch("window.fullscreen", 1, [&] { CLuaWindow::push(m_lua, w); }); })); + m_listeners.push_back(bus()->m_events.window.updateRules.listen([this](PHLWINDOW w) { dispatch("window.update_rules", 1, [&] { CLuaWindow::push(m_lua, w); }); })); + m_listeners.push_back(bus()->m_events.window.moveToWorkspace.listen([this](PHLWINDOW w, PHLWORKSPACE ws) { + dispatch("window.move_to_workspace", 2, [&] { + CLuaWindow::push(m_lua, w); + CLuaWorkspace::push(m_lua, ws); + }); + })); + + m_listeners.push_back(bus()->m_events.layer.opened.listen([this](PHLLS ls) { dispatch("layer.opened", 1, [&] { CLuaLayerSurface::push(m_lua, ls); }); })); + m_listeners.push_back(bus()->m_events.layer.closed.listen([this](PHLLS ls) { dispatch("layer.closed", 1, [&] { CLuaLayerSurface::push(m_lua, ls); }); })); + + m_listeners.push_back(bus()->m_events.monitor.added.listen([this](PHLMONITOR mon) { dispatch("monitor.added", 1, [&] { CLuaMonitor::push(m_lua, mon); }); })); + m_listeners.push_back(bus()->m_events.monitor.removed.listen([this](PHLMONITOR mon) { dispatch("monitor.removed", 1, [&] { CLuaMonitor::push(m_lua, mon); }); })); + m_listeners.push_back(bus()->m_events.monitor.focused.listen([this](PHLMONITOR mon) { dispatch("monitor.focused", 1, [&] { CLuaMonitor::push(m_lua, mon); }); })); + m_listeners.push_back(bus()->m_events.monitor.layoutChanged.listen([this] { dispatch("monitor.layout_changed", 0, [] {}); })); + + m_listeners.push_back(bus()->m_events.workspace.active.listen([this](PHLWORKSPACE ws) { dispatch("workspace.active", 1, [&] { CLuaWorkspace::push(m_lua, ws); }); })); + m_listeners.push_back(bus()->m_events.workspace.created.listen([this](PHLWORKSPACEREF wsRef) { + const auto ws = wsRef.lock(); + if (!ws) + return; + dispatch("workspace.created", 1, [&] { CLuaWorkspace::push(m_lua, ws); }); + })); + m_listeners.push_back(bus()->m_events.workspace.removed.listen([this](PHLWORKSPACEREF wsRef) { + const auto ws = wsRef.lock(); + if (!ws) + return; + dispatch("workspace.removed", 1, [&] { CLuaWorkspace::push(m_lua, ws); }); + })); + m_listeners.push_back(bus()->m_events.workspace.moveToMonitor.listen([this](PHLWORKSPACE ws, PHLMONITOR mon) { + dispatch("workspace.move_to_monitor", 2, [&] { + CLuaWorkspace::push(m_lua, ws); + CLuaMonitor::push(m_lua, mon); + }); + })); + + m_listeners.push_back(bus()->m_events.config.reloaded.listen([this] { dispatch("config.reloaded", 0, [] {}); })); + m_listeners.push_back( + bus()->m_events.keybinds.submap.listen([this](const std::string& submap) { dispatch("keybinds.submap", 1, [&] { lua_pushstring(m_lua, submap.c_str()); }); })); + m_listeners.push_back(bus()->m_events.screenshare.state.listen([this](bool state, uint8_t type, const std::string& name) { + dispatch("screenshare.state", 3, [&] { + lua_pushboolean(m_lua, state); + lua_pushinteger(m_lua, sc(type)); + lua_pushstring(m_lua, name.c_str()); + }); + })); + + m_listeners.push_back(bus()->m_events.start.listen([this]() { dispatch("hyprland.start", 0, [] {}); })); + m_listeners.push_back(bus()->m_events.exit.listen([this]() { dispatch("hyprland.shutdown", 0, [] {}); })); +} + +CLuaEventHandler::~CLuaEventHandler() { + clearEvents(); +} + +std::optional CLuaEventHandler::registerEvent(const std::string& name, int luaRef) { + if (!knownEvents().contains(name)) + return std::nullopt; + + const auto handle = m_nextHandle++; + m_callbacks[name].push_back(handle); + m_subscriptions[handle] = {.eventName = name, .luaRef = luaRef}; + + return handle; +} + +bool CLuaEventHandler::unregisterEvent(uint64_t handle) { + const auto it = m_subscriptions.find(handle); + if (it == m_subscriptions.end()) + return false; + + luaL_unref(m_lua, LUA_REGISTRYINDEX, it->second.luaRef); + + auto callbacksIt = m_callbacks.find(it->second.eventName); + if (callbacksIt != m_callbacks.end()) { + std::erase(callbacksIt->second, handle); + if (callbacksIt->second.empty()) + m_callbacks.erase(callbacksIt); + } + + m_subscriptions.erase(it); + m_activeHandles.erase(handle); + m_reentrancyWarnedHandles.erase(handle); + + return true; +} + +void CLuaEventHandler::clearEvents() { + for (const auto& s : m_subscriptions) { + luaL_unref(m_lua, LUA_REGISTRYINDEX, s.second.luaRef); + } + + m_subscriptions.clear(); + m_activeHandles.clear(); + m_reentrancyWarnedHandles.clear(); + m_callbacks.clear(); +} + +const std::unordered_set& CLuaEventHandler::knownEvents() { + static const std::unordered_set EVENTS = { + "window.open", + "window.open_early", + "window.close", + "window.destroy", + "window.kill", + "window.active", + "window.urgent", + "window.title", + "window.class", + "window.pin", + "window.fullscreen", + "window.update_rules", + "window.move_to_workspace", + "layer.opened", + "layer.closed", + "monitor.added", + "monitor.removed", + "monitor.focused", + "monitor.layout_changed", + "workspace.active", + "workspace.created", + "workspace.removed", + "workspace.move_to_monitor", + "config.reloaded", + "keybinds.submap", + "screenshare.state", + "hyprland.start", + "hyprland.shutdown", + }; + return EVENTS; +} diff --git a/src/config/lua/LuaEventHandler.hpp b/src/config/lua/LuaEventHandler.hpp new file mode 100644 index 000000000..a48d78004 --- /dev/null +++ b/src/config/lua/LuaEventHandler.hpp @@ -0,0 +1,61 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include + +#include "../../helpers/memory/Memory.hpp" +#include "../../helpers/signal/Signal.hpp" +#include "../../desktop/DesktopTypes.hpp" + +extern "C" { +#include +} + +namespace Config::Lua { + + // Manages hl.on() event subscriptions for a single Lua state lifetime. + // Destroyed (and recreated) on every config reload so callbacks never double-up. + // + // Hyprland objects (window, workspace, monitor, layer surface) are exposed to Lua + // as typed userdata holding a weak pointer. Field accesses read live C++ state via + // __index; accessing a field on a destroyed object raises a Lua error. + class CLuaEventHandler { + public: + explicit CLuaEventHandler(lua_State* L); + ~CLuaEventHandler(); + + // Store a Lua function (as a registry ref) to be called when `name` fires. + // Returns a subscription handle, or std::nullopt if the event name is unknown. + std::optional registerEvent(const std::string& name, int luaRef); + bool unregisterEvent(uint64_t handle); + + void clearEvents(); + + static const std::unordered_set& knownEvents(); + + private: + struct SSubscription { + std::string eventName; + int luaRef = -1; + }; + + lua_State* m_lua = nullptr; + std::unordered_map> m_callbacks; + std::unordered_map m_subscriptions; + std::unordered_set m_activeHandles; + std::unordered_set m_reentrancyWarnedHandles; + uint64_t m_nextHandle = 1; + size_t m_dispatchDepth = 0; + std::vector m_listeners; + + static constexpr size_t MAX_DISPATCH_DEPTH = 32; + + void dispatch(const std::string& name, int nargs, const std::function& pushArgs); + }; + +} diff --git a/src/config/lua/bindings/LuaBindingsConfigRules.cpp b/src/config/lua/bindings/LuaBindingsConfigRules.cpp new file mode 100644 index 000000000..14e172649 --- /dev/null +++ b/src/config/lua/bindings/LuaBindingsConfigRules.cpp @@ -0,0 +1,1295 @@ +#include "LuaBindingsInternal.hpp" + +#include "../objects/LuaLayerRule.hpp" +#include "../objects/LuaWindowRule.hpp" + +#include "../types/LuaConfigBool.hpp" +#include "../types/LuaConfigCssGap.hpp" +#include "../types/LuaConfigFloat.hpp" +#include "../types/LuaConfigGradient.hpp" +#include "../types/LuaConfigInt.hpp" +#include "../types/LuaConfigString.hpp" +#include "../types/LuaConfigVec2.hpp" + +#include "../../supplementary/executor/Executor.hpp" +#include "../../supplementary/propRefresher/PropRefresher.hpp" +#include "../../shared/animation/AnimationTree.hpp" +#include "../../shared/monitor/MonitorRuleManager.hpp" +#include "../../shared/monitor/Parser.hpp" +#include "../../shared/workspace/WorkspaceRuleManager.hpp" + +#include "../../../desktop/rule/Engine.hpp" +#include "../../../desktop/rule/layerRule/LayerRule.hpp" +#include "../../../desktop/rule/layerRule/LayerRuleEffectContainer.hpp" +#include "../../../desktop/rule/windowRule/WindowRule.hpp" +#include "../../../desktop/rule/windowRule/WindowRuleEffectContainer.hpp" +#include "../../../layout/LayoutManager.hpp" +#include "../../../layout/supplementary/WorkspaceAlgoMatcher.hpp" +#include "../../../managers/animation/AnimationManager.hpp" +#include "../../../managers/input/InputManager.hpp" +#include "../../../managers/input/trackpad/TrackpadGestures.hpp" +#include "../../../managers/input/trackpad/gestures/CloseGesture.hpp" +#include "../../../managers/input/trackpad/gestures/CursorZoomGesture.hpp" +#include "../../../managers/input/trackpad/gestures/DispatcherGesture.hpp" +#include "../../../managers/input/trackpad/gestures/FloatGesture.hpp" +#include "../../../managers/input/trackpad/gestures/FullscreenGesture.hpp" +#include "../../../managers/input/trackpad/gestures/LuaFunctionGesture.hpp" +#include "../../../managers/input/trackpad/gestures/MoveGesture.hpp" +#include "../../../managers/input/trackpad/gestures/ResizeGesture.hpp" +#include "../../../managers/input/trackpad/gestures/SpecialWorkspaceGesture.hpp" +#include "../../../managers/input/trackpad/gestures/WorkspaceSwipeGesture.hpp" +#include "../../../managers/permissions/DynamicPermissionManager.hpp" + +#include + +using namespace Config; +using namespace Config::Lua; +using namespace Config::Lua::Bindings; +using namespace Hyprutils::Utils; + +namespace { + struct SFieldDesc { + const char* name; + std::function factory; + }; + + struct SMonitorFieldDesc { + const char* name; + std::function factory; + std::function apply; + }; + + struct SLayerRuleEffectDesc { + const char* name; + std::function factory; + uint16_t effect; + }; + + struct SWorkspaceRuleFieldDesc { + const char* name; + std::function factory; + std::function apply; + }; + + using LE = Desktop::Rule::eLayerRuleEffect; + + inline const SMonitorFieldDesc MONITOR_FIELDS[] = { + {"mode", []() -> ILuaConfigValue* { return new CLuaConfigString("preferred"); }, + [](ILuaConfigValue* v, CMonitorRuleParser& p) { return p.parseMode(*sc(v->data())); }}, + {"position", []() -> ILuaConfigValue* { return new CLuaConfigString("auto"); }, + [](ILuaConfigValue* v, CMonitorRuleParser& p) { return p.parsePosition(*sc(v->data())); }}, + {"scale", []() -> ILuaConfigValue* { return new CLuaConfigString("auto"); }, + [](ILuaConfigValue* v, CMonitorRuleParser& p) { return p.parseScale(*sc(v->data())); }}, + {"reserved", []() -> ILuaConfigValue* { return new CLuaConfigCssGap(0); }, + [](ILuaConfigValue* v, CMonitorRuleParser& p) { + const auto& gap = *sc(v->data()); + return p.setReserved(Desktop::CReservedArea(gap.m_top, gap.m_right, gap.m_bottom, gap.m_left)); + }}, + {"reserved_area", []() -> ILuaConfigValue* { return new CLuaConfigCssGap(0); }, + [](ILuaConfigValue* v, CMonitorRuleParser& p) { + const auto& gap = *sc(v->data()); + return p.setReserved(Desktop::CReservedArea(gap.m_top, gap.m_right, gap.m_bottom, gap.m_left)); + }}, + {"disabled", []() -> ILuaConfigValue* { return new CLuaConfigBool(false); }, + [](ILuaConfigValue* v, CMonitorRuleParser& p) { + if (*sc(v->data())) + p.setDisabled(); + return true; + }}, + {"transform", []() -> ILuaConfigValue* { return new CLuaConfigInt(0, 0, 7); }, + [](ILuaConfigValue* v, CMonitorRuleParser& p) { + p.rule().m_transform = sc(*sc(v->data())); + return true; + }}, + {"mirror", []() -> ILuaConfigValue* { return new CLuaConfigString(STRVAL_EMPTY); }, + [](ILuaConfigValue* v, CMonitorRuleParser& p) { + p.setMirror(*sc(v->data())); + return true; + }}, + {"bitdepth", []() -> ILuaConfigValue* { return new CLuaConfigInt(8); }, + [](ILuaConfigValue* v, CMonitorRuleParser& p) { + p.rule().m_enable10bit = *sc(v->data()) == 10; + return true; + }}, + {"cm", []() -> ILuaConfigValue* { return new CLuaConfigString("srgb"); }, + [](ILuaConfigValue* v, CMonitorRuleParser& p) { return p.parseCM(*sc(v->data())); }}, + {"sdr_eotf", []() -> ILuaConfigValue* { return new CLuaConfigString("default"); }, + [](ILuaConfigValue* v, CMonitorRuleParser& p) { + p.rule().m_sdrEotf = NTransferFunction::fromString(*sc(v->data())); + return true; + }}, + {"sdrbrightness", []() -> ILuaConfigValue* { return new CLuaConfigFloat(1.F); }, + [](ILuaConfigValue* v, CMonitorRuleParser& p) { + p.rule().m_sdrBrightness = *sc(v->data()); + return true; + }}, + {"sdrsaturation", []() -> ILuaConfigValue* { return new CLuaConfigFloat(1.F); }, + [](ILuaConfigValue* v, CMonitorRuleParser& p) { + p.rule().m_sdrSaturation = *sc(v->data()); + return true; + }}, + {"vrr", []() -> ILuaConfigValue* { return new CLuaConfigInt(0, 0, 3); }, + [](ILuaConfigValue* v, CMonitorRuleParser& p) { + p.rule().m_vrr = sc(*sc(v->data())); + return true; + }}, + {"icc", []() -> ILuaConfigValue* { return new CLuaConfigString(STRVAL_EMPTY); }, + [](ILuaConfigValue* v, CMonitorRuleParser& p) { return p.parseICC(*sc(v->data())); }}, + {"supports_wide_color", []() -> ILuaConfigValue* { return new CLuaConfigInt(0, -1, 1); }, + [](ILuaConfigValue* v, CMonitorRuleParser& p) { + p.rule().m_supportsWideColor = sc(*sc(v->data())); + return true; + }}, + {"supports_hdr", []() -> ILuaConfigValue* { return new CLuaConfigInt(0, -1, 1); }, + [](ILuaConfigValue* v, CMonitorRuleParser& p) { + p.rule().m_supportsHDR = sc(*sc(v->data())); + return true; + }}, + {"sdr_min_luminance", []() -> ILuaConfigValue* { return new CLuaConfigFloat(0.2F); }, + [](ILuaConfigValue* v, CMonitorRuleParser& p) { + p.rule().m_sdrMinLuminance = *sc(v->data()); + return true; + }}, + {"sdr_max_luminance", []() -> ILuaConfigValue* { return new CLuaConfigInt(80); }, + [](ILuaConfigValue* v, CMonitorRuleParser& p) { + p.rule().m_sdrMaxLuminance = sc(*sc(v->data())); + return true; + }}, + {"min_luminance", []() -> ILuaConfigValue* { return new CLuaConfigFloat(-1.F); }, + [](ILuaConfigValue* v, CMonitorRuleParser& p) { + p.rule().m_minLuminance = *sc(v->data()); + return true; + }}, + {"max_luminance", []() -> ILuaConfigValue* { return new CLuaConfigInt(-1); }, + [](ILuaConfigValue* v, CMonitorRuleParser& p) { + p.rule().m_maxLuminance = sc(*sc(v->data())); + return true; + }}, + {"max_avg_luminance", []() -> ILuaConfigValue* { return new CLuaConfigInt(-1); }, + [](ILuaConfigValue* v, CMonitorRuleParser& p) { + p.rule().m_maxAvgLuminance = sc(*sc(v->data())); + return true; + }}, + }; + + static_assert(sizeof(Internal::WINDOW_RULE_EFFECT_DESCS) / sizeof(Internal::SWindowRuleEffectDesc) == Internal::WE::WINDOW_RULE_EFFECT_LAST_STATIC - 1); + + inline const SLayerRuleEffectDesc LAYER_RULE_EFFECT_DESCS[] = { + {"no_anim", []() -> ILuaConfigValue* { return new CLuaConfigBool(false); }, LE::LAYER_RULE_EFFECT_NO_ANIM}, + {"blur", []() -> ILuaConfigValue* { return new CLuaConfigBool(false); }, LE::LAYER_RULE_EFFECT_BLUR}, + {"blur_popups", []() -> ILuaConfigValue* { return new CLuaConfigBool(false); }, LE::LAYER_RULE_EFFECT_BLUR_POPUPS}, + {"ignore_alpha", []() -> ILuaConfigValue* { return new CLuaConfigFloat(0.F, 0.F, 1.F); }, LE::LAYER_RULE_EFFECT_IGNORE_ALPHA}, + {"dim_around", []() -> ILuaConfigValue* { return new CLuaConfigBool(false); }, LE::LAYER_RULE_EFFECT_DIM_AROUND}, + {"xray", []() -> ILuaConfigValue* { return new CLuaConfigBool(false); }, LE::LAYER_RULE_EFFECT_XRAY}, + {"animation", []() -> ILuaConfigValue* { return new CLuaConfigString(STRVAL_EMPTY); }, LE::LAYER_RULE_EFFECT_ANIMATION}, + {"order", []() -> ILuaConfigValue* { return new CLuaConfigInt(0); }, LE::LAYER_RULE_EFFECT_ORDER}, + {"above_lock", []() -> ILuaConfigValue* { return new CLuaConfigInt(0, 0, 2); }, LE::LAYER_RULE_EFFECT_ABOVE_LOCK}, + {"no_screen_share", []() -> ILuaConfigValue* { return new CLuaConfigBool(false); }, LE::LAYER_RULE_EFFECT_NO_SCREEN_SHARE}, + }; + + static_assert(sizeof(LAYER_RULE_EFFECT_DESCS) / sizeof(SLayerRuleEffectDesc) == LE::LAYER_RULE_EFFECT_LAST_STATIC - 1); + + inline const SWorkspaceRuleFieldDesc WORKSPACE_RULE_FIELDS[] = { + {"monitor", []() -> ILuaConfigValue* { return new CLuaConfigString(STRVAL_EMPTY); }, + [](ILuaConfigValue* v, Config::CWorkspaceRule& r) { r.m_monitor = *sc(v->data()); }}, + {"default", []() -> ILuaConfigValue* { return new CLuaConfigBool(false); }, + [](ILuaConfigValue* v, Config::CWorkspaceRule& r) { r.m_isDefault = *sc(v->data()); }}, + {"persistent", []() -> ILuaConfigValue* { return new CLuaConfigBool(false); }, + [](ILuaConfigValue* v, Config::CWorkspaceRule& r) { r.m_isPersistent = *sc(v->data()); }}, + {"gaps_in", []() -> ILuaConfigValue* { return new CLuaConfigCssGap(5); }, + [](ILuaConfigValue* v, Config::CWorkspaceRule& r) { r.m_gapsIn = *sc(v->data()); }}, + {"gaps_out", []() -> ILuaConfigValue* { return new CLuaConfigCssGap(20); }, + [](ILuaConfigValue* v, Config::CWorkspaceRule& r) { r.m_gapsOut = *sc(v->data()); }}, + {"float_gaps", []() -> ILuaConfigValue* { return new CLuaConfigCssGap(0); }, + [](ILuaConfigValue* v, Config::CWorkspaceRule& r) { r.m_floatGaps = *sc(v->data()); }}, + {"border_size", []() -> ILuaConfigValue* { return new CLuaConfigInt(-1); }, + [](ILuaConfigValue* v, Config::CWorkspaceRule& r) { r.m_borderSize = *sc(v->data()); }}, + {"no_border", []() -> ILuaConfigValue* { return new CLuaConfigBool(false); }, + [](ILuaConfigValue* v, Config::CWorkspaceRule& r) { r.m_noBorder = *sc(v->data()); }}, + {"no_rounding", []() -> ILuaConfigValue* { return new CLuaConfigBool(false); }, + [](ILuaConfigValue* v, Config::CWorkspaceRule& r) { r.m_noRounding = *sc(v->data()); }}, + {"decorate", []() -> ILuaConfigValue* { return new CLuaConfigBool(true); }, + [](ILuaConfigValue* v, Config::CWorkspaceRule& r) { r.m_decorate = *sc(v->data()); }}, + {"no_shadow", []() -> ILuaConfigValue* { return new CLuaConfigBool(false); }, + [](ILuaConfigValue* v, Config::CWorkspaceRule& r) { r.m_noShadow = *sc(v->data()); }}, + {"on_created_empty", []() -> ILuaConfigValue* { return new CLuaConfigString(STRVAL_EMPTY); }, + [](ILuaConfigValue* v, Config::CWorkspaceRule& r) { r.m_onCreatedEmptyRunCmd = *sc(v->data()); }}, + {"default_name", []() -> ILuaConfigValue* { return new CLuaConfigString(STRVAL_EMPTY); }, + [](ILuaConfigValue* v, Config::CWorkspaceRule& r) { r.m_defaultName = *sc(v->data()); }}, + {"layout", []() -> ILuaConfigValue* { return new CLuaConfigString(STRVAL_EMPTY); }, + [](ILuaConfigValue* v, Config::CWorkspaceRule& r) { r.m_layout = *sc(v->data()); }}, + {"animation", []() -> ILuaConfigValue* { return new CLuaConfigString(STRVAL_EMPTY); }, + [](ILuaConfigValue* v, Config::CWorkspaceRule& r) { r.m_animationStyle = *sc(v->data()); }}, + }; + + inline const SFieldDesc DEVICE_FIELDS[] = { + {"sensitivity", []() -> ILuaConfigValue* { return new CLuaConfigFloat(0.F, -1.F, 1.F); }}, + {"accel_profile", []() -> ILuaConfigValue* { return new CLuaConfigString(STRVAL_EMPTY); }}, + {"rotation", []() -> ILuaConfigValue* { return new CLuaConfigInt(0, 0, 359); }}, + {"kb_file", []() -> ILuaConfigValue* { return new CLuaConfigString(STRVAL_EMPTY); }}, + {"kb_layout", []() -> ILuaConfigValue* { return new CLuaConfigString("us"); }}, + {"kb_variant", []() -> ILuaConfigValue* { return new CLuaConfigString(STRVAL_EMPTY); }}, + {"kb_options", []() -> ILuaConfigValue* { return new CLuaConfigString(STRVAL_EMPTY); }}, + {"kb_rules", []() -> ILuaConfigValue* { return new CLuaConfigString(STRVAL_EMPTY); }}, + {"kb_model", []() -> ILuaConfigValue* { return new CLuaConfigString(STRVAL_EMPTY); }}, + {"repeat_rate", []() -> ILuaConfigValue* { return new CLuaConfigInt(25, 0, 200); }}, + {"repeat_delay", []() -> ILuaConfigValue* { return new CLuaConfigInt(600, 0, 2000); }}, + {"natural_scroll", []() -> ILuaConfigValue* { return new CLuaConfigBool(false); }}, + {"tap_button_map", []() -> ILuaConfigValue* { return new CLuaConfigString(STRVAL_EMPTY); }}, + {"numlock_by_default", []() -> ILuaConfigValue* { return new CLuaConfigBool(false); }}, + {"resolve_binds_by_sym", []() -> ILuaConfigValue* { return new CLuaConfigBool(false); }}, + {"disable_while_typing", []() -> ILuaConfigValue* { return new CLuaConfigBool(true); }}, + {"clickfinger_behavior", []() -> ILuaConfigValue* { return new CLuaConfigBool(false); }}, + {"middle_button_emulation", []() -> ILuaConfigValue* { return new CLuaConfigBool(false); }}, + {"tap_to_click", []() -> ILuaConfigValue* { return new CLuaConfigBool(true); }}, + {"tap_and_drag", []() -> ILuaConfigValue* { return new CLuaConfigBool(true); }}, + {"drag_lock", []() -> ILuaConfigValue* { return new CLuaConfigInt(0, 0, 2); }}, + {"left_handed", []() -> ILuaConfigValue* { return new CLuaConfigBool(false); }}, + {"scroll_method", []() -> ILuaConfigValue* { return new CLuaConfigString(STRVAL_EMPTY); }}, + {"scroll_button", []() -> ILuaConfigValue* { return new CLuaConfigInt(0, 0, 300); }}, + {"scroll_button_lock", []() -> ILuaConfigValue* { return new CLuaConfigBool(false); }}, + {"scroll_points", []() -> ILuaConfigValue* { return new CLuaConfigString(STRVAL_EMPTY); }}, + {"scroll_factor", []() -> ILuaConfigValue* { return new CLuaConfigFloat(1.F, 0.F, 100.F); }}, + {"transform", []() -> ILuaConfigValue* { return new CLuaConfigInt(-1); }}, + {"output", []() -> ILuaConfigValue* { return new CLuaConfigString(STRVAL_EMPTY); }}, + {"enabled", []() -> ILuaConfigValue* { return new CLuaConfigBool(true); }}, + {"region_position", []() -> ILuaConfigValue* { return new CLuaConfigVec2({0, 0}); }}, + {"absolute_region_position", []() -> ILuaConfigValue* { return new CLuaConfigBool(false); }}, + {"region_size", []() -> ILuaConfigValue* { return new CLuaConfigVec2({0, 0}); }}, + {"relative_input", []() -> ILuaConfigValue* { return new CLuaConfigBool(false); }}, + {"active_area_position", []() -> ILuaConfigValue* { return new CLuaConfigVec2({0, 0}); }}, + {"active_area_size", []() -> ILuaConfigValue* { return new CLuaConfigVec2({0, 0}); }}, + {"flip_x", []() -> ILuaConfigValue* { return new CLuaConfigBool(false); }}, + {"flip_y", []() -> ILuaConfigValue* { return new CLuaConfigBool(false); }}, + {"drag_3fg", []() -> ILuaConfigValue* { return new CLuaConfigInt(0, 0, 2); }}, + {"keybinds", []() -> ILuaConfigValue* { return new CLuaConfigBool(true); }}, + {"share_states", []() -> ILuaConfigValue* { return new CLuaConfigInt(0, 0, 2); }}, + {"release_pressed_on_close", []() -> ILuaConfigValue* { return new CLuaConfigBool(false); }}, + }; + +} + +static int hlCurve(lua_State* L) { + CLuaConfigString nameParser(""); + lua_pushvalue(L, 1); + auto nameErr = nameParser.parse(L); + lua_pop(L, 1); + if (nameErr.errorCode != PARSE_ERROR_OK) + return Internal::configError(L, std::format("hl.curve: first argument (name) must be a string: {}", nameErr.message)); + + const auto& name = nameParser.parsed(); + + if (!lua_istable(L, 2)) + return Internal::configError(L, "hl.curve: second argument must be a table, e.g. { type = \"bezier\", points = { {0, 0}, {1, 1} } }"); + + CLuaConfigString typeParser(""); + auto typeErr = Internal::parseTableField(L, 2, "type", typeParser); + if (typeErr.errorCode != PARSE_ERROR_OK) + return Internal::configError(L, std::format("hl.curve(\"{}\"): {}", name, typeErr.message)); + + const auto& curveType = typeParser.parsed(); + + if (curveType == "bezier") { + lua_getfield(L, 2, "points"); + if (!lua_istable(L, -1)) { + lua_pop(L, 1); + return Internal::configError(L, std::format("hl.curve(\"{}\"): missing or invalid \"points\" field, expected a table of two points", name)); + } + int pointsIdx = lua_gettop(L); + + if (luaL_len(L, pointsIdx) != 2) { + lua_pop(L, 1); + return Internal::configError(L, std::format("hl.curve(\"{}\"): \"points\" must contain exactly 2 points, e.g. {{ {{0, 0}}, {{1, 1}} }}", name)); + } + + float coords[4] = {}; + for (int pt = 1; pt <= 2; pt++) { + lua_rawgeti(L, pointsIdx, pt); + if (!lua_istable(L, -1) || luaL_len(L, -1) != 2) { + lua_pop(L, 2); + return Internal::configError(L, std::format("hl.curve(\"{}\"): point {} must be a table of 2 numbers, e.g. {{0.25, 0.1}}", name, pt)); + } + int ptIdx = lua_gettop(L); + + for (int comp = 0; comp < 2; comp++) { + lua_rawgeti(L, ptIdx, comp + 1); + CLuaConfigFloat coordParser(0.F, -1.F, 2.F); + auto coordErr = coordParser.parse(L); + lua_pop(L, 1); + if (coordErr.errorCode != PARSE_ERROR_OK) { + lua_pop(L, 2); + return Internal::configError(L, std::format("hl.curve(\"{}\"): point {}[{}]: {}", name, pt, comp + 1, coordErr.message)); + } + coords[((pt - 1) * 2) + comp] = coordParser.parsed(); + } + + lua_pop(L, 1); + } + lua_pop(L, 1); + + g_pAnimationManager->addBezierWithName(name, Vector2D(coords[0], coords[1]), Vector2D(coords[2], coords[3])); + } else if (curveType == "spring") { + + Hyprutils::Animation::SSpringCurve curve; + + { + CScopeGuard x([L] { lua_pop(L, 1); }); + + lua_getfield(L, 2, "stiffness"); + + if (!lua_isnumber(L, -1)) + return Internal::configError(L, std::format("hl.curve(\"{}\"): stiffness expects a number", name)); + + curve.stiffness = lua_tonumber(L, -1); + + if (curve.stiffness <= 0.5F) + return Internal::configError(L, std::format("hl.curve(\"{}\"): stiffness expects a number >= 0.5", name)); + } + + { + CScopeGuard x([L] { lua_pop(L, 1); }); + + lua_getfield(L, 2, "dampening"); + + if (!lua_isnumber(L, -1)) + return Internal::configError(L, std::format("hl.curve(\"{}\"): dampening expects a number", name)); + + curve.damping = lua_tonumber(L, -1); + + if (curve.damping <= 0.5F) + return Internal::configError(L, std::format("hl.curve(\"{}\"): dampening expects a number >= 0.5", name)); + } + + { + CScopeGuard x([L] { lua_pop(L, 1); }); + + lua_getfield(L, 2, "mass"); + + if (!lua_isnumber(L, -1)) + return Internal::configError(L, std::format("hl.curve(\"{}\"): mass expects a number", name)); + + curve.mass = lua_tonumber(L, -1); + + if (curve.mass <= 0.5F) + return Internal::configError(L, std::format("hl.curve(\"{}\"): mass expects a number >= 0.5", name)); + } + + g_pAnimationManager->addSpringWithName(name, curve); + } else + return Internal::configError(L, std::format(R"(hl.curve("{}"): unknown curve type "{}", expected "bezier" or "spring")", name, curveType)); + + return 0; +} + +static int hlAnimation(lua_State* L) { + if (!lua_istable(L, 1)) + return Internal::configError(L, R"(hl.animation: expected a table, e.g. { leaf = "global", enabled = true, speed = 5, bezier = "default" })"); + + CLuaConfigString leafParser(""); + auto leafErr = Internal::parseTableField(L, 1, "leaf", leafParser); + if (leafErr.errorCode != PARSE_ERROR_OK) + return Internal::configError(L, std::format("hl.animation: {}", leafErr.message)); + + const auto leaf = leafParser.parsed(); + + if (!Config::animationTree()->nodeExists(leaf)) + return Internal::configError(L, std::format("hl.animation: no such animation leaf \"{}\"", leaf)); + + CLuaConfigBool enabledParser(true); + auto enabledErr = Internal::parseTableField(L, 1, "enabled", enabledParser); + if (enabledErr.errorCode != PARSE_ERROR_OK) + return Internal::configError(L, std::format("hl.animation(\"{}\"): {}", leaf, enabledErr.message)); + + bool enabled = enabledParser.parsed(); + + if (!enabled) { + Config::animationTree()->setConfigForNode(leaf, false, 1, "default"); + return 0; + } + + CLuaConfigFloat speedParser(0.F, 0.F, 100.F); + auto speedErr = Internal::parseTableField(L, 1, "speed", speedParser); + if (speedErr.errorCode != PARSE_ERROR_OK) + return Internal::configError(L, std::format("hl.animation(\"{}\"): {}", leaf, speedErr.message)); + + float speed = speedParser.parsed(); + + if (speed <= 0) + return Internal::configError(L, std::format("hl.animation(\"{}\"): speed must be greater than 0", leaf)); + + std::string curveName; + + if (Internal::hasTableField(L, 1, "bezier")) { + CLuaConfigString bezierParser(""); + auto bezierErr = Internal::parseTableField(L, 1, "bezier", bezierParser); + if (bezierErr.errorCode != PARSE_ERROR_OK) + return Internal::configError(L, std::format("hl.animation(\"{}\"): {}", leaf, bezierErr.message)); + + const auto& bezierName = bezierParser.parsed(); + + if (!g_pAnimationManager->bezierExists(bezierName)) + return Internal::configError(L, std::format(R"(hl.animation("{}"): no such bezier "{}")", leaf, bezierName)); + + curveName = bezierName; + } else if (Internal::hasTableField(L, 1, "spring")) { + CLuaConfigString springParser(""); + auto springErr = Internal::parseTableField(L, 1, "spring", springParser); + if (springErr.errorCode != PARSE_ERROR_OK) + return Internal::configError(L, std::format("hl.animation(\"{}\"): {}", leaf, springErr.message)); + + const auto& springName = springParser.parsed(); + + if (!g_pAnimationManager->springExists(springName)) + return Internal::configError(L, std::format(R"(hl.animation("{}"): no such spring "{}")", leaf, springName)); + + curveName = "spring:" + springName; + } else + return Internal::configError(L, std::format(R"(hl.animation("{}"): bezier or spring is required)", leaf)); + + std::string style; + lua_getfield(L, 1, "style"); + if (!lua_isnil(L, -1)) { + CLuaConfigString styleParser(""); + auto styleErr = styleParser.parse(L); + if (styleErr.errorCode != PARSE_ERROR_OK) { + lua_pop(L, 1); + return Internal::configError(L, std::format(R"(hl.animation("{}"): field "style": {})", leaf, styleErr.message)); + } + style = styleParser.parsed(); + } + lua_pop(L, 1); + + if (!style.empty()) { + auto err = g_pAnimationManager->styleValidInConfigVar(leaf, style); + if (!err.empty()) + return Internal::configError(L, std::format("hl.animation(\"{}\"): {}", leaf, err)); + } + + Config::animationTree()->setConfigForNode(leaf, true, speed, curveName, style); + return 0; +} + +static int hlEnv(lua_State* L) { + auto* mgr = sc(lua_touserdata(L, lua_upvalueindex(1))); + + CLuaConfigString nameParser(""); + lua_pushvalue(L, 1); + auto nameErr = nameParser.parse(L); + lua_pop(L, 1); + if (nameErr.errorCode != PARSE_ERROR_OK) + return Internal::configError(L, std::format("hl.env: first argument (name) must be a string: {}", nameErr.message)); + + const auto& name = nameParser.parsed(); + + if (name.empty()) + return Internal::configError(L, "hl.env: name must not be empty"); + + CLuaConfigString valueParser(""); + lua_pushvalue(L, 2); + auto valueErr = valueParser.parse(L); + lua_pop(L, 1); + if (valueErr.errorCode != PARSE_ERROR_OK) + return Internal::configError(L, std::format("hl.env: second argument (value) must be a string: {}", valueErr.message)); + + const auto& value = valueParser.parsed(); + + if (!mgr->isFirstLaunch()) { + const auto* ENV = getenv(name.c_str()); + if (ENV && ENV == value) + return 0; + } + + setenv(name.c_str(), value.c_str(), 1); + + bool dbus = false; + if (!lua_isnoneornil(L, 3)) { + CLuaConfigBool dbusParser(false); + lua_pushvalue(L, 3); + auto dbusErr = dbusParser.parse(L); + lua_pop(L, 1); + if (dbusErr.errorCode != PARSE_ERROR_OK) + return Internal::configError(L, std::format("hl.env: third argument (dbus) must be a boolean: {}", dbusErr.message)); + + dbus = dbusParser.parsed(); + } + + if (dbus) { + std::string CMD; +#ifdef USES_SYSTEMD + CMD = "systemctl --user import-environment '" + name + "' && hash dbus-update-activation-environment 2>/dev/null && "; +#endif + CMD += "dbus-update-activation-environment --systemd '" + name + "'"; + if (mgr->isFirstLaunch()) + Config::Supplementary::executor()->addExecOnce({CMD, false}); + else + Config::Supplementary::executor()->spawnRaw(CMD); + } + + return 0; +} + +static int hlPluginLoad(lua_State* L) { + auto* mgr = sc(lua_touserdata(L, lua_upvalueindex(1))); + + CLuaConfigString pathParser(""); + lua_pushvalue(L, 1); + auto pathErr = pathParser.parse(L); + lua_pop(L, 1); + if (pathErr.errorCode != PARSE_ERROR_OK) + return Internal::configError(L, std::format("hl.plugin.load: first argument (path) must be a string: {}", pathErr.message)); + + const auto& path = pathParser.parsed(); + + if (path.empty()) + return Internal::configError(L, "hl.plugin.load: path must not be empty"); + + mgr->m_registeredPlugins.emplace_back(path); + return 0; +} + +static int hlPermission(lua_State* L) { + auto* mgr = sc(lua_touserdata(L, lua_upvalueindex(1))); + + std::string binary; + std::string typeStr; + std::string modeStr; + + if (lua_istable(L, 1)) { + auto b = Internal::tableOptStr(L, 1, "binary"); + if (!b) + b = Internal::tableOptStr(L, 1, "target"); + auto t = Internal::tableOptStr(L, 1, "type"); + auto m = Internal::tableOptStr(L, 1, "mode"); + + if (!b || !t || !m) + return Internal::configError(L, "hl.permission: expected { binary, type, mode }"); + + binary = *b; + typeStr = *t; + modeStr = *m; + } else { + binary = luaL_checkstring(L, 1); + typeStr = luaL_checkstring(L, 2); + modeStr = luaL_checkstring(L, 3); + } + + if (binary.empty()) + return Internal::configError(L, "hl.permission: binary must not be empty"); + + eDynamicPermissionType type = PERMISSION_TYPE_UNKNOWN; + eDynamicPermissionAllowMode mode = PERMISSION_RULE_ALLOW_MODE_UNKNOWN; + + if (typeStr == "screencopy") + type = PERMISSION_TYPE_SCREENCOPY; + else if (typeStr == "cursorpos") + type = PERMISSION_TYPE_CURSOR_POS; + else if (typeStr == "plugin") + type = PERMISSION_TYPE_PLUGIN; + else if (typeStr == "keyboard" || typeStr == "keeb") + type = PERMISSION_TYPE_KEYBOARD; + + if (modeStr == "ask") + mode = PERMISSION_RULE_ALLOW_MODE_ASK; + else if (modeStr == "allow") + mode = PERMISSION_RULE_ALLOW_MODE_ALLOW; + else if (modeStr == "deny") + mode = PERMISSION_RULE_ALLOW_MODE_DENY; + + if (type == PERMISSION_TYPE_UNKNOWN) + return Internal::configError(L, "hl.permission: unknown permission type"); + if (mode == PERMISSION_RULE_ALLOW_MODE_UNKNOWN) + return Internal::configError(L, "hl.permission: unknown permission allow mode"); + + if (mgr->isFirstLaunch() && g_pDynamicPermissionManager) + g_pDynamicPermissionManager->addConfigPermissionRule(binary, type, mode); + + return 0; +} + +static int hlWorkspaceRule(lua_State* L) { + auto* self = sc(lua_touserdata(L, lua_upvalueindex(1))); + + if (!lua_istable(L, 1)) { + self->addError("hl.workspace_rule: argument must be a table"); + return 0; + } + + const std::string sourceInfo = Internal::getSourceInfo(L); + + lua_getfield(L, 1, "workspace"); + if (!lua_isstring(L, -1)) { + self->addError(std::format("{}: hl.workspace_rule: 'workspace' field is required and must be a string", sourceInfo)); + lua_pop(L, 1); + return 0; + } + const std::string wsStr = lua_tostring(L, -1); + lua_pop(L, 1); + + bool enabled = true; + lua_getfield(L, 1, "enabled"); + if (lua_isboolean(L, -1)) + enabled = lua_toboolean(L, -1) != 0; + lua_pop(L, 1); + + const auto& [wsId, wsName, isAutoID] = getWorkspaceIDNameFromString(wsStr); + + Config::CWorkspaceRule wsRule; + wsRule.m_workspaceString = wsStr; + wsRule.m_workspaceName = wsName; + wsRule.m_workspaceId = isAutoID ? WORKSPACE_INVALID : wsId; + wsRule.m_enabled = enabled; + + lua_pushnil(L); + while (lua_next(L, 1) != 0) { + if (lua_type(L, -2) != LUA_TSTRING) { + lua_pop(L, 1); + continue; + } + + std::string_view key = lua_tostring(L, -2); + if (key == "workspace" || key == "enabled") { + lua_pop(L, 1); + continue; + } + + if (key == "layout_opts") { + if (!lua_istable(L, -1)) { + self->addError(std::format("{}: hl.workspace_rule: field 'layout_opts' must be a table", sourceInfo)); + lua_pop(L, 1); + continue; + } + + const int optsIdx = lua_gettop(L); + lua_pushnil(L); + while (lua_next(L, optsIdx) != 0) { + if (lua_type(L, -2) != LUA_TSTRING) { + self->addError(std::format("{}: hl.workspace_rule: field 'layout_opts' keys must be strings", sourceInfo)); + lua_pop(L, 1); + continue; + } + + std::string optKey = lua_tostring(L, -2); + std::string optVal; + + if (lua_type(L, -1) == LUA_TBOOLEAN) + optVal = lua_toboolean(L, -1) ? "true" : "false"; + else if (lua_type(L, -1) == LUA_TNUMBER) { + if (lua_isinteger(L, -1)) + optVal = std::to_string(lua_tointeger(L, -1)); + else + optVal = std::to_string(lua_tonumber(L, -1)); + } else if (lua_isstring(L, -1)) + optVal = lua_tostring(L, -1); + else { + self->addError(std::format("{}: hl.workspace_rule: field 'layout_opts.{}' must be string, bool, or number", sourceInfo, optKey)); + lua_pop(L, 1); + continue; + } + + wsRule.m_layoutopts[std::move(optKey)] = std::move(optVal); + lua_pop(L, 1); + } + + lua_pop(L, 1); + continue; + } + + const auto* desc = Internal::findDescByName(WORKSPACE_RULE_FIELDS, key); + if (!desc) { + self->addError(std::format("{}: hl.workspace_rule: unknown field '{}'", sourceInfo, key)); + lua_pop(L, 1); + continue; + } + + auto val = UP(desc->factory()); + auto err = val->parse(L); + if (err.errorCode != PARSE_ERROR_OK) + self->addError(std::format("{}: hl.workspace_rule: field '{}': {}", sourceInfo, key, err.message)); + else + desc->apply(val.get(), wsRule); + + lua_pop(L, 1); + } + + Config::workspaceRuleMgr()->replaceOrAdd(std::move(wsRule)); + + Supplementary::refresher()->scheduleRefresh(Supplementary::REFRESH_MONITOR_STATES | Config::Supplementary::REFRESH_WINDOW_STATES); + + return 0; +} + +static int hlGesture(lua_State* L) { + if (!lua_istable(L, 1)) + return Internal::configError(L, "hl.gesture: expected a table, e.g. { fingers = 3, direction = \"horizontal\", action = \"workspace\" }"); + + CLuaConfigInt fingersParser(0, 2, 9); + auto fingersErr = Internal::parseTableField(L, 1, "fingers", fingersParser); + if (fingersErr.errorCode != PARSE_ERROR_OK) + return Internal::configError(L, std::format("hl.gesture: {}", fingersErr.message)); + + size_t fingerCount = fingersParser.parsed(); + + CLuaConfigString dirParser(""); + auto dirErr = Internal::parseTableField(L, 1, "direction", dirParser); + if (dirErr.errorCode != PARSE_ERROR_OK) + return Internal::configError(L, std::format("hl.gesture: {}", dirErr.message)); + + const auto direction = g_pTrackpadGestures->dirForString(dirParser.parsed()); + if (direction == TRACKPAD_GESTURE_DIR_NONE) + return Internal::configError(L, std::format("hl.gesture: invalid direction \"{}\"", dirParser.parsed())); + + int functionRef = LUA_NOREF; + + { + // check if the action arg is a lua fn, that's fine + // we can ref that fucker and call him later + lua_getfield(L, 1, "action"); + + if (lua_isfunction(L, -1)) { + lua_pushvalue(L, -1); + functionRef = luaL_ref(L, LUA_REGISTRYINDEX); + Lua::mgr()->registerLuaRef(functionRef); + } + + lua_pop(L, 1); + } + + // bitch ass macro because it's kinda long to get these things and it's ugly +#define GET_ACTION_STRING(var, name) \ + std::string var; \ + lua_getfield(L, 1, name); \ + if (!lua_isnil(L, -1)) { \ + CLuaConfigString argParser(""); \ + auto argErr = argParser.parse(L); \ + if (argErr.errorCode != PARSE_ERROR_OK) { \ + lua_pop(L, 1); \ + return Internal::configError(L, std::format("hl.gesture: field \"" name "\": {}", argErr.message)); \ + } \ + var = argParser.parsed(); \ + } \ + lua_pop(L, 1); + + GET_ACTION_STRING(zoomLevel, "zoom_level"); + GET_ACTION_STRING(workspaceName, "workspace_name"); + GET_ACTION_STRING(mode, "mode"); + +#undef GET_ACTION_STRING + + uint32_t modMask = 0; + lua_getfield(L, 1, "mods"); + if (!lua_isnil(L, -1)) { + CLuaConfigString modsParser(""); + auto modsErr = modsParser.parse(L); + if (modsErr.errorCode != PARSE_ERROR_OK) { + lua_pop(L, 1); + return Internal::configError(L, std::format("hl.gesture: field \"mods\": {}", modsErr.message)); + } + modMask = g_pKeybindManager->stringToModMask(modsParser.parsed()); + } + lua_pop(L, 1); + + float deltaScale = 1.F; + lua_getfield(L, 1, "scale"); + if (!lua_isnil(L, -1)) { + CLuaConfigFloat scaleParser(1.F, 0.1F, 10.F); + auto scaleErr = scaleParser.parse(L); + if (scaleErr.errorCode != PARSE_ERROR_OK) { + lua_pop(L, 1); + return Internal::configError(L, std::format("hl.gesture: field \"scale\": {}", scaleErr.message)); + } + deltaScale = scaleParser.parsed(); + } + lua_pop(L, 1); + + bool disableInhibit = false; + lua_getfield(L, 1, "disable_inhibit"); + if (!lua_isnil(L, -1)) { + CLuaConfigBool disableInhibitParser(false); + auto disableInhibitErr = disableInhibitParser.parse(L); + if (disableInhibitErr.errorCode != PARSE_ERROR_OK) { + lua_pop(L, 1); + return Internal::configError(L, std::format("hl.gesture: field \"disable_inhibit\": {}", disableInhibitErr.message)); + } + disableInhibit = disableInhibitParser.parsed(); + } + lua_pop(L, 1); + + std::expected result; + + if (functionRef != LUA_NOREF) { + // this is a lua fn gesture + result = g_pTrackpadGestures->addGesture(makeUnique(functionRef), fingerCount, direction, modMask, deltaScale, disableInhibit); + } else { + CLuaConfigString actionParser(""); + auto actionErr = Internal::parseTableField(L, 1, "action", actionParser); + if (actionErr.errorCode != PARSE_ERROR_OK) + return Internal::configError(L, std::format("hl.gesture: {}", actionErr.message)); + + const auto& action = actionParser.parsed(); + + if (action == "workspace") + result = g_pTrackpadGestures->addGesture(makeUnique(), fingerCount, direction, modMask, deltaScale, disableInhibit); + else if (action == "resize") + result = g_pTrackpadGestures->addGesture(makeUnique(), fingerCount, direction, modMask, deltaScale, disableInhibit); + else if (action == "move") + result = g_pTrackpadGestures->addGesture(makeUnique(), fingerCount, direction, modMask, deltaScale, disableInhibit); + else if (action == "special") + result = g_pTrackpadGestures->addGesture(makeUnique(workspaceName), fingerCount, direction, modMask, deltaScale, disableInhibit); + else if (action == "close") + result = g_pTrackpadGestures->addGesture(makeUnique(), fingerCount, direction, modMask, deltaScale, disableInhibit); + else if (action == "float") + result = g_pTrackpadGestures->addGesture(makeUnique(mode), fingerCount, direction, modMask, deltaScale, disableInhibit); + else if (action == "fullscreen") + result = g_pTrackpadGestures->addGesture(makeUnique(mode), fingerCount, direction, modMask, deltaScale, disableInhibit); + else if (action == "cursorZoom") + result = g_pTrackpadGestures->addGesture(makeUnique(zoomLevel, mode), fingerCount, direction, modMask, deltaScale, disableInhibit); + else if (action == "unset") + result = g_pTrackpadGestures->removeGesture(fingerCount, direction, modMask, deltaScale, disableInhibit); + else + return Internal::configError(L, std::format("hl.gesture: unknown action \"{}\"", action)); + } + + if (!result) + return Internal::configError(L, std::format("hl.gesture: {}", result.error())); + + return 0; +} + +static int hlConfig(lua_State* L) { + auto* self = sc(lua_touserdata(L, lua_upvalueindex(1))); + + if (!lua_istable(L, 1)) { + self->addError("hl.config: argument must be a table"); + return 0; + } + + const std::string sourceInfo = Internal::getSourceInfo(L); + + std::function walk = [&](const std::string& prefix, int tableIdx) { + lua_pushnil(L); + while (lua_next(L, tableIdx) != 0) { + if (lua_type(L, -2) != LUA_TSTRING) { + lua_pop(L, 1); + continue; + } + + const std::string key = lua_tostring(L, -2); + std::string fullKey; + if (!prefix.empty()) { + fullKey.reserve(prefix.size() + 1 + key.size()); + fullKey = prefix; + fullKey += '.'; + } + fullKey += key; + + auto it = self->m_configValues.find(fullKey); + + if (it == self->m_configValues.end() && lua_istable(L, -1)) + walk(fullKey, lua_gettop(L)); + else { + if (it == self->m_configValues.end()) + self->addError(std::format("{}: unknown config key '{}'", sourceInfo, fullKey)); + else { + const auto err = it->second->parse(L); + if (err.errorCode != PARSE_ERROR_OK) + self->addError(std::format("{}: error setting '{}': {}", sourceInfo, it->first, err.message)); + else if (self->isDynamicParse()) + Supplementary::refresher()->scheduleRefresh(it->second->refreshBits()); + } + } + + lua_pop(L, 1); + } + }; + + walk("", 1); + return 0; +} + +static int hlGetConfig(lua_State* L) { + auto* self = sc(lua_touserdata(L, lua_upvalueindex(1))); + + std::string key = luaL_checkstring(L, 1); + + auto it = self->m_configValues.find(key); + if (it == self->m_configValues.end()) { + std::ranges::replace(key, ':', '.'); + it = self->m_configValues.find(key); + } + + if (it == self->m_configValues.end()) { + lua_pushnil(L); + const auto msg = std::format("unknown config key '{}'", key); + lua_pushstring(L, msg.c_str()); + return 2; + } + + it->second->push(L); + return 1; +} + +static int hlDevice(lua_State* L) { + auto* self = sc(lua_touserdata(L, lua_upvalueindex(1))); + + if (!lua_istable(L, 1)) { + self->addError("hl.device: argument must be a table"); + return 0; + } + + const std::string sourceInfo = Internal::getSourceInfo(L); + + lua_getfield(L, 1, "name"); + if (!lua_isstring(L, -1)) { + self->addError(std::format("{}: hl.device: 'name' field is required and must be a string", sourceInfo)); + lua_pop(L, 1); + return 0; + } + std::string devName = lua_tostring(L, -1); + lua_pop(L, 1); + std::ranges::replace(devName, ' ', '-'); + + lua_pushnil(L); + while (lua_next(L, 1) != 0) { + if (lua_type(L, -2) != LUA_TSTRING) { + lua_pop(L, 1); + continue; + } + + const char* key = lua_tostring(L, -2); + + if (std::string_view{key} == "name") { + lua_pop(L, 1); + continue; + } + + const auto* desc = Internal::findDescByName(DEVICE_FIELDS, key); + + if (!desc) { + self->addError(std::format("{}: hl.device: unknown field '{}'", sourceInfo, key)); + lua_pop(L, 1); + continue; + } + + auto val = UP(desc->factory()); + auto err = val->parse(L); + if (err.errorCode != PARSE_ERROR_OK) + self->addError(std::format("{}: hl.device: field '{}': {}", sourceInfo, key, err.message)); + else + self->m_deviceConfigs[devName].values.insert_or_assign(key, std::move(val)); + + lua_pop(L, 1); + } + + Supplementary::refresher()->scheduleRefresh(Supplementary::REFRESH_INPUT_DEVICES); + + return 0; +} + +static int hlMonitor(lua_State* L) { + auto* self = sc(lua_touserdata(L, lua_upvalueindex(1))); + + if (!lua_istable(L, 1)) { + self->addError("hl.monitor: argument must be a table"); + return 0; + } + + const std::string sourceInfo = Internal::getSourceInfo(L); + + lua_getfield(L, 1, "output"); + if (!lua_isstring(L, -1)) { + self->addError(std::format("{}: hl.monitor: 'output' field is required and must be a string", sourceInfo)); + lua_pop(L, 1); + return 0; + } + const std::string output = lua_tostring(L, -1); + lua_pop(L, 1); + + CMonitorRuleParser parser(output); + + const auto existing = std::ranges::find_if(Config::monitorRuleMgr()->all(), [&output](const auto& rule) { return rule.m_name == output; }); + if (existing != Config::monitorRuleMgr()->all().end()) + parser.rule() = *existing; + + lua_pushnil(L); + while (lua_next(L, 1) != 0) { + if (lua_type(L, -2) != LUA_TSTRING) { + lua_pop(L, 1); + continue; + } + + const char* key = lua_tostring(L, -2); + + if (std::string_view{key} == "output") { + lua_pop(L, 1); + continue; + } + + const auto* desc = Internal::findDescByName(MONITOR_FIELDS, key); + + if (!desc) { + self->addError(std::format("{}: hl.monitor: unknown field '{}'", sourceInfo, key)); + lua_pop(L, 1); + continue; + } + + auto val = UP(desc->factory()); + auto err = val->parse(L); + if (err.errorCode != PARSE_ERROR_OK) + self->addError(std::format("{}: hl.monitor: field '{}': {}", sourceInfo, key, err.message)); + else if (!desc->apply(val.get(), parser)) + self->addError(std::format("{}: hl.monitor: error applying field '{}'", sourceInfo, key)); + + lua_pop(L, 1); + } + + Config::monitorRuleMgr()->add(std::move(parser.rule())); + + Supplementary::refresher()->scheduleRefresh(Supplementary::REFRESH_MONITOR_STATES); + + return 0; +} + +static int hlWindowRule(lua_State* L) { + auto* self = sc(lua_touserdata(L, lua_upvalueindex(1))); + + if (!lua_istable(L, 1)) { + self->addError("hl.window_rule: argument must be a table"); + return 0; + } + + const std::string sourceInfo = Internal::getSourceInfo(L); + + std::string name; + lua_getfield(L, 1, "name"); + if (lua_isstring(L, -1)) + name = lua_tostring(L, -1); + lua_pop(L, 1); + + bool enabled = true; + lua_getfield(L, 1, "enabled"); + if (lua_isboolean(L, -1)) + enabled = lua_toboolean(L, -1) != 0; + lua_pop(L, 1); + + SP rule; + if (!name.empty() && self->m_luaWindowRules.contains(name)) { + rule = self->m_luaWindowRules[name]; + } else { + rule = makeShared(name); + if (!name.empty()) + self->m_luaWindowRules[name] = rule; + Desktop::Rule::ruleEngine()->registerRule(SP(rule)); + } + rule->setEnabled(enabled); + + lua_getfield(L, 1, "match"); + if (lua_istable(L, -1)) { + int matchIdx = lua_gettop(L); + lua_pushnil(L); + while (lua_next(L, matchIdx) != 0) { + if (lua_type(L, -2) == LUA_TSTRING) { + std::string matchKey = lua_tostring(L, -2); + std::string matchVal; + if (lua_type(L, -1) == LUA_TBOOLEAN) + matchVal = lua_toboolean(L, -1) ? "true" : "false"; + else if (lua_type(L, -1) == LUA_TNUMBER) + matchVal = std::to_string(lua_tointeger(L, -1)); + else if (lua_isstring(L, -1)) + matchVal = lua_tostring(L, -1); + else { + self->addError(std::format("{}: hl.window_rule: match value for '{}' must be string, bool, or number", sourceInfo, matchKey)); + lua_pop(L, 1); + continue; + } + auto prop = Desktop::Rule::matchPropFromString(matchKey); + if (prop.has_value()) + rule->registerMatch(*prop, matchVal); + else + self->addError(std::format("{}: hl.window_rule: unknown match property '{}'", sourceInfo, matchKey)); + } + lua_pop(L, 1); + } + } + lua_pop(L, 1); + + lua_pushnil(L); + while (lua_next(L, 1) != 0) { + if (lua_type(L, -2) != LUA_TSTRING) { + lua_pop(L, 1); + continue; + } + std::string_view key = lua_tostring(L, -2); + if (key == "name" || key == "enabled" || key == "match") { + lua_pop(L, 1); + continue; + } + + const auto* desc = Internal::findDescByName(Internal::WINDOW_RULE_EFFECT_DESCS, key); + if (!desc) { + const auto dynamicEffect = Desktop::Rule::windowEffects()->get(key); + if (!dynamicEffect.has_value()) { + self->addError(std::format("{}: hl.window_rule: unknown field '{}'", sourceInfo, key)); + lua_pop(L, 1); + continue; + } + + auto val = Internal::ruleValueToString(L); + if (!val) + self->addError(std::format("{}: hl.window_rule: field '{}': {}", sourceInfo, key, val.error())); + else { + auto res = rule->addEffect(*dynamicEffect, *val); + if (!res) + self->addError(std::format("{}: hl.window_rule: field '{}': {}", sourceInfo, key, res.error())); + } + + lua_pop(L, 1); + continue; + } + + auto res = Internal::addWindowRuleEffectFromLua(L, *desc, rule); + if (!res) + self->addError(std::format("{}: hl.window_rule: field '{}': {}", sourceInfo, key, res.error())); + + lua_pop(L, 1); + } + + Supplementary::refresher()->scheduleRefresh(Supplementary::REFRESH_WINDOW_STATES); + + Objects::CLuaWindowRule::push(L, rule); + return 1; +} + +static int hlLayerRule(lua_State* L) { + auto* self = sc(lua_touserdata(L, lua_upvalueindex(1))); + + if (!lua_istable(L, 1)) { + self->addError("hl.layer_rule: argument must be a table"); + return 0; + } + + const std::string sourceInfo = Internal::getSourceInfo(L); + + std::string name; + lua_getfield(L, 1, "name"); + if (lua_isstring(L, -1)) + name = lua_tostring(L, -1); + lua_pop(L, 1); + + bool enabled = true; + lua_getfield(L, 1, "enabled"); + if (lua_isboolean(L, -1)) + enabled = lua_toboolean(L, -1) != 0; + lua_pop(L, 1); + + SP rule; + if (!name.empty() && self->m_luaLayerRules.contains(name)) { + rule = self->m_luaLayerRules[name]; + } else { + rule = makeShared(name); + if (!name.empty()) + self->m_luaLayerRules[name] = rule; + Desktop::Rule::ruleEngine()->registerRule(SP(rule)); + } + rule->setEnabled(enabled); + + lua_getfield(L, 1, "match"); + if (lua_istable(L, -1)) { + int matchIdx = lua_gettop(L); + lua_pushnil(L); + while (lua_next(L, matchIdx) != 0) { + if (lua_type(L, -2) == LUA_TSTRING) { + std::string matchKey = lua_tostring(L, -2); + std::string matchVal; + if (lua_type(L, -1) == LUA_TBOOLEAN) + matchVal = lua_toboolean(L, -1) ? "true" : "false"; + else if (lua_isstring(L, -1)) + matchVal = lua_tostring(L, -1); + else { + self->addError(std::format("{}: hl.layer_rule: match value for '{}' must be string or bool", sourceInfo, matchKey)); + lua_pop(L, 1); + continue; + } + auto prop = Desktop::Rule::matchPropFromString(matchKey); + if (prop.has_value()) + rule->registerMatch(*prop, matchVal); + else + self->addError(std::format("{}: hl.layer_rule: unknown match property '{}'", sourceInfo, matchKey)); + } + lua_pop(L, 1); + } + } + lua_pop(L, 1); + + lua_pushnil(L); + while (lua_next(L, 1) != 0) { + if (lua_type(L, -2) != LUA_TSTRING) { + lua_pop(L, 1); + continue; + } + std::string_view key = lua_tostring(L, -2); + if (key == "name" || key == "enabled" || key == "match") { + lua_pop(L, 1); + continue; + } + + const auto* desc = Internal::findDescByName(LAYER_RULE_EFFECT_DESCS, key); + if (!desc) { + const auto dynamicEffect = Desktop::Rule::layerEffects()->get(key); + if (!dynamicEffect.has_value()) { + self->addError(std::format("{}: hl.layer_rule: unknown field '{}'", sourceInfo, key)); + lua_pop(L, 1); + continue; + } + + auto val = Internal::ruleValueToString(L); + if (!val) + self->addError(std::format("{}: hl.layer_rule: field '{}': {}", sourceInfo, key, val.error())); + else { + auto res = rule->addEffect(*dynamicEffect, *val); + if (!res) + self->addError(std::format("{}: hl.layer_rule: field '{}': {}", sourceInfo, key, res.error())); + } + + lua_pop(L, 1); + continue; + } + + auto val = UP(desc->factory()); + auto err = val->parse(L); + if (err.errorCode != PARSE_ERROR_OK) + self->addError(std::format("{}: hl.layer_rule: field '{}': {}", sourceInfo, key, err.message)); + else { + auto str = val->toString(); + auto res = rule->addEffect(desc->effect, str); + if (!res) + self->addError(std::format("{}: hl.layer_rule: field '{}': {}", sourceInfo, key, res.error())); + } + + lua_pop(L, 1); + } + + Supplementary::refresher()->scheduleRefresh(Supplementary::REFRESH_RULES); + + Objects::CLuaLayerRule::push(L, rule); + return 1; +} + +void Internal::registerConfigRuleBindings(lua_State* L, CConfigManager* mgr) { + Internal::setMgrFn(L, mgr, "config", hlConfig); + Internal::setMgrFn(L, mgr, "get_config", hlGetConfig); + Internal::setMgrFn(L, mgr, "device", hlDevice); + Internal::setMgrFn(L, mgr, "monitor", hlMonitor); + Internal::setMgrFn(L, mgr, "window_rule", hlWindowRule); + Internal::setMgrFn(L, mgr, "layer_rule", hlLayerRule); + Internal::setMgrFn(L, mgr, "workspace_rule", hlWorkspaceRule); + Internal::setMgrFn(L, mgr, "env", hlEnv); + Internal::setMgrFn(L, mgr, "permission", hlPermission); + + lua_newtable(L); + Internal::setMgrFn(L, mgr, "load", hlPluginLoad); + lua_setfield(L, -2, "plugin"); + + Internal::setFn(L, "gesture", hlGesture); + Internal::setFn(L, "curve", hlCurve); + Internal::setFn(L, "animation", hlAnimation); +} diff --git a/src/config/lua/bindings/LuaBindingsDispatchers.cpp b/src/config/lua/bindings/LuaBindingsDispatchers.cpp new file mode 100644 index 000000000..fc84895ac --- /dev/null +++ b/src/config/lua/bindings/LuaBindingsDispatchers.cpp @@ -0,0 +1,1272 @@ +#include "LuaBindingsInternal.hpp" + +#include + +#include "../../supplementary/executor/Executor.hpp" + +#include "../../../managers/SeatManager.hpp" +#include "../../../devices/IKeyboard.hpp" +#include "../../../desktop/rule/windowRule/WindowRule.hpp" + +using namespace Config; +using namespace Config::Lua; +using namespace Config::Lua::Bindings; +using namespace Hyprutils::String; + +namespace CA = Config::Actions; + +static constexpr auto ERR = CA::eActionErrorLevel::ERROR; +static constexpr auto WARN = CA::eActionErrorLevel::WARNING; +static constexpr auto INFO = CA::eActionErrorLevel::INFO; +static constexpr auto C_UNKNOWN = CA::eActionErrorCode::UNKNOWN; +static constexpr auto C_INVARG = CA::eActionErrorCode::INVALID_ARGUMENT; +static constexpr auto C_NOTFOUND = CA::eActionErrorCode::NOT_FOUND; +static constexpr auto C_NOTARGET = CA::eActionErrorCode::NO_TARGET; +static constexpr auto C_UNAVAIL = CA::eActionErrorCode::UNAVAILABLE; +static constexpr auto C_EXECFAIL = CA::eActionErrorCode::EXECUTION_FAILED; + +static int dsp_moveCursorToCorner(lua_State* L) { + return Internal::checkResult(L, CA::moveCursorToCorner((int)lua_tonumber(L, lua_upvalueindex(1)), Internal::windowFromUpval(L, 2))); +} + +static int dsp_moveCursor(lua_State* L) { + return Internal::checkResult(L, CA::moveCursor(Vector2D{lua_tonumber(L, lua_upvalueindex(1)), lua_tonumber(L, lua_upvalueindex(2))})); +} + +static int dsp_toggleGroup(lua_State* L) { + return Internal::checkResult(L, CA::toggleGroup(Internal::windowFromUpval(L, 1))); +} + +static int dsp_changeGroupActive(lua_State* L) { + return Internal::checkResult(L, CA::changeGroupActive(lua_toboolean(L, lua_upvalueindex(1)), Internal::windowFromUpval(L, 2))); +} + +static int dsp_setGroupActive(lua_State* L) { + return Internal::checkResult(L, CA::setGroupActive((int)lua_tonumber(L, lua_upvalueindex(1)), Internal::windowFromUpval(L, 2))); +} + +static int dsp_moveGroupWindow(lua_State* L) { + return Internal::checkResult(L, CA::moveGroupWindow(lua_toboolean(L, lua_upvalueindex(1)))); +} + +static int dsp_lockGroups(lua_State* L) { + return Internal::checkResult(L, CA::lockGroups(sc((int)lua_tonumber(L, lua_upvalueindex(1))))); +} + +static int dsp_lockActiveGroup(lua_State* L) { + return Internal::checkResult(L, CA::lockActiveGroup(sc((int)lua_tonumber(L, lua_upvalueindex(1))))); +} + +static int hlCursorMoveToCorner(lua_State* L) { + if (!lua_istable(L, 1)) + return Internal::configError(L, "hl.cursor.move_to_corner: expected a table { corner, window? }"); + + lua_pushnumber(L, Internal::requireTableFieldNum(L, 1, "corner", "hl.cursor.move_to_corner")); + Internal::pushWindowUpval(L, 1); + lua_pushcclosure(L, dsp_moveCursorToCorner, 2); + return 1; +} + +static int hlCursorMove(lua_State* L) { + if (!lua_istable(L, 1)) + return Internal::configError(L, "hl.cursor.move: expected a table { x, y }"); + + lua_pushnumber(L, Internal::requireTableFieldNum(L, 1, "x", "hl.cursor.move")); + lua_pushnumber(L, Internal::requireTableFieldNum(L, 1, "y", "hl.cursor.move")); + lua_pushcclosure(L, dsp_moveCursor, 2); + return 1; +} + +static int hlGroupToggle(lua_State* L) { + Internal::pushWindowUpval(L, 1); + lua_pushcclosure(L, dsp_toggleGroup, 1); + return 1; +} + +static int hlGroupNext(lua_State* L) { + lua_pushboolean(L, true); + Internal::pushWindowUpval(L, 1); + lua_pushcclosure(L, dsp_changeGroupActive, 2); + return 1; +} + +static int hlGroupPrev(lua_State* L) { + lua_pushboolean(L, false); + Internal::pushWindowUpval(L, 1); + lua_pushcclosure(L, dsp_changeGroupActive, 2); + return 1; +} + +static int hlGroupMoveWindow(lua_State* L) { + bool forward = true; + if (lua_istable(L, 1)) { + auto f = Internal::tableOptBool(L, 1, "forward"); + if (f) + forward = *f; + } + lua_pushboolean(L, forward); + lua_pushcclosure(L, dsp_moveGroupWindow, 1); + return 1; +} + +static int hlGroupActive(lua_State* L) { + if (!lua_istable(L, 1)) + return Internal::configError(L, "hl.group.active: expected a table { index, window? }"); + + lua_pushnumber(L, Internal::requireTableFieldNum(L, 1, "index", "hl.group.active")); + Internal::pushWindowUpval(L, 1); + lua_pushcclosure(L, dsp_setGroupActive, 2); + return 1; +} + +static int hlGroupLock(lua_State* L) { + const auto action = Internal::tableToggleAction(L, 1); + + lua_pushnumber(L, (int)action); + lua_pushcclosure(L, dsp_lockGroups, 1); + return 1; +} + +static int hlGroupLockActive(lua_State* L) { + const auto action = Internal::tableToggleAction(L, 1); + + lua_pushnumber(L, (int)action); + lua_pushcclosure(L, dsp_lockActiveGroup, 1); + return 1; +} + +static int dsp_execCmd(lua_State* L) { + auto proc = lua_tostring(L, lua_upvalueindex(1)); + + if (std::string_view{proc}.empty()) + return Internal::dispatcherError(L, "Invalid process string", ERR, C_INVARG); + + std::optional pid; + auto ruleRet = Internal::buildRuleFromTable(L, lua_upvalueindex(2)); + + if (!ruleRet) + return ruleRet.error(); + + if (*ruleRet) + pid = Config::Supplementary::executor()->spawn(Config::Supplementary::SExecRequest{.exec = proc, .rule = std::move(*ruleRet)}); + else + pid = Config::Supplementary::executor()->spawn(proc); + + if (!pid.has_value()) + return Internal::dispatcherError(L, "Failed to start process", ERR, C_EXECFAIL); + return Internal::pushSuccessResult(L); +} + +static int dsp_execRaw(lua_State* L) { + auto proc = Config::Supplementary::executor()->spawnRaw(lua_tostring(L, lua_upvalueindex(1))); + if (!proc || !*proc) + return Internal::dispatcherError(L, "Failed to start process", ERR, C_EXECFAIL); + return Internal::pushSuccessResult(L); +} + +static int dsp_exit(lua_State* L) { + return Internal::checkResult(L, CA::exit()); +} + +static int dsp_submap(lua_State* L) { + return Internal::checkResult(L, CA::setSubmap(lua_tostring(L, lua_upvalueindex(1)))); +} + +static int dsp_pass(lua_State* L) { + const auto PWINDOW = g_pCompositor->getWindowByRegex(lua_tostring(L, lua_upvalueindex(1))); + if (!PWINDOW) + return Internal::dispatcherError(L, "hl.pass: window not found", WARN, C_NOTFOUND); + return Internal::checkResult(L, CA::pass(PWINDOW)); +} + +static int dsp_layoutMsg(lua_State* L) { + return Internal::checkResult(L, CA::layoutMessage(lua_tostring(L, lua_upvalueindex(1)))); +} + +static int dsp_dpms(lua_State* L) { + auto action = sc((int)lua_tonumber(L, lua_upvalueindex(1))); + std::optional mon = std::nullopt; + + if (!lua_isnil(L, lua_upvalueindex(2))) { + auto m = g_pCompositor->getMonitorFromString(lua_tostring(L, lua_upvalueindex(2))); + if (m) + mon = m; + } + + return Internal::checkResult(L, CA::dpms(action, mon)); +} + +static int dsp_event(lua_State* L) { + return Internal::checkResult(L, CA::event(lua_tostring(L, lua_upvalueindex(1)))); +} + +static int dsp_global(lua_State* L) { + return Internal::checkResult(L, CA::global(lua_tostring(L, lua_upvalueindex(1)))); +} + +static int dsp_forceRendererReload(lua_State* L) { + return Internal::checkResult(L, CA::forceRendererReload()); +} + +static int dsp_forceIdle(lua_State* L) { + return Internal::checkResult(L, CA::forceIdle((float)lua_tonumber(L, lua_upvalueindex(1)))); +} + +static int hlExecCmd(lua_State* L) { + const auto proc = luaL_checkstring(L, 1); + const bool hasRuleArg = !lua_isnoneornil(L, 2); + + lua_pushstring(L, proc); + + if (hasRuleArg) + lua_pushvalue(L, 2); + else + lua_pushnil(L); + + lua_pushcclosure(L, dsp_execCmd, 2); + return 1; +} + +static int hlExecRaw(lua_State* L) { + lua_pushstring(L, luaL_checkstring(L, 1)); + lua_pushcclosure(L, dsp_execRaw, 1); + return 1; +} + +static int hlExit(lua_State* L) { + lua_pushcclosure(L, dsp_exit, 0); + return 1; +} + +static int hlSubmap(lua_State* L) { + lua_pushstring(L, luaL_checkstring(L, 1)); + lua_pushcclosure(L, dsp_submap, 1); + return 1; +} + +static int hlPass(lua_State* L) { + if (!lua_istable(L, 1)) + return Internal::configError(L, "hl.pass: expected a table { window }"); + + const auto w = Internal::requireTableFieldWindowSelector(L, 1, "window", "hl.pass"); + lua_pushstring(L, w.c_str()); + lua_pushcclosure(L, dsp_pass, 1); + return 1; +} + +static int hlLayout(lua_State* L) { + lua_pushstring(L, luaL_checkstring(L, 1)); + lua_pushcclosure(L, dsp_layoutMsg, 1); + return 1; +} + +static int hlDpms(lua_State* L) { + CA::eTogglableAction action = Internal::tableToggleAction(L, 1); + std::optional monStr; + + if (lua_istable(L, 1)) + monStr = Internal::tableOptMonitorSelector(L, 1, "monitor", "hl.dpms"); + + lua_pushnumber(L, (int)action); + if (monStr) + lua_pushstring(L, monStr->c_str()); + else + lua_pushnil(L); + lua_pushcclosure(L, dsp_dpms, 2); + return 1; +} + +static int hlEvent(lua_State* L) { + lua_pushstring(L, luaL_checkstring(L, 1)); + lua_pushcclosure(L, dsp_event, 1); + return 1; +} + +static int hlGlobal(lua_State* L) { + lua_pushstring(L, luaL_checkstring(L, 1)); + lua_pushcclosure(L, dsp_global, 1); + return 1; +} + +static int hlForceRendererReload(lua_State* L) { + lua_pushcclosure(L, dsp_forceRendererReload, 0); + return 1; +} + +static int hlForceIdle(lua_State* L) { + lua_pushnumber(L, luaL_checknumber(L, 1)); + lua_pushcclosure(L, dsp_forceIdle, 1); + return 1; +} + +static std::expected resolveKeycode(const std::string& key) { + if (isNumber(key) && std::stoi(key) > 9) + return (uint32_t)std::stoi(key); + + if (key.starts_with("code:") && isNumber(key.substr(5))) + return (uint32_t)std::stoi(key.substr(5)); + + if (key.starts_with("mouse:") && isNumber(key.substr(6))) { + uint32_t code = std::stoi(key.substr(6)); + if (code < 272) + return std::unexpected("invalid mouse button"); + return code; + } + + const auto KEYSYM = xkb_keysym_from_name(key.c_str(), XKB_KEYSYM_CASE_INSENSITIVE); + + const auto KB = g_pSeatManager->m_keyboard; + if (!KB) + return std::unexpected("no keyboard"); + + const auto KEYPAIRSTRING = std::format("{}{}", rc(KB.get()), key); + + if (g_pKeybindManager->m_keyToCodeCache.contains(KEYPAIRSTRING)) + return g_pKeybindManager->m_keyToCodeCache[KEYPAIRSTRING]; + + xkb_keymap* km = KB->m_xkbKeymap; + xkb_state* ks = KB->m_xkbState; + xkb_keycode_t keycode_min = xkb_keymap_min_keycode(km); + xkb_keycode_t keycode_max = xkb_keymap_max_keycode(km); + uint32_t keycode = 0; + + for (xkb_keycode_t kc = keycode_min; kc <= keycode_max; ++kc) { + xkb_keysym_t sym = xkb_state_key_get_one_sym(ks, kc); + if (sym == KEYSYM) { + keycode = kc; + g_pKeybindManager->m_keyToCodeCache[KEYPAIRSTRING] = keycode; + } + } + + if (!keycode) + return std::unexpected("key not found"); + + return keycode; +} + +static int dsp_sendShortcut(lua_State* L) { + const uint32_t modMask = g_pKeybindManager->stringToModMask(lua_tostring(L, lua_upvalueindex(1))); + const std::string key = lua_tostring(L, lua_upvalueindex(2)); + + auto keycodeResult = resolveKeycode(key); + if (!keycodeResult) + return Internal::dispatcherError(L, std::format("send_shortcut: {}", keycodeResult.error()), ERR, C_INVARG); + + PHLWINDOW window = nullptr; + if (!lua_isnil(L, lua_upvalueindex(3))) { + window = g_pCompositor->getWindowByRegex(lua_tostring(L, lua_upvalueindex(3))); + if (!window) + return Internal::dispatcherError(L, "send_shortcut: window not found", WARN, C_NOTFOUND); + } + + return Internal::checkResult(L, CA::pass(modMask, *keycodeResult, window)); +} + +static int dsp_sendKeyState(lua_State* L) { + const uint32_t modMask = g_pKeybindManager->stringToModMask(lua_tostring(L, lua_upvalueindex(1))); + const std::string key = lua_tostring(L, lua_upvalueindex(2)); + const uint32_t keyState = (uint32_t)lua_tonumber(L, lua_upvalueindex(3)); + + auto keycodeResult = resolveKeycode(key); + if (!keycodeResult) + return Internal::dispatcherError(L, std::format("send_key_state: {}", keycodeResult.error()), ERR, C_INVARG); + + PHLWINDOW window = nullptr; + if (!lua_isnil(L, lua_upvalueindex(4))) { + window = g_pCompositor->getWindowByRegex(lua_tostring(L, lua_upvalueindex(4))); + if (!window) + return Internal::dispatcherError(L, "send_key_state: window not found", WARN, C_NOTFOUND); + } + + return Internal::checkResult(L, CA::sendKeyState(modMask, *keycodeResult, keyState, window)); +} + +static int hlSendShortcut(lua_State* L) { + if (!lua_istable(L, 1)) + return Internal::configError(L, "send_shortcut: expected a table { mods, key, window? }"); + + const auto mods = Internal::requireTableFieldStr(L, 1, "mods", "hl.send_shortcut"); + const auto key = Internal::requireTableFieldStr(L, 1, "key", "hl.send_shortcut"); + + lua_pushstring(L, mods.c_str()); + lua_pushstring(L, key.c_str()); + Internal::pushWindowUpval(L, 1); + lua_pushcclosure(L, dsp_sendShortcut, 3); + return 1; +} + +static int hlSendKeyState(lua_State* L) { + if (!lua_istable(L, 1)) + return Internal::configError(L, "send_key_state: expected a table { mods, key, state, window? }"); + + const auto mods = Internal::requireTableFieldStr(L, 1, "mods", "hl.send_key_state"); + const auto key = Internal::requireTableFieldStr(L, 1, "key", "hl.send_key_state"); + const auto stateStr = Internal::requireTableFieldStr(L, 1, "state", "hl.send_key_state"); + + uint32_t keyState = 0; + if (stateStr == "down") + keyState = 1; + else if (stateStr == "repeat") + keyState = 2; + else if (stateStr != "up") + return Internal::configError(L, "send_key_state: 'state' must be \"down\", \"up\", or \"repeat\""); + + lua_pushstring(L, mods.c_str()); + lua_pushstring(L, key.c_str()); + lua_pushnumber(L, keyState); + Internal::pushWindowUpval(L, 1); + lua_pushcclosure(L, dsp_sendKeyState, 4); + return 1; +} + +static int dsp_moveToWorkspace(lua_State* L) { + auto ws = Internal::resolveWorkspaceStr(lua_tostring(L, lua_upvalueindex(1))); + if (!ws) + return Internal::dispatcherError(L, "Invalid workspace", ERR, C_INVARG); + + bool silent = lua_toboolean(L, lua_upvalueindex(2)); + return Internal::checkResult(L, CA::moveToWorkspace(ws, silent, Internal::windowFromUpval(L, 3))); +} + +static int dsp_moveToMonitor(lua_State* L) { + auto mon = Internal::resolveMonitorStr(lua_tostring(L, lua_upvalueindex(1))); + if (!mon) + return Internal::dispatcherError(L, "Invalid monitor / monitor doesn't exist", ERR, C_INVARG); + + bool silent = lua_toboolean(L, lua_upvalueindex(2)); + return Internal::checkResult(L, CA::moveToWorkspace(mon->m_activeWorkspace, silent, Internal::windowFromUpval(L, 3))); +} + +static int dsp_closeWindow(lua_State* L) { + return Internal::checkResult(L, CA::closeWindow(Internal::windowFromUpval(L, 1))); +} + +static int dsp_killWindow(lua_State* L) { + return Internal::checkResult(L, CA::killWindow(Internal::windowFromUpval(L, 1))); +} + +static int dsp_signalWindow(lua_State* L) { + return Internal::checkResult(L, CA::signalWindow((int)lua_tonumber(L, lua_upvalueindex(1)), Internal::windowFromUpval(L, 2))); +} + +static int dsp_floatWindow(lua_State* L) { + return Internal::checkResult(L, CA::floatWindow(sc((int)lua_tonumber(L, lua_upvalueindex(1))), Internal::windowFromUpval(L, 2))); +} + +static int dsp_fullscreenWindow(lua_State* L) { + return Internal::checkResult(L, CA::fullscreenWindow(sc((int)lua_tonumber(L, lua_upvalueindex(1))), Internal::windowFromUpval(L, 2))); +} + +static int dsp_fullscreenWindowWithAction(lua_State* L) { + const auto mode = sc((int)lua_tonumber(L, lua_upvalueindex(1))); + const int actionRaw = (int)lua_tonumber(L, lua_upvalueindex(2)); + auto maybeW = Internal::windowFromUpval(L, 3); + + if (actionRaw == 0) { + return Internal::checkResult(L, CA::fullscreenWindow(mode, maybeW)); + } + + const auto target = maybeW.value_or(Desktop::focusState()->window()); + if (!target) + return Internal::dispatcherError(L, "hl.window.fullscreen: no target", WARN, C_NOTARGET); + + const bool currentlyMode = target->isEffectiveInternalFSMode(mode); + + if (actionRaw == 1) { + if (!currentlyMode) + return Internal::checkResult(L, CA::fullscreenWindow(mode, maybeW)); + return Internal::pushSuccessResult(L); + } + + if (actionRaw == 2) { + if (currentlyMode) + return Internal::checkResult(L, CA::fullscreenWindow(mode, maybeW)); + return Internal::pushSuccessResult(L); + } + + return Internal::dispatcherError(L, "hl.window.fullscreen: invalid action", ERR, C_INVARG); +} + +static int dsp_fullscreenState(lua_State* L) { + const auto desiredInternal = sc((int)lua_tonumber(L, lua_upvalueindex(1))); + const auto desiredClient = sc((int)lua_tonumber(L, lua_upvalueindex(2))); + const int actionRaw = (int)lua_tonumber(L, lua_upvalueindex(3)); // 0: toggle, 1: set, 2: unset + auto maybeW = Internal::windowFromUpval(L, 4); + + const auto target = maybeW.value_or(Desktop::focusState()->window()); + if (!target) + return Internal::pushSuccessResult(L); + + const auto CURRENT = target->m_fullscreenState; + const bool atDesiredState = CURRENT.internal == desiredInternal && CURRENT.client == desiredClient; + + if (actionRaw == 0) { + return Internal::checkResult(L, CA::fullscreenWindow(desiredInternal, desiredClient, maybeW)); + } + + if (actionRaw == 1) { + if (!atDesiredState) + return Internal::checkResult(L, CA::fullscreenWindow(desiredInternal, desiredClient, maybeW)); + return Internal::pushSuccessResult(L); + } + + if (actionRaw == 2) { + if (atDesiredState) + return Internal::checkResult(L, CA::fullscreenWindow(desiredInternal, desiredClient, maybeW)); + return Internal::pushSuccessResult(L); + } + + return Internal::dispatcherError(L, "hl.window.fullscreen_state: invalid action", ERR, C_INVARG); +} + +static int dsp_pseudoWindow(lua_State* L) { + return Internal::checkResult(L, CA::pseudoWindow(sc((int)lua_tonumber(L, lua_upvalueindex(1))), Internal::windowFromUpval(L, 2))); +} + +static int dsp_moveInDirection(lua_State* L) { + return Internal::checkResult(L, CA::moveInDirection(sc((int)lua_tonumber(L, lua_upvalueindex(1))), Internal::windowFromUpval(L, 2))); +} + +static int dsp_swapInDirection(lua_State* L) { + return Internal::checkResult(L, CA::swapInDirection(sc((int)lua_tonumber(L, lua_upvalueindex(1))), Internal::windowFromUpval(L, 2))); +} + +static int dsp_center(lua_State* L) { + return Internal::checkResult(L, CA::center(Internal::windowFromUpval(L, 1))); +} + +static int dsp_cycleNext(lua_State* L) { + bool next = lua_toboolean(L, lua_upvalueindex(1)); + int tiledRaw = (int)lua_tonumber(L, lua_upvalueindex(2)); + int floatingRaw = (int)lua_tonumber(L, lua_upvalueindex(3)); + std::optional tiled = tiledRaw < 0 ? std::nullopt : std::optional(tiledRaw > 0); + std::optional floating = floatingRaw < 0 ? std::nullopt : std::optional(floatingRaw > 0); + return Internal::checkResult(L, CA::cycleNext(next, tiled, floating, Internal::windowFromUpval(L, 4))); +} + +static int dsp_swapNext(lua_State* L) { + return Internal::checkResult(L, CA::swapNext(lua_toboolean(L, lua_upvalueindex(1)), Internal::windowFromUpval(L, 2))); +} + +static int dsp_swapWithWindow(lua_State* L) { + auto source = Internal::windowFromUpval(L, 1); + + const auto targetSelector = lua_tostring(L, lua_upvalueindex(2)); + const auto target = g_pCompositor->getWindowByRegex(targetSelector); + if (!target) + return Internal::dispatcherError(L, "hl.window.swap: target window not found", WARN, C_NOTFOUND); + + return Internal::checkResult(L, CA::swapWith(target, source)); +} + +static int dsp_tagWindow(lua_State* L) { + return Internal::checkResult(L, CA::tag(lua_tostring(L, lua_upvalueindex(1)), Internal::windowFromUpval(L, 2))); +} + +static int dsp_toggleSwallow(lua_State* L) { + return Internal::checkResult(L, CA::toggleSwallow()); +} + +static int dsp_resize(lua_State* L) { + Vector2D value{lua_tonumber(L, lua_upvalueindex(1)), lua_tonumber(L, lua_upvalueindex(2))}; + return Internal::checkResult(L, CA::resize(value, lua_toboolean(L, lua_upvalueindex(3)), Internal::windowFromUpval(L, 4))); +} + +static int dsp_move(lua_State* L) { + Vector2D value{lua_tonumber(L, lua_upvalueindex(1)), lua_tonumber(L, lua_upvalueindex(2))}; + return Internal::checkResult(L, CA::move(value, lua_toboolean(L, lua_upvalueindex(3)), Internal::windowFromUpval(L, 4))); +} + +static int dsp_pinWindow(lua_State* L) { + return Internal::checkResult(L, CA::pinWindow(sc((int)lua_tonumber(L, lua_upvalueindex(1))), Internal::windowFromUpval(L, 2))); +} + +static int dsp_bringToTop(lua_State* L) { + return Internal::checkResult(L, CA::alterZOrder("top")); +} + +static int dsp_alterZOrder(lua_State* L) { + return Internal::checkResult(L, CA::alterZOrder(lua_tostring(L, lua_upvalueindex(1)), Internal::windowFromUpval(L, 2))); +} + +static int dsp_setProp(lua_State* L) { + return Internal::checkResult(L, CA::setProp(lua_tostring(L, lua_upvalueindex(1)), lua_tostring(L, lua_upvalueindex(2)), Internal::windowFromUpval(L, 3))); +} + +static int dsp_moveIntoGroup(lua_State* L) { + return Internal::checkResult(L, CA::moveIntoGroup(sc((int)lua_tonumber(L, lua_upvalueindex(1))), Internal::windowFromUpval(L, 2))); +} + +static int dsp_moveOutOfGroup(lua_State* L) { + return Internal::checkResult(L, CA::moveOutOfGroup(sc((int)lua_tonumber(L, lua_upvalueindex(1))), Internal::windowFromUpval(L, 2))); +} + +static int dsp_moveWindowOrGroup(lua_State* L) { + return Internal::checkResult(L, CA::moveWindowOrGroup(sc((int)lua_tonumber(L, lua_upvalueindex(1))), Internal::windowFromUpval(L, 2))); +} + +static int dsp_moveIntoOrCreateGroup(lua_State* L) { + return Internal::checkResult(L, CA::moveIntoOrCreateGroup(sc((int)lua_tonumber(L, lua_upvalueindex(1))), Internal::windowFromUpval(L, 2))); +} + +static int dsp_denyFromGroup(lua_State* L) { + return Internal::checkResult(L, CA::denyWindowFromGroup(sc((int)lua_tonumber(L, lua_upvalueindex(1))))); +} + +static int dsp_mouseDrag(lua_State* L) { + return Internal::checkResult(L, CA::mouse("movewindow")); +} + +static int dsp_mouseResize(lua_State* L) { + return Internal::checkResult(L, CA::mouse("resizewindow")); +} + +static int hlWindowClose(lua_State* L) { + Internal::pushWindowUpval(L, 1); + lua_pushcclosure(L, dsp_closeWindow, 1); + return 1; +} + +static int hlWindowKill(lua_State* L) { + Internal::pushWindowUpval(L, 1); + lua_pushcclosure(L, dsp_killWindow, 1); + return 1; +} + +static int hlWindowSignal(lua_State* L) { + if (!lua_istable(L, 1)) + return Internal::configError(L, "hl.window.signal: expected a table { signal, window? }"); + + lua_pushnumber(L, Internal::requireTableFieldNum(L, 1, "signal", "hl.window.signal")); + Internal::pushWindowUpval(L, 1); + lua_pushcclosure(L, dsp_signalWindow, 2); + return 1; +} + +static int hlWindowFloat(lua_State* L) { + const auto action = Internal::tableToggleAction(L, 1); + + lua_pushnumber(L, (int)action); + Internal::pushWindowUpval(L, 1); + lua_pushcclosure(L, dsp_floatWindow, 2); + return 1; +} + +static int hlWindowFullscreen(lua_State* L) { + eFullscreenMode mode = FSMODE_FULLSCREEN; + int action = 0; // 0: toggle, 1: set, 2: unset + if (lua_istable(L, 1)) { + auto m = Internal::tableOptStr(L, 1, "mode"); + if (m) { + if (*m == "maximized" || *m == "1") + mode = FSMODE_MAXIMIZED; + else if (*m == "fullscreen" || *m == "0") + mode = FSMODE_FULLSCREEN; + else + return Internal::configError(L, "hl.window.fullscreen: invalid mode \"{}\" (expected fullscreen/maximized)", *m); + } + + auto a = Internal::tableOptStr(L, 1, "action"); + if (a) { + if (*a == "toggle") + action = 0; + else if (*a == "set") + action = 1; + else if (*a == "unset") + action = 2; + else + return Internal::configError(L, "hl.window.fullscreen: invalid action \"{}\" (expected toggle/set/unset)", *a); + } + } + lua_pushnumber(L, (int)mode); + if (action == 0) { + Internal::pushWindowUpval(L, 1); + lua_pushcclosure(L, dsp_fullscreenWindow, 2); + } else { + lua_pushnumber(L, action); + Internal::pushWindowUpval(L, 1); + lua_pushcclosure(L, dsp_fullscreenWindowWithAction, 3); + } + return 1; +} + +static int hlWindowFullscreenState(lua_State* L) { + if (!lua_istable(L, 1)) + return Internal::configError(L, "hl.window.fullscreen_state: expected a table { internal, client, action?, window? }"); + + int action = 1; // default to set semantics + if (auto a = Internal::tableOptStr(L, 1, "action"); a) { + if (*a == "toggle") + action = 0; + else if (*a == "set") + action = 1; + else if (*a == "unset") + action = 2; + else + return Internal::configError(L, "hl.window.fullscreen_state: invalid action \"{}\" (expected toggle/set/unset)", *a); + } + + auto im = Internal::tableOptNum(L, 1, "internal"); + auto cm = Internal::tableOptNum(L, 1, "client"); + if (!im || !cm) + return Internal::configError(L, "hl.window.fullscreen_state: 'internal' and 'client' are required"); + + lua_pushnumber(L, (int)*im); + lua_pushnumber(L, (int)*cm); + lua_pushnumber(L, action); + Internal::pushWindowUpval(L, 1); + lua_pushcclosure(L, dsp_fullscreenState, 4); + return 1; +} + +static int hlWindowPseudo(lua_State* L) { + const auto action = Internal::tableToggleAction(L, 1); + + lua_pushnumber(L, (int)action); + Internal::pushWindowUpval(L, 1); + lua_pushcclosure(L, dsp_pseudoWindow, 2); + return 1; +} + +static int hlWindowMove(lua_State* L) { + if (!lua_istable(L, 1)) + return Internal::configError(L, "hl.window.move: expected a table, e.g. { direction = \"left\" }"); + + auto dirStr = Internal::tableOptStr(L, 1, "direction"); + if (dirStr) { + auto dir = Internal::parseDirectionStr(*dirStr); + if (dir == Math::DIRECTION_DEFAULT) + return Internal::configError(L, "hl.window.move: invalid direction \"{}\" (expected left/right/up/down)", *dirStr); + + auto groupAware = Internal::tableOptBool(L, 1, "group_aware"); + if (groupAware && *groupAware) { + lua_pushnumber(L, (int)dir); + Internal::pushWindowUpval(L, 1); + lua_pushcclosure(L, dsp_moveWindowOrGroup, 2); + return 1; + } + + lua_pushnumber(L, (int)dir); + Internal::pushWindowUpval(L, 1); + lua_pushcclosure(L, dsp_moveInDirection, 2); + return 1; + } + + auto x = Internal::tableOptNum(L, 1, "x"); + auto y = Internal::tableOptNum(L, 1, "y"); + if (x && y) { + bool relative = Internal::tableOptBool(L, 1, "relative").value_or(false); + lua_pushnumber(L, *x); + lua_pushnumber(L, *y); + lua_pushboolean(L, relative); + Internal::pushWindowUpval(L, 1); + lua_pushcclosure(L, dsp_move, 4); + return 1; + } + + auto ws = Internal::tableOptWorkspaceSelector(L, 1, "workspace", "hl.window.move"); + if (ws) { + auto follow = Internal::tableOptBool(L, 1, "follow"); + bool silent = follow.has_value() && !*follow; + lua_pushstring(L, ws->c_str()); + lua_pushboolean(L, silent); + Internal::pushWindowUpval(L, 1); + lua_pushcclosure(L, dsp_moveToWorkspace, 3); + return 1; + } + + auto mon = Internal::tableOptMonitorSelector(L, 1, "monitor", "hl.window.move"); + if (mon) { + auto follow = Internal::tableOptBool(L, 1, "follow"); + bool silent = follow.has_value() && !*follow; + lua_pushstring(L, mon->c_str()); + lua_pushboolean(L, silent); + Internal::pushWindowUpval(L, 1); + lua_pushcclosure(L, dsp_moveToMonitor, 3); + return 1; + } + + auto intoGroup = Internal::tableOptStr(L, 1, "into_group"); + if (intoGroup) { + auto dir = Internal::parseDirectionStr(*intoGroup); + if (dir == Math::DIRECTION_DEFAULT) + return Internal::configError(L, "hl.window.move: invalid into_group direction \"{}\"", *intoGroup); + lua_pushnumber(L, (int)dir); + Internal::pushWindowUpval(L, 1); + lua_pushcclosure(L, dsp_moveIntoGroup, 2); + return 1; + } + + auto intoOrCreateGroup = Internal::tableOptStr(L, 1, "into_or_create_group"); + if (intoOrCreateGroup) { + auto dir = Internal::parseDirectionStr(*intoOrCreateGroup); + if (dir == Math::DIRECTION_DEFAULT) + return Internal::configError(L, "hl.window.move: invalid into_or_create_group direction \"{}\"", *intoOrCreateGroup); + lua_pushnumber(L, (int)dir); + Internal::pushWindowUpval(L, 1); + lua_pushcclosure(L, dsp_moveIntoOrCreateGroup, 2); + return 1; + } + + auto outOfGroup = Internal::tableOptStr(L, 1, "out_of_group"); + if (outOfGroup) { + auto dir = Internal::parseDirectionStr(*outOfGroup); + lua_pushnumber(L, (int)dir); + Internal::pushWindowUpval(L, 1); + lua_pushcclosure(L, dsp_moveOutOfGroup, 2); + return 1; + } + + auto outOfGroupBool = Internal::tableOptBool(L, 1, "out_of_group"); + if (outOfGroupBool && *outOfGroupBool) { + lua_pushnumber(L, (int)Math::DIRECTION_DEFAULT); + Internal::pushWindowUpval(L, 1); + lua_pushcclosure(L, dsp_moveOutOfGroup, 2); + return 1; + } + + return Internal::configError(L, "hl.window.move: unrecognized arguments. Expected one of: direction, x+y(+relative), workspace, into_group, out_of_group"); +} + +static int hlWindowSwap(lua_State* L) { + if (!lua_istable(L, 1)) + return Internal::configError(L, "hl.window.swap: expected a table, e.g. { direction = \"left\" }"); + + auto dirStr = Internal::tableOptStr(L, 1, "direction"); + if (dirStr) { + auto dir = Internal::parseDirectionStr(*dirStr); + if (dir == Math::DIRECTION_DEFAULT) + return Internal::configError(L, "hl.window.swap: invalid direction \"{}\" (expected left/right/up/down)", *dirStr); + lua_pushnumber(L, (int)dir); + Internal::pushWindowUpval(L, 1); + lua_pushcclosure(L, dsp_swapInDirection, 2); + return 1; + } + + auto target = Internal::tableOptWindowSelector(L, 1, "target", "hl.window.swap"); + if (!target) + target = Internal::tableOptWindowSelector(L, 1, "with", "hl.window.swap"); + if (!target) + target = Internal::tableOptWindowSelector(L, 1, "other", "hl.window.swap"); + + if (target) { + Internal::pushWindowUpval(L, 1); + lua_pushstring(L, target->c_str()); + lua_pushcclosure(L, dsp_swapWithWindow, 2); + return 1; + } + + auto next = Internal::tableOptBool(L, 1, "next"); + if (next && *next) { + lua_pushboolean(L, true); + Internal::pushWindowUpval(L, 1); + lua_pushcclosure(L, dsp_swapNext, 2); + return 1; + } + + auto prev = Internal::tableOptBool(L, 1, "prev"); + if (prev && *prev) { + lua_pushboolean(L, false); + Internal::pushWindowUpval(L, 1); + lua_pushcclosure(L, dsp_swapNext, 2); + return 1; + } + + return Internal::configError(L, "hl.window.swap: unrecognized arguments. Expected one of: direction, target/with/other, next, prev"); +} + +static int hlWindowCenter(lua_State* L) { + Internal::pushWindowUpval(L, 1); + lua_pushcclosure(L, dsp_center, 1); + return 1; +} + +static int hlWindowCycleNext(lua_State* L) { + bool next = true; + int tiled = -1; + int floating = -1; + if (lua_istable(L, 1)) { + auto n = Internal::tableOptBool(L, 1, "next"); + if (n) + next = *n; + auto t = Internal::tableOptBool(L, 1, "tiled"); + if (t) + tiled = *t ? 1 : 0; + auto f = Internal::tableOptBool(L, 1, "floating"); + if (f) + floating = *f ? 1 : 0; + } + lua_pushboolean(L, next); + lua_pushnumber(L, tiled); + lua_pushnumber(L, floating); + Internal::pushWindowUpval(L, 1); + lua_pushcclosure(L, dsp_cycleNext, 4); + return 1; +} + +static int hlWindowTag(lua_State* L) { + if (!lua_istable(L, 1)) + return Internal::configError(L, "hl.window.tag: expected a table { tag, window? }"); + + const auto tag = Internal::requireTableFieldStr(L, 1, "tag", "hl.window.tag"); + lua_pushstring(L, tag.c_str()); + Internal::pushWindowUpval(L, 1); + lua_pushcclosure(L, dsp_tagWindow, 2); + return 1; +} + +static int hlWindowToggleSwallow(lua_State* L) { + lua_pushcclosure(L, dsp_toggleSwallow, 0); + return 1; +} + +static int hlWindowResizeExact(lua_State* L) { + if (!lua_istable(L, 1)) + return Internal::configError(L, "hl.window.resize: expected a table { x, y, relative?, window? }"); + auto x = Internal::tableOptNum(L, 1, "x"); + auto y = Internal::tableOptNum(L, 1, "y"); + bool relative = Internal::tableOptBool(L, 1, "relative").value_or(false); + if (!x || !y) + return Internal::configError(L, "hl.window.resize: 'x' and 'y' are required"); + lua_pushnumber(L, *x); + lua_pushnumber(L, *y); + lua_pushboolean(L, relative); + Internal::pushWindowUpval(L, 1); + lua_pushcclosure(L, dsp_resize, 4); + return 1; +} + +static int hlWindowPin(lua_State* L) { + const auto action = Internal::tableToggleAction(L, 1); + + lua_pushnumber(L, (int)action); + Internal::pushWindowUpval(L, 1); + lua_pushcclosure(L, dsp_pinWindow, 2); + return 1; +} + +static int hlWindowBringToTop(lua_State* L) { + lua_pushcclosure(L, dsp_bringToTop, 0); + return 1; +} + +static int hlWindowAlterZOrder(lua_State* L) { + if (!lua_istable(L, 1)) + return Internal::configError(L, "hl.window.alter_zorder: expected a table { mode, window? }"); + + const auto mode = Internal::requireTableFieldStr(L, 1, "mode", "hl.window.alter_zorder"); + lua_pushstring(L, mode.c_str()); + Internal::pushWindowUpval(L, 1); + lua_pushcclosure(L, dsp_alterZOrder, 2); + return 1; +} + +static int hlWindowSetProp(lua_State* L) { + if (!lua_istable(L, 1)) + return Internal::configError(L, "hl.window.set_prop: expected a table { prop, value, window? }"); + + const auto prop = Internal::requireTableFieldStr(L, 1, "prop", "hl.window.set_prop"); + const auto value = Internal::requireTableFieldStr(L, 1, "value", "hl.window.set_prop"); + lua_pushstring(L, prop.c_str()); + lua_pushstring(L, value.c_str()); + Internal::pushWindowUpval(L, 1); + lua_pushcclosure(L, dsp_setProp, 3); + return 1; +} + +static int hlWindowDenyFromGroup(lua_State* L) { + const auto action = Internal::tableToggleAction(L, 1); + + lua_pushnumber(L, (int)action); + lua_pushcclosure(L, dsp_denyFromGroup, 1); + return 1; +} + +static int hlWindowDrag(lua_State* L) { + lua_pushcclosure(L, dsp_mouseDrag, 0); + return 1; +} + +static int hlWindowResize(lua_State* L) { + if (lua_gettop(L) == 0 || lua_isnil(L, 1)) { + lua_pushcclosure(L, dsp_mouseResize, 0); + return 1; + } + + if (!lua_istable(L, 1)) + return Internal::configError(L, "hl.window.resize: expected no args, or a table { x, y, relative?, window? }"); + + return hlWindowResizeExact(L); +} + +static int dsp_moveFocus(lua_State* L) { + return Internal::checkResult(L, CA::moveFocus(sc((int)lua_tonumber(L, lua_upvalueindex(1))))); +} + +static int dsp_focusMonitor(lua_State* L) { + const auto PMONITOR = g_pCompositor->getMonitorFromString(lua_tostring(L, lua_upvalueindex(1))); + if (!PMONITOR) + return Internal::dispatcherError(L, "hl.focus.monitor: monitor not found", WARN, C_NOTFOUND); + return Internal::checkResult(L, CA::focusMonitor(PMONITOR)); +} + +static int dsp_focusWindowBySelector(lua_State* L) { + const auto PWINDOW = g_pCompositor->getWindowByRegex(lua_tostring(L, lua_upvalueindex(1))); + if (!PWINDOW) + return Internal::dispatcherError(L, "hl.focus: window not found", WARN, C_NOTFOUND); + return Internal::checkResult(L, CA::focus(PWINDOW)); +} + +static int dsp_focusUrgentOrLast(lua_State* L) { + return Internal::checkResult(L, CA::focusUrgentOrLast()); +} + +static int dsp_focusCurrentOrLast(lua_State* L) { + return Internal::checkResult(L, CA::focusCurrentOrLast()); +} + +static int dsp_changeWorkspace(lua_State* L) { + return Internal::checkResult(L, CA::changeWorkspace(std::string(lua_tostring(L, lua_upvalueindex(1))))); +} + +static int dsp_focusWorkspaceOnCurrentMonitor(lua_State* L) { + auto ws = Internal::resolveWorkspaceStr(lua_tostring(L, lua_upvalueindex(1))); + if (!ws) + return Internal::dispatcherError(L, "Invalid workspace", ERR, C_INVARG); + return Internal::checkResult(L, CA::changeWorkspaceOnCurrentMonitor(ws)); +} + +static int hlFocus(lua_State* L) { + if (!lua_istable(L, 1)) + return Internal::configError(L, "hl.focus: expected a table, e.g. { direction = \"left\" }"); + + auto dirStr = Internal::tableOptStr(L, 1, "direction"); + if (dirStr) { + auto dir = Internal::parseDirectionStr(*dirStr); + if (dir == Math::DIRECTION_DEFAULT) + return Internal::configError(L, "hl.focus: invalid direction \"{}\" (expected left/right/up/down)", *dirStr); + lua_pushnumber(L, (int)dir); + lua_pushcclosure(L, dsp_moveFocus, 1); + return 1; + } + + auto monStr = Internal::tableOptMonitorSelector(L, 1, "monitor", "hl.focus"); + if (monStr) { + lua_pushstring(L, monStr->c_str()); + lua_pushcclosure(L, dsp_focusMonitor, 1); + return 1; + } + + auto wsStr = Internal::tableOptWorkspaceSelector(L, 1, "workspace", "hl.focus"); + if (wsStr) { + auto onCurrentMon = Internal::tableOptBool(L, 1, "on_current_monitor"); + + lua_pushstring(L, wsStr->c_str()); + + if (onCurrentMon.value_or(false)) + lua_pushcclosure(L, dsp_focusWorkspaceOnCurrentMonitor, 1); + else + lua_pushcclosure(L, dsp_changeWorkspace, 1); + + return 1; + } + + auto winStr = Internal::tableOptWindowSelector(L, 1, "window", "hl.focus"); + if (winStr) { + lua_pushstring(L, winStr->c_str()); + lua_pushcclosure(L, dsp_focusWindowBySelector, 1); + return 1; + } + + auto urgent = Internal::tableOptBool(L, 1, "urgent_or_last"); + if (urgent && *urgent) { + lua_pushcclosure(L, dsp_focusUrgentOrLast, 0); + return 1; + } + + auto last = Internal::tableOptBool(L, 1, "last"); + if (last && *last) { + lua_pushcclosure(L, dsp_focusCurrentOrLast, 0); + return 1; + } + + return Internal::configError(L, "hl.focus: unrecognized arguments. Expected one of: direction, monitor, window, urgent_or_last, last"); +} + +static int dsp_toggleSpecial(lua_State* L) { + std::string name = lua_isnil(L, lua_upvalueindex(1)) ? "" : lua_tostring(L, lua_upvalueindex(1)); + const auto& [workspaceID, workspaceName, isAutoID] = getWorkspaceIDNameFromString("special:" + name); + if (workspaceID == WORKSPACE_INVALID || !g_pCompositor->isWorkspaceSpecial(workspaceID)) + return Internal::dispatcherError(L, "Invalid special workspace", ERR, C_INVARG); + + auto ws = g_pCompositor->getWorkspaceByID(workspaceID); + if (!ws) { + const auto PMONITOR = Desktop::focusState()->monitor(); + if (PMONITOR) + ws = g_pCompositor->createNewWorkspace(workspaceID, PMONITOR->m_id, workspaceName); + } + if (!ws) + return Internal::dispatcherError(L, "Could not resolve special workspace", ERR, C_UNAVAIL); + + return Internal::checkResult(L, CA::toggleSpecial(ws)); +} + +static int dsp_renameWorkspace(lua_State* L) { + const auto PWS = g_pCompositor->getWorkspaceByString(lua_tostring(L, lua_upvalueindex(1))); + if (!PWS) + return Internal::dispatcherError(L, "hl.workspace.rename: no such workspace", WARN, C_NOTFOUND); + std::string name = lua_isnil(L, lua_upvalueindex(2)) ? "" : lua_tostring(L, lua_upvalueindex(2)); + return Internal::checkResult(L, CA::renameWorkspace(PWS, name)); +} + +static int dsp_moveWorkspaceToMonitor(lua_State* L) { + const auto WORKSPACEID = getWorkspaceIDNameFromString(lua_tostring(L, lua_upvalueindex(1))).id; + if (WORKSPACEID == WORKSPACE_INVALID) + return Internal::dispatcherError(L, "Invalid workspace", ERR, C_INVARG); + const auto PWORKSPACE = g_pCompositor->getWorkspaceByID(WORKSPACEID); + if (!PWORKSPACE) + return Internal::dispatcherError(L, "Workspace not found", WARN, C_NOTFOUND); + const auto PMONITOR = g_pCompositor->getMonitorFromString(lua_tostring(L, lua_upvalueindex(2))); + if (!PMONITOR) + return Internal::dispatcherError(L, "Monitor not found", WARN, C_NOTFOUND); + return Internal::checkResult(L, CA::moveToMonitor(PWORKSPACE, PMONITOR)); +} + +static int dsp_moveCurrentWorkspaceToMonitor(lua_State* L) { + const auto PMONITOR = g_pCompositor->getMonitorFromString(lua_tostring(L, lua_upvalueindex(1))); + if (!PMONITOR) + return Internal::dispatcherError(L, "Monitor not found", WARN, C_NOTFOUND); + const auto PCURRENTWORKSPACE = Desktop::focusState()->monitor()->m_activeWorkspace; + if (!PCURRENTWORKSPACE) + return Internal::dispatcherError(L, "Invalid workspace", ERR, C_INVARG); + return Internal::checkResult(L, CA::moveToMonitor(PCURRENTWORKSPACE, PMONITOR)); +} + +static int dsp_swapActiveWorkspaces(lua_State* L) { + const auto PMON1 = g_pCompositor->getMonitorFromString(lua_tostring(L, lua_upvalueindex(1))); + const auto PMON2 = g_pCompositor->getMonitorFromString(lua_tostring(L, lua_upvalueindex(2))); + if (!PMON1 || !PMON2) + return Internal::dispatcherError(L, "Monitor not found", WARN, C_NOTFOUND); + return Internal::checkResult(L, CA::swapActiveWorkspaces(PMON1, PMON2)); +} + +static int hlWorkspaceToggleSpecial(lua_State* L) { + lua_pushstring(L, lua_tostring(L, 1)); + lua_pushcclosure(L, dsp_toggleSpecial, 1); + return 1; +} + +static int hlWorkspaceRename(lua_State* L) { + if (!lua_istable(L, 1)) + return Internal::configError(L, "hl.workspace.rename: expected a table { id, name? }"); + + const auto id = Internal::requireTableFieldWorkspaceSelector(L, 1, "id", "hl.workspace.rename"); + auto name = Internal::tableOptStr(L, 1, "name"); + + lua_pushstring(L, id.c_str()); + if (name) + lua_pushstring(L, name->c_str()); + else + lua_pushnil(L); + lua_pushcclosure(L, dsp_renameWorkspace, 2); + return 1; +} + +static int hlWorkspaceMove(lua_State* L) { + if (!lua_istable(L, 1)) + return Internal::configError(L, "hl.workspace.move: expected a table, e.g. { monitor = \"DP-1\" }"); + + const auto mon = Internal::requireTableFieldMonitorSelector(L, 1, "monitor", "hl.workspace.move"); + + auto id = Internal::tableOptWorkspaceSelector(L, 1, "id", "hl.workspace.move"); + if (id) { + lua_pushstring(L, id->c_str()); + lua_pushstring(L, mon.c_str()); + lua_pushcclosure(L, dsp_moveWorkspaceToMonitor, 2); + return 1; + } + + lua_pushstring(L, mon.c_str()); + lua_pushcclosure(L, dsp_moveCurrentWorkspaceToMonitor, 1); + return 1; +} + +static int hlWorkspaceSwapMonitors(lua_State* L) { + if (!lua_istable(L, 1)) + return Internal::configError(L, "hl.workspace.swap_monitors: expected a table { monitor1, monitor2 }"); + + const auto m1 = Internal::requireTableFieldMonitorSelector(L, 1, "monitor1", "hl.workspace.swap_monitors"); + const auto m2 = Internal::requireTableFieldMonitorSelector(L, 1, "monitor2", "hl.workspace.swap_monitors"); + lua_pushstring(L, m1.c_str()); + lua_pushstring(L, m2.c_str()); + lua_pushcclosure(L, dsp_swapActiveWorkspaces, 2); + return 1; +} + +void Internal::registerDispatcherBindings(lua_State* L) { + lua_newtable(L); + + { + lua_newtable(L); + Internal::setFn(L, "move_to_corner", hlCursorMoveToCorner); + Internal::setFn(L, "move", hlCursorMove); + lua_setfield(L, -2, "cursor"); + + lua_newtable(L); + Internal::setFn(L, "toggle", hlGroupToggle); + Internal::setFn(L, "next", hlGroupNext); + Internal::setFn(L, "prev", hlGroupPrev); + Internal::setFn(L, "active", hlGroupActive); + Internal::setFn(L, "move_window", hlGroupMoveWindow); + Internal::setFn(L, "lock", hlGroupLock); + Internal::setFn(L, "lock_active", hlGroupLockActive); + lua_setfield(L, -2, "group"); + + lua_newtable(L); + Internal::setFn(L, "close", hlWindowClose); + Internal::setFn(L, "kill", hlWindowKill); + Internal::setFn(L, "signal", hlWindowSignal); + Internal::setFn(L, "float", hlWindowFloat); + Internal::setFn(L, "fullscreen", hlWindowFullscreen); + Internal::setFn(L, "fullscreen_state", hlWindowFullscreenState); + Internal::setFn(L, "pseudo", hlWindowPseudo); + Internal::setFn(L, "move", hlWindowMove); + Internal::setFn(L, "swap", hlWindowSwap); + Internal::setFn(L, "center", hlWindowCenter); + Internal::setFn(L, "cycle_next", hlWindowCycleNext); + Internal::setFn(L, "tag", hlWindowTag); + Internal::setFn(L, "toggle_swallow", hlWindowToggleSwallow); + Internal::setFn(L, "pin", hlWindowPin); + Internal::setFn(L, "bring_to_top", hlWindowBringToTop); + Internal::setFn(L, "alter_zorder", hlWindowAlterZOrder); + Internal::setFn(L, "set_prop", hlWindowSetProp); + Internal::setFn(L, "deny_from_group", hlWindowDenyFromGroup); + Internal::setFn(L, "drag", hlWindowDrag); + Internal::setFn(L, "resize", hlWindowResize); + lua_setfield(L, -2, "window"); + + lua_newtable(L); + Internal::setFn(L, "rename", hlWorkspaceRename); + Internal::setFn(L, "move", hlWorkspaceMove); + Internal::setFn(L, "swap_monitors", hlWorkspaceSwapMonitors); + Internal::setFn(L, "toggle_special", hlWorkspaceToggleSpecial); + lua_setfield(L, -2, "workspace"); + + Internal::setFn(L, "exec_cmd", hlExecCmd); + Internal::setFn(L, "exec_raw", hlExecRaw); + Internal::setFn(L, "exit", hlExit); + Internal::setFn(L, "submap", hlSubmap); + Internal::setFn(L, "pass", hlPass); + Internal::setFn(L, "send_shortcut", hlSendShortcut); + Internal::setFn(L, "send_key_state", hlSendKeyState); + Internal::setFn(L, "layout", hlLayout); + Internal::setFn(L, "dpms", hlDpms); + Internal::setFn(L, "event", hlEvent); + Internal::setFn(L, "global", hlGlobal); + Internal::setFn(L, "force_renderer_reload", hlForceRendererReload); + Internal::setFn(L, "force_idle", hlForceIdle); + Internal::setFn(L, "focus", hlFocus); + } + + lua_setfield(L, -2, "dsp"); +} diff --git a/src/config/lua/bindings/LuaBindingsInternal.cpp b/src/config/lua/bindings/LuaBindingsInternal.cpp new file mode 100644 index 000000000..e0cef1464 --- /dev/null +++ b/src/config/lua/bindings/LuaBindingsInternal.cpp @@ -0,0 +1,587 @@ +#include "LuaBindingsInternal.hpp" + +#include "../../../desktop/rule/windowRule/WindowRule.hpp" + +using namespace Config; +using namespace Config::Lua; +using namespace Config::Lua::Bindings; + +namespace CA = Config::Actions; + +namespace { + constexpr const char* LUA_WINDOW_MT = "HL.Window"; + constexpr const char* LUA_WORKSPACE_MT = "HL.Workspace"; + constexpr const char* LUA_MONITOR_MT = "HL.Monitor"; +} + +std::string Internal::argStr(lua_State* L, int idx) { + if (lua_type(L, idx) == LUA_TNUMBER) + return std::to_string((long long)lua_tonumber(L, idx)); + + size_t n = 0; + const char* s = luaL_checklstring(L, idx, &n); + return {s, n}; +} + +std::optional Internal::tableOptStr(lua_State* L, int idx, const char* field) { + lua_getfield(L, idx, field); + if (lua_isnil(L, -1)) { + lua_pop(L, 1); + return std::nullopt; + } + + const char* s = lua_tostring(L, -1); + lua_pop(L, 1); + return s ? std::optional(std::string(s)) : std::nullopt; +} + +std::optional Internal::tableOptNum(lua_State* L, int idx, const char* field) { + lua_getfield(L, idx, field); + if (lua_isnil(L, -1) || !lua_isnumber(L, -1)) { + lua_pop(L, 1); + return std::nullopt; + } + + const double v = lua_tonumber(L, -1); + lua_pop(L, 1); + return v; +} + +std::optional Internal::tableOptBool(lua_State* L, int idx, const char* field) { + lua_getfield(L, idx, field); + if (lua_isnil(L, -1)) { + lua_pop(L, 1); + return std::nullopt; + } + + const bool v = lua_toboolean(L, -1); + lua_pop(L, 1); + return v; +} + +PHLMONITOR Internal::monitorFromLuaSelectorOrObject(lua_State* L, int idx, const char* fnName) { + idx = lua_absindex(L, idx); + + if (lua_isnil(L, idx)) + return nullptr; + + if (auto* ref = sc(luaL_testudata(L, idx, LUA_MONITOR_MT)); ref) + return ref->lock(); + + if (lua_isstring(L, idx) || lua_isnumber(L, idx)) + return g_pCompositor->getMonitorFromString(argStr(L, idx)); + + Internal::configError(L, "{}: expected a monitor object or selector", fnName); + return nullptr; +} + +PHLWORKSPACE Internal::workspaceFromLuaSelectorOrObject(lua_State* L, int idx, const char* fnName) { + idx = lua_absindex(L, idx); + + if (lua_isnil(L, idx)) + return nullptr; + + if (auto* ref = sc(luaL_testudata(L, idx, LUA_WORKSPACE_MT)); ref) { + auto ws = ref->lock(); + if (!ws || ws->inert()) + return nullptr; + + return ws; + } + + if (lua_isstring(L, idx) || lua_isnumber(L, idx)) + return g_pCompositor->getWorkspaceByString(argStr(L, idx)); + + Internal::configError(L, "{}: expected a workspace object or selector", fnName); + return nullptr; +} + +PHLWINDOW Internal::windowFromLuaSelectorOrObject(lua_State* L, int idx, const char* fnName) { + idx = lua_absindex(L, idx); + + if (lua_isnil(L, idx)) + return nullptr; + + if (auto* ref = sc(luaL_testudata(L, idx, LUA_WINDOW_MT)); ref) + return ref->lock(); + + if (lua_isstring(L, idx) || lua_isnumber(L, idx)) + return g_pCompositor->getWindowByRegex(argStr(L, idx)); + + Internal::configError(L, "{}: expected a window object or selector", fnName); + return nullptr; +} + +std::optional Internal::tableOptMonitor(lua_State* L, int idx, const char* field, const char* fnName) { + idx = lua_absindex(L, idx); + lua_getfield(L, idx, field); + if (lua_isnil(L, -1)) { + lua_pop(L, 1); + return std::nullopt; + } + + auto mon = monitorFromLuaSelectorOrObject(L, -1, fnName); + lua_pop(L, 1); + return mon; +} + +std::optional Internal::tableOptWorkspace(lua_State* L, int idx, const char* field, const char* fnName) { + idx = lua_absindex(L, idx); + lua_getfield(L, idx, field); + if (lua_isnil(L, -1)) { + lua_pop(L, 1); + return std::nullopt; + } + + auto ws = workspaceFromLuaSelectorOrObject(L, -1, fnName); + lua_pop(L, 1); + return ws; +} + +std::optional Internal::tableOptWindow(lua_State* L, int idx, const char* field, const char* fnName) { + idx = lua_absindex(L, idx); + lua_getfield(L, idx, field); + if (lua_isnil(L, -1)) { + lua_pop(L, 1); + return std::nullopt; + } + + auto window = windowFromLuaSelectorOrObject(L, -1, fnName); + lua_pop(L, 1); + return window; +} + +std::optional Internal::monitorSelectorFromLuaSelectorOrObject(lua_State* L, int idx, const char* fnName) { + idx = lua_absindex(L, idx); + + if (lua_isnil(L, idx)) + return std::nullopt; + + if (auto* ref = sc(luaL_testudata(L, idx, LUA_MONITOR_MT)); ref) { + const auto mon = ref->lock(); + if (!mon) { + Internal::configError(L, "{}: monitor object is expired", fnName); + return std::nullopt; + } + + return mon->m_name; + } + + if (lua_isstring(L, idx) || lua_isnumber(L, idx)) + return argStr(L, idx); + + Internal::configError(L, "{}: expected a monitor object or selector", fnName); + return std::nullopt; +} + +std::optional Internal::workspaceSelectorFromLuaSelectorOrObject(lua_State* L, int idx, const char* fnName) { + idx = lua_absindex(L, idx); + + if (lua_isnil(L, idx)) + return std::nullopt; + + if (auto* ref = sc(luaL_testudata(L, idx, LUA_WORKSPACE_MT)); ref) { + const auto ws = ref->lock(); + if (!ws || ws->inert()) { + Internal::configError(L, "{}: workspace object is expired", fnName); + return std::nullopt; + } + + return std::to_string(ws->m_id); + } + + if (lua_isstring(L, idx) || lua_isnumber(L, idx)) + return argStr(L, idx); + + Internal::configError(L, "{}: expected a workspace object or selector", fnName); + return std::nullopt; +} + +std::optional Internal::windowSelectorFromLuaSelectorOrObject(lua_State* L, int idx, const char* fnName) { + idx = lua_absindex(L, idx); + + if (lua_isnil(L, idx)) + return std::nullopt; + + if (auto* ref = sc(luaL_testudata(L, idx, LUA_WINDOW_MT)); ref) { + const auto w = ref->lock(); + if (!w) { + Internal::configError(L, "{}: window object is expired", fnName); + return std::nullopt; + } + + return std::format("address:0x{:x}", reinterpret_cast(w.get())); + } + + if (lua_isstring(L, idx) || lua_isnumber(L, idx)) + return argStr(L, idx); + + Internal::configError(L, "{}: expected a window object or selector", fnName); + return std::nullopt; +} + +std::optional Internal::tableOptMonitorSelector(lua_State* L, int idx, const char* field, const char* fnName) { + idx = lua_absindex(L, idx); + lua_getfield(L, idx, field); + if (lua_isnil(L, -1)) { + lua_pop(L, 1); + return std::nullopt; + } + + auto selector = monitorSelectorFromLuaSelectorOrObject(L, -1, fnName); + lua_pop(L, 1); + return selector; +} + +std::optional Internal::tableOptWorkspaceSelector(lua_State* L, int idx, const char* field, const char* fnName) { + idx = lua_absindex(L, idx); + lua_getfield(L, idx, field); + if (lua_isnil(L, -1)) { + lua_pop(L, 1); + return std::nullopt; + } + + auto selector = workspaceSelectorFromLuaSelectorOrObject(L, -1, fnName); + lua_pop(L, 1); + return selector; +} + +std::optional Internal::tableOptWindowSelector(lua_State* L, int idx, const char* field, const char* fnName) { + idx = lua_absindex(L, idx); + lua_getfield(L, idx, field); + if (lua_isnil(L, -1)) { + lua_pop(L, 1); + return std::nullopt; + } + + auto selector = windowSelectorFromLuaSelectorOrObject(L, -1, fnName); + lua_pop(L, 1); + return selector; +} + +std::string Internal::requireTableFieldMonitorSelector(lua_State* L, int idx, const char* field, const char* fnName) { + auto selector = tableOptMonitorSelector(L, idx, field, fnName); + if (!selector) { + Internal::configError(L, "{}: '{}' is required", fnName, field); + return ""; + } + + return *selector; +} + +std::string Internal::requireTableFieldWorkspaceSelector(lua_State* L, int idx, const char* field, const char* fnName) { + auto selector = tableOptWorkspaceSelector(L, idx, field, fnName); + if (!selector) { + Internal::configError(L, "{}: '{}' is required", fnName, field); + return ""; + } + + return *selector; +} + +std::string Internal::requireTableFieldWindowSelector(lua_State* L, int idx, const char* field, const char* fnName) { + auto selector = tableOptWindowSelector(L, idx, field, fnName); + if (!selector) { + Internal::configError(L, "{}: '{}' is required", fnName, field); + return ""; + } + + return *selector; +} + +Math::eDirection Internal::parseDirectionStr(const std::string& str) { + if (str == "left" || str == "l") + return Math::DIRECTION_LEFT; + if (str == "right" || str == "r") + return Math::DIRECTION_RIGHT; + if (str == "up" || str == "u" || str == "t") + return Math::DIRECTION_UP; + if (str == "down" || str == "d" || str == "b") + return Math::DIRECTION_DOWN; + return Math::DIRECTION_DEFAULT; +} + +CA::eTogglableAction Internal::parseToggleStr(const std::string& str) { + if (str.empty() || str == "toggle") + return CA::TOGGLE_ACTION_TOGGLE; + if (str == "enable" || str == "on") + return CA::TOGGLE_ACTION_ENABLE; + if (str == "disable" || str == "off") + return CA::TOGGLE_ACTION_DISABLE; + return CA::TOGGLE_ACTION_TOGGLE; +} + +std::optional Internal::windowFromUpval(lua_State* L, int idx) { + if (lua_isnil(L, lua_upvalueindex(idx))) + return std::nullopt; + + return g_pCompositor->getWindowByRegex(lua_tostring(L, lua_upvalueindex(idx))); +} + +void Internal::pushWindowUpval(lua_State* L, int tableIdx) { + if (lua_istable(L, tableIdx)) { + auto selector = tableOptWindowSelector(L, tableIdx, "window", "window selector"); + if (!selector) + lua_pushnil(L); + else + lua_pushstring(L, selector->c_str()); + } else + lua_pushnil(L); +} + +static auto logLevelForActionError(CA::eActionErrorLevel level) { + switch (level) { + case CA::eActionErrorLevel::SILENT: return Log::DEBUG; + case CA::eActionErrorLevel::INFO: return Log::INFO; + case CA::eActionErrorLevel::WARNING: return Log::WARN; + case CA::eActionErrorLevel::ERROR: return Log::ERR; + } + + return Log::ERR; +} + +void Internal::reportError(lua_State* L, const CA::SActionError& e) { + Log::logger->log(logLevelForActionError(e.level), "Lua {} ({}): {}", CA::toString(e.level), CA::toString(e.code), e.message); + + if (auto mgr = Config::Lua::mgr(); mgr) { + mgr->addEvalIssue(e); + + if (e.level == CA::eActionErrorLevel::ERROR) + mgr->addError(std::string{e.message}); + } +} + +int Internal::pushSuccessResult(lua_State* L, const CA::SActionResult& r) { + lua_newtable(L); + lua_pushboolean(L, true); + lua_setfield(L, -2, "ok"); + lua_pushboolean(L, r.passEvent); + lua_setfield(L, -2, "pass_event"); + return 1; +} + +int Internal::pushErrorResult(lua_State* L, const CA::SActionError& e) { + lua_newtable(L); + lua_pushboolean(L, false); + lua_setfield(L, -2, "ok"); + lua_pushstring(L, e.message.c_str()); + lua_setfield(L, -2, "error"); + lua_pushstring(L, CA::toString(e.level)); + lua_setfield(L, -2, "level"); + lua_pushstring(L, CA::toString(e.code)); + lua_setfield(L, -2, "code"); + return 1; +} + +int Internal::checkResult(lua_State* L, const CA::ActionResult& r) { + if (!r) { + auto error = r.error(); + error.message = std::format("{}: {}", getSourceInfo(L), std::move(error.message)); + Internal::reportError(L, error); + return Internal::pushErrorResult(L, error); + } + + return Internal::pushSuccessResult(L, *r); +} + +PHLWORKSPACE Internal::resolveWorkspaceStr(const std::string& args) { + const auto& [id, name, isAutoID] = getWorkspaceIDNameFromString(args); + if (id == WORKSPACE_INVALID) + return nullptr; + + auto ws = g_pCompositor->getWorkspaceByID(id); + if (!ws) { + const auto PMONITOR = Desktop::focusState()->monitor(); + if (PMONITOR) + ws = g_pCompositor->createNewWorkspace(id, PMONITOR->m_id, name, false); + } + + return ws; +} + +PHLMONITOR Internal::resolveMonitorStr(const std::string& args) { + auto mon = g_pCompositor->getMonitorFromString(args); + return mon; +} + +std::string Internal::getSourceInfo(lua_State* L, int stackLevel) { + lua_Debug ar = {}; + std::string sourceInfo = "?:?"; + + if (lua_getstack(L, stackLevel, &ar) && lua_getinfo(L, "Sl", &ar)) { + const char* src = ar.source; + if (src && src[0] == '@') + src++; + + sourceInfo = std::format("{}:{}", src ? src : "?", ar.currentline); + } + + return sourceInfo; +} + +std::string Internal::requireTableFieldStr(lua_State* L, int idx, const char* field, const char* fnName) { + auto value = tableOptStr(L, idx, field); + if (!value) { + Internal::configError(L, "{}: '{}' is required", fnName, field); + return ""; + } + + return *value; +} + +double Internal::requireTableFieldNum(lua_State* L, int idx, const char* field, const char* fnName) { + auto value = tableOptNum(L, idx, field); + if (!value) { + Internal::configError(L, "{}: '{}' is required", fnName, field); + return 0; + } + + return *value; +} + +CA::eTogglableAction Internal::tableToggleAction(lua_State* L, int idx, const char* field) { + if (!lua_istable(L, idx)) + return CA::TOGGLE_ACTION_TOGGLE; + + if (const auto action = tableOptStr(L, idx, field); action.has_value()) + return parseToggleStr(*action); + + 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); + lua_setfield(L, -2, name); +} + +int Internal::configError(lua_State* L, std::string s, CA::eActionErrorLevel level, CA::eActionErrorCode code, int stackLevel) { + s = std::format("{}: {}", getSourceInfo(L, stackLevel), std::move(s)); + + Internal::reportError(L, CA::SActionError{std::move(s), level, code}); + return 0; +} + +int Internal::dispatcherError(lua_State* L, std::string s, CA::eActionErrorLevel level, CA::eActionErrorCode code, int stackLevel) { + s = std::format("{}: {}", getSourceInfo(L, stackLevel), std::move(s)); + + CA::SActionError error{std::move(s), level, code}; + Internal::reportError(L, error); + return Internal::pushErrorResult(L, error); +} + +std::expected Internal::ruleValueToString(lua_State* L) { + if (lua_type(L, -1) == LUA_TBOOLEAN) + return lua_toboolean(L, -1) ? "true" : "false"; + + if (lua_isinteger(L, -1)) + return std::to_string(lua_tointeger(L, -1)); + + if (lua_isnumber(L, -1)) + return std::to_string(lua_tonumber(L, -1)); + + if (lua_isstring(L, -1)) + return std::string(lua_tostring(L, -1)); + + return std::unexpected("value must be a string, bool, or number"); +} + +std::expected Internal::addWindowRuleEffectFromLua(lua_State* L, const SWindowRuleEffectDesc& desc, const SP& rule) { + auto val = UP(desc.factory()); + auto err = val->parse(L); + + if (err.errorCode != PARSE_ERROR_OK) { + const bool allowLegacyString = desc.effect == WE::WINDOW_RULE_EFFECT_BORDER_COLOR && lua_isstring(L, -1); + if (!allowLegacyString) + return std::unexpected(err.message); + + return rule->addEffect(desc.effect, lua_tostring(L, -1)); + } + + if (const auto expr = dc(val.get()); expr) + return rule->addEffect(desc.effect, expr->parsed()); + + return rule->addEffect(desc.effect, val->toString()); +} + +std::expected, int> Internal::buildRuleFromTable(lua_State* L, int idx) { + SP rule; + + if (!lua_isnoneornil(L, idx)) { + if (!lua_istable(L, idx)) + return std::unexpected(Internal::configError(L, "buildRuleFromTable: failed to build table for exec rules from argument")); + + int optsIdx = lua_absindex(L, idx); + bool hasRuleEffects = false; + + rule = makeShared(); + + lua_pushnil(L); + while (lua_next(L, optsIdx) != 0) { + if (lua_type(L, -2) != LUA_TSTRING) { + lua_pop(L, 1); + return std::unexpected(Internal::configError(L, "buildRuleFromTable: effect key must be a string")); + } + + std::string key = lua_tostring(L, -2); + + if (key == "floating") + key = "float"; + + const auto* desc = Internal::findDescByName(Internal::WINDOW_RULE_EFFECT_DESCS, key); + if (!desc) { + const auto dynamicEffect = Desktop::Rule::windowEffects()->get(key); + if (!dynamicEffect.has_value()) { + lua_pop(L, 1); + return std::unexpected(Internal::configError(L, "buildRuleFromTable: unknown effect '{}'", key)); + } + + auto val = ruleValueToString(L); + if (!val) { + lua_pop(L, 1); + return std::unexpected(Internal::configError(L, "buildRuleFromTable: effect '{}': {}", key, val.error())); + } + + auto res = rule->addEffect(*dynamicEffect, *val); + if (!res) { + lua_pop(L, 1); + return std::unexpected(Internal::configError(L, "buildRuleFromTable: effect '{}': {}", key, res.error())); + } + hasRuleEffects = true; + + lua_pop(L, 1); + continue; + } + + auto res = Internal::addWindowRuleEffectFromLua(L, *desc, rule); + if (!res) { + lua_pop(L, 1); + return std::unexpected(Internal::configError(L, "buildRuleFromTable: effect '{}': {}", key, res.error())); + } + + hasRuleEffects = true; + lua_pop(L, 1); + } + + if (!hasRuleEffects) + return nullptr; + } + + return rule; +} + +bool Internal::hasTableField(lua_State* L, int tableIdx, const char* field) { + lua_getfield(L, tableIdx, field); + if (lua_isnoneornil(L, -1)) { + lua_pop(L, 1); + return false; + } + + lua_pop(L, 1); + return true; +} diff --git a/src/config/lua/bindings/LuaBindingsInternal.hpp b/src/config/lua/bindings/LuaBindingsInternal.hpp new file mode 100644 index 000000000..0cced6b47 --- /dev/null +++ b/src/config/lua/bindings/LuaBindingsInternal.hpp @@ -0,0 +1,206 @@ +#pragma once + +#include "../LuaBindings.hpp" + +#include "../ConfigManager.hpp" +#include "../types/LuaConfigValue.hpp" +#include "../types/LuaConfigBool.hpp" +#include "../types/LuaConfigFloat.hpp" +#include "../types/LuaConfigGradient.hpp" +#include "../types/LuaConfigInt.hpp" +#include "../types/LuaConfigString.hpp" +#include "../types/LuaConfigVec2.hpp" +#include "../types/LuaConfigExpressionVec2.hpp" + +#include "../../../Compositor.hpp" +#include "../../../helpers/MiscFunctions.hpp" +#include "../../../helpers/Monitor.hpp" +#include "../../../desktop/state/FocusState.hpp" +#include "../../../desktop/rule/windowRule/WindowRuleEffectContainer.hpp" +#include "../../../managers/KeybindManager.hpp" +#include "../../shared/actions/ConfigActions.hpp" + +#include +#include +#include +#include +#include +#include + +extern "C" { +#include +#include +} + +namespace Desktop::Rule { + class CWindowRule; +} + +namespace Config::Lua::Bindings::Internal { + + struct SWindowRuleEffectDesc { + const char* name; + std::function factory; + uint16_t effect; + }; + + using WE = Desktop::Rule::eWindowRuleEffect; + + inline const SWindowRuleEffectDesc WINDOW_RULE_EFFECT_DESCS[] = { + {"float", []() -> ILuaConfigValue* { return new CLuaConfigBool(false); }, WE::WINDOW_RULE_EFFECT_FLOAT}, + {"tile", []() -> ILuaConfigValue* { return new CLuaConfigBool(false); }, WE::WINDOW_RULE_EFFECT_TILE}, + {"fullscreen", []() -> ILuaConfigValue* { return new CLuaConfigBool(false); }, WE::WINDOW_RULE_EFFECT_FULLSCREEN}, + {"maximize", []() -> ILuaConfigValue* { return new CLuaConfigBool(false); }, WE::WINDOW_RULE_EFFECT_MAXIMIZE}, + {"center", []() -> ILuaConfigValue* { return new CLuaConfigBool(false); }, WE::WINDOW_RULE_EFFECT_CENTER}, + {"pseudo", []() -> ILuaConfigValue* { return new CLuaConfigBool(false); }, WE::WINDOW_RULE_EFFECT_PSEUDO}, + {"no_initial_focus", []() -> ILuaConfigValue* { return new CLuaConfigBool(false); }, WE::WINDOW_RULE_EFFECT_NOINITIALFOCUS}, + {"pin", []() -> ILuaConfigValue* { return new CLuaConfigBool(false); }, WE::WINDOW_RULE_EFFECT_PIN}, + {"fullscreen_state", []() -> ILuaConfigValue* { return new CLuaConfigString(STRVAL_EMPTY); }, WE::WINDOW_RULE_EFFECT_FULLSCREENSTATE}, + {"move", []() -> ILuaConfigValue* { return new CLuaConfigExpressionVec2(); }, WE::WINDOW_RULE_EFFECT_MOVE}, + {"size", []() -> ILuaConfigValue* { return new CLuaConfigExpressionVec2(); }, WE::WINDOW_RULE_EFFECT_SIZE}, + {"monitor", []() -> ILuaConfigValue* { return new CLuaConfigString(STRVAL_EMPTY); }, WE::WINDOW_RULE_EFFECT_MONITOR}, + {"workspace", []() -> ILuaConfigValue* { return new CLuaConfigString(STRVAL_EMPTY); }, WE::WINDOW_RULE_EFFECT_WORKSPACE}, + {"group", []() -> ILuaConfigValue* { return new CLuaConfigString(STRVAL_EMPTY); }, WE::WINDOW_RULE_EFFECT_GROUP}, + {"suppress_event", []() -> ILuaConfigValue* { return new CLuaConfigString(STRVAL_EMPTY); }, WE::WINDOW_RULE_EFFECT_SUPPRESSEVENT}, + {"content", []() -> ILuaConfigValue* { return new CLuaConfigString(STRVAL_EMPTY); }, WE::WINDOW_RULE_EFFECT_CONTENT}, + {"no_close_for", []() -> ILuaConfigValue* { return new CLuaConfigInt(0); }, WE::WINDOW_RULE_EFFECT_NOCLOSEFOR}, + {"scrolling_width", []() -> ILuaConfigValue* { return new CLuaConfigFloat(0.F); }, WE::WINDOW_RULE_EFFECT_SCROLLING_WIDTH}, + {"rounding", []() -> ILuaConfigValue* { return new CLuaConfigInt(0, 0, 20); }, WE::WINDOW_RULE_EFFECT_ROUNDING}, + {"border_size", []() -> ILuaConfigValue* { return new CLuaConfigInt(0); }, WE::WINDOW_RULE_EFFECT_BORDER_SIZE}, + {"rounding_power", []() -> ILuaConfigValue* { return new CLuaConfigFloat(2.F, 1.F, 10.F); }, WE::WINDOW_RULE_EFFECT_ROUNDING_POWER}, + {"scroll_mouse", []() -> ILuaConfigValue* { return new CLuaConfigFloat(1.F, 0.01F, 10.F); }, WE::WINDOW_RULE_EFFECT_SCROLL_MOUSE}, + {"scroll_touchpad", []() -> ILuaConfigValue* { return new CLuaConfigFloat(1.F, 0.01F, 10.F); }, WE::WINDOW_RULE_EFFECT_SCROLL_TOUCHPAD}, + {"animation", []() -> ILuaConfigValue* { return new CLuaConfigString(STRVAL_EMPTY); }, WE::WINDOW_RULE_EFFECT_ANIMATION}, + {"idle_inhibit", []() -> ILuaConfigValue* { return new CLuaConfigString(STRVAL_EMPTY); }, WE::WINDOW_RULE_EFFECT_IDLE_INHIBIT}, + {"opacity", []() -> ILuaConfigValue* { return new CLuaConfigString(STRVAL_EMPTY); }, WE::WINDOW_RULE_EFFECT_OPACITY}, + {"tag", []() -> ILuaConfigValue* { return new CLuaConfigString(STRVAL_EMPTY); }, WE::WINDOW_RULE_EFFECT_TAG}, + {"max_size", []() -> ILuaConfigValue* { return new CLuaConfigExpressionVec2(); }, WE::WINDOW_RULE_EFFECT_MAX_SIZE}, + {"min_size", []() -> ILuaConfigValue* { return new CLuaConfigExpressionVec2(); }, WE::WINDOW_RULE_EFFECT_MIN_SIZE}, + {"border_color", []() -> ILuaConfigValue* { return new CLuaConfigGradient(CHyprColor(0xFF000000)); }, WE::WINDOW_RULE_EFFECT_BORDER_COLOR}, + {"persistent_size", []() -> ILuaConfigValue* { return new CLuaConfigBool(false); }, WE::WINDOW_RULE_EFFECT_PERSISTENT_SIZE}, + {"allows_input", []() -> ILuaConfigValue* { return new CLuaConfigBool(false); }, WE::WINDOW_RULE_EFFECT_ALLOWS_INPUT}, + {"dim_around", []() -> ILuaConfigValue* { return new CLuaConfigBool(false); }, WE::WINDOW_RULE_EFFECT_DIM_AROUND}, + {"decorate", []() -> ILuaConfigValue* { return new CLuaConfigBool(true); }, WE::WINDOW_RULE_EFFECT_DECORATE}, + {"focus_on_activate", []() -> ILuaConfigValue* { return new CLuaConfigBool(false); }, WE::WINDOW_RULE_EFFECT_FOCUS_ON_ACTIVATE}, + {"keep_aspect_ratio", []() -> ILuaConfigValue* { return new CLuaConfigBool(false); }, WE::WINDOW_RULE_EFFECT_KEEP_ASPECT_RATIO}, + {"nearest_neighbor", []() -> ILuaConfigValue* { return new CLuaConfigBool(false); }, WE::WINDOW_RULE_EFFECT_NEAREST_NEIGHBOR}, + {"no_anim", []() -> ILuaConfigValue* { return new CLuaConfigBool(false); }, WE::WINDOW_RULE_EFFECT_NO_ANIM}, + {"no_blur", []() -> ILuaConfigValue* { return new CLuaConfigBool(false); }, WE::WINDOW_RULE_EFFECT_NO_BLUR}, + {"no_dim", []() -> ILuaConfigValue* { return new CLuaConfigBool(false); }, WE::WINDOW_RULE_EFFECT_NO_DIM}, + {"no_focus", []() -> ILuaConfigValue* { return new CLuaConfigBool(false); }, WE::WINDOW_RULE_EFFECT_NO_FOCUS}, + {"no_follow_mouse", []() -> ILuaConfigValue* { return new CLuaConfigBool(false); }, WE::WINDOW_RULE_EFFECT_NO_FOLLOW_MOUSE}, + {"no_max_size", []() -> ILuaConfigValue* { return new CLuaConfigBool(false); }, WE::WINDOW_RULE_EFFECT_NO_MAX_SIZE}, + {"no_shadow", []() -> ILuaConfigValue* { return new CLuaConfigBool(false); }, WE::WINDOW_RULE_EFFECT_NO_SHADOW}, + {"no_shortcuts_inhibit", []() -> ILuaConfigValue* { return new CLuaConfigBool(false); }, WE::WINDOW_RULE_EFFECT_NO_SHORTCUTS_INHIBIT}, + {"opaque", []() -> ILuaConfigValue* { return new CLuaConfigBool(false); }, WE::WINDOW_RULE_EFFECT_OPAQUE}, + {"force_rgbx", []() -> ILuaConfigValue* { return new CLuaConfigBool(false); }, WE::WINDOW_RULE_EFFECT_FORCE_RGBX}, + {"sync_fullscreen", []() -> ILuaConfigValue* { return new CLuaConfigBool(false); }, WE::WINDOW_RULE_EFFECT_SYNC_FULLSCREEN}, + {"immediate", []() -> ILuaConfigValue* { return new CLuaConfigBool(false); }, WE::WINDOW_RULE_EFFECT_IMMEDIATE}, + {"xray", []() -> ILuaConfigValue* { return new CLuaConfigBool(false); }, WE::WINDOW_RULE_EFFECT_XRAY}, + {"render_unfocused", []() -> ILuaConfigValue* { return new CLuaConfigBool(false); }, WE::WINDOW_RULE_EFFECT_RENDER_UNFOCUSED}, + {"no_screen_share", []() -> ILuaConfigValue* { return new CLuaConfigBool(false); }, WE::WINDOW_RULE_EFFECT_NO_SCREEN_SHARE}, + {"no_vrr", []() -> ILuaConfigValue* { return new CLuaConfigBool(false); }, WE::WINDOW_RULE_EFFECT_NO_VRR}, + {"stay_focused", []() -> ILuaConfigValue* { return new CLuaConfigBool(false); }, WE::WINDOW_RULE_EFFECT_STAY_FOCUSED}, + }; + + std::string argStr(lua_State* L, int idx); + std::optional tableOptStr(lua_State* L, int idx, const char* field); + std::optional tableOptNum(lua_State* L, int idx, const char* field); + std::optional tableOptBool(lua_State* L, int idx, const char* field); + + PHLMONITOR monitorFromLuaSelectorOrObject(lua_State* L, int idx, const char* fnName); + PHLWORKSPACE workspaceFromLuaSelectorOrObject(lua_State* L, int idx, const char* fnName); + PHLWINDOW windowFromLuaSelectorOrObject(lua_State* L, int idx, const char* fnName); + std::optional tableOptMonitor(lua_State* L, int idx, const char* field, const char* fnName); + std::optional tableOptWorkspace(lua_State* L, int idx, const char* field, const char* fnName); + std::optional tableOptWindow(lua_State* L, int idx, const char* field, const char* fnName); + + std::optional monitorSelectorFromLuaSelectorOrObject(lua_State* L, int idx, const char* fnName); + std::optional workspaceSelectorFromLuaSelectorOrObject(lua_State* L, int idx, const char* fnName); + std::optional windowSelectorFromLuaSelectorOrObject(lua_State* L, int idx, const char* fnName); + + std::optional tableOptMonitorSelector(lua_State* L, int idx, const char* field, const char* fnName); + std::optional tableOptWorkspaceSelector(lua_State* L, int idx, const char* field, const char* fnName); + std::optional tableOptWindowSelector(lua_State* L, int idx, const char* field, const char* fnName); + + std::string requireTableFieldMonitorSelector(lua_State* L, int idx, const char* field, const char* fnName); + std::string requireTableFieldWorkspaceSelector(lua_State* L, int idx, const char* field, const char* fnName); + std::string requireTableFieldWindowSelector(lua_State* L, int idx, const char* field, const char* fnName); + + Math::eDirection parseDirectionStr(const std::string& str); + Config::Actions::eTogglableAction parseToggleStr(const std::string& str); + + std::optional windowFromUpval(lua_State* L, int idx); + void pushWindowUpval(lua_State* L, int tableIdx); + int checkResult(lua_State* L, const Config::Actions::ActionResult& r); + int pushSuccessResult(lua_State* L, const Config::Actions::SActionResult& r = {}); + int pushErrorResult(lua_State* L, const Config::Actions::SActionError& e); + void reportError(lua_State* L, const Config::Actions::SActionError& e); + PHLWORKSPACE resolveWorkspaceStr(const std::string& args); + PHLMONITOR resolveMonitorStr(const std::string& args); + std::string getSourceInfo(lua_State* L, int stackLevel = 1); + + std::string requireTableFieldStr(lua_State* L, int idx, const char* field, const char* fnName); + double requireTableFieldNum(lua_State* L, int idx, const char* field, const char* fnName); + Config::Actions::eTogglableAction tableToggleAction(lua_State* L, int idx, const char* field = "action"); + + std::expected ruleValueToString(lua_State* L); + std::expected addWindowRuleEffectFromLua(lua_State* L, const SWindowRuleEffectDesc& desc, const SP& rule); + std::expected, int> buildRuleFromTable(lua_State* L, int idx); + + int configError(lua_State* L, std::string s, Config::Actions::eActionErrorLevel level = Config::Actions::eActionErrorLevel::ERROR, + Config::Actions::eActionErrorCode code = Config::Actions::eActionErrorCode::UNKNOWN, int stackLevel = 1); + int dispatcherError(lua_State* L, std::string s, Config::Actions::eActionErrorLevel level = Config::Actions::eActionErrorLevel::ERROR, + Config::Actions::eActionErrorCode code = Config::Actions::eActionErrorCode::UNKNOWN, int stackLevel = 1); + + template + int configError(lua_State* L, std::format_string fmt, Args&&... args) { + return configError(L, std::format(fmt, std::forward(args)...)); + } + + template + int dispatcherError(lua_State* L, Config::Actions::eActionErrorLevel level, Config::Actions::eActionErrorCode code, std::format_string fmt, Args&&... args) { + return dispatcherError(L, std::format(fmt, std::forward(args)...), level, code); + } + + template + int configError(lua_State* L, int stackLevel, std::format_string fmt, Args&&... args) { + return configError(L, std::format(fmt, std::forward(args)...), Config::Actions::eActionErrorLevel::ERROR, Config::Actions::eActionErrorCode::UNKNOWN, stackLevel); + } + + template + const T* findDescByName(const T (&descs)[N], std::string_view key) { + for (const auto& desc : descs) { + if (key == desc.name) + return &desc; + } + + return nullptr; + } + + void setFn(lua_State* L, const char* name, lua_CFunction fn); + void setMgrFn(lua_State* L, CConfigManager* mgr, const char* name, lua_CFunction fn); + + template + SParseError parseTableField(lua_State* L, int tableIdx, const char* field, T& parser) { + lua_getfield(L, tableIdx, field); + if (lua_isnil(L, -1)) { + lua_pop(L, 1); + return {.errorCode = PARSE_ERROR_BAD_VALUE, .message = std::format("missing required field \"{}\"", field)}; + } + + auto err = parser.parse(L); + lua_pop(L, 1); + if (err.errorCode != PARSE_ERROR_OK) + err.message = std::format("field \"{}\": {}", field, err.message); + return err; + } + + bool hasTableField(lua_State* L, int tableIdx, const char* field); + void registerToplevelBindings(lua_State* L, CConfigManager* mgr); + void registerQueryBindings(lua_State* L); + void registerNotificationBindings(lua_State* L); + void registerConfigRuleBindings(lua_State* L, CConfigManager* mgr); + void registerBindingsImpl(lua_State* L, CConfigManager* mgr); + void registerDispatcherBindings(lua_State* L); +} diff --git a/src/config/lua/bindings/LuaBindingsNotification.cpp b/src/config/lua/bindings/LuaBindingsNotification.cpp new file mode 100644 index 000000000..b7a54546a --- /dev/null +++ b/src/config/lua/bindings/LuaBindingsNotification.cpp @@ -0,0 +1,135 @@ +#include "LuaBindingsInternal.hpp" + +#include "../objects/LuaNotification.hpp" + +#include "../../../helpers/MiscFunctions.hpp" +#include "../../../notification/NotificationOverlay.hpp" + +#include +#include +#include +#include +#include + +using namespace Config; +using namespace Config::Lua; +using namespace Config::Lua::Bindings; + +static std::optional iconFromString(std::string iconName) { + std::ranges::transform(iconName, iconName.begin(), [](const unsigned char c) { return std::tolower(c); }); + + static constexpr std::array, 10> ICON_NAMES = { + std::pair{"warning", ICON_WARNING}, std::pair{"warn", ICON_WARNING}, std::pair{"info", ICON_INFO}, std::pair{"hint", ICON_HINT}, + std::pair{"error", ICON_ERROR}, std::pair{"err", ICON_ERROR}, std::pair{"confused", ICON_CONFUSED}, std::pair{"question", ICON_CONFUSED}, + std::pair{"ok", ICON_OK}, std::pair{"none", ICON_NONE}, + }; + + for (const auto& [name, icon] : ICON_NAMES) { + if (name == iconName) + return icon; + } + + return std::nullopt; +} + +static std::optional parseIconArg(lua_State* L, int idx) { + if (lua_isnumber(L, idx)) { + const auto raw = sc(lua_tonumber(L, idx)); + if (raw >= ICON_WARNING && raw <= ICON_NONE) + return sc(raw); + + return std::nullopt; + } + + if (lua_isstring(L, idx)) + return iconFromString(lua_tostring(L, idx)); + + return std::nullopt; +} + +static std::optional parseColorArg(lua_State* L, int idx) { + if (lua_isnumber(L, idx)) + return CHyprColor(sc(lua_tonumber(L, idx))); + + if (lua_isstring(L, idx)) { + auto parsed = configStringToInt(lua_tostring(L, idx)); + if (!parsed) + return std::nullopt; + + return CHyprColor(sc(*parsed)); + } + + return std::nullopt; +} + +static int hlNotificationCreate(lua_State* L) { + if (!lua_istable(L, 1)) + return Internal::configError(L, "hl.notification.create: expected a table { text, duration, icon?, color?, font_size? }"); + + const auto text = Internal::requireTableFieldStr(L, 1, "text", "hl.notification.create"); + + std::optional duration = Internal::tableOptNum(L, 1, "duration"); + if (!duration) + duration = Internal::tableOptNum(L, 1, "timeout"); + if (!duration) + duration = Internal::tableOptNum(L, 1, "time"); + + if (!duration) + return Internal::configError(L, "hl.notification.create: 'duration' (or 'timeout' / 'time') is required"); + if (*duration < 0) + return Internal::configError(L, "hl.notification.create: duration must be >= 0"); + + eIcons icon = ICON_NONE; + lua_getfield(L, 1, "icon"); + if (!lua_isnil(L, -1)) { + const auto parsedIcon = parseIconArg(L, -1); + if (!parsedIcon) + return Internal::configError(L, "hl.notification.create: invalid 'icon' (expected icon name or number)"); + + icon = *parsedIcon; + } + lua_pop(L, 1); + + CHyprColor color = CHyprColor(0); + lua_getfield(L, 1, "color"); + if (!lua_isnil(L, -1)) { + const auto parsedColor = parseColorArg(L, -1); + if (!parsedColor) + return Internal::configError(L, "hl.notification.create: invalid 'color' (expected color string or number)"); + + color = *parsedColor; + } + lua_pop(L, 1); + + float fontSize = 13.F; + if (const auto parsedFontSize = Internal::tableOptNum(L, 1, "font_size"); parsedFontSize.has_value()) { + if (*parsedFontSize <= 0) + return Internal::configError(L, "hl.notification.create: 'font_size' must be > 0"); + + fontSize = sc(*parsedFontSize); + } + + auto notification = Notification::overlay()->addNotification(text, color, sc(*duration), icon, fontSize); + Objects::CLuaNotification::push(L, notification); + return 1; +} + +static int hlGetNotifications(lua_State* L) { + lua_newtable(L); + + const auto notifications = Notification::overlay()->getNotifications(); + int i = 1; + for (const auto& notification : notifications) { + Objects::CLuaNotification::push(L, notification); + lua_rawseti(L, -2, i++); + } + + return 1; +} + +void Internal::registerNotificationBindings(lua_State* L) { + lua_newtable(L); + Internal::setFn(L, "create", hlNotificationCreate); + Internal::setFn(L, "get", hlGetNotifications); + lua_setfield(L, -2, "notification"); +} diff --git a/src/config/lua/bindings/LuaBindingsQuery.cpp b/src/config/lua/bindings/LuaBindingsQuery.cpp new file mode 100644 index 000000000..6e630388b --- /dev/null +++ b/src/config/lua/bindings/LuaBindingsQuery.cpp @@ -0,0 +1,400 @@ +#include "LuaBindingsInternal.hpp" + +#include "../objects/LuaLayerSurface.hpp" +#include "../objects/LuaMonitor.hpp" +#include "../objects/LuaWindow.hpp" +#include "../objects/LuaWorkspace.hpp" + +#include "../../../desktop/history/WindowHistoryTracker.hpp" +#include "../../../desktop/history/WorkspaceHistoryTracker.hpp" +#include "../../../desktop/rule/windowRule/WindowRuleEffectContainer.hpp" +#include "../../../desktop/view/LayerSurface.hpp" +#include "../../../desktop/view/Window.hpp" +#include "../../../managers/input/InputManager.hpp" + +using namespace Config; +using namespace Config::Lua; +using namespace Config::Lua::Bindings; + +namespace { + struct SWindowQuery { + std::optional monitor; + std::optional workspace; + std::optional floating; + std::optional mapped; + std::optional className; + std::optional title; + std::optional tag; + }; + + struct SLayerQuery { + std::optional monitor; + std::optional namespace_; + }; +} + +static bool windowMatchesQuery(const PHLWINDOW& w, const SWindowQuery& query) { + if (query.monitor) { + const auto mon = w->m_monitor.lock(); + if (mon != *query.monitor) + return false; + } + + if (query.workspace && w->m_workspace != *query.workspace) + return false; + + if (query.floating && w->m_isFloating != *query.floating) + return false; + + if (query.mapped && w->m_isMapped != *query.mapped) + return false; + + if (query.className && w->m_class != *query.className) + return false; + + if (query.title && w->m_title != *query.title) + return false; + + if (query.tag) { + if (!w->m_ruleApplicator || !w->m_ruleApplicator->m_tagKeeper.isTagged(*query.tag, true)) + return false; + } + + return true; +} + +static void pushWindowsMatchingQuery(lua_State* L, const SWindowQuery& query) { + lua_newtable(L); + + int i = 1; + for (const auto& w : g_pCompositor->m_windows) { + if (!windowMatchesQuery(w, query)) + continue; + + Objects::CLuaWindow::push(L, w); + lua_rawseti(L, -2, i++); + } +} + +static void parseWindowQueryFromTable(lua_State* L, int idx, const char* fnName, SWindowQuery& query) { + query.monitor = Internal::tableOptMonitor(L, idx, "monitor", fnName); + query.workspace = Internal::tableOptWorkspace(L, idx, "workspace", fnName); + + if (const auto floating = Internal::tableOptBool(L, idx, "floating"); floating.has_value()) + query.floating = floating; + + if (const auto mapped = Internal::tableOptBool(L, idx, "mapped"); mapped.has_value()) + query.mapped = mapped; + + query.className = Internal::tableOptStr(L, idx, "class"); + query.title = Internal::tableOptStr(L, idx, "title"); + query.tag = Internal::tableOptStr(L, idx, "tag"); +} + +static void parseLayerQueryFromTable(lua_State* L, int idx, const char* fnName, SLayerQuery& query) { + query.monitor = Internal::tableOptMonitor(L, idx, "monitor", fnName); + query.namespace_ = Internal::tableOptStr(L, idx, "namespace"); +} + +static PHLMONITOR monitorFromOptionalArg(lua_State* L, int idx, const char* fnName) { + if (lua_gettop(L) < idx || lua_isnil(L, idx)) + return Desktop::focusState()->monitor(); + + return Internal::monitorFromLuaSelectorOrObject(L, idx, fnName); +} + +static int hlGetWindows(lua_State* L) { + SWindowQuery query; + query.mapped = true; + + if (lua_gettop(L) >= 1 && !lua_isnil(L, 1)) { + if (!lua_istable(L, 1)) + return Internal::configError(L, "hl.get_windows: expected no args or a table of filters"); + + parseWindowQueryFromTable(L, 1, "hl.get_windows", query); + } + + pushWindowsMatchingQuery(L, query); + return 1; +} + +static int hlGetActiveWindow(lua_State* L) { + auto PWINDOW = Desktop::focusState()->window(); + if (!PWINDOW) { + lua_pushnil(L); + return 1; + } + + Config::Lua::Objects::CLuaWindow::push(L, PWINDOW); + return 1; +} + +static int hlGetWindow(lua_State* L) { + const auto PWINDOW = Internal::windowFromLuaSelectorOrObject(L, 1, "hl.get_window"); + if (!PWINDOW) { + lua_pushnil(L); + return 1; + } + + Objects::CLuaWindow::push(L, PWINDOW); + return 1; +} + +static int hlGetUrgentWindow(lua_State* L) { + const auto PWINDOW = g_pCompositor->getUrgentWindow(); + if (!PWINDOW) { + lua_pushnil(L); + return 1; + } + + Objects::CLuaWindow::push(L, PWINDOW); + return 1; +} + +static int hlGetWorkspaces(lua_State* L) { + lua_newtable(L); + int i = 1; + for (const auto& wsRef : g_pCompositor->getWorkspaces()) { + const auto ws = wsRef.lock(); + if (!ws || ws->inert()) + continue; + Objects::CLuaWorkspace::push(L, ws); + lua_rawseti(L, -2, i++); + } + return 1; +} + +static int hlGetWorkspace(lua_State* L) { + const auto PWORKSPACE = Internal::workspaceFromLuaSelectorOrObject(L, 1, "hl.get_workspace"); + if (!PWORKSPACE) { + lua_pushnil(L); + return 1; + } + + Objects::CLuaWorkspace::push(L, PWORKSPACE); + return 1; +} + +static int hlGetActiveWorkspace(lua_State* L) { + const auto PMONITOR = monitorFromOptionalArg(L, 1, "hl.get_active_workspace"); + if (!PMONITOR || !PMONITOR->m_activeWorkspace) { + lua_pushnil(L); + return 1; + } + + Objects::CLuaWorkspace::push(L, PMONITOR->m_activeWorkspace); + return 1; +} + +static int hlGetActiveSpecialWorkspace(lua_State* L) { + const auto PMONITOR = monitorFromOptionalArg(L, 1, "hl.get_active_special_workspace"); + if (!PMONITOR || !PMONITOR->m_activeSpecialWorkspace) { + lua_pushnil(L); + return 1; + } + + Objects::CLuaWorkspace::push(L, PMONITOR->m_activeSpecialWorkspace); + return 1; +} + +static int hlGetMonitors(lua_State* L) { + lua_newtable(L); + int i = 1; + for (const auto& mon : g_pCompositor->m_monitors) { + Objects::CLuaMonitor::push(L, mon); + lua_rawseti(L, -2, i++); + } + return 1; +} + +static int hlGetMonitor(lua_State* L) { + const auto PMONITOR = Internal::monitorFromLuaSelectorOrObject(L, 1, "hl.get_monitor"); + if (!PMONITOR) { + lua_pushnil(L); + return 1; + } + + Objects::CLuaMonitor::push(L, PMONITOR); + return 1; +} + +static int hlGetActiveMonitor(lua_State* L) { + const auto PMONITOR = Desktop::focusState()->monitor(); + if (!PMONITOR) { + lua_pushnil(L); + return 1; + } + + Objects::CLuaMonitor::push(L, PMONITOR); + return 1; +} + +static int hlGetMonitorAt(lua_State* L) { + double x = 0; + double y = 0; + + if (lua_istable(L, 1)) { + const auto tx = Internal::tableOptNum(L, 1, "x"); + const auto ty = Internal::tableOptNum(L, 1, "y"); + if (!tx || !ty) + return Internal::configError(L, "hl.get_monitor_at: expected a table { x, y }"); + + x = *tx; + y = *ty; + } else { + x = luaL_checknumber(L, 1); + y = luaL_checknumber(L, 2); + } + + const auto PMONITOR = g_pCompositor->getMonitorFromVector(Vector2D{x, y}); + if (!PMONITOR) { + lua_pushnil(L); + return 1; + } + + Objects::CLuaMonitor::push(L, PMONITOR); + return 1; +} + +static int hlGetMonitorAtCursor(lua_State* L) { + const auto PMONITOR = g_pCompositor->getMonitorFromCursor(); + if (!PMONITOR) { + lua_pushnil(L); + return 1; + } + + Objects::CLuaMonitor::push(L, PMONITOR); + return 1; +} + +static int hlGetCursorPos(lua_State* L) { + if (!g_pInputManager) { + lua_pushnil(L); + return 1; + } + + const auto pos = g_pInputManager->getMouseCoordsInternal(); + + lua_newtable(L); + lua_pushnumber(L, pos.x); + lua_setfield(L, -2, "x"); + lua_pushnumber(L, pos.y); + lua_setfield(L, -2, "y"); + return 1; +} + +static int hlGetLastWindow(lua_State* L) { + const auto current = Desktop::focusState()->window(); + const auto& fullHistory = Desktop::History::windowTracker()->fullHistory(); + + for (auto it = fullHistory.rbegin(); it != fullHistory.rend(); ++it) { + const auto candidate = it->lock(); + if (!candidate || !candidate->m_isMapped) + continue; + + if (current && candidate == current) + continue; + + Objects::CLuaWindow::push(L, candidate); + return 1; + } + + lua_pushnil(L); + return 1; +} + +static int hlGetLastWorkspace(lua_State* L) { + const bool hadMonitorArg = lua_gettop(L) >= 1 && !lua_isnil(L, 1); + const auto PMONITOR = hadMonitorArg ? Internal::monitorFromLuaSelectorOrObject(L, 1, "hl.get_last_workspace") : Desktop::focusState()->monitor(); + if (!PMONITOR || !PMONITOR->m_activeWorkspace) { + lua_pushnil(L); + return 1; + } + + const auto current = PMONITOR->m_activeWorkspace; + + auto previous = hadMonitorArg ? Desktop::History::workspaceTracker()->previousWorkspace(current, PMONITOR) : Desktop::History::workspaceTracker()->previousWorkspace(current); + + auto ws = previous.workspace.lock(); + if ((!ws || ws->inert()) && previous.id != WORKSPACE_INVALID) + ws = g_pCompositor->getWorkspaceByID(previous.id); + + if (!ws || ws->inert()) { + lua_pushnil(L); + return 1; + } + + Objects::CLuaWorkspace::push(L, ws); + return 1; +} + +static int hlGetLayers(lua_State* L) { + SLayerQuery query; + + if (lua_gettop(L) >= 1 && !lua_isnil(L, 1)) { + if (!lua_istable(L, 1)) + return Internal::configError(L, "hl.get_layers: expected no args or a table of filters"); + + parseLayerQueryFromTable(L, 1, "hl.get_layers", query); + } + + lua_newtable(L); + int i = 1; + for (const auto& mon : g_pCompositor->m_monitors) { + if (query.monitor && mon != *query.monitor) + continue; + + for (const auto& level : mon->m_layerSurfaceLayers) { + for (const auto& lsRef : level) { + const auto ls = lsRef.lock(); + if (!ls) + continue; + + if (query.namespace_ && ls->m_namespace != *query.namespace_) + continue; + + Objects::CLuaLayerSurface::push(L, ls); + lua_rawseti(L, -2, i++); + } + } + } + return 1; +} + +static int hlGetWorkspaceWindows(lua_State* L) { + const auto ws = Internal::workspaceFromLuaSelectorOrObject(L, 1, "hl.get_workspace_windows"); + + SWindowQuery query; + query.workspace = ws; + query.mapped = true; + + pushWindowsMatchingQuery(L, query); + return 1; +} + +static int hlGetCurrentSubmap(lua_State* L) { + lua_pushstring(L, Config::Actions::state()->m_currentSubmap.c_str()); + return 1; +} + +void Internal::registerQueryBindings(lua_State* L) { + Internal::setFn(L, "get_windows", hlGetWindows); + Internal::setFn(L, "get_window", hlGetWindow); + Internal::setFn(L, "get_active_window", hlGetActiveWindow); + Internal::setFn(L, "get_urgent_window", hlGetUrgentWindow); + Internal::setFn(L, "get_workspaces", hlGetWorkspaces); + Internal::setFn(L, "get_workspace", hlGetWorkspace); + Internal::setFn(L, "get_active_workspace", hlGetActiveWorkspace); + Internal::setFn(L, "get_active_special_workspace", hlGetActiveSpecialWorkspace); + Internal::setFn(L, "get_monitors", hlGetMonitors); + Internal::setFn(L, "get_monitor", hlGetMonitor); + Internal::setFn(L, "get_active_monitor", hlGetActiveMonitor); + Internal::setFn(L, "get_monitor_at", hlGetMonitorAt); + Internal::setFn(L, "get_monitor_at_cursor", hlGetMonitorAtCursor); + Internal::setFn(L, "get_layers", hlGetLayers); + Internal::setFn(L, "get_workspace_windows", hlGetWorkspaceWindows); + Internal::setFn(L, "get_cursor_pos", hlGetCursorPos); + Internal::setFn(L, "get_last_window", hlGetLastWindow); + Internal::setFn(L, "get_last_workspace", hlGetLastWorkspace); + Internal::setFn(L, "get_current_submap", hlGetCurrentSubmap); +} diff --git a/src/config/lua/bindings/LuaBindingsRegistration.cpp b/src/config/lua/bindings/LuaBindingsRegistration.cpp new file mode 100644 index 000000000..0cb593649 --- /dev/null +++ b/src/config/lua/bindings/LuaBindingsRegistration.cpp @@ -0,0 +1,97 @@ +#include "LuaBindingsInternal.hpp" + +#include "../objects/LuaEventSubscription.hpp" +#include "../objects/LuaKeybind.hpp" +#include "../objects/LuaLayerRule.hpp" +#include "../objects/LuaNotification.hpp" +#include "../objects/LuaTimer.hpp" +#include "../objects/LuaWindowRule.hpp" + +using namespace Config; +using namespace Config::Lua; +using namespace Config::Lua::Bindings; + +static int hlPrint(lua_State* L) { + const int n = lua_gettop(L); + std::string out; + for (int i = 1; i <= n; i++) { + size_t len = 0; + const char* s = luaL_tolstring(L, i, &len); + if (i > 1) + out += '\t'; + out.append(s, len); + lua_pop(L, 1); + } + Log::logger->log(Log::INFO, "[Lua] {}", out); + return 0; +} + +static SDispatchResult dispatchResultFromLua(lua_State* L, int idx) { + SDispatchResult result; + + if (!lua_istable(L, idx)) + return result; + + lua_getfield(L, idx, "pass_event"); + result.passEvent = lua_toboolean(L, -1); + lua_pop(L, 1); + + lua_getfield(L, idx, "ok"); + if (lua_isboolean(L, -1)) + result.success = lua_toboolean(L, -1); + lua_pop(L, 1); + + if (!result.success) { + lua_getfield(L, idx, "error"); + if (lua_isstring(L, -1)) + result.error = lua_tostring(L, -1); + lua_pop(L, 1); + } + + return result; +} + +void Internal::registerBindingsImpl(lua_State* L, CConfigManager* mgr) { + Objects::CLuaTimer{}.setup(L); + Objects::CLuaEventSubscription{}.setup(L); + Objects::CLuaWindowRule{}.setup(L); + Objects::CLuaLayerRule{}.setup(L); + Objects::CLuaKeybind{}.setup(L); + Objects::CLuaNotification{}.setup(L); + + g_pKeybindManager->m_dispatchers["__lua"] = [L](std::string arg) -> SDispatchResult { + int ref = std::stoi(arg); + lua_rawgeti(L, LUA_REGISTRYINDEX, ref); + + int status = LUA_OK; + if (auto* mgr = CConfigManager::fromLuaState(L); mgr) + status = mgr->guardedPCall(0, 1, 0, CConfigManager::LUA_TIMEOUT_KEYBIND_CALLBACK_MS, "keybind callback"); + else + status = lua_pcall(L, 0, 1, 0); + + if (status != LUA_OK) { + Config::Lua::Bindings::Internal::reportError(L, + Config::Actions::SActionError{std::format("error in keybind lambda: {}", lua_tostring(L, -1)), + Config::Actions::eActionErrorLevel::ERROR, Config::Actions::eActionErrorCode::LUA_ERROR}); + lua_pop(L, 1); + return {.success = false, .error = "lua keybind error"}; + } + + auto result = dispatchResultFromLua(L, -1); + lua_pop(L, 1); + return result; + }; + + lua_newtable(L); + + Internal::registerConfigRuleBindings(L, mgr); + Internal::registerToplevelBindings(L, mgr); + Internal::registerQueryBindings(L); + Internal::registerDispatcherBindings(L); + Internal::registerNotificationBindings(L); + + lua_setglobal(L, "hl"); + + lua_pushcfunction(L, hlPrint); + lua_setglobal(L, "print"); +} diff --git a/src/config/lua/bindings/LuaBindingsToplevel.cpp b/src/config/lua/bindings/LuaBindingsToplevel.cpp new file mode 100644 index 000000000..8bdd1c193 --- /dev/null +++ b/src/config/lua/bindings/LuaBindingsToplevel.cpp @@ -0,0 +1,428 @@ +#include "LuaBindingsInternal.hpp" + +#include "../objects/LuaEventSubscription.hpp" +#include "../objects/LuaKeybind.hpp" +#include "../objects/LuaTimer.hpp" + +#include "../../supplementary/executor/Executor.hpp" + +#include "../../../devices/IKeyboard.hpp" +#include "../../../managers/eventLoop/EventLoopManager.hpp" + +#include +#include + +using namespace Config; +using namespace Config::Lua; +using namespace Config::Lua::Bindings; +using namespace Hyprutils::String; + +static std::optional modFromSv(std::string_view sv) { + if (sv == "SHIFT") + return HL_MODIFIER_SHIFT; + if (sv == "CAPS") + return HL_MODIFIER_CAPS; + if (sv == "CTRL" || sv == "CONTROL") + return HL_MODIFIER_CTRL; + if (sv == "ALT" || sv == "MOD1") + return HL_MODIFIER_ALT; + if (sv == "MOD2") + return HL_MODIFIER_MOD2; + if (sv == "MOD3") + return HL_MODIFIER_MOD3; + if (sv == "SUPER" || sv == "WIN" || sv == "LOGO" || sv == "MOD4" || sv == "META") + return HL_MODIFIER_META; + if (sv == "MOD5") + return HL_MODIFIER_MOD5; + + return std::nullopt; +} + +static bool isSymSpecial(std::string_view sv) { + if (sv == "mouse_down" || sv == "mouse_up" || sv == "mouse_left" || sv == "mouse_right") + return true; + + return sv.starts_with("switch:") || sv.starts_with("mouse:"); +} + +static std::expected parseKeyString(SKeybind& kb, std::string_view sv) { + bool modsEnded = false, specialSym = false; + CVarList2 vl(sv, 0, '+', true); + + uint32_t modMask = 0; + std::vector keysyms; + std::string lastKeyArg; + + if (sv == "catchall") { + kb.catchAll = true; + return {}; + } + + for (const auto& a : vl) { + auto arg = Hyprutils::String::trim(a); + + auto mask = modFromSv(arg); + + if (!mask) + modsEnded = true; + + if (modsEnded && mask) + return std::unexpected("Modifiers must come first in the list"); + + if (mask) { + modMask |= *mask; + continue; + } + + if (specialSym) + return std::unexpected("Cannot combine special syms (e.g. mouse_down + Q)"); + + if (isSymSpecial(arg)) { + if (!keysyms.empty()) + return std::unexpected("Cannot combine special syms (e.g. mouse_down + Q)"); + + specialSym = true; + kb.key = arg; + continue; + } + + auto sym = xkb_keysym_from_name(std::string{arg}.c_str(), XKB_KEYSYM_CASE_INSENSITIVE); + + if (sym == XKB_KEY_NoSymbol) { + if (arg.contains(' ')) + return std::unexpected(std::format("Unknown keysym: \"{}\", did you forget a +?", arg)); + + if (arg == "Enter") + return std::unexpected(std::format(R"(Unknown keysym: "{}", did you mean "Return"?)", arg)); + + return std::unexpected(std::format("Unknown keysym: \"{}\"", arg)); + } + + lastKeyArg = arg; + keysyms.emplace_back(sym); + } + + kb.modmask = modMask; + kb.sMkKeys = std::move(keysyms); + if (!specialSym && !lastKeyArg.empty()) + kb.key = lastKeyArg; + return {}; +} + +static int hlBind(lua_State* L) { + auto* mgr = sc(lua_touserdata(L, lua_upvalueindex(1))); + + std::string_view keys = luaL_checkstring(L, 1); + + SKeybind kb; + kb.submap.name = mgr->m_currentSubmap; + kb.submap.reset = mgr->m_currentSubmapReset; + + 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)) + 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); + kb.displayKey = keys; + + int optsIdx = 3; + + if (lua_istable(L, optsIdx)) { + auto getBool = [&](const char* field) -> bool { + lua_getfield(L, optsIdx, field); + const bool v = lua_toboolean(L, -1); + lua_pop(L, 1); + return v; + }; + + auto readOptString = [&](const char* field) -> std::optional { + lua_getfield(L, optsIdx, field); + + if (lua_isnil(L, -1)) { + lua_pop(L, 1); + return std::nullopt; + } + + if (!lua_isstring(L, -1)) { + lua_pop(L, 1); + Internal::configError(L, "hl.bind: opts.{} must be a string", field); + return std::nullopt; + } + + std::string result = lua_tostring(L, -1); + lua_pop(L, 1); + return result; + }; + + kb.repeat = getBool("repeating"); + kb.locked = getBool("locked"); + kb.release = getBool("release"); + kb.nonConsuming = getBool("non_consuming"); + kb.autoConsuming = getBool("auto_consuming"); + kb.transparent = getBool("transparent"); + kb.ignoreMods = getBool("ignore_mods"); + kb.dontInhibit = getBool("dont_inhibit"); + kb.longPress = getBool("long_press"); + kb.submapUniversal = getBool("submap_universal"); + + if (auto description = readOptString("description"); description.has_value()) { + kb.description = *description; + kb.hasDescription = true; + } else if (auto desc = readOptString("desc"); desc.has_value()) { + kb.description = *desc; + kb.hasDescription = true; + } + + bool click = false; + bool drag = false; + + if (getBool("click")) { + click = true; + kb.release = true; + } + + if (getBool("drag")) { + drag = true; + kb.release = true; + } + + if (click && drag) + return Internal::configError(L, "hl.bind: click and drag are exclusive"); + + if ((kb.longPress || kb.release) && kb.repeat) + return Internal::configError(L, "hl.bind: long_press / release is incompatible with repeat"); + + if (kb.mouse && (kb.repeat || kb.release || kb.locked)) + return Internal::configError(L, "hl.bind: mouse is exclusive"); + + kb.click = click; + kb.drag = drag; + + lua_getfield(L, optsIdx, "device"); + if (lua_istable(L, -1)) { + lua_getfield(L, -1, "inclusive"); + kb.deviceInclusive = lua_isnil(L, -1) ? true : lua_toboolean(L, -1); + lua_pop(L, 1); + + lua_getfield(L, -1, "list"); + if (lua_istable(L, -1)) { + lua_pushnil(L); + while (lua_next(L, -2)) { + if (lua_isstring(L, -1)) + kb.devices.emplace(lua_tostring(L, -1)); + lua_pop(L, 1); + } + } + lua_pop(L, 1); + } + lua_pop(L, 1); + } + + const auto BIND = g_pKeybindManager->addKeybind(kb); + Objects::CLuaKeybind::push(L, BIND); + return 1; +} + +static int hlDefineSubmap(lua_State* L) { + auto* mgr = sc(lua_touserdata(L, lua_upvalueindex(1))); + const char* name = luaL_checkstring(L, 1); + + std::string reset; + int fnIdx = 2; + if (lua_gettop(L) >= 3 && lua_isstring(L, 2)) { + reset = lua_tostring(L, 2); + fnIdx = 3; + } + + luaL_checktype(L, fnIdx, LUA_TFUNCTION); + + std::string prev = mgr->m_currentSubmap; + std::string prevReset = mgr->m_currentSubmapReset; + mgr->m_currentSubmap = name; + mgr->m_currentSubmapReset = reset; + + lua_pushvalue(L, fnIdx); + if (mgr->guardedPCall(0, 0, 0, CConfigManager::LUA_TIMEOUT_DISPATCH_MS, std::format("hl.define_submap(\"{}\")", name)) != LUA_OK) { + mgr->addError(std::format("hl.define_submap: error in submap \"{}\": {}", name, lua_tostring(L, -1))); + lua_pop(L, 1); + } + + mgr->m_currentSubmap = prev; + mgr->m_currentSubmapReset = prevReset; + return 0; +} + +static int hlVersion(lua_State* L) { + lua_pushstring(L, HYPRLAND_VERSION); + return 1; +} + +static int hlExecCmd(lua_State* L) { + auto cmd = Internal::argStr(L, 1); + + if (cmd.empty()) + return Internal::configError(L, "hl.exec_cmd: expected command as first argument"); + + auto rule = Internal::buildRuleFromTable(L, 2); + + if (!rule) + return rule.error(); + + Config::Supplementary::executor()->spawn(Supplementary::SExecRequest{cmd, !*rule, *rule}); + + return 0; +} + +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())"); + + 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"); + else + status = lua_pcall(L, 0, 1, 0); + + if (status != LUA_OK) { + const char* err = lua_tostring(L, -1); + lua_pop(L, 1); + return Internal::dispatcherError(L, std::format("hl.dispatch: {}", err ? err : "unknown error"), Config::Actions::eActionErrorLevel::ERROR, + Config::Actions::eActionErrorCode::LUA_ERROR); + } + + if (lua_isnil(L, -1)) { + lua_pop(L, 1); + return Internal::pushSuccessResult(L); + } + + return 1; +} + +static int hlOn(lua_State* L) { + auto* mgr = sc(lua_touserdata(L, lua_upvalueindex(1))); + const char* eventName = luaL_checkstring(L, 1); + luaL_checktype(L, 2, LUA_TFUNCTION); + + lua_pushvalue(L, 2); + int ref = luaL_ref(L, LUA_REGISTRYINDEX); + + const auto handle = mgr->m_eventHandler->registerEvent(eventName, ref); + if (!handle.has_value()) { + luaL_unref(L, LUA_REGISTRYINDEX, ref); + const auto& known = CLuaEventHandler::knownEvents(); + std::string list; + for (const auto& e : known) { + list += e + ", "; + } + list.pop_back(); + list.pop_back(); + return Internal::configError(L, "hl.on: unknown event \"{}\". Known events:{}", eventName, list); + } + + Objects::CLuaEventSubscription::push(L, mgr->m_eventHandler.get(), *handle); + return 1; +} + +static int hlUnbind(lua_State* L) { + if (lua_isstring(L, 1) && std::string_view(lua_tostring(L, 1)) == "all" && lua_gettop(L) == 1) { + g_pKeybindManager->clearKeybinds(); + return 0; + } + + const char* str = luaL_checkstring(L, 1); + g_pKeybindManager->removeKeybind(str); + + return 0; +} + +static int hlTimer(lua_State* L) { + auto* mgr = sc(lua_touserdata(L, lua_upvalueindex(1))); + + luaL_checktype(L, 1, LUA_TFUNCTION); + luaL_checktype(L, 2, LUA_TTABLE); + + lua_getfield(L, 2, "timeout"); + if (!lua_isnumber(L, -1)) + return Internal::configError(L, "hl.timer: opts.timeout must be a number (ms)"); + int timeoutMs = (int)lua_tonumber(L, -1); + lua_pop(L, 1); + + if (timeoutMs <= 0) + return Internal::configError(L, "hl.timer: opts.timeout must be > 0"); + + lua_getfield(L, 2, "type"); + if (!lua_isstring(L, -1)) + return Internal::configError(L, "hl.timer: opts.type must be \"repeat\" or \"oneshot\""); + std::string type = lua_tostring(L, -1); + lua_pop(L, 1); + + bool repeat = false; + if (type == "repeat") + repeat = true; + else if (type != "oneshot") + return Internal::configError(L, "hl.timer: opts.type must be \"repeat\" or \"oneshot\""); + + lua_pushvalue(L, 1); + int ref = luaL_ref(L, LUA_REGISTRYINDEX); + + auto timer = makeShared( + std::chrono::milliseconds(timeoutMs), + [L, ref, repeat, timeoutMs, mgr](SP self, void* data) { + // update repeat already so that if we call set_timeout inside + // our timer it doesn't get overwritten + if (repeat) + self->updateTimeout(std::chrono::milliseconds(timeoutMs)); + + lua_rawgeti(L, LUA_REGISTRYINDEX, ref); + int status = LUA_OK; + if (mgr) + status = mgr->guardedPCall(0, 0, 0, CConfigManager::LUA_TIMEOUT_TIMER_CALLBACK_MS, "hl.timer callback"); + else + status = lua_pcall(L, 0, 0, 0); + + if (status != LUA_OK) { + Log::logger->log(Log::ERR, "[Lua] error in timer callback: {}", lua_tostring(L, -1)); + lua_pop(L, 1); + } + + if (!repeat) { + const auto HAS = std::ranges::find_if(mgr->m_luaTimers, [&self](const auto& lt) { return lt.timer == self; }) != mgr->m_luaTimers.end(); + + // avoid double-unref if this timer triggered a reload + if (HAS) { + luaL_unref(L, LUA_REGISTRYINDEX, ref); + std::erase_if(mgr->m_luaTimers, [&self](const auto& lt) { return lt.timer == self; }); + } + } + }, + nullptr); + + mgr->m_luaTimers.emplace_back(CConfigManager::SLuaTimer{timer, ref}); + if (g_pEventLoopManager) + g_pEventLoopManager->addTimer(timer); + + Objects::CLuaTimer::push(L, timer, timeoutMs); + return 1; +} + +void Internal::registerToplevelBindings(lua_State* L, CConfigManager* mgr) { + Internal::setMgrFn(L, mgr, "on", hlOn); + Internal::setMgrFn(L, mgr, "bind", hlBind); + Internal::setMgrFn(L, mgr, "define_submap", hlDefineSubmap); + Internal::setMgrFn(L, mgr, "timer", hlTimer); + + Internal::setFn(L, "dispatch", hlDispatch); + Internal::setFn(L, "version", hlVersion); + Internal::setFn(L, "exec_cmd", hlExecCmd); + + Internal::setFn(L, "unbind", hlUnbind); +} diff --git a/src/config/lua/objects/LuaEventSubscription.cpp b/src/config/lua/objects/LuaEventSubscription.cpp new file mode 100644 index 000000000..c37491d75 --- /dev/null +++ b/src/config/lua/objects/LuaEventSubscription.cpp @@ -0,0 +1,74 @@ +#include "LuaEventSubscription.hpp" + +#include +#include + +using namespace Config::Lua; + +static constexpr const char* MT = "HL.EventSubscription"; + +namespace { + struct SEventSubscriptionRef { + CLuaEventHandler* handler = nullptr; + uint64_t handle = 0; + bool active = false; + }; +} + +static int eventSubscriptionEq(lua_State* L) { + const auto* lhs = sc(luaL_checkudata(L, 1, MT)); + const auto* rhs = sc(luaL_checkudata(L, 2, MT)); + + lua_pushboolean(L, lhs->handler == rhs->handler && lhs->handle == rhs->handle); + return 1; +} + +static int eventSubscriptionToString(lua_State* L) { + const auto* ref = sc(luaL_checkudata(L, 1, MT)); + + const auto str = std::format("HL.EventSubscription({},{})", ref->handle, ref->active ? "active" : "inactive"); + lua_pushstring(L, str.c_str()); + return 1; +} + +static int eventSubscriptionRemove(lua_State* L) { + auto* ref = sc(luaL_checkudata(L, 1, MT)); + + if (!ref->active || !ref->handler) + return 0; + + ref->handler->unregisterEvent(ref->handle); + ref->active = false; + return 0; +} + +static int eventSubscriptionIsActive(lua_State* L) { + auto* ref = sc(luaL_checkudata(L, 1, MT)); + + lua_pushboolean(L, ref->active); + return 1; +} + +static int eventSubscriptionIndex(lua_State* L) { + luaL_checkudata(L, 1, MT); + const std::string_view key = luaL_checkstring(L, 2); + + if (key == "remove") + lua_pushcfunction(L, eventSubscriptionRemove); + else if (key == "is_active") + lua_pushcfunction(L, eventSubscriptionIsActive); + else + lua_pushnil(L); + + return 1; +} + +void Objects::CLuaEventSubscription::setup(lua_State* L) { + registerMetatable(L, MT, eventSubscriptionIndex, gcRef, eventSubscriptionEq, eventSubscriptionToString); +} + +void Objects::CLuaEventSubscription::push(lua_State* L, CLuaEventHandler* handler, uint64_t handle) { + new (lua_newuserdata(L, sizeof(SEventSubscriptionRef))) SEventSubscriptionRef{.handler = handler, .handle = handle, .active = true}; + luaL_getmetatable(L, MT); + lua_setmetatable(L, -2); +} diff --git a/src/config/lua/objects/LuaEventSubscription.hpp b/src/config/lua/objects/LuaEventSubscription.hpp new file mode 100644 index 000000000..11cdfc62e --- /dev/null +++ b/src/config/lua/objects/LuaEventSubscription.hpp @@ -0,0 +1,12 @@ +#pragma once + +#include "LuaObjectHelpers.hpp" +#include "../LuaEventHandler.hpp" + +namespace Config::Lua::Objects { + class CLuaEventSubscription : public ILuaObjectWrapper { + public: + void setup(lua_State* L) override; + static void push(lua_State* L, CLuaEventHandler* handler, uint64_t handle); + }; +} diff --git a/src/config/lua/objects/LuaGroup.cpp b/src/config/lua/objects/LuaGroup.cpp new file mode 100644 index 000000000..551f8a915 --- /dev/null +++ b/src/config/lua/objects/LuaGroup.cpp @@ -0,0 +1,83 @@ +#include "LuaGroup.hpp" +#include "LuaWindow.hpp" +#include "LuaObjectHelpers.hpp" + +#include "../../../desktop/view/Group.hpp" + +#include + +using namespace Config::Lua; + +static constexpr const char* MT = "HL.Group"; + +static int groupEq(lua_State* L) { + const auto* lhs = sc*>(luaL_checkudata(L, 1, MT)); + const auto* rhs = sc*>(luaL_checkudata(L, 2, MT)); + + lua_pushboolean(L, lhs->lock() == rhs->lock()); + return 1; +} + +static int groupToString(lua_State* L) { + const auto* ref = sc*>(luaL_checkudata(L, 1, MT)); + const auto group = ref->lock(); + + if (!group) + lua_pushstring(L, "HL.Group(expired)"); + else + lua_pushfstring(L, "HL.Group(%p)", group.get()); + + return 1; +} + +static int groupIndex(lua_State* L) { + auto* ref = sc*>(luaL_checkudata(L, 1, MT)); + const auto group = ref->lock(); + if (!group) { + Log::logger->log(Log::DEBUG, "[lua] Tried to access an expired object"); + lua_pushnil(L); + return 1; + } + + const std::string_view key = luaL_checkstring(L, 2); + + if (key == "locked") + lua_pushboolean(L, group->locked()); + else if (key == "denied") + lua_pushboolean(L, group->denied()); + else if (key == "size") + lua_pushinteger(L, sc(group->size())); + else if (key == "current_index") + lua_pushinteger(L, sc(group->getCurrentIdx()) + 1); + else if (key == "current") { + const auto current = group->current(); + if (current) + Objects::CLuaWindow::push(L, current); + else + lua_pushnil(L); + } else if (key == "members") { + lua_newtable(L); + int i = 1; + for (const auto& grouped : group->windows()) { + const auto groupedWindow = grouped.lock(); + if (!groupedWindow) + continue; + + Objects::CLuaWindow::push(L, groupedWindow); + lua_rawseti(L, -2, i++); + } + } else + lua_pushnil(L); + + return 1; +} + +void Objects::CLuaGroup::setup(lua_State* L) { + registerMetatable(L, MT, groupIndex, gcRef>, groupEq, groupToString); +} + +void Objects::CLuaGroup::push(lua_State* L, SP group) { + new (lua_newuserdata(L, sizeof(WP))) WP(group); + luaL_getmetatable(L, MT); + lua_setmetatable(L, -2); +} diff --git a/src/config/lua/objects/LuaGroup.hpp b/src/config/lua/objects/LuaGroup.hpp new file mode 100644 index 000000000..b7d638c6c --- /dev/null +++ b/src/config/lua/objects/LuaGroup.hpp @@ -0,0 +1,14 @@ +#pragma once + +#include +#include + +#include "../../../desktop/view/Group.hpp" + +namespace Config::Lua::Objects { + class CLuaGroup { + public: + static void setup(lua_State* L); + static void push(lua_State* L, SP group); + }; +}; diff --git a/src/config/lua/objects/LuaKeybind.cpp b/src/config/lua/objects/LuaKeybind.cpp new file mode 100644 index 000000000..ea2ce43d8 --- /dev/null +++ b/src/config/lua/objects/LuaKeybind.cpp @@ -0,0 +1,182 @@ +#include "LuaKeybind.hpp" + +#include +#include + +using namespace Config::Lua; + +static constexpr const char* MT = "HL.Keybind"; + +namespace { + std::optional> getKeybindFromUserdata(lua_State* L) { + auto* ref = sc*>(luaL_checkudata(L, 1, MT)); + return ref->lock(); + } + + void pushDeviceList(lua_State* L, const SKeybind& keybind) { + lua_newtable(L); + int i = 1; + for (const auto& device : keybind.devices) { + lua_pushstring(L, device.c_str()); + lua_rawseti(L, -2, i++); + } + } +} + +static int keybindEq(lua_State* L) { + const auto* lhs = sc*>(luaL_checkudata(L, 1, MT)); + const auto* rhs = sc*>(luaL_checkudata(L, 2, MT)); + + lua_pushboolean(L, lhs->lock() == rhs->lock()); + return 1; +} + +static int keybindToString(lua_State* L) { + const auto* ref = sc*>(luaL_checkudata(L, 1, MT)); + const auto keybind = ref->lock(); + + if (!keybind) + lua_pushstring(L, "HL.Keybind(expired)"); + else + lua_pushfstring(L, "HL.Keybind(%p)", keybind.get()); + + return 1; +} + +static int keybindSetEnabled(lua_State* L) { + luaL_checktype(L, 2, LUA_TBOOLEAN); + + const auto keybind = getKeybindFromUserdata(L); + if (!keybind) + return 0; + + (*keybind)->enabled = lua_toboolean(L, 2); + return 0; +} + +static int keybindIsEnabled(lua_State* L) { + const auto keybind = getKeybindFromUserdata(L); + if (!keybind) { + lua_pushnil(L); + return 1; + } + + lua_pushboolean(L, (*keybind)->enabled); + return 1; +} + +static int keybindRemove(lua_State* L) { + const auto keybind = getKeybindFromUserdata(L); + if (!keybind || !g_pKeybindManager) + return 0; + + if ((*keybind)->handler == "__lua") { + try { + const int ref = std::stoi((*keybind)->arg); + if (ref > 0) + luaL_unref(L, LUA_REGISTRYINDEX, ref); + } catch (...) { + // invalid ref, ignore + } + + (*keybind)->arg = std::to_string(LUA_NOREF); + } + + g_pKeybindManager->removeKeybind((*keybind)->modmask, SParsedKey{.key = (*keybind)->key, .keycode = (*keybind)->keycode, .catchAll = (*keybind)->catchAll}); + return 0; +} + +static int keybindGetDescription(lua_State* L) { + const auto keybind = getKeybindFromUserdata(L); + if (!keybind) { + lua_pushnil(L); + return 1; + } + + if (!(*keybind)->hasDescription) + lua_pushnil(L); + else + lua_pushstring(L, (*keybind)->description.c_str()); + + return 1; +} + +static int keybindIndex(lua_State* L) { + const auto keybind = getKeybindFromUserdata(L); + const std::string_view key = luaL_checkstring(L, 2); + + if (key == "set_enabled") + lua_pushcfunction(L, keybindSetEnabled); + else if (key == "is_enabled") + lua_pushcfunction(L, keybindIsEnabled); + else if (key == "remove" || key == "unbind") + lua_pushcfunction(L, keybindRemove); + else if (!keybind) + lua_pushnil(L); + else if (key == "enabled") + lua_pushboolean(L, (*keybind)->enabled); + else if (key == "has_description") + lua_pushboolean(L, (*keybind)->hasDescription); + else if (key == "description") + return keybindGetDescription(L); + else if (key == "display_key") + lua_pushstring(L, (*keybind)->displayKey.c_str()); + else if (key == "submap") + lua_pushstring(L, (*keybind)->submap.name.c_str()); + else if (key == "handler") + lua_pushstring(L, (*keybind)->handler.c_str()); + else if (key == "arg") + lua_pushstring(L, (*keybind)->arg.c_str()); + else if (key == "modmask") + lua_pushinteger(L, sc((*keybind)->modmask)); + else if (key == "key") + lua_pushstring(L, (*keybind)->key.c_str()); + else if (key == "keycode") + lua_pushinteger(L, sc((*keybind)->keycode)); + else if (key == "catchall") + lua_pushboolean(L, (*keybind)->catchAll); + else if (key == "repeating") + lua_pushboolean(L, (*keybind)->repeat); + else if (key == "locked") + lua_pushboolean(L, (*keybind)->locked); + else if (key == "release") + lua_pushboolean(L, (*keybind)->release); + else if (key == "non_consuming") + lua_pushboolean(L, (*keybind)->nonConsuming); + else if (key == "auto_consuming") + lua_pushboolean(L, (*keybind)->autoConsuming); + else if (key == "transparent") + lua_pushboolean(L, (*keybind)->transparent); + else if (key == "ignore_mods") + lua_pushboolean(L, (*keybind)->ignoreMods); + else if (key == "long_press") + lua_pushboolean(L, (*keybind)->longPress); + else if (key == "dont_inhibit") + lua_pushboolean(L, (*keybind)->dontInhibit); + else if (key == "click") + lua_pushboolean(L, (*keybind)->click); + else if (key == "drag") + lua_pushboolean(L, (*keybind)->drag); + else if (key == "submap_universal") + lua_pushboolean(L, (*keybind)->submapUniversal); + else if (key == "mouse") + lua_pushboolean(L, (*keybind)->mouse); + else if (key == "device_inclusive") + lua_pushboolean(L, (*keybind)->deviceInclusive); + else if (key == "devices") + pushDeviceList(L, **keybind); + else + lua_pushnil(L); + + return 1; +} + +void Objects::CLuaKeybind::setup(lua_State* L) { + registerMetatable(L, MT, keybindIndex, gcRef>, keybindEq, keybindToString); +} + +void Objects::CLuaKeybind::push(lua_State* L, const SP& keybind) { + new (lua_newuserdata(L, sizeof(WP))) WP(keybind); + luaL_getmetatable(L, MT); + lua_setmetatable(L, -2); +} diff --git a/src/config/lua/objects/LuaKeybind.hpp b/src/config/lua/objects/LuaKeybind.hpp new file mode 100644 index 000000000..c55ed998c --- /dev/null +++ b/src/config/lua/objects/LuaKeybind.hpp @@ -0,0 +1,12 @@ +#pragma once + +#include "LuaObjectHelpers.hpp" +#include "../../../managers/KeybindManager.hpp" + +namespace Config::Lua::Objects { + class CLuaKeybind : public ILuaObjectWrapper { + public: + void setup(lua_State* L) override; + static void push(lua_State* L, const SP& keybind); + }; +} diff --git a/src/config/lua/objects/LuaLayerRule.cpp b/src/config/lua/objects/LuaLayerRule.cpp new file mode 100644 index 000000000..22e062720 --- /dev/null +++ b/src/config/lua/objects/LuaLayerRule.cpp @@ -0,0 +1,80 @@ +#include "LuaLayerRule.hpp" + +#include "../../../desktop/rule/Engine.hpp" + +#include + +using namespace Config::Lua; + +static constexpr const char* MT = "HL.LayerRule"; + +// +static int layerRuleEq(lua_State* L) { + const auto* lhs = sc*>(luaL_checkudata(L, 1, MT)); + const auto* rhs = sc*>(luaL_checkudata(L, 2, MT)); + + lua_pushboolean(L, lhs->lock() == rhs->lock()); + return 1; +} + +static int layerRuleToString(lua_State* L) { + const auto* ref = sc*>(luaL_checkudata(L, 1, MT)); + const auto rule = ref->lock(); + + if (!rule) + lua_pushstring(L, "HL.LayerRule(expired)"); + else + lua_pushfstring(L, "HL.LayerRule(%p)", rule.get()); + + return 1; +} + +static int layerRuleSetEnabled(lua_State* L) { + auto* ref = sc*>(luaL_checkudata(L, 1, MT)); + luaL_checktype(L, 2, LUA_TBOOLEAN); + + const auto rule = ref->lock(); + if (!rule) + return 0; + + rule->setEnabled(lua_toboolean(L, 2)); + Desktop::Rule::ruleEngine()->updateAllRules(); + return 0; +} + +static int layerRuleIsEnabled(lua_State* L) { + auto* ref = sc*>(luaL_checkudata(L, 1, MT)); + + const auto rule = ref->lock(); + if (!rule) { + lua_pushnil(L); + return 1; + } + + lua_pushboolean(L, rule->isEnabled()); + return 1; +} + +static int layerRuleIndex(lua_State* L) { + luaL_checkudata(L, 1, MT); + const std::string_view key = luaL_checkstring(L, 2); + + if (key == "set_enabled") + lua_pushcfunction(L, layerRuleSetEnabled); + else if (key == "is_enabled") + lua_pushcfunction(L, layerRuleIsEnabled); + else + lua_pushnil(L); + + return 1; +} + +void Objects::CLuaLayerRule::setup(lua_State* L) { + registerMetatable(L, MT, layerRuleIndex, gcRef>, layerRuleEq, layerRuleToString); +} + +void Objects::CLuaLayerRule::push(lua_State* L, const SP& rule) { + new (lua_newuserdata(L, sizeof(WP))) WP(rule); + luaL_getmetatable(L, MT); + lua_setmetatable(L, -2); +} diff --git a/src/config/lua/objects/LuaLayerRule.hpp b/src/config/lua/objects/LuaLayerRule.hpp new file mode 100644 index 000000000..2e4d24e73 --- /dev/null +++ b/src/config/lua/objects/LuaLayerRule.hpp @@ -0,0 +1,12 @@ +#pragma once + +#include "LuaObjectHelpers.hpp" +#include "../../../desktop/rule/layerRule/LayerRule.hpp" + +namespace Config::Lua::Objects { + class CLuaLayerRule : public ILuaObjectWrapper { + public: + void setup(lua_State* L) override; + static void push(lua_State* L, const SP& rule); + }; +} diff --git a/src/config/lua/objects/LuaLayerSurface.cpp b/src/config/lua/objects/LuaLayerSurface.cpp new file mode 100644 index 000000000..99b221545 --- /dev/null +++ b/src/config/lua/objects/LuaLayerSurface.cpp @@ -0,0 +1,88 @@ +#include "LuaLayerSurface.hpp" +#include "LuaMonitor.hpp" +#include "LuaObjectHelpers.hpp" + +#include "../../../desktop/view/LayerSurface.hpp" + +#include +#include + +using namespace Config::Lua; + +static constexpr const char* MT = "HL.LayerSurface"; + +// +static int layerSurfaceEq(lua_State* L) { + const auto* lhs = sc(luaL_checkudata(L, 1, MT)); + const auto* rhs = sc(luaL_checkudata(L, 2, MT)); + + lua_pushboolean(L, lhs->lock() == rhs->lock()); + return 1; +} + +static int layerSurfaceToString(lua_State* L) { + const auto* ref = sc(luaL_checkudata(L, 1, MT)); + const auto ls = ref->lock(); + + if (!ls) + lua_pushstring(L, "HL.LayerSurface(expired)"); + else + lua_pushfstring(L, "HL.LayerSurface(%p)", ls.get()); + + return 1; +} + +static int layerSurfaceIndex(lua_State* L) { + auto* ref = sc(luaL_checkudata(L, 1, MT)); + const auto ls = ref->lock(); + if (!ls) { + Log::logger->log(Log::DEBUG, "[lua] Tried to access an expired object"); + lua_pushnil(L); + return 1; + } + + const std::string_view key = luaL_checkstring(L, 2); + + if (key == "address") + lua_pushstring(L, std::format("0x{:x}", reinterpret_cast(ls.get())).c_str()); + else if (key == "x") + lua_pushinteger(L, ls->m_geometry.x); + else if (key == "y") + lua_pushinteger(L, ls->m_geometry.y); + else if (key == "w") + lua_pushinteger(L, ls->m_geometry.width); + else if (key == "h") + lua_pushinteger(L, ls->m_geometry.height); + else if (key == "namespace") + lua_pushstring(L, ls->m_namespace.c_str()); + else if (key == "pid") + lua_pushinteger(L, sc(ls->getPID())); + else if (key == "monitor") { + const auto mon = ls->m_monitor.lock(); + if (mon) + Objects::CLuaMonitor::push(L, mon); + else + lua_pushnil(L); + } else if (key == "mapped") + lua_pushboolean(L, ls->m_mapped); + else if (key == "layer") + lua_pushinteger(L, sc(ls->m_layer)); + else if (key == "interactivity") + lua_pushinteger(L, sc(ls->m_interactivity)); + else if (key == "above_fullscreen") + lua_pushboolean(L, ls->m_aboveFullscreen); + else + lua_pushnil(L); + + return 1; +} + +void Objects::CLuaLayerSurface::setup(lua_State* L) { + registerMetatable(L, MT, layerSurfaceIndex, gcRef, layerSurfaceEq, layerSurfaceToString); +} + +void Objects::CLuaLayerSurface::push(lua_State* L, PHLLS ls) { + new (lua_newuserdata(L, sizeof(PHLLSREF))) PHLLSREF(ls ? ls->m_self : nullptr); + luaL_getmetatable(L, MT); + lua_setmetatable(L, -2); +} diff --git a/src/config/lua/objects/LuaLayerSurface.hpp b/src/config/lua/objects/LuaLayerSurface.hpp new file mode 100644 index 000000000..bfbd93e7c --- /dev/null +++ b/src/config/lua/objects/LuaLayerSurface.hpp @@ -0,0 +1,12 @@ +#pragma once + +#include "LuaObjectHelpers.hpp" +#include "../../../desktop/DesktopTypes.hpp" + +namespace Config::Lua::Objects { + class CLuaLayerSurface : public ILuaObjectWrapper { + public: + void setup(lua_State* L) override; + static void push(lua_State* L, PHLLS ls); + }; +} diff --git a/src/config/lua/objects/LuaMonitor.cpp b/src/config/lua/objects/LuaMonitor.cpp new file mode 100644 index 000000000..9d0701c1f --- /dev/null +++ b/src/config/lua/objects/LuaMonitor.cpp @@ -0,0 +1,122 @@ +#include "LuaMonitor.hpp" +#include "LuaWorkspace.hpp" +#include "LuaObjectHelpers.hpp" + +#include "../../../helpers/Monitor.hpp" +#include "../../../desktop/state/FocusState.hpp" + +#include + +using namespace Config::Lua; + +static constexpr const char* MT = "HL.Monitor"; + +// +static int monitorEq(lua_State* L) { + const auto* lhs = sc(luaL_checkudata(L, 1, MT)); + const auto* rhs = sc(luaL_checkudata(L, 2, MT)); + + lua_pushboolean(L, lhs->lock() == rhs->lock()); + return 1; +} + +static int monitorToString(lua_State* L) { + const auto* ref = sc(luaL_checkudata(L, 1, MT)); + const auto mon = ref->lock(); + + if (!mon) + lua_pushstring(L, "HL.Monitor(expired)"); + else + lua_pushfstring(L, "HL.Monitor(%d:%s)", mon->m_id, mon->m_name.c_str()); + + return 1; +} + +static int monitorIndex(lua_State* L) { + auto* ref = sc(luaL_checkudata(L, 1, MT)); + const auto mon = ref->lock(); + if (!mon) { + Log::logger->log(Log::DEBUG, "[lua] Tried to access an expired object"); + lua_pushnil(L); + return 1; + } + + const std::string_view key = luaL_checkstring(L, 2); + + if (key == "id") + lua_pushinteger(L, sc(mon->m_id)); + else if (key == "name") + lua_pushstring(L, mon->m_name.c_str()); + else if (key == "description") + lua_pushstring(L, mon->m_shortDescription.c_str()); + else if (key == "width") + lua_pushinteger(L, sc(mon->m_pixelSize.x)); + else if (key == "height") + lua_pushinteger(L, sc(mon->m_pixelSize.y)); + else if (key == "refresh_rate") + lua_pushnumber(L, mon->m_refreshRate); + else if (key == "x") + lua_pushinteger(L, sc(mon->m_position.x)); + else if (key == "y") + lua_pushinteger(L, sc(mon->m_position.y)); + else if (key == "active_workspace") { + if (mon->m_activeWorkspace) + Objects::CLuaWorkspace::push(L, mon->m_activeWorkspace); + else + lua_pushnil(L); + } else if (key == "active_special_workspace") { + if (mon->m_activeSpecialWorkspace) + Objects::CLuaWorkspace::push(L, mon->m_activeSpecialWorkspace); + else + lua_pushnil(L); + } else if (key == "position") { + lua_newtable(L); + lua_pushinteger(L, sc(mon->m_position.x)); + lua_setfield(L, -2, "x"); + lua_pushinteger(L, sc(mon->m_position.y)); + lua_setfield(L, -2, "y"); + } else if (key == "size") { + lua_newtable(L); + lua_pushinteger(L, sc(mon->m_pixelSize.x)); + lua_setfield(L, -2, "width"); + lua_pushinteger(L, sc(mon->m_pixelSize.y)); + lua_setfield(L, -2, "height"); + } else if (key == "scale") + lua_pushnumber(L, mon->m_scale); + else if (key == "transform") + lua_pushinteger(L, sc(mon->m_transform)); + else if (key == "dpms_status") + lua_pushboolean(L, mon->m_dpmsStatus); + else if (key == "vrr_active") + lua_pushboolean(L, mon->m_vrrActive); + else if (key == "is_mirror") + lua_pushboolean(L, mon->isMirror()); + else if (key == "mirrors") { + lua_newtable(L); + + int i = 1; + for (const auto& mirrorRef : mon->m_mirrors) { + const auto mirror = mirrorRef.lock(); + if (!mirror) + continue; + + Objects::CLuaMonitor::push(L, mirror); + lua_rawseti(L, -2, i++); + } + } else if (key == "focused") + lua_pushboolean(L, mon == Desktop::focusState()->monitor()); + else + lua_pushnil(L); + + return 1; +} + +void Objects::CLuaMonitor::setup(lua_State* L) { + registerMetatable(L, MT, monitorIndex, gcRef, monitorEq, monitorToString); +} + +void Objects::CLuaMonitor::push(lua_State* L, PHLMONITOR mon) { + new (lua_newuserdata(L, sizeof(PHLMONITORREF))) PHLMONITORREF(mon ? mon->m_self : nullptr); + luaL_getmetatable(L, MT); + lua_setmetatable(L, -2); +} diff --git a/src/config/lua/objects/LuaMonitor.hpp b/src/config/lua/objects/LuaMonitor.hpp new file mode 100644 index 000000000..585d51e83 --- /dev/null +++ b/src/config/lua/objects/LuaMonitor.hpp @@ -0,0 +1,12 @@ +#pragma once + +#include "LuaObjectHelpers.hpp" +#include "../../../desktop/DesktopTypes.hpp" + +namespace Config::Lua::Objects { + class CLuaMonitor : public ILuaObjectWrapper { + public: + void setup(lua_State* L) override; + static void push(lua_State* L, PHLMONITOR mon); + }; +} diff --git a/src/config/lua/objects/LuaNotification.cpp b/src/config/lua/objects/LuaNotification.cpp new file mode 100644 index 000000000..f116e2ebe --- /dev/null +++ b/src/config/lua/objects/LuaNotification.cpp @@ -0,0 +1,377 @@ +#include "LuaNotification.hpp" + +#include "../../../helpers/MiscFunctions.hpp" + +#include +#include +#include +#include +#include + +using namespace Config::Lua; + +static constexpr const char* MT = "HL.Notification"; + +namespace { + struct SNotificationRef { + WP notification; + bool paused = false; + }; + + std::optional parseColor(lua_State* L, int idx) { + if (lua_isnumber(L, idx)) + return CHyprColor(sc(lua_tonumber(L, idx))); + + if (lua_isstring(L, idx)) { + auto parsed = configStringToInt(lua_tostring(L, idx)); + if (!parsed) + return std::nullopt; + + return CHyprColor(sc(*parsed)); + } + + return std::nullopt; + } + + std::optional iconFromString(std::string iconName) { + std::ranges::transform(iconName, iconName.begin(), [](const unsigned char c) { return std::tolower(c); }); + + static constexpr std::array, 10> ICON_NAMES = { + std::pair{"warning", ICON_WARNING}, std::pair{"warn", ICON_WARNING}, std::pair{"info", ICON_INFO}, std::pair{"hint", ICON_HINT}, + std::pair{"error", ICON_ERROR}, std::pair{"err", ICON_ERROR}, std::pair{"confused", ICON_CONFUSED}, std::pair{"question", ICON_CONFUSED}, + std::pair{"ok", ICON_OK}, std::pair{"none", ICON_NONE}, + }; + + for (const auto& [name, icon] : ICON_NAMES) { + if (name == iconName) + return icon; + } + + return std::nullopt; + } + + std::optional parseIcon(lua_State* L, int idx) { + if (lua_isnumber(L, idx)) { + const auto raw = sc(lua_tonumber(L, idx)); + if (raw >= ICON_WARNING && raw <= ICON_NONE) + return sc(raw); + + return std::nullopt; + } + + if (lua_isstring(L, idx)) + return iconFromString(lua_tostring(L, idx)); + + return std::nullopt; + } +} + +static int notificationEq(lua_State* L) { + const auto* lhs = sc(luaL_checkudata(L, 1, MT)); + const auto* rhs = sc(luaL_checkudata(L, 2, MT)); + + lua_pushboolean(L, lhs->notification.lock() == rhs->notification.lock()); + return 1; +} + +static int notificationToString(lua_State* L) { + const auto* ref = sc(luaL_checkudata(L, 1, MT)); + const auto notification = ref->notification.lock(); + + if (!notification) + lua_pushstring(L, "HL.Notification(expired)"); + else + lua_pushfstring(L, "HL.Notification(%p)", notification.get()); + + return 1; +} + +static int notificationGC(lua_State* L) { + auto* ref = sc(luaL_checkudata(L, 1, MT)); + + if (ref->paused) { + if (const auto notification = ref->notification.lock(); notification) + notification->unlock(); + } + + ref->~SNotificationRef(); + return 0; +} + +static int notificationPause(lua_State* L) { + auto* ref = sc(luaL_checkudata(L, 1, MT)); + + const auto notification = ref->notification.lock(); + if (!notification) + return 0; + + if (ref->paused) + return 0; + + notification->lock(); + ref->paused = true; + return 0; +} + +static int notificationResume(lua_State* L) { + auto* ref = sc(luaL_checkudata(L, 1, MT)); + + const auto notification = ref->notification.lock(); + if (!notification) + return 0; + + if (!ref->paused) + return 0; + + notification->unlock(); + ref->paused = false; + return 0; +} + +static int notificationSetPaused(lua_State* L) { + luaL_checkudata(L, 1, MT); + luaL_checktype(L, 2, LUA_TBOOLEAN); + + if (lua_toboolean(L, 2)) + return notificationPause(L); + + return notificationResume(L); +} + +static int notificationIsPaused(lua_State* L) { + auto* ref = sc(luaL_checkudata(L, 1, MT)); + + const auto notification = ref->notification.lock(); + if (!notification) { + lua_pushnil(L); + return 1; + } + + lua_pushboolean(L, notification->isLocked()); + return 1; +} + +static int notificationSetText(lua_State* L) { + auto* ref = sc(luaL_checkudata(L, 1, MT)); + const auto* text = luaL_checkstring(L, 2); + + if (const auto notification = ref->notification.lock(); notification) + notification->setText(text); + + return 0; +} + +static int notificationSetTimeout(lua_State* L) { + auto* ref = sc(luaL_checkudata(L, 1, MT)); + + const auto timeoutMs = sc(luaL_checknumber(L, 2)); + if (timeoutMs < 0.F) + return Config::Lua::Bindings::Internal::configError(L, "HL.Notification:set_timeout: timeout must be >= 0"); + + if (const auto notification = ref->notification.lock(); notification) + notification->resetTimeout(timeoutMs); + + return 0; +} + +static int notificationSetColor(lua_State* L) { + auto* ref = sc(luaL_checkudata(L, 1, MT)); + + const auto color = parseColor(L, 2); + if (!color) + return Config::Lua::Bindings::Internal::configError(L, "HL.Notification:set_color: expected a color string or number"); + + if (const auto notification = ref->notification.lock(); notification) + notification->setColor(*color); + + return 0; +} + +static int notificationSetIcon(lua_State* L) { + auto* ref = sc(luaL_checkudata(L, 1, MT)); + + const auto icon = parseIcon(L, 2); + if (!icon) + return Config::Lua::Bindings::Internal::configError(L, "HL.Notification:set_icon: expected an icon name or number"); + + if (const auto notification = ref->notification.lock(); notification) + notification->setIcon(*icon); + + return 0; +} + +static int notificationSetFontSize(lua_State* L) { + auto* ref = sc(luaL_checkudata(L, 1, MT)); + + const auto fontSize = sc(luaL_checknumber(L, 2)); + if (fontSize <= 0.F) + return Config::Lua::Bindings::Internal::configError(L, "HL.Notification:set_font_size: font size must be > 0"); + + if (const auto notification = ref->notification.lock(); notification) + notification->setFontSize(fontSize); + + return 0; +} + +static int notificationDismiss(lua_State* L) { + auto* ref = sc(luaL_checkudata(L, 1, MT)); + + if (const auto notification = ref->notification.lock(); notification) + Notification::overlay()->dismissNotification(notification); + + return 0; +} + +static int notificationGetText(lua_State* L) { + auto* ref = sc(luaL_checkudata(L, 1, MT)); + + const auto notification = ref->notification.lock(); + if (!notification) { + lua_pushnil(L); + return 1; + } + + lua_pushstring(L, notification->text().c_str()); + return 1; +} + +static int notificationGetTimeout(lua_State* L) { + auto* ref = sc(luaL_checkudata(L, 1, MT)); + + const auto notification = ref->notification.lock(); + if (!notification) { + lua_pushnil(L); + return 1; + } + + lua_pushnumber(L, notification->timeMs()); + return 1; +} + +static int notificationGetColor(lua_State* L) { + auto* ref = sc(luaL_checkudata(L, 1, MT)); + + const auto notification = ref->notification.lock(); + if (!notification) { + lua_pushnil(L); + return 1; + } + + lua_pushinteger(L, sc(notification->color().getAsHex())); + return 1; +} + +static int notificationGetIcon(lua_State* L) { + auto* ref = sc(luaL_checkudata(L, 1, MT)); + + const auto notification = ref->notification.lock(); + if (!notification) { + lua_pushnil(L); + return 1; + } + + lua_pushinteger(L, sc(notification->icon())); + return 1; +} + +static int notificationGetFontSize(lua_State* L) { + auto* ref = sc(luaL_checkudata(L, 1, MT)); + + const auto notification = ref->notification.lock(); + if (!notification) { + lua_pushnil(L); + return 1; + } + + lua_pushnumber(L, notification->fontSize()); + return 1; +} + +static int notificationGetElapsed(lua_State* L) { + auto* ref = sc(luaL_checkudata(L, 1, MT)); + + const auto notification = ref->notification.lock(); + if (!notification) { + lua_pushnil(L); + return 1; + } + + lua_pushnumber(L, notification->timeElapsedMs()); + return 1; +} + +static int notificationGetElapsedSinceCreation(lua_State* L) { + auto* ref = sc(luaL_checkudata(L, 1, MT)); + + const auto notification = ref->notification.lock(); + if (!notification) { + lua_pushnil(L); + return 1; + } + + lua_pushnumber(L, notification->timeElapsedSinceCreationMs()); + return 1; +} + +static int notificationIsAlive(lua_State* L) { + auto* ref = sc(luaL_checkudata(L, 1, MT)); + + lua_pushboolean(L, ref->notification.lock().get() != nullptr); + return 1; +} + +static int notificationIndex(lua_State* L) { + luaL_checkudata(L, 1, MT); + const std::string_view key = luaL_checkstring(L, 2); + + if (key == "pause") + lua_pushcfunction(L, notificationPause); + else if (key == "resume") + lua_pushcfunction(L, notificationResume); + else if (key == "set_paused") + lua_pushcfunction(L, notificationSetPaused); + else if (key == "is_paused") + lua_pushcfunction(L, notificationIsPaused); + else if (key == "set_text") + lua_pushcfunction(L, notificationSetText); + else if (key == "set_timeout") + lua_pushcfunction(L, notificationSetTimeout); + else if (key == "set_color") + lua_pushcfunction(L, notificationSetColor); + else if (key == "set_icon") + lua_pushcfunction(L, notificationSetIcon); + else if (key == "set_font_size") + lua_pushcfunction(L, notificationSetFontSize); + else if (key == "dismiss") + lua_pushcfunction(L, notificationDismiss); + else if (key == "get_text") + lua_pushcfunction(L, notificationGetText); + else if (key == "get_timeout") + lua_pushcfunction(L, notificationGetTimeout); + else if (key == "get_color") + lua_pushcfunction(L, notificationGetColor); + else if (key == "get_icon") + lua_pushcfunction(L, notificationGetIcon); + else if (key == "get_font_size") + lua_pushcfunction(L, notificationGetFontSize); + else if (key == "get_elapsed") + lua_pushcfunction(L, notificationGetElapsed); + else if (key == "get_elapsed_since_creation") + lua_pushcfunction(L, notificationGetElapsedSinceCreation); + else if (key == "is_alive") + lua_pushcfunction(L, notificationIsAlive); + else + lua_pushnil(L); + + return 1; +} + +void Objects::CLuaNotification::setup(lua_State* L) { + registerMetatable(L, MT, notificationIndex, notificationGC, notificationEq, notificationToString); +} + +void Objects::CLuaNotification::push(lua_State* L, const SP& notification) { + new (lua_newuserdata(L, sizeof(SNotificationRef))) SNotificationRef{.notification = WP(notification)}; + luaL_getmetatable(L, MT); + lua_setmetatable(L, -2); +} diff --git a/src/config/lua/objects/LuaNotification.hpp b/src/config/lua/objects/LuaNotification.hpp new file mode 100644 index 000000000..2ac8b0ebb --- /dev/null +++ b/src/config/lua/objects/LuaNotification.hpp @@ -0,0 +1,12 @@ +#pragma once + +#include "LuaObjectHelpers.hpp" +#include "../../../notification/NotificationOverlay.hpp" + +namespace Config::Lua::Objects { + class CLuaNotification : public ILuaObjectWrapper { + public: + void setup(lua_State* L) override; + static void push(lua_State* L, const SP& notification); + }; +} diff --git a/src/config/lua/objects/LuaObjectHelpers.hpp b/src/config/lua/objects/LuaObjectHelpers.hpp new file mode 100644 index 000000000..69c284efc --- /dev/null +++ b/src/config/lua/objects/LuaObjectHelpers.hpp @@ -0,0 +1,51 @@ +#pragma once + +#include "../bindings/LuaBindingsInternal.hpp" + +#include "../../../helpers/memory/Memory.hpp" + +extern "C" { +#include +} + +namespace Config::Lua::Objects { + + template + inline int gcRef(lua_State* L) { + sc(lua_touserdata(L, 1))->~T(); + return 0; + } + + inline int readOnlyNewIndex(lua_State* L) { + return Config::Lua::Bindings::Internal::configError(L, "attempt to modify read-only hl object"); + } + + inline void registerMetatable(lua_State* L, const char* name, lua_CFunction indexFn, lua_CFunction gcFn, lua_CFunction eqFn = nullptr, lua_CFunction toStringFn = nullptr) { + luaL_newmetatable(L, name); + lua_pushcfunction(L, indexFn); + lua_setfield(L, -2, "__index"); + lua_pushcfunction(L, readOnlyNewIndex); + lua_setfield(L, -2, "__newindex"); + lua_pushcfunction(L, gcFn); + lua_setfield(L, -2, "__gc"); + + if (eqFn) { + lua_pushcfunction(L, eqFn); + lua_setfield(L, -2, "__eq"); + } + + if (toStringFn) { + lua_pushcfunction(L, toStringFn); + lua_setfield(L, -2, "__tostring"); + } + + lua_pop(L, 1); + } + + class ILuaObjectWrapper { + public: + virtual ~ILuaObjectWrapper() = default; + virtual void setup(lua_State* L) = 0; + }; + +} diff --git a/src/config/lua/objects/LuaTimer.cpp b/src/config/lua/objects/LuaTimer.cpp new file mode 100644 index 000000000..46f9a5192 --- /dev/null +++ b/src/config/lua/objects/LuaTimer.cpp @@ -0,0 +1,109 @@ +#include "LuaTimer.hpp" + +#include +#include + +using namespace Config::Lua; + +static constexpr const char* MT = "HL.Timer"; + +namespace { + struct STimerRef { + WP timer; + int timeoutMs = 0; + }; +} + +static int timerEq(lua_State* L) { + const auto* lhs = sc(luaL_checkudata(L, 1, MT)); + const auto* rhs = sc(luaL_checkudata(L, 2, MT)); + + lua_pushboolean(L, lhs->timer.lock() == rhs->timer.lock()); + return 1; +} + +static int timerToString(lua_State* L) { + const auto* ref = sc(luaL_checkudata(L, 1, MT)); + const auto timer = ref->timer.lock(); + + if (!timer) + lua_pushstring(L, "HL.Timer(expired)"); + else + lua_pushfstring(L, "HL.Timer(%p)", timer.get()); + + return 1; +} + +static int timerSetEnabled(lua_State* L) { + auto* ref = sc(luaL_checkudata(L, 1, MT)); + luaL_checktype(L, 2, LUA_TBOOLEAN); + + const auto timer = ref->timer.lock(); + if (!timer) + return 0; + + if (lua_toboolean(L, 2)) + timer->updateTimeout(std::chrono::milliseconds(std::max(ref->timeoutMs, 1))); + else + timer->updateTimeout(std::nullopt); + + return 0; +} + +static int timerSetTimeout(lua_State* L) { + auto* ref = sc(luaL_checkudata(L, 1, MT)); + luaL_checktype(L, 2, LUA_TNUMBER); + + const auto timer = ref->timer.lock(); + if (!timer) + return 0; + + uint64_t x = lua_tointeger(L, 2); + + if (x < 1) + return Config::Lua::Bindings::Internal::configError(L, "hl.timer:set_timeout: timeout must be greater or equal to 1ms"); + + timer->updateTimeout(std::chrono::milliseconds(x)); + ref->timeoutMs = x; + + return 0; +} + +static int timerIsEnabled(lua_State* L) { + auto* ref = sc(luaL_checkudata(L, 1, MT)); + + const auto timer = ref->timer.lock(); + if (!timer) { + lua_pushnil(L); + return 1; + } + + lua_pushboolean(L, timer->armed()); + return 1; +} + +static int timerIndex(lua_State* L) { + luaL_checkudata(L, 1, MT); + const std::string_view key = luaL_checkstring(L, 2); + + if (key == "set_enabled") + lua_pushcfunction(L, timerSetEnabled); + else if (key == "is_enabled") + lua_pushcfunction(L, timerIsEnabled); + else if (key == "set_timeout") + lua_pushcfunction(L, timerSetTimeout); + else + lua_pushnil(L); + + return 1; +} + +void Objects::CLuaTimer::setup(lua_State* L) { + registerMetatable(L, MT, timerIndex, gcRef, timerEq, timerToString); +} + +void Objects::CLuaTimer::push(lua_State* L, const SP& timer, int timeoutMs) { + new (lua_newuserdata(L, sizeof(STimerRef))) STimerRef{WP(timer), timeoutMs}; + luaL_getmetatable(L, MT); + lua_setmetatable(L, -2); +} diff --git a/src/config/lua/objects/LuaTimer.hpp b/src/config/lua/objects/LuaTimer.hpp new file mode 100644 index 000000000..0a6028485 --- /dev/null +++ b/src/config/lua/objects/LuaTimer.hpp @@ -0,0 +1,12 @@ +#pragma once + +#include "LuaObjectHelpers.hpp" +#include "../../../managers/eventLoop/EventLoopTimer.hpp" + +namespace Config::Lua::Objects { + class CLuaTimer : public ILuaObjectWrapper { + public: + void setup(lua_State* L) override; + static void push(lua_State* L, const SP& timer, int timeoutMs); + }; +} diff --git a/src/config/lua/objects/LuaWindow.cpp b/src/config/lua/objects/LuaWindow.cpp new file mode 100644 index 000000000..7fe87334d --- /dev/null +++ b/src/config/lua/objects/LuaWindow.cpp @@ -0,0 +1,251 @@ +#include "LuaWindow.hpp" +#include "LuaWorkspace.hpp" +#include "LuaMonitor.hpp" +#include "LuaGroup.hpp" +#include "LuaObjectHelpers.hpp" + +#include "../../../desktop/view/Window.hpp" +#include "../../../desktop/view/Group.hpp" +#include "../../../desktop/Workspace.hpp" +#include "../../../desktop/state/FocusState.hpp" +#include "../../../desktop/history/WindowHistoryTracker.hpp" +#include "../../../layout/algorithm/Algorithm.hpp" +#include "../../../layout/algorithm/tiled/master/MasterAlgorithm.hpp" +#include "../../../layout/algorithm/tiled/scrolling/ScrollingAlgorithm.hpp" +#include "../../../layout/algorithm/tiled/dwindle/DwindleAlgorithm.hpp" +#include "../../../layout/space/Space.hpp" +#include "../../../layout/supplementary/WorkspaceAlgoMatcher.hpp" +#include "../../../managers/input/InputManager.hpp" + +#include +#include + +using namespace Config::Lua; + +static constexpr const char* MT = "HL.Window"; + +// +static int getFocusHistoryID(PHLWINDOW wnd) { + const auto& history = Desktop::History::windowTracker()->fullHistory(); + for (size_t i = 0; i < history.size(); ++i) { + if (history[i].lock() == wnd) + return sc(history.size() - i - 1); // reverse order for backwards compat + } + + return -1; +} + +static int windowEq(lua_State* L) { + const auto* lhs = sc(luaL_checkudata(L, 1, MT)); + const auto* rhs = sc(luaL_checkudata(L, 2, MT)); + + lua_pushboolean(L, lhs->lock() == rhs->lock()); + return 1; +} + +static int windowToString(lua_State* L) { + const auto* ref = sc(luaL_checkudata(L, 1, MT)); + const auto w = ref->lock(); + + if (!w) + lua_pushstring(L, "HL.Window(expired)"); + else + lua_pushfstring(L, "HL.Window(%p)", w.get()); + + return 1; +} + +static int windowIndex(lua_State* L) { + auto* ref = sc(luaL_checkudata(L, 1, MT)); + const auto w = ref->lock(); + if (!w) { + Log::logger->log(Log::DEBUG, "[lua] Tried to access an expired object"); + lua_pushnil(L); + return 1; + } + + const std::string_view key = luaL_checkstring(L, 2); + + if (key == "address") + lua_pushstring(L, std::format("0x{:x}", reinterpret_cast(w.get())).c_str()); + else if (key == "mapped") + lua_pushboolean(L, w->m_isMapped); + else if (key == "hidden") + lua_pushboolean(L, w->isHidden()); + else if (key == "visible") + lua_pushboolean(L, w->visible()); + else if (key == "accepts_input") + lua_pushboolean(L, w->acceptsInput()); + else if (key == "at") { + lua_newtable(L); + lua_pushinteger(L, sc(w->m_realPosition->goal().x)); + lua_setfield(L, -2, "x"); + lua_pushinteger(L, sc(w->m_realPosition->goal().y)); + lua_setfield(L, -2, "y"); + } else if (key == "size") { + lua_newtable(L); + lua_pushinteger(L, sc(w->m_realSize->goal().x)); + lua_setfield(L, -2, "x"); + lua_pushinteger(L, sc(w->m_realSize->goal().y)); + lua_setfield(L, -2, "y"); + } else if (key == "workspace") { + if (w->m_workspace) + Objects::CLuaWorkspace::push(L, w->m_workspace); + else + lua_pushnil(L); + } else if (key == "floating") + lua_pushboolean(L, w->m_isFloating); + else if (key == "monitor") { + const auto mon = w->m_monitor.lock(); + if (mon) + Objects::CLuaMonitor::push(L, mon); + else + lua_pushnil(L); + } else if (key == "class") + lua_pushstring(L, w->m_class.c_str()); + else if (key == "title") + lua_pushstring(L, w->m_title.c_str()); + else if (key == "initial_class") + lua_pushstring(L, w->m_initialClass.c_str()); + else if (key == "initial_title") + lua_pushstring(L, w->m_initialTitle.c_str()); + else if (key == "pid") + lua_pushinteger(L, sc(w->getPID())); + else if (key == "xwayland") + lua_pushboolean(L, w->m_isX11); + else if (key == "pinned") + lua_pushboolean(L, w->m_pinned); + else if (key == "fullscreen") + lua_pushinteger(L, sc(sc(w->m_fullscreenState.internal))); + else if (key == "fullscreen_client") + lua_pushinteger(L, sc(sc(w->m_fullscreenState.client))); + else if (key == "over_fullscreen") + lua_pushboolean(L, w->m_createdOverFullscreen); + else if (key == "group") { + if (!w->m_group) { + lua_pushnil(L); + return 1; + } + + Objects::CLuaGroup::push(L, w->m_group); + } else if (key == "tags") { + lua_newtable(L); + + int i = 1; + for (const auto& tag : w->m_ruleApplicator->m_tagKeeper.getTags()) { + lua_pushstring(L, tag.c_str()); + lua_rawseti(L, -2, i++); + } + } else if (key == "swallowing") { + const auto swallowed = w->m_swallowed.lock(); + if (swallowed) + Objects::CLuaWindow::push(L, swallowed); + else + lua_pushnil(L); + } else if (key == "focus_history_id") + lua_pushinteger(L, sc(getFocusHistoryID(w))); + else if (key == "inhibiting_idle") + lua_pushboolean(L, g_pInputManager && g_pInputManager->isWindowInhibiting(w, false)); + else if (key == "xdg_tag") { + const auto xdgTag = w->xdgTag(); + if (xdgTag) + lua_pushstring(L, xdgTag->c_str()); + else + lua_pushnil(L); + } else if (key == "xdg_description") { + const auto xdgDescription = w->xdgDescription(); + if (xdgDescription) + lua_pushstring(L, xdgDescription->c_str()); + else + lua_pushnil(L); + } else if (key == "content_type") + lua_pushstring(L, NContentType::toString(w->getContentType()).c_str()); + else if (key == "stable_id") + lua_pushinteger(L, sc(w->m_stableID)); + else if (key == "layout") { + const auto target = w->layoutTarget(); + if (!target || target->floating() || !w->m_workspace || !w->m_workspace->m_space) { + lua_pushnil(L); + return 1; + } + + const auto& algo = w->m_workspace->m_space->algorithm(); + if (!algo || !algo->tiledAlgo()) { + lua_pushnil(L); + return 1; + } + + const auto& tiledAlgo = algo->tiledAlgo(); + const std::string name = Layout::Supplementary::algoMatcher()->getNameForTiledAlgo(&typeid(*tiledAlgo.get())); + + lua_newtable(L); + lua_pushstring(L, name.c_str()); + lua_setfield(L, -2, "name"); + + if (const auto* master = dynamic_cast(tiledAlgo.get())) { + const auto node = master->getNodeFromTarget(target); + if (node) { + lua_pushboolean(L, node->isMaster); + lua_setfield(L, -2, "is_master"); + + lua_pushnumber(L, node->percMaster); + lua_setfield(L, -2, "perc_master"); + + lua_pushnumber(L, node->percSize); + lua_setfield(L, -2, "perc_size"); + } + } else if (auto* scrolling = dynamic_cast(tiledAlgo.get())) { + const auto data = scrolling->dataFor(target); + if (data) { + const auto col = data->column.lock(); + if (col) { + const auto scrollingData = col->scrollingData.lock(); + + lua_newtable(L); + + if (scrollingData) { + lua_pushinteger(L, sc(scrollingData->idx(col))); + lua_setfield(L, -2, "index"); + } + + lua_pushnumber(L, col->getColumnWidth()); + lua_setfield(L, -2, "width"); + + lua_newtable(L); + int i = 1; + for (const auto& td : col->targetDatas) { + const auto t = td->target.lock(); + if (t) { + const auto win = t->window(); + if (win) { + Objects::CLuaWindow::push(L, win); + lua_rawseti(L, -2, i++); + } + } + } + lua_setfield(L, -2, "windows"); + + lua_setfield(L, -2, "column"); + + lua_pushinteger(L, sc(col->idx(target))); + lua_setfield(L, -2, "index_in_column"); + } + } + } + } else if (key == "active") { + lua_pushboolean(L, w == Desktop::focusState()->window()); + } else + lua_pushnil(L); + + return 1; +} + +void Objects::CLuaWindow::setup(lua_State* L) { + registerMetatable(L, MT, windowIndex, gcRef, windowEq, windowToString); +} + +void Objects::CLuaWindow::push(lua_State* L, PHLWINDOW w) { + new (lua_newuserdata(L, sizeof(PHLWINDOWREF))) PHLWINDOWREF(w ? w->m_self : nullptr); + luaL_getmetatable(L, MT); + lua_setmetatable(L, -2); +} diff --git a/src/config/lua/objects/LuaWindow.hpp b/src/config/lua/objects/LuaWindow.hpp new file mode 100644 index 000000000..49f86149b --- /dev/null +++ b/src/config/lua/objects/LuaWindow.hpp @@ -0,0 +1,12 @@ +#pragma once + +#include "LuaObjectHelpers.hpp" +#include "../../../desktop/DesktopTypes.hpp" + +namespace Config::Lua::Objects { + class CLuaWindow : public ILuaObjectWrapper { + public: + void setup(lua_State* L) override; + static void push(lua_State* L, PHLWINDOW w); + }; +} diff --git a/src/config/lua/objects/LuaWindowRule.cpp b/src/config/lua/objects/LuaWindowRule.cpp new file mode 100644 index 000000000..63fd1a2ca --- /dev/null +++ b/src/config/lua/objects/LuaWindowRule.cpp @@ -0,0 +1,80 @@ +#include "LuaWindowRule.hpp" + +#include "../../../desktop/rule/Engine.hpp" + +#include + +using namespace Config::Lua; + +static constexpr const char* MT = "HL.WindowRule"; + +// +static int windowRuleEq(lua_State* L) { + const auto* lhs = sc*>(luaL_checkudata(L, 1, MT)); + const auto* rhs = sc*>(luaL_checkudata(L, 2, MT)); + + lua_pushboolean(L, lhs->lock() == rhs->lock()); + return 1; +} + +static int windowRuleToString(lua_State* L) { + const auto* ref = sc*>(luaL_checkudata(L, 1, MT)); + const auto rule = ref->lock(); + + if (!rule) + lua_pushstring(L, "HL.WindowRule(expired)"); + else + lua_pushfstring(L, "HL.WindowRule(%p)", rule.get()); + + return 1; +} + +static int windowRuleSetEnabled(lua_State* L) { + auto* ref = sc*>(luaL_checkudata(L, 1, MT)); + luaL_checktype(L, 2, LUA_TBOOLEAN); + + const auto rule = ref->lock(); + if (!rule) + return 0; + + rule->setEnabled(lua_toboolean(L, 2)); + Desktop::Rule::ruleEngine()->updateAllRules(); + return 0; +} + +static int windowRuleIsEnabled(lua_State* L) { + auto* ref = sc*>(luaL_checkudata(L, 1, MT)); + + const auto rule = ref->lock(); + if (!rule) { + lua_pushnil(L); + return 1; + } + + lua_pushboolean(L, rule->isEnabled()); + return 1; +} + +static int windowRuleIndex(lua_State* L) { + luaL_checkudata(L, 1, MT); + const std::string_view key = luaL_checkstring(L, 2); + + if (key == "set_enabled") + lua_pushcfunction(L, windowRuleSetEnabled); + else if (key == "is_enabled") + lua_pushcfunction(L, windowRuleIsEnabled); + else + lua_pushnil(L); + + return 1; +} + +void Objects::CLuaWindowRule::setup(lua_State* L) { + registerMetatable(L, MT, windowRuleIndex, gcRef>, windowRuleEq, windowRuleToString); +} + +void Objects::CLuaWindowRule::push(lua_State* L, const SP& rule) { + new (lua_newuserdata(L, sizeof(WP))) WP(rule); + luaL_getmetatable(L, MT); + lua_setmetatable(L, -2); +} diff --git a/src/config/lua/objects/LuaWindowRule.hpp b/src/config/lua/objects/LuaWindowRule.hpp new file mode 100644 index 000000000..92d0651da --- /dev/null +++ b/src/config/lua/objects/LuaWindowRule.hpp @@ -0,0 +1,12 @@ +#pragma once + +#include "LuaObjectHelpers.hpp" +#include "../../../desktop/rule/windowRule/WindowRule.hpp" + +namespace Config::Lua::Objects { + class CLuaWindowRule : public ILuaObjectWrapper { + public: + void setup(lua_State* L) override; + static void push(lua_State* L, const SP& rule); + }; +} diff --git a/src/config/lua/objects/LuaWorkspace.cpp b/src/config/lua/objects/LuaWorkspace.cpp new file mode 100644 index 000000000..3f89b41a1 --- /dev/null +++ b/src/config/lua/objects/LuaWorkspace.cpp @@ -0,0 +1,174 @@ +#include "LuaWorkspace.hpp" +#include "LuaMonitor.hpp" +#include "LuaWindow.hpp" +#include "LuaGroup.hpp" +#include "LuaObjectHelpers.hpp" + +#include "../../../desktop/Workspace.hpp" +#include "../../../desktop/view/Group.hpp" +#include "../../../helpers/Monitor.hpp" +#include "../../../layout/space/Space.hpp" +#include "../../../layout/algorithm/Algorithm.hpp" +#include "../../../layout/algorithm/TiledAlgorithm.hpp" +#include "../../../layout/supplementary/WorkspaceAlgoMatcher.hpp" +#include "../../../Compositor.hpp" + +#include +#include + +using namespace Config::Lua; + +static constexpr const char* MT = "HL.Workspace"; + +// +static int workspaceEq(lua_State* L) { + const auto* lhs = sc(luaL_checkudata(L, 1, MT)); + const auto* rhs = sc(luaL_checkudata(L, 2, MT)); + + lua_pushboolean(L, lhs->lock() == rhs->lock()); + return 1; +} + +static int workspaceToString(lua_State* L) { + const auto* ref = sc(luaL_checkudata(L, 1, MT)); + const auto ws = ref->lock(); + + if (!ws || ws->inert()) + lua_pushstring(L, "HL.Workspace(expired)"); + else + lua_pushfstring(L, "HL.Workspace(%d:%s)", ws->m_id, ws->m_name.c_str()); + + return 1; +} + +static int workspaceGetWindows(lua_State* L) { + auto* ref = sc(luaL_checkudata(L, 1, MT)); + const auto ws = ref->lock(); + if (!ws || ws->inert()) { + lua_newtable(L); + return 1; + } + + lua_newtable(L); + int idx = 1; + for (auto const& w : g_pCompositor->m_windows) { + if (w->m_workspace == ws) { + Objects::CLuaWindow::push(L, w); + lua_rawseti(L, -2, idx++); + } + } + return 1; +} + +static int workspaceGetGroups(lua_State* L) { + auto* ref = sc(luaL_checkudata(L, 1, MT)); + const auto ws = ref->lock(); + if (!ws || ws->inert()) { + lua_newtable(L); + return 1; + } + + lua_newtable(L); + int idx = 1; + + std::vector pushedGroups; + + for (auto const& w : g_pCompositor->m_windows) { + if (w->m_workspace != ws || !w->m_group) + continue; + + if (std::ranges::find(pushedGroups, w->m_group.get()) != pushedGroups.end()) + continue; + + pushedGroups.push_back(w->m_group.get()); + + Objects::CLuaGroup::push(L, w->m_group); + + lua_rawseti(L, -2, idx++); + } + + return 1; +} + +static int workspaceIndex(lua_State* L) { + auto* ref = sc(luaL_checkudata(L, 1, MT)); + const auto ws = ref->lock(); + if (!ws || ws->inert()) { + Log::logger->log(Log::DEBUG, "[lua] Tried to access an expired object"); + lua_pushnil(L); + return 1; + } + + const std::string_view key = luaL_checkstring(L, 2); + + if (key == "id") + lua_pushinteger(L, sc(ws->m_id)); + else if (key == "name") + lua_pushstring(L, ws->m_name.c_str()); + else if (key == "monitor") { + const auto mon = ws->m_monitor.lock(); + if (mon) + Objects::CLuaMonitor::push(L, mon); + else + lua_pushnil(L); + } else if (key == "windows") + lua_pushinteger(L, sc(ws->getWindows())); + else if (key == "visible") + lua_pushboolean(L, ws->isVisible()); + else if (key == "special") + lua_pushboolean(L, ws->m_isSpecialWorkspace); + else if (key == "active") { + const auto mon = ws->m_monitor.lock(); + lua_pushboolean(L, mon && (mon->m_activeWorkspace == ws || mon->m_activeSpecialWorkspace == ws)); + } else if (key == "has_urgent") + lua_pushboolean(L, ws->hasUrgentWindow()); + else if (key == "fullscreen_mode") + lua_pushinteger(L, sc(ws->m_fullscreenMode)); + else if (key == "has_fullscreen") + lua_pushboolean(L, ws->m_hasFullscreenWindow); + else if (key == "is_persistent") + lua_pushboolean(L, ws->isPersistent()); + else if (key == "is_empty") + lua_pushboolean(L, ws->getWindows() == 0); + else if (key == "config_name") + lua_pushstring(L, ws->getConfigName().c_str()); + else if (key == "tiled_layout") { + std::string layoutName = "unknown"; + if (ws->m_space && ws->m_space->algorithm() && ws->m_space->algorithm()->tiledAlgo()) { + const auto& TILED_ALGO = ws->m_space->algorithm()->tiledAlgo(); + layoutName = Layout::Supplementary::algoMatcher()->getNameForTiledAlgo(&typeid(*TILED_ALGO.get())); + } + lua_pushstring(L, layoutName.c_str()); + } else if (key == "last_window") { + const auto lastWindow = ws->m_lastFocusedWindow.lock(); + if (lastWindow) + Objects::CLuaWindow::push(L, lastWindow); + else + lua_pushnil(L); + } else if (key == "fullscreen_window") { + const auto fsWindow = ws->getFullscreenWindow(); + if (fsWindow) + Objects::CLuaWindow::push(L, fsWindow); + else + lua_pushnil(L); + } else if (key == "get_windows") + lua_pushcfunction(L, workspaceGetWindows); + else if (key == "get_groups") + lua_pushcfunction(L, workspaceGetGroups); + else if (key == "groups") + lua_pushinteger(L, sc(ws->getGroups())); + else + lua_pushnil(L); + + return 1; +} + +void Objects::CLuaWorkspace::setup(lua_State* L) { + registerMetatable(L, MT, workspaceIndex, gcRef, workspaceEq, workspaceToString); +} + +void Objects::CLuaWorkspace::push(lua_State* L, PHLWORKSPACE ws) { + new (lua_newuserdata(L, sizeof(PHLWORKSPACEREF))) PHLWORKSPACEREF(ws ? ws->m_self : nullptr); + luaL_getmetatable(L, MT); + lua_setmetatable(L, -2); +} diff --git a/src/config/lua/objects/LuaWorkspace.hpp b/src/config/lua/objects/LuaWorkspace.hpp new file mode 100644 index 000000000..db07feba1 --- /dev/null +++ b/src/config/lua/objects/LuaWorkspace.hpp @@ -0,0 +1,12 @@ +#pragma once + +#include "LuaObjectHelpers.hpp" +#include "../../../desktop/DesktopTypes.hpp" + +namespace Config::Lua::Objects { + class CLuaWorkspace : public ILuaObjectWrapper { + public: + void setup(lua_State* L) override; + static void push(lua_State* L, PHLWORKSPACE ws); + }; +} diff --git a/src/config/lua/types/LuaConfigBool.cpp b/src/config/lua/types/LuaConfigBool.cpp new file mode 100644 index 000000000..15413598f --- /dev/null +++ b/src/config/lua/types/LuaConfigBool.cpp @@ -0,0 +1,52 @@ +#include "LuaConfigBool.hpp" + +using namespace Config; +using namespace Config::Lua; + +CLuaConfigBool::CLuaConfigBool(Config::BOOL def) : m_default(def), m_data(def) { + ; +} + +SParseError CLuaConfigBool::parse(lua_State* s) { + if (lua_isnumber(s, -1)) { + auto number = lua_tonumber(s, -1); + if (number != 0 && number != 1) + return {.errorCode = PARSE_ERROR_BAD_TYPE, .message = "boolean type requires a bool, or 0/1."}; + + m_data = !!number; + m_bSetByUser = true; + return {.errorCode = PARSE_ERROR_OK}; + } + + if (lua_isboolean(s, -1)) { + m_data = lua_toboolean(s, -1); + m_bSetByUser = true; + return {.errorCode = PARSE_ERROR_OK}; + } + + return {.errorCode = PARSE_ERROR_BAD_TYPE, .message = "boolean type requires a bool"}; +} + +const std::type_info* CLuaConfigBool::underlying() { + return &typeid(decltype(m_data)); +} + +void const* CLuaConfigBool::data() { + return &m_data; +} + +std::string CLuaConfigBool::toString() { + return m_data ? "1" : "0"; +} + +void CLuaConfigBool::push(lua_State* s) { + lua_pushboolean(s, m_data); +} + +const Config::BOOL& CLuaConfigBool::parsed() { + return m_data; +} + +void CLuaConfigBool::reset() { + m_data = m_default; +} diff --git a/src/config/lua/types/LuaConfigBool.hpp b/src/config/lua/types/LuaConfigBool.hpp new file mode 100644 index 000000000..b4644aa4c --- /dev/null +++ b/src/config/lua/types/LuaConfigBool.hpp @@ -0,0 +1,24 @@ +#pragma once + +#include "LuaConfigValue.hpp" + +namespace Config::Lua { + class CLuaConfigBool : public ILuaConfigValue { + public: + CLuaConfigBool(Config::BOOL def); + virtual ~CLuaConfigBool() = default; + + virtual SParseError parse(lua_State* s); + virtual const std::type_info* underlying(); + virtual void const* data(); + virtual std::string toString(); + virtual void push(lua_State* s); + virtual void reset(); + + const Config::BOOL& parsed(); + + private: + Config::BOOL m_default = false; + Config::BOOL m_data = false; + }; +}; diff --git a/src/config/lua/types/LuaConfigColor.cpp b/src/config/lua/types/LuaConfigColor.cpp new file mode 100644 index 000000000..c58b232b2 --- /dev/null +++ b/src/config/lua/types/LuaConfigColor.cpp @@ -0,0 +1,63 @@ +#include "LuaConfigColor.hpp" + +#include +#include + +#include "../../../helpers/Color.hpp" +#include "../../../helpers/MiscFunctions.hpp" + +using namespace Config; +using namespace Config::Lua; + +static std::expected parseColorString(const std::string& str) { + auto result = configStringToInt(str); + if (!result) + return std::unexpected(std::format("invalid color \"{}\"", str)); + return CHyprColor(sc(*result)); +} + +CLuaConfigColor::CLuaConfigColor(Config::INTEGER def) : m_default(def), m_data(def) { + ; +} + +SParseError CLuaConfigColor::parse(lua_State* s) { + if (lua_isstring(s, -1)) { + auto data = lua_tostring(s, -1); + + auto col = parseColorString(data); + + if (!col) + return {.errorCode = PARSE_ERROR_BAD_VALUE, .message = col.error()}; + + m_data = col->getAsHex(); + m_bSetByUser = true; + return {.errorCode = PARSE_ERROR_OK}; + } + + return {.errorCode = PARSE_ERROR_BAD_TYPE, .message = "color type requires a color string"}; +} + +const std::type_info* CLuaConfigColor::underlying() { + return &typeid(decltype(m_data)); +} + +void const* CLuaConfigColor::data() { + return &m_data; +} + +std::string CLuaConfigColor::toString() { + return std::format("0x{:08X}", (uint32_t)m_data); +} + +void CLuaConfigColor::push(lua_State* s) { + const auto col = toString(); + lua_pushstring(s, col.c_str()); +} + +const Config::INTEGER& CLuaConfigColor::parsed() { + return m_data; +} + +void CLuaConfigColor::reset() { + m_data = m_default; +} diff --git a/src/config/lua/types/LuaConfigColor.hpp b/src/config/lua/types/LuaConfigColor.hpp new file mode 100644 index 000000000..15ff1ddf0 --- /dev/null +++ b/src/config/lua/types/LuaConfigColor.hpp @@ -0,0 +1,25 @@ +#pragma once + +#include "LuaConfigValue.hpp" + +namespace Config::Lua { + class CLuaConfigColor : public ILuaConfigValue { + public: + CLuaConfigColor(Config::INTEGER def); + virtual ~CLuaConfigColor() = default; + + virtual SParseError parse(lua_State* s); + virtual const std::type_info* underlying(); + virtual void const* data(); + virtual std::string toString(); + virtual void push(lua_State* s); + virtual void reset(); + + const Config::INTEGER& parsed(); + + private: + // colors are stored as ints for compat + Config::INTEGER m_default = 0; + Config::INTEGER m_data = 0; + }; +}; diff --git a/src/config/lua/types/LuaConfigCssGap.cpp b/src/config/lua/types/LuaConfigCssGap.cpp new file mode 100644 index 000000000..31f2fb217 --- /dev/null +++ b/src/config/lua/types/LuaConfigCssGap.cpp @@ -0,0 +1,104 @@ +#include "LuaConfigCssGap.hpp" + +using namespace Config; +using namespace Config::Lua; + +CLuaConfigCssGap::CLuaConfigCssGap(Config::INTEGER def, std::optional min, std::optional max) : m_default(def), m_data(def), m_min(min), m_max(max) { + ; +} + +static std::string checkRange(int64_t v, const std::string& field, std::optional min, std::optional max) { + if (min.has_value() && v < *min) + return std::format("gap \"{}\" value {} is less than the minimum of {}", field, v, *min); + if (max.has_value() && v > *max) + return std::format("gap \"{}\" value {} is more than the maximum of {}", field, v, *max); + return {}; +} + +SParseError CLuaConfigCssGap::parse(lua_State* s) { + // accept a plain integer + if (lua_isinteger(s, -1) || lua_isnumber(s, -1)) { + int64_t v = sc(lua_tonumber(s, -1)); + auto e = checkRange(v, "global", m_min, m_max); + if (!e.empty()) + return {.errorCode = PARSE_ERROR_OUT_OF_RANGE, .message = e}; + m_data.reset(v); + m_bSetByUser = true; + return {.errorCode = PARSE_ERROR_OK}; + } + + if (!lua_istable(s, -1)) + return {.errorCode = PARSE_ERROR_BAD_TYPE, .message = "css_gap type requires an integer or a table with optional \"top\", \"right\", \"bottom\", \"left\" fields"}; + + const auto readField = [&](const char* name, int64_t fallback) -> std::expected { + lua_getfield(s, -1, name); + int64_t val = fallback; + if (!lua_isnil(s, -1)) { + if (!lua_isinteger(s, -1) && !lua_isnumber(s, -1)) { + lua_pop(s, 1); + return std::unexpected(SParseError{.errorCode = PARSE_ERROR_BAD_TYPE, .message = std::format("css_gap \"{}\" must be an integer", name)}); + } + val = sc(lua_tointeger(s, -1)); + auto e = checkRange(val, name, m_min, m_max); + if (!e.empty()) { + lua_pop(s, 1); + return std::unexpected(SParseError{.errorCode = PARSE_ERROR_OUT_OF_RANGE, .message = e}); + } + } + lua_pop(s, 1); + return val; + }; + + auto top = readField("top", 0); + if (!top) + return top.error(); + auto right = readField("right", 0); + if (!right) + return right.error(); + auto bottom = readField("bottom", 0); + if (!bottom) + return bottom.error(); + auto left = readField("left", 0); + if (!left) + return left.error(); + + m_data = CCssGapData(*top, *right, *bottom, *left); + m_bSetByUser = true; + return {.errorCode = PARSE_ERROR_OK}; +} + +const std::type_info* CLuaConfigCssGap::underlying() { + return &typeid(decltype(m_data)); +} + +void const* CLuaConfigCssGap::data() { + return dc(&m_data); +} + +std::string CLuaConfigCssGap::toString() { + return m_data.toString(); +} + +void CLuaConfigCssGap::push(lua_State* s) { + lua_createtable(s, 0, 4); + + lua_pushinteger(s, m_data.m_top); + lua_setfield(s, -2, "top"); + + lua_pushinteger(s, m_data.m_right); + lua_setfield(s, -2, "right"); + + lua_pushinteger(s, m_data.m_bottom); + lua_setfield(s, -2, "bottom"); + + lua_pushinteger(s, m_data.m_left); + lua_setfield(s, -2, "left"); +} + +const CCssGapData& CLuaConfigCssGap::parsed() { + return m_data; +} + +void CLuaConfigCssGap::reset() { + m_data = CCssGapData(m_default); +} diff --git a/src/config/lua/types/LuaConfigCssGap.hpp b/src/config/lua/types/LuaConfigCssGap.hpp new file mode 100644 index 000000000..368c059b0 --- /dev/null +++ b/src/config/lua/types/LuaConfigCssGap.hpp @@ -0,0 +1,28 @@ +#pragma once + +#include "LuaConfigValue.hpp" +#include "../../shared/complex/ComplexDataTypes.hpp" + +#include + +namespace Config::Lua { + class CLuaConfigCssGap : public ILuaConfigValue { + public: + CLuaConfigCssGap(Config::INTEGER def, std::optional min = std::nullopt, std::optional max = std::nullopt); + virtual ~CLuaConfigCssGap() = default; + + virtual SParseError parse(lua_State* s); + virtual const std::type_info* underlying(); + virtual void const* data(); + virtual std::string toString(); + virtual void push(lua_State* s); + virtual void reset(); + + const CCssGapData& parsed(); + + private: + Config::INTEGER m_default = 0; + CCssGapData m_data; + std::optional m_min, m_max; + }; +}; diff --git a/src/config/lua/types/LuaConfigExpressionVec2.cpp b/src/config/lua/types/LuaConfigExpressionVec2.cpp new file mode 100644 index 000000000..baabf02f9 --- /dev/null +++ b/src/config/lua/types/LuaConfigExpressionVec2.cpp @@ -0,0 +1,100 @@ +#include "LuaConfigExpressionVec2.hpp" + +#include +#include + +using namespace Config; +using namespace Config::Lua; + +CLuaConfigExpressionVec2::CLuaConfigExpressionVec2(Math::SExpressionVec2 def) : m_default(std::move(def)), m_data(m_default) { + ; +} + +static std::expected expressionVec2ElementToString(lua_State* s, int idx) { + if (lua_isinteger(s, idx)) + return std::to_string(lua_tointeger(s, idx)); + + if (lua_isnumber(s, idx)) + return std::format("{}", lua_tonumber(s, idx)); + + if (lua_isstring(s, idx)) + return std::string{lua_tostring(s, idx)}; + + return std::unexpected("expression vec2 elements must be strings or numbers"); +} + +SParseError CLuaConfigExpressionVec2::parse(lua_State* s) { + Math::SExpressionVec2 vec; + + if (lua_isstring(s, -1)) { + auto parsed = Math::parseExpressionVec2(lua_tostring(s, -1)); + if (!parsed) + return {.errorCode = PARSE_ERROR_BAD_VALUE, .message = parsed.error()}; + + vec = std::move(*parsed); + } else { + if (!lua_istable(s, -1)) + return {.errorCode = PARSE_ERROR_BAD_TYPE, .message = "expression vec2 type requires an array or string"}; + + if (lua_rawlen(s, -1) != 2) + return {.errorCode = PARSE_ERROR_BAD_VALUE, .message = "expression vec2 type requires exactly 2 elements"}; + + lua_rawgeti(s, -1, 1); + auto x = expressionVec2ElementToString(s, -1); + lua_pop(s, 1); + if (!x) + return {.errorCode = PARSE_ERROR_BAD_TYPE, .message = x.error()}; + + lua_rawgeti(s, -1, 2); + auto y = expressionVec2ElementToString(s, -1); + lua_pop(s, 1); + if (!y) + return {.errorCode = PARSE_ERROR_BAD_TYPE, .message = y.error()}; + + if (x->empty() || y->empty()) + return {.errorCode = PARSE_ERROR_BAD_VALUE, .message = "expression vec2 elements must not be empty"}; + + vec = {std::move(*x), std::move(*y)}; + } + + m_data = std::move(vec); + m_bSetByUser = true; + + return {.errorCode = PARSE_ERROR_OK}; +} + +const std::type_info* CLuaConfigExpressionVec2::underlying() { + return &typeid(decltype(m_data)); +} + +void const* CLuaConfigExpressionVec2::data() { + return &m_data; +} + +std::string CLuaConfigExpressionVec2::toString() { + return m_data.toString(); +} + +void CLuaConfigExpressionVec2::push(lua_State* s) { + lua_createtable(s, 2, 2); + + lua_pushstring(s, m_data.x.c_str()); + lua_rawseti(s, -2, 1); + + lua_pushstring(s, m_data.y.c_str()); + lua_rawseti(s, -2, 2); + + lua_pushstring(s, m_data.x.c_str()); + lua_setfield(s, -2, "x"); + + lua_pushstring(s, m_data.y.c_str()); + lua_setfield(s, -2, "y"); +} + +const Math::SExpressionVec2& CLuaConfigExpressionVec2::parsed() { + return m_data; +} + +void CLuaConfigExpressionVec2::reset() { + m_data = m_default; +} diff --git a/src/config/lua/types/LuaConfigExpressionVec2.hpp b/src/config/lua/types/LuaConfigExpressionVec2.hpp new file mode 100644 index 000000000..cf8b398df --- /dev/null +++ b/src/config/lua/types/LuaConfigExpressionVec2.hpp @@ -0,0 +1,26 @@ +#pragma once + +#include "LuaConfigValue.hpp" + +#include "../../../helpers/math/Expression.hpp" + +namespace Config::Lua { + class CLuaConfigExpressionVec2 : public ILuaConfigValue { + public: + CLuaConfigExpressionVec2(Math::SExpressionVec2 def = {}); + virtual ~CLuaConfigExpressionVec2() = default; + + virtual SParseError parse(lua_State* s); + virtual const std::type_info* underlying(); + virtual void const* data(); + virtual std::string toString(); + virtual void push(lua_State* s); + virtual void reset(); + + const Math::SExpressionVec2& parsed(); + + private: + Math::SExpressionVec2 m_default; + Math::SExpressionVec2 m_data; + }; +}; diff --git a/src/config/lua/types/LuaConfigFloat.cpp b/src/config/lua/types/LuaConfigFloat.cpp new file mode 100644 index 000000000..9d2574b68 --- /dev/null +++ b/src/config/lua/types/LuaConfigFloat.cpp @@ -0,0 +1,80 @@ +#include "LuaConfigFloat.hpp" + +using namespace Config; +using namespace Config::Lua; + +CLuaConfigFloat::CLuaConfigFloat(Config::FLOAT def, std::optional min, std::optional max) : m_default(def), m_data(def), m_min(min), m_max(max) { + ; +} + +SParseError CLuaConfigFloat::parse(lua_State* s) { + if (lua_isboolean(s, -1)) { + auto data = lua_toboolean(s, -1) ? 1 : 0; + + if (m_min.has_value() && data < *m_min) + return {.errorCode = PARSE_ERROR_OUT_OF_RANGE, + .message = std::format("value {} (automatically casted from boolean) is less than the minimum of {}", data ? "true" : "false", *m_min)}; + + if (m_max.has_value() && data > *m_max) + return {.errorCode = PARSE_ERROR_OUT_OF_RANGE, + .message = std::format("value {} (automatically casted from boolean) is more than the maximum of {}", data ? "true" : "false", *m_max)}; + + m_data = data; + m_bSetByUser = true; + return {.errorCode = PARSE_ERROR_OK}; + } + + if (lua_isinteger(s, -1)) { + auto data = lua_tointeger(s, -1); + + if (m_min.has_value() && data < *m_min) + return {.errorCode = PARSE_ERROR_OUT_OF_RANGE, .message = std::format("value {} is less than the minimum of {:.2f}", data, *m_min)}; + + if (m_max.has_value() && data > *m_max) + return {.errorCode = PARSE_ERROR_OUT_OF_RANGE, .message = std::format("value {} is more than the maximum of {:.2f}", data, *m_max)}; + + m_data = data; + m_bSetByUser = true; + return {.errorCode = PARSE_ERROR_OK}; + } + + if (lua_isnumber(s, -1)) { + auto data = lua_tonumber(s, -1); + + if (m_min.has_value() && data < *m_min) + return {.errorCode = PARSE_ERROR_OUT_OF_RANGE, .message = std::format("value {:.2f} is less than the minimum of {:.2f}", data, *m_min)}; + + if (m_max.has_value() && data > *m_max) + return {.errorCode = PARSE_ERROR_OUT_OF_RANGE, .message = std::format("value {:.2f} is more than the maximum of {:.2f}", data, *m_max)}; + + m_data = data; + m_bSetByUser = true; + return {.errorCode = PARSE_ERROR_OK}; + } + + return {.errorCode = PARSE_ERROR_BAD_TYPE, .message = "float type requires a number"}; +} + +const std::type_info* CLuaConfigFloat::underlying() { + return &typeid(decltype(m_data)); +} + +void const* CLuaConfigFloat::data() { + return &m_data; +} + +std::string CLuaConfigFloat::toString() { + return std::to_string(m_data); +} + +void CLuaConfigFloat::push(lua_State* s) { + lua_pushnumber(s, m_data); +} + +const Config::FLOAT& CLuaConfigFloat::parsed() { + return m_data; +} + +void CLuaConfigFloat::reset() { + m_data = m_default; +} diff --git a/src/config/lua/types/LuaConfigFloat.hpp b/src/config/lua/types/LuaConfigFloat.hpp new file mode 100644 index 000000000..e6602b5a3 --- /dev/null +++ b/src/config/lua/types/LuaConfigFloat.hpp @@ -0,0 +1,27 @@ +#pragma once + +#include "LuaConfigValue.hpp" + +#include + +namespace Config::Lua { + class CLuaConfigFloat : public ILuaConfigValue { + public: + CLuaConfigFloat(Config::FLOAT def, std::optional min = std::nullopt, std::optional max = std::nullopt); + virtual ~CLuaConfigFloat() = default; + + virtual SParseError parse(lua_State* s); + virtual const std::type_info* underlying(); + virtual void const* data(); + virtual std::string toString(); + virtual void push(lua_State* s); + virtual void reset(); + + const Config::FLOAT& parsed(); + + private: + Config::FLOAT m_default = 0.F; + Config::FLOAT m_data = 0.F; + std::optional m_min, m_max; + }; +}; diff --git a/src/config/lua/types/LuaConfigFontWeight.cpp b/src/config/lua/types/LuaConfigFontWeight.cpp new file mode 100644 index 000000000..44deac5ce --- /dev/null +++ b/src/config/lua/types/LuaConfigFontWeight.cpp @@ -0,0 +1,69 @@ +#include "LuaConfigFontWeight.hpp" + +#include +#include +#include + +#include "../../shared/complex/ComplexDataTypes.hpp" + +using namespace Config; +using namespace Config::Lua; + +CLuaConfigFontWeight::CLuaConfigFontWeight(Config::INTEGER def) : m_default(def), m_data(def) { + ; +} + +SParseError CLuaConfigFontWeight::parse(lua_State* s) { + if (lua_isinteger(s, -1) || lua_isnumber(s, -1)) { + int64_t v = sc(lua_tonumber(s, -1)); + if (v < 0) + return {.errorCode = PARSE_ERROR_OUT_OF_RANGE, .message = std::format("font weight {} must not be negative", v)}; + m_data.m_value = v; + m_bSetByUser = true; + return {.errorCode = PARSE_ERROR_OK}; + } + + if (lua_isstring(s, -1)) { + std::string str = lua_tostring(s, -1); + std::string lc = str; + std::ranges::transform(str, lc.begin(), ::tolower); + + auto it = CFontWeightConfigValueData::WEIGHTS.find(lc); + if (it != CFontWeightConfigValueData::WEIGHTS.end()) { + m_data.m_value = it->second; + m_bSetByUser = true; + return {.errorCode = PARSE_ERROR_OK}; + } + + if (Hyprutils::String::isNumber(str)) + return {.errorCode = PARSE_ERROR_BAD_VALUE, .message = std::format("font weight \"{}\" was not found. Did you mean to put in an integer (without quotes?)", str)}; + + return {.errorCode = PARSE_ERROR_BAD_VALUE, .message = std::format("font weight \"{}\" was not found", str)}; + } + + return {.errorCode = PARSE_ERROR_BAD_TYPE, .message = "font weight type requires a number or a weight name string"}; +} + +const std::type_info* CLuaConfigFontWeight::underlying() { + return &typeid(IComplexConfigValue*); +} + +void const* CLuaConfigFontWeight::data() { + return dc(&m_data); +} + +std::string CLuaConfigFontWeight::toString() { + return m_data.toString(); +} + +void CLuaConfigFontWeight::push(lua_State* s) { + lua_pushinteger(s, m_data.m_value); +} + +const CFontWeightConfigValueData& CLuaConfigFontWeight::parsed() { + return m_data; +} + +void CLuaConfigFontWeight::reset() { + m_data = CFontWeightConfigValueData(m_default); +} diff --git a/src/config/lua/types/LuaConfigFontWeight.hpp b/src/config/lua/types/LuaConfigFontWeight.hpp new file mode 100644 index 000000000..ab0c4a5b9 --- /dev/null +++ b/src/config/lua/types/LuaConfigFontWeight.hpp @@ -0,0 +1,25 @@ +#pragma once + +#include "LuaConfigValue.hpp" +#include "../../shared/complex/ComplexDataTypes.hpp" + +namespace Config::Lua { + class CLuaConfigFontWeight : public ILuaConfigValue { + public: + CLuaConfigFontWeight(Config::INTEGER def = 400); + virtual ~CLuaConfigFontWeight() = default; + + virtual SParseError parse(lua_State* s); + virtual const std::type_info* underlying(); + virtual void const* data(); + virtual std::string toString(); + virtual void push(lua_State* s); + virtual void reset(); + + const CFontWeightConfigValueData& parsed(); + + private: + Config::INTEGER m_default = 400; + CFontWeightConfigValueData m_data; + }; +}; diff --git a/src/config/lua/types/LuaConfigGradient.cpp b/src/config/lua/types/LuaConfigGradient.cpp new file mode 100644 index 000000000..6e5838a92 --- /dev/null +++ b/src/config/lua/types/LuaConfigGradient.cpp @@ -0,0 +1,133 @@ +#include "LuaConfigGradient.hpp" +#include "../../../helpers/MiscFunctions.hpp" + +#include + +using namespace Config; +using namespace Config::Lua; + +static std::expected parseColorString(const std::string& str) { + auto result = configStringToInt(str); + if (!result) + return std::unexpected(std::format("invalid color \"{}\"", str)); + return CHyprColor(sc(*result)); +} + +CLuaConfigGradient::CLuaConfigGradient(CHyprColor def) : m_default(def), m_data(def) { + ; +} + +SParseError CLuaConfigGradient::parse(lua_State* s) { + // accept a single color string + if (lua_isstring(s, -1)) { + std::string str = lua_tostring(s, -1); + auto col = parseColorString(str); + if (!col) + return {.errorCode = PARSE_ERROR_BAD_VALUE, .message = col.error()}; + m_data.reset(*col); + m_bSetByUser = true; + return {.errorCode = PARSE_ERROR_OK}; + } + + if (!lua_istable(s, -1)) + return {.errorCode = PARSE_ERROR_BAD_TYPE, .message = R"(gradient type requires a color string or a table with "colors" and optional "angle")"}; + + // read colors array + lua_getfield(s, -1, "colors"); + if (!lua_istable(s, -1)) { + lua_pop(s, 1); + return {.errorCode = PARSE_ERROR_BAD_VALUE, .message = "gradient table must have a \"colors\" array"}; + } + + std::vector colors; + int len = sc(lua_rawlen(s, -1)); + if (len == 0) { + lua_pop(s, 1); + return {.errorCode = PARSE_ERROR_BAD_VALUE, .message = "gradient \"colors\" must not be empty"}; + } + + for (int i = 1; i <= len; ++i) { + lua_rawgeti(s, -1, i); + if (!lua_isstring(s, -1)) { + lua_pop(s, 2); + return {.errorCode = PARSE_ERROR_BAD_TYPE, .message = std::format("gradient color at index {} must be a string", i)}; + } + std::string str = lua_tostring(s, -1); + lua_pop(s, 1); + + auto col = parseColorString(str); + if (!col) { + lua_pop(s, 1); + return {.errorCode = PARSE_ERROR_BAD_VALUE, .message = col.error()}; + } + colors.emplace_back(*col); + } + lua_pop(s, 1); // pop colors table + + // read optional angle (degrees) + float angle = 0.F; + lua_getfield(s, -1, "angle"); + if (!lua_isnil(s, -1)) { + if (!lua_isnumber(s, -1)) { + lua_pop(s, 1); + return {.errorCode = PARSE_ERROR_BAD_TYPE, .message = "gradient \"angle\" must be a number (degrees)"}; + } + angle = sc(lua_tonumber(s, -1) * std::numbers::pi / 180.0); + } + lua_pop(s, 1); + + m_data.m_colors = std::move(colors); + m_data.m_angle = angle; + m_data.updateColorsOk(); + m_bSetByUser = true; + + return {.errorCode = PARSE_ERROR_OK}; +} + +const std::type_info* CLuaConfigGradient::underlying() { + return &typeid(decltype(m_data)); +} + +void const* CLuaConfigGradient::data() { + return dc(&m_data); +} + +std::string CLuaConfigGradient::toString() { + std::string result; + + for (size_t i = 0; i < m_data.m_colors.size(); ++i) { + if (i > 0) + result += ' '; + result += std::format("0x{:08X}", m_data.m_colors[i].getAsHex()); + } + + if (!result.empty()) + result += ' '; + + result += std::format("{}deg", sc(m_data.m_angle * 180.0 / std::numbers::pi_v)); + + return result; +} + +void CLuaConfigGradient::push(lua_State* s) { + lua_createtable(s, 0, 2); + + lua_createtable(s, m_data.m_colors.size(), 0); + for (size_t i = 0; i < m_data.m_colors.size(); ++i) { + const auto col = std::format("0x{:08X}", m_data.m_colors[i].getAsHex()); + lua_pushstring(s, col.c_str()); + lua_rawseti(s, -2, i + 1); + } + lua_setfield(s, -2, "colors"); + + lua_pushnumber(s, m_data.m_angle * 180.0 / std::numbers::pi); + lua_setfield(s, -2, "angle"); +} + +const CGradientValueData& CLuaConfigGradient::parsed() { + return m_data; +} + +void CLuaConfigGradient::reset() { + m_data = CGradientValueData(m_default); +} diff --git a/src/config/lua/types/LuaConfigGradient.hpp b/src/config/lua/types/LuaConfigGradient.hpp new file mode 100644 index 000000000..a1ccd5a6f --- /dev/null +++ b/src/config/lua/types/LuaConfigGradient.hpp @@ -0,0 +1,25 @@ +#pragma once + +#include "LuaConfigValue.hpp" +#include "../../shared/complex/ComplexDataTypes.hpp" + +namespace Config::Lua { + class CLuaConfigGradient : public ILuaConfigValue { + public: + CLuaConfigGradient(CHyprColor def); + virtual ~CLuaConfigGradient() = default; + + virtual SParseError parse(lua_State* s); + virtual const std::type_info* underlying(); + virtual void const* data(); + virtual std::string toString(); + virtual void push(lua_State* s); + virtual void reset(); + + const CGradientValueData& parsed(); + + private: + CHyprColor m_default; + CGradientValueData m_data; + }; +}; diff --git a/src/config/lua/types/LuaConfigInt.cpp b/src/config/lua/types/LuaConfigInt.cpp new file mode 100644 index 000000000..ef96a056c --- /dev/null +++ b/src/config/lua/types/LuaConfigInt.cpp @@ -0,0 +1,88 @@ +#include "LuaConfigInt.hpp" + +using namespace Config; +using namespace Config::Lua; + +CLuaConfigInt::CLuaConfigInt(Config::INTEGER def, std::optional min, std::optional max, + std::optional> map) : m_default(def), m_data(def), m_min(min), m_max(max), m_map(std::move(map)) { + ; +} + +SParseError CLuaConfigInt::parse(lua_State* s) { + if (lua_isboolean(s, -1)) { + auto data = lua_toboolean(s, -1) ? 1 : 0; + + if (m_min.has_value() && data < *m_min) + return {.errorCode = PARSE_ERROR_OUT_OF_RANGE, + .message = std::format("value {} (automatically casted from boolean) is less than the minimum of {}", data ? "true" : "false", *m_min)}; + + if (m_max.has_value() && data > *m_max) + return {.errorCode = PARSE_ERROR_OUT_OF_RANGE, + .message = std::format("value {} (automatically casted from boolean) is more than the maximum of {}", data ? "true" : "false", *m_max)}; + + m_data = data; + m_bSetByUser = true; + return {.errorCode = PARSE_ERROR_OK}; + } + + if (lua_isstring(s, -1) && !lua_isinteger(s, -1)) { + if (!m_map.has_value()) + return {.errorCode = PARSE_ERROR_BAD_TYPE, .message = "integer type requires a bool or an integer"}; + + const std::string str = lua_tostring(s, -1); + const auto it = m_map->find(str); + if (it == m_map->end()) { + std::string keys; + for (const auto& [k, _] : *m_map) { + if (!keys.empty()) + keys += ", "; + keys += "\"" + k + "\""; + } + return {.errorCode = PARSE_ERROR_BAD_VALUE, .message = std::format("unknown string value \"{}\", acceptable values are: {}", str, keys)}; + } + + m_data = it->second; + m_bSetByUser = true; + return {.errorCode = PARSE_ERROR_OK}; + } + + if (lua_isinteger(s, -1)) { + auto data = lua_tointeger(s, -1); + + if (m_min.has_value() && data < *m_min) + return {.errorCode = PARSE_ERROR_OUT_OF_RANGE, .message = std::format("value {} is less than the minimum of {}", data, *m_min)}; + + if (m_max.has_value() && data > *m_max) + return {.errorCode = PARSE_ERROR_OUT_OF_RANGE, .message = std::format("value {} is more than the maximum of {}", data, *m_max)}; + + m_data = data; + m_bSetByUser = true; + return {.errorCode = PARSE_ERROR_OK}; + } + + return {.errorCode = PARSE_ERROR_BAD_TYPE, .message = "integer type requires a bool or an integer"}; +} + +const std::type_info* CLuaConfigInt::underlying() { + return &typeid(decltype(m_data)); +} + +void const* CLuaConfigInt::data() { + return &m_data; +} + +std::string CLuaConfigInt::toString() { + return std::to_string(m_data); +} + +void CLuaConfigInt::push(lua_State* s) { + lua_pushinteger(s, m_data); +} + +const Config::INTEGER& CLuaConfigInt::parsed() { + return m_data; +} + +void CLuaConfigInt::reset() { + m_data = m_default; +} diff --git a/src/config/lua/types/LuaConfigInt.hpp b/src/config/lua/types/LuaConfigInt.hpp new file mode 100644 index 000000000..7ff2f39c5 --- /dev/null +++ b/src/config/lua/types/LuaConfigInt.hpp @@ -0,0 +1,31 @@ +#pragma once + +#include "LuaConfigValue.hpp" + +#include +#include +#include + +namespace Config::Lua { + class CLuaConfigInt : public ILuaConfigValue { + public: + CLuaConfigInt(Config::INTEGER def, std::optional min = std::nullopt, std::optional max = std::nullopt, + std::optional> map = std::nullopt); + virtual ~CLuaConfigInt() = default; + + virtual SParseError parse(lua_State* s); + virtual const std::type_info* underlying(); + virtual void const* data(); + virtual std::string toString(); + virtual void push(lua_State* s); + virtual void reset(); + + const Config::INTEGER& parsed(); + + private: + Config::INTEGER m_default = 0; + Config::INTEGER m_data = 0; + std::optional m_min, m_max; + std::optional> m_map; + }; +}; diff --git a/src/config/lua/types/LuaConfigString.cpp b/src/config/lua/types/LuaConfigString.cpp new file mode 100644 index 000000000..c16a587bb --- /dev/null +++ b/src/config/lua/types/LuaConfigString.cpp @@ -0,0 +1,56 @@ +#include "LuaConfigString.hpp" + +#include "../../../macros.hpp" + +using namespace Config; +using namespace Config::Lua; + +CLuaConfigString::CLuaConfigString(Config::STRING def, std::optional(const std::string&)>>&& validator) : + m_default(def), m_data(def), m_validator(std::move(validator)) { + ; +} + +SParseError CLuaConfigString::parse(lua_State* s) { + if (!lua_isstring(s, -1)) + return {.errorCode = PARSE_ERROR_BAD_TYPE, .message = "string type requires a string"}; + + std::string str = lua_tostring(s, -1); + + if (m_validator.has_value()) { + auto res = m_validator.value()(str); + if (!res) + return {.errorCode = PARSE_ERROR_BAD_VALUE, .message = res.error()}; + } + + m_data = std::move(str); + m_bSetByUser = true; + + return {.errorCode = PARSE_ERROR_OK}; +} + +const std::type_info* CLuaConfigString::underlying() { + return &typeid(decltype(m_data)); +} + +void const* CLuaConfigString::data() { + return &m_data; +} + +std::string CLuaConfigString::toString() { + return m_data; +} + +void CLuaConfigString::push(lua_State* s) { + if (m_data == STRVAL_EMPTY) + lua_pushliteral(s, ""); + else + lua_pushstring(s, m_data.c_str()); +} + +const Config::STRING& CLuaConfigString::parsed() { + return m_data; +} + +void CLuaConfigString::reset() { + m_data = m_default; +} diff --git a/src/config/lua/types/LuaConfigString.hpp b/src/config/lua/types/LuaConfigString.hpp new file mode 100644 index 000000000..d976b6abc --- /dev/null +++ b/src/config/lua/types/LuaConfigString.hpp @@ -0,0 +1,29 @@ +#pragma once + +#include "LuaConfigValue.hpp" + +#include +#include +#include + +namespace Config::Lua { + class CLuaConfigString : public ILuaConfigValue { + public: + CLuaConfigString(Config::STRING def, std::optional(const Config::STRING&)>>&& validator = std::nullopt); + virtual ~CLuaConfigString() = default; + + virtual SParseError parse(lua_State* s); + virtual const std::type_info* underlying(); + virtual void const* data(); + virtual std::string toString(); + virtual void push(lua_State* s); + virtual void reset(); + + const Config::STRING& parsed(); + + private: + Config::STRING m_default = "[[EMPTY]]"; + Config::STRING m_data = "[[EMPTY]]"; + std::optional(const std::string&)>> m_validator; + }; +}; diff --git a/src/config/lua/types/LuaConfigUtils.cpp b/src/config/lua/types/LuaConfigUtils.cpp new file mode 100644 index 000000000..91702aebe --- /dev/null +++ b/src/config/lua/types/LuaConfigUtils.cpp @@ -0,0 +1,51 @@ +#include "LuaConfigUtils.hpp" +#include "LuaConfigInt.hpp" +#include "LuaConfigFloat.hpp" +#include "LuaConfigBool.hpp" +#include "LuaConfigString.hpp" +#include "LuaConfigColor.hpp" +#include "LuaConfigVec2.hpp" +#include "LuaConfigCssGap.hpp" +#include "LuaConfigFontWeight.hpp" +#include "LuaConfigGradient.hpp" +#include "../../values/types/IntValue.hpp" +#include "../../values/types/FloatValue.hpp" +#include "../../values/types/BoolValue.hpp" +#include "../../values/types/StringValue.hpp" +#include "../../values/types/ColorValue.hpp" +#include "../../values/types/Vec2Value.hpp" +#include "../../values/types/CssGapValue.hpp" +#include "../../values/types/FontWeightValue.hpp" +#include "../../values/types/GradientValue.hpp" + +using namespace Config; +using namespace Config::Lua; + +UP Lua::fromGenericValue(SP v) { + const auto refreshBits = v->refreshBits(); + const auto withRefresh = [refreshBits](UP val) -> UP { + val->setRefreshBits(refreshBits); + return val; + }; + + if (auto p = dc(v.get())) + return withRefresh(makeUnique(p->defaultVal())); + if (auto p = dc(v.get())) + return withRefresh(makeUnique(p->defaultVal())); + if (auto p = dc(v.get())) + return withRefresh(makeUnique(p->defaultVal())); + if (auto p = dc(v.get())) + return withRefresh(makeUnique(p->defaultVal())); + if (auto p = dc(v.get())) + return withRefresh(makeUnique(p->defaultVal())); + if (auto p = dc(v.get())) + return withRefresh(makeUnique(p->defaultVal())); + if (auto p = dc(v.get())) + return withRefresh(makeUnique(p->defaultVal().m_top)); + if (auto p = dc(v.get())) + return withRefresh(makeUnique(p->defaultVal().m_value)); + if (auto p = dc(v.get())) + return withRefresh(makeUnique(p->defaultVal().m_colors.empty() ? CHyprColor{} : p->defaultVal().m_colors.front())); + + return nullptr; +} diff --git a/src/config/lua/types/LuaConfigUtils.hpp b/src/config/lua/types/LuaConfigUtils.hpp new file mode 100644 index 000000000..d61cf47a9 --- /dev/null +++ b/src/config/lua/types/LuaConfigUtils.hpp @@ -0,0 +1,9 @@ +#pragma once + +#include "LuaConfigValue.hpp" +#include "../../../helpers/memory/Memory.hpp" +#include "../../values/types/IValue.hpp" + +namespace Config::Lua { + UP fromGenericValue(SP v); +}; \ No newline at end of file diff --git a/src/config/lua/types/LuaConfigValue.cpp b/src/config/lua/types/LuaConfigValue.cpp new file mode 100644 index 000000000..38141baad --- /dev/null +++ b/src/config/lua/types/LuaConfigValue.cpp @@ -0,0 +1,20 @@ +#include "LuaConfigValue.hpp" + +using namespace Config; +using namespace Config::Lua; + +void ILuaConfigValue::resetSetByUser() { + m_bSetByUser = false; +} + +bool ILuaConfigValue::setByUser() { + return m_bSetByUser; +} + +void ILuaConfigValue::setRefreshBits(Supplementary::PropRefreshBits bits) { + m_refreshBits = bits; +} + +Supplementary::PropRefreshBits ILuaConfigValue::refreshBits() const { + return m_refreshBits; +} diff --git a/src/config/lua/types/LuaConfigValue.hpp b/src/config/lua/types/LuaConfigValue.hpp new file mode 100644 index 000000000..2cce98e39 --- /dev/null +++ b/src/config/lua/types/LuaConfigValue.hpp @@ -0,0 +1,49 @@ +#pragma once + +#include +#include + +#include "../../shared/Types.hpp" +#include "../../supplementary/propRefresher/PropRefresher.hpp" + +extern "C" { +#include +} + +namespace Config::Lua { + enum eParseError : uint8_t { + PARSE_ERROR_OK = 0, + PARSE_ERROR_OUT_OF_RANGE, + PARSE_ERROR_BAD_VALUE, + PARSE_ERROR_BAD_TYPE, + }; + + struct SParseError { + eParseError errorCode = PARSE_ERROR_OK; + std::string message; + }; + + class ILuaConfigValue { + protected: + ILuaConfigValue() = default; + bool m_bSetByUser = false; + + private: + Supplementary::PropRefreshBits m_refreshBits = 0; + + public: + virtual ~ILuaConfigValue() = default; + + virtual SParseError parse(lua_State* s) = 0; + virtual const std::type_info* underlying() = 0; + virtual void const* data() = 0; + virtual std::string toString() = 0; + virtual void push(lua_State* s) = 0; + + virtual void reset() = 0; + virtual void resetSetByUser(); + virtual bool setByUser(); + virtual void setRefreshBits(Supplementary::PropRefreshBits bits); + virtual Supplementary::PropRefreshBits refreshBits() const; + }; +}; diff --git a/src/config/lua/types/LuaConfigVec2.cpp b/src/config/lua/types/LuaConfigVec2.cpp new file mode 100644 index 000000000..5e92de620 --- /dev/null +++ b/src/config/lua/types/LuaConfigVec2.cpp @@ -0,0 +1,100 @@ +#include "LuaConfigVec2.hpp" + +#include +#include + +using namespace Config; +using namespace Config::Lua; + +CLuaConfigVec2::CLuaConfigVec2(Config::VEC2 def, std::optional(const Config::VEC2&)>>&& validator) : + m_default(def), m_data(def), m_validator(std::move(validator)) { + ; +} + +SParseError CLuaConfigVec2::parse(lua_State* s) { + Config::VEC2 vec = {}; + + if (lua_isstring(s, -1)) { + std::string input = lua_tostring(s, -1); + std::ranges::replace(input, ',', ' '); + + std::istringstream in(input); + double x = 0.F, y = 0.F; + std::string tail; + if (!(in >> x >> y) || (in >> tail)) + return {.errorCode = PARSE_ERROR_BAD_VALUE, .message = "vec2 string requires exactly 2 numbers (e.g. \"1 1\")"}; + + vec = {x, y}; + } else { + if (!lua_istable(s, -1)) + return {.errorCode = PARSE_ERROR_BAD_TYPE, .message = "vec2 type requires an array or string"}; + + if (lua_rawlen(s, -1) != 2) + return {.errorCode = PARSE_ERROR_BAD_VALUE, .message = "vec2 type requires exactly 2 elements"}; + + lua_rawgeti(s, -1, 1); + if (!lua_isnumber(s, -1)) { + lua_pop(s, 1); + return {.errorCode = PARSE_ERROR_BAD_TYPE, .message = "vec2 elements must be numbers"}; + } + double x = lua_tonumber(s, -1); + lua_pop(s, 1); + + lua_rawgeti(s, -1, 2); + if (!lua_isnumber(s, -1)) { + lua_pop(s, 1); + return {.errorCode = PARSE_ERROR_BAD_TYPE, .message = "vec2 elements must be numbers"}; + } + double y = lua_tonumber(s, -1); + lua_pop(s, 1); + + vec = {x, y}; + } + + if (m_validator.has_value()) { + auto res = m_validator.value()(vec); + if (!res) + return {.errorCode = PARSE_ERROR_BAD_VALUE, .message = res.error()}; + } + + m_data = vec; + m_bSetByUser = true; + + return {.errorCode = PARSE_ERROR_OK}; +} + +const std::type_info* CLuaConfigVec2::underlying() { + return &typeid(decltype(m_data)); +} + +void const* CLuaConfigVec2::data() { + return &m_data; +} + +std::string CLuaConfigVec2::toString() { + return std::format("{} {}", (int)m_data.x, (int)m_data.y); +} + +void CLuaConfigVec2::push(lua_State* s) { + lua_createtable(s, 2, 2); + + lua_pushnumber(s, m_data.x); + lua_rawseti(s, -2, 1); + + lua_pushnumber(s, m_data.y); + lua_rawseti(s, -2, 2); + + lua_pushnumber(s, m_data.x); + lua_setfield(s, -2, "x"); + + lua_pushnumber(s, m_data.y); + lua_setfield(s, -2, "y"); +} + +const Config::VEC2& CLuaConfigVec2::parsed() { + return m_data; +} + +void CLuaConfigVec2::reset() { + m_data = m_default; +} diff --git a/src/config/lua/types/LuaConfigVec2.hpp b/src/config/lua/types/LuaConfigVec2.hpp new file mode 100644 index 000000000..c65601210 --- /dev/null +++ b/src/config/lua/types/LuaConfigVec2.hpp @@ -0,0 +1,29 @@ +#pragma once + +#include "LuaConfigValue.hpp" + +#include +#include +#include + +namespace Config::Lua { + class CLuaConfigVec2 : public ILuaConfigValue { + public: + CLuaConfigVec2(Config::VEC2 def, std::optional(const Config::VEC2&)>>&& validator = std::nullopt); + virtual ~CLuaConfigVec2() = default; + + virtual SParseError parse(lua_State* s); + virtual const std::type_info* underlying(); + virtual void const* data(); + virtual std::string toString(); + virtual void push(lua_State* s); + virtual void reset(); + + const Config::VEC2& parsed(); + + private: + Config::VEC2 m_default = {}; + Config::VEC2 m_data = {}; + std::optional(const Config::VEC2&)>> m_validator; + }; +}; diff --git a/src/config/shared/ConfigErrors.hpp b/src/config/shared/ConfigErrors.hpp new file mode 100644 index 000000000..b66510dcd --- /dev/null +++ b/src/config/shared/ConfigErrors.hpp @@ -0,0 +1,68 @@ +#pragma once + +#include +#include +#include +#include + +namespace Config { + enum class eConfigErrorLevel : uint8_t { + SILENT = 0, + INFO, + WARNING, + ERROR, + }; + + enum class eConfigErrorCode : uint8_t { + UNKNOWN = 0, + INVALID_ARGUMENT, + NOT_FOUND, + NO_TARGET, + INVALID_STATE, + UNAVAILABLE, + EXECUTION_FAILED, + LUA_ERROR, + INTERNAL, + }; + + struct SConfigError { + SConfigError() = default; + SConfigError(std::string msg, eConfigErrorLevel lvl = eConfigErrorLevel::ERROR, eConfigErrorCode c = eConfigErrorCode::UNKNOWN) : + message(std::move(msg)), level(lvl), code(c) {} + + std::string message; + eConfigErrorLevel level = eConfigErrorLevel::ERROR; + eConfigErrorCode code = eConfigErrorCode::UNKNOWN; + }; + + using ErrorResult = std::expected; + + inline const char* toString(eConfigErrorLevel level) { + switch (level) { + case eConfigErrorLevel::SILENT: return "silent"; + case eConfigErrorLevel::INFO: return "info"; + case eConfigErrorLevel::WARNING: return "warning"; + case eConfigErrorLevel::ERROR: return "error"; + } + return "unknown"; + } + + inline const char* toString(eConfigErrorCode code) { + switch (code) { + case eConfigErrorCode::UNKNOWN: return "unknown"; + case eConfigErrorCode::INVALID_ARGUMENT: return "invalid_argument"; + case eConfigErrorCode::NOT_FOUND: return "not_found"; + case eConfigErrorCode::NO_TARGET: return "no_target"; + case eConfigErrorCode::INVALID_STATE: return "invalid_state"; + case eConfigErrorCode::UNAVAILABLE: return "unavailable"; + case eConfigErrorCode::EXECUTION_FAILED: return "execution_failed"; + case eConfigErrorCode::LUA_ERROR: return "lua_error"; + case eConfigErrorCode::INTERNAL: return "internal"; + } + return "unknown"; + } + + inline std::unexpected configError(std::string message, eConfigErrorLevel level = eConfigErrorLevel::ERROR, eConfigErrorCode code = eConfigErrorCode::UNKNOWN) { + return std::unexpected(SConfigError{std::move(message), level, code}); + } +} diff --git a/src/config/shared/Types.hpp b/src/config/shared/Types.hpp index 6b0ad9d43..098afed3a 100644 --- a/src/config/shared/Types.hpp +++ b/src/config/shared/Types.hpp @@ -7,9 +7,15 @@ namespace Config { class ICustomConfigValueData; + // LEGACY: remove when hyprlang gone + struct SVec2 { + float x = 0, y = 0; + }; + + typedef bool BOOL; typedef int64_t INTEGER; typedef float FLOAT; - typedef Vector2D VEC2; + typedef SVec2 VEC2; typedef std::string STRING; typedef ICustomConfigValueData* COMPLEX; }; \ No newline at end of file diff --git a/src/config/shared/actions/ConfigActions.cpp b/src/config/shared/actions/ConfigActions.cpp new file mode 100644 index 000000000..5db1aaf1e --- /dev/null +++ b/src/config/shared/actions/ConfigActions.cpp @@ -0,0 +1,1699 @@ +#include "ConfigActions.hpp" +#include "../../../desktop/state/FocusState.hpp" +#include "../../../desktop/view/Window.hpp" +#include "../../../desktop/view/Group.hpp" +#include "../../../desktop/history/WindowHistoryTracker.hpp" +#include "../../../desktop/history/WorkspaceHistoryTracker.hpp" +#include "../../../Compositor.hpp" +#include "../../../managers/SeatManager.hpp" +#include "../../../managers/PointerManager.hpp" +#include "../../../managers/EventManager.hpp" +#include "../../../managers/KeybindManager.hpp" +#include "../../../managers/input/InputManager.hpp" +#include "../../../layout/LayoutManager.hpp" +#include "../../../layout/space/Space.hpp" +#include "../../../render/Renderer.hpp" +#include "../../../config/ConfigValue.hpp" +#include "../../../config/shared/monitor/MonitorRuleManager.hpp" +#include "../../../protocols/IdleNotify.hpp" +#include "../../../protocols/GlobalShortcuts.hpp" +#include "../../../event/EventBus.hpp" +#include "../../../managers/XWaylandManager.hpp" +#include "../../../layout/algorithm/Algorithm.hpp" +#include "../../../layout/algorithm/tiled/master/MasterAlgorithm.hpp" +#include "../../../layout/algorithm/tiled/monocle/MonocleAlgorithm.hpp" + +#include +#include + +using namespace Config; +using namespace Config::Actions; + +UP& Actions::state() { + static UP p = makeUnique(); + return p; +} + +static PHLWINDOW xtract(std::optional window) { + return window.value_or(Desktop::focusState()->window()); +} + +static void updateRelativeCursorCoords() { + static auto PNOWARPS = CConfigValue("cursor:no_warps"); + + if (*PNOWARPS) + return; + + if (Desktop::focusState()->window()) + Desktop::focusState()->window()->m_relativeCursorCoordsOnLastWarp = g_pInputManager->getMouseCoordsInternal() - Desktop::focusState()->window()->m_position; +} + +static void switchToWindow(PHLWINDOW PWINDOWTOCHANGETO, bool forceFSCycle = false) { + static auto PFOLLOWMOUSE = CConfigValue("input:follow_mouse"); + static auto PNOWARPS = CConfigValue("cursor:no_warps"); + + const auto PLASTWINDOW = Desktop::focusState()->window(); + + if (PWINDOWTOCHANGETO == PLASTWINDOW || !PWINDOWTOCHANGETO) + return; + + g_pInputManager->unconstrainMouse(); + + if (PLASTWINDOW && PLASTWINDOW->m_workspace == PWINDOWTOCHANGETO->m_workspace && PLASTWINDOW->isFullscreen()) + Desktop::focusState()->fullWindowFocus(PWINDOWTOCHANGETO, Desktop::FOCUS_REASON_KEYBIND, nullptr, forceFSCycle); + else { + updateRelativeCursorCoords(); + Desktop::focusState()->fullWindowFocus(PWINDOWTOCHANGETO, Desktop::FOCUS_REASON_KEYBIND, nullptr, forceFSCycle); + PWINDOWTOCHANGETO->warpCursor(); + + if (*PNOWARPS == 0 || *PFOLLOWMOUSE < 2) { + g_pInputManager->m_forcedFocus = PWINDOWTOCHANGETO; + g_pInputManager->simulateMouseMovement(); + g_pInputManager->m_forcedFocus.reset(); + } + + if (PLASTWINDOW && PLASTWINDOW->m_monitor != PWINDOWTOCHANGETO->m_monitor) { + const auto PNEWMON = PWINDOWTOCHANGETO->m_monitor.lock(); + Desktop::focusState()->rawMonitorFocus(PNEWMON); + } + } +} + +static bool tryMoveFocusToMonitor(PHLMONITOR monitor) { + if (!monitor) + return false; + + const auto LASTMONITOR = Desktop::focusState()->monitor(); + if (!LASTMONITOR || LASTMONITOR == monitor) + return false; + + static auto PFOLLOWMOUSE = CConfigValue("input:follow_mouse"); + static auto PNOWARPS = CConfigValue("cursor:no_warps"); + + g_pInputManager->unconstrainMouse(); + + const auto PNEWMAINWORKSPACE = monitor->m_activeWorkspace; + const auto PNEWWORKSPACE = monitor->m_activeSpecialWorkspace ? monitor->m_activeSpecialWorkspace : PNEWMAINWORKSPACE; + const auto PNEWWINDOW = PNEWWORKSPACE->getLastFocusedWindow(); + + if (PNEWWINDOW) { + updateRelativeCursorCoords(); + Desktop::focusState()->fullWindowFocus(PNEWWINDOW, Desktop::FOCUS_REASON_KEYBIND); + PNEWWINDOW->warpCursor(); + + if (*PNOWARPS == 0 || *PFOLLOWMOUSE < 2) { + g_pInputManager->m_forcedFocus = PNEWWINDOW; + g_pInputManager->simulateMouseMovement(); + g_pInputManager->m_forcedFocus.reset(); + } + } else { + Desktop::focusState()->rawWindowFocus(nullptr, Desktop::FOCUS_REASON_KEYBIND); + g_pCompositor->warpCursorTo(monitor->middle()); + } + Desktop::focusState()->rawMonitorFocus(monitor); + + return true; +} + +static std::string dirToString(Math::eDirection dir) { + switch (dir) { + case Math::DIRECTION_LEFT: return "l"; + case Math::DIRECTION_RIGHT: return "r"; + case Math::DIRECTION_UP: return "u"; + case Math::DIRECTION_DOWN: return "d"; + default: return ""; + } +} + +ActionResult Actions::closeWindow(std::optional w) { + auto window = xtract(w); + if (!window) + return {}; + + if (window->m_closeableSince > Time::steadyNow()) + return actionError("can't close window, it's not closeable yet (noclosefor)", eActionErrorLevel::WARNING, eActionErrorCode::INVALID_STATE); + + window->sendClose(); + + return {}; +} + +ActionResult Actions::killWindow(std::optional w) { + auto window = xtract(w); + if (!window) + return {}; + + kill(window->getPID(), SIGKILL); + + return {}; +} + +ActionResult Actions::signalWindow(int sig, std::optional w) { + auto window = xtract(w); + if (!window) + return {}; + + if (sig < 1 || sig > 31) + return std::unexpected(std::format("Invalid signal number {}", sig)); + + kill(window->getPID(), sig); + + return {}; +} + +ActionResult Actions::floatWindow(eTogglableAction action, std::optional w) { + auto window = xtract(w); + if (!window) + return {}; + + bool wantFloat = false; + switch (action) { + case TOGGLE_ACTION_TOGGLE: wantFloat = !window->m_isFloating; break; + case TOGGLE_ACTION_ENABLE: wantFloat = true; break; + case TOGGLE_ACTION_DISABLE: wantFloat = false; break; + } + + if (wantFloat == window->m_isFloating) + return {}; + + if (g_layoutManager->dragController()->target()) + CKeybindManager::changeMouseBindMode(MBIND_INVALID); + + g_layoutManager->changeFloatingMode(window->layoutTarget()); + + if (window->m_isFloating) + g_pCompositor->changeWindowZOrder(window, true); + + if (window->m_workspace) { + window->m_workspace->updateWindows(); + window->m_workspace->updateWindowData(); + } + + g_pCompositor->updateAllWindowsAnimatedDecorationValues(); + + return {}; +} + +ActionResult Actions::pseudoWindow(eTogglableAction action, std::optional w) { + auto window = xtract(w); + if (!window) + return {}; + + bool wantPseudo = false; + switch (action) { + case TOGGLE_ACTION_TOGGLE: wantPseudo = !window->layoutTarget()->isPseudo(); break; + case TOGGLE_ACTION_ENABLE: wantPseudo = true; break; + case TOGGLE_ACTION_DISABLE: wantPseudo = false; break; + } + + window->layoutTarget()->setPseudo(wantPseudo); + + return {}; +} + +ActionResult Actions::pinWindow(eTogglableAction action, std::optional w) { + auto window = xtract(w); + if (!window) + return {}; + + if (!window->m_isFloating || window->isFullscreen()) + return actionError("Window does not qualify to be pinned", eActionErrorLevel::WARNING, eActionErrorCode::INVALID_STATE); + + bool wantPin = false; + switch (action) { + case TOGGLE_ACTION_TOGGLE: wantPin = !window->m_pinned; break; + case TOGGLE_ACTION_ENABLE: wantPin = true; break; + case TOGGLE_ACTION_DISABLE: wantPin = false; break; + } + + if (wantPin == window->m_pinned) + return {}; + + window->m_pinned = wantPin; + window->updateFullscreenInputState(); + *window->alpha(Desktop::View::WINDOW_ALPHA_FULLSCREEN) = window->isBlockedByFullscreen() ? 0.F : 1.F; + + const auto PMONITOR = window->m_monitor.lock(); + if (!PMONITOR) + return actionError("Window has no monitor", eActionErrorLevel::WARNING, eActionErrorCode::INVALID_STATE); + + window->layoutTarget()->assignToSpace(PMONITOR->m_activeWorkspace->m_space); + window->m_ruleApplicator->propertiesChanged(Desktop::Rule::RULE_PROP_PINNED); + + const auto PWORKSPACE = window->m_workspace; + PWORKSPACE->m_lastFocusedWindow = + g_pCompositor->vectorToWindowUnified(g_pInputManager->getMouseCoordsInternal(), Desktop::View::RESERVED_EXTENTS | Desktop::View::INPUT_EXTENTS); + + g_pEventManager->postEvent(SHyprIPCEvent{.event = "pin", .data = std::format("{:x},{}", rc(window.get()), sc(window->m_pinned))}); + Event::bus()->m_events.window.pin.emit(window); + + g_pHyprRenderer->damageWindow(window, true); + + return {}; +} + +ActionResult Actions::fullscreenWindow(eFullscreenMode mode, std::optional w) { + auto window = xtract(w); + if (!window) + return {}; + + if (window->isEffectiveInternalFSMode(mode)) + g_pCompositor->setWindowFullscreenInternal(window, FSMODE_NONE); + else + g_pCompositor->setWindowFullscreenInternal(window, mode); + + return {}; +} + +ActionResult Actions::fullscreenWindow(eFullscreenMode internalMode, eFullscreenMode clientMode, std::optional w) { + auto window = xtract(w); + if (!window) + return {}; + + window->m_ruleApplicator->syncFullscreenOverride(Desktop::Types::COverridableVar(false, Desktop::Types::PRIORITY_SET_PROP)); + + const Desktop::View::SFullscreenState STATE = {.internal = internalMode, .client = clientMode}; + + if (window->m_fullscreenState.internal == STATE.internal && window->m_fullscreenState.client == STATE.client) + g_pCompositor->setWindowFullscreenState(window, Desktop::View::SFullscreenState{.internal = FSMODE_NONE, .client = FSMODE_NONE}); + else + g_pCompositor->setWindowFullscreenState(window, STATE); + + window->m_ruleApplicator->syncFullscreenOverride( + Desktop::Types::COverridableVar(window->m_fullscreenState.internal == window->m_fullscreenState.client, Desktop::Types::PRIORITY_SET_PROP)); + + return {}; +} + +ActionResult Actions::moveToWorkspace(PHLWORKSPACE ws, bool silent, std::optional w) { + auto window = xtract(w); + if (!window) + return {}; + + if (!ws) + return actionError("No workspace to move to", eActionErrorLevel::WARNING, eActionErrorCode::INVALID_ARGUMENT); + + if (ws->m_id == window->workspaceID()) + return {}; + + const auto POLDWS = window->m_workspace; + + updateRelativeCursorCoords(); + g_pHyprRenderer->damageWindow(window); + + if (silent) { + const auto OLDMIDDLE = window->middle(); + g_pCompositor->moveWindowToWorkspaceSafe(window, ws); + + if (window == Desktop::focusState()->window()) { + if (const auto PATCOORDS = + g_pCompositor->vectorToWindowUnified(OLDMIDDLE, Desktop::View::RESERVED_EXTENTS | Desktop::View::INPUT_EXTENTS | Desktop::View::ALLOW_FLOATING, window); + PATCOORDS) + Desktop::focusState()->fullWindowFocus(PATCOORDS, Desktop::FOCUS_REASON_KEYBIND); + else + g_pInputManager->refocus(); + } + } else { + PHLMONITOR pMonitor = nullptr; + + const auto FULLSCREENMODE = window->m_fullscreenState.internal; + g_pCompositor->moveWindowToWorkspaceSafe(window, ws); + pMonitor = ws->m_monitor.lock(); + Desktop::focusState()->rawMonitorFocus(pMonitor); + g_pCompositor->setWindowFullscreenInternal(window, FULLSCREENMODE); + + POLDWS->m_lastFocusedWindow = POLDWS->getFirstWindow(); + + if (ws->m_isSpecialWorkspace) + pMonitor->setSpecialWorkspace(ws); + else if (POLDWS->m_isSpecialWorkspace) + POLDWS->m_monitor.lock()->setSpecialWorkspace(nullptr); + + pMonitor->changeWorkspace(ws); + + Desktop::focusState()->fullWindowFocus(window, Desktop::FOCUS_REASON_KEYBIND); + window->warpCursor(); + } + + return {}; +} + +ActionResult Actions::moveFocus(Math::eDirection dir) { + static auto PFULLCYCLE = CConfigValue("binds:movefocus_cycles_fullscreen"); + static auto PGROUPCYCLE = CConfigValue("binds:movefocus_cycles_groupfirst"); + static auto PMONITORFALLBACK = CConfigValue("binds:window_direction_monitor_fallback"); + + const auto PLASTWINDOW = Desktop::focusState()->window(); + if (!PLASTWINDOW || !PLASTWINDOW->aliveAndVisible()) { + if (*PMONITORFALLBACK) + tryMoveFocusToMonitor(g_pCompositor->getMonitorInDirection(dir)); + return {}; + } + + const auto PWINDOWTOCHANGETO = *PFULLCYCLE && PLASTWINDOW->isFullscreen() ? + g_pCompositor->getWindowCycle(PLASTWINDOW, true, {}, false, dir != Math::DIRECTION_DOWN && dir != Math::DIRECTION_RIGHT, true) : + g_pCompositor->getWindowInDirection(PLASTWINDOW, dir); + + if (*PGROUPCYCLE && PLASTWINDOW->m_group) { + auto isTheOnlyGroupOnWs = !PWINDOWTOCHANGETO && g_pCompositor->m_monitors.size() == 1; + if (dir == Math::DIRECTION_LEFT && (PLASTWINDOW != PLASTWINDOW->m_group->head() || isTheOnlyGroupOnWs)) { + PLASTWINDOW->m_group->moveCurrent(false); + return {}; + } else if (dir == Math::DIRECTION_RIGHT && (PLASTWINDOW != PLASTWINDOW->m_group->tail() || isTheOnlyGroupOnWs)) { + PLASTWINDOW->m_group->moveCurrent(true); + return {}; + } + } + + if (PWINDOWTOCHANGETO) { + switchToWindow(PWINDOWTOCHANGETO, *PFULLCYCLE && PLASTWINDOW->isFullscreen()); + return {}; + } + + if (*PMONITORFALLBACK && tryMoveFocusToMonitor(g_pCompositor->getMonitorInDirection(dir))) + return {}; + + static auto PNOFALLBACK = CConfigValue("general:no_focus_fallback"); + if (*PNOFALLBACK) + return actionError(std::format("Nothing to focus to in direction {}", Math::toString(dir)), eActionErrorLevel::INFO, eActionErrorCode::NOT_FOUND); + + const auto PMONITOR = PLASTWINDOW->m_monitor.lock(); + if (!PMONITOR) + return actionError("Window has no monitor", eActionErrorLevel::WARNING, eActionErrorCode::INVALID_STATE); + + if (dir == Math::DIRECTION_LEFT || dir == Math::DIRECTION_RIGHT) { + if (STICKS(PLASTWINDOW->m_position.x, PMONITOR->m_position.x) && STICKS(PLASTWINDOW->m_size.x, PMONITOR->m_size.x)) + return {}; + } else if (STICKS(PLASTWINDOW->m_position.y, PMONITOR->m_position.y) && STICKS(PLASTWINDOW->m_size.y, PMONITOR->m_size.y)) + return {}; + + CBox box = PMONITOR->logicalBox(); + switch (dir) { + case Math::DIRECTION_LEFT: + box.x += box.w; + box.w = 1; + break; + case Math::DIRECTION_RIGHT: + box.x -= 1; + box.w = 1; + break; + case Math::DIRECTION_UP: + box.y += box.h; + box.h = 1; + break; + case Math::DIRECTION_DOWN: + box.y -= 1; + box.h = 1; + break; + default: break; + } + + const auto PWINDOWCANDIDATE = g_pCompositor->getWindowInDirection(box, PMONITOR->m_activeSpecialWorkspace ? PMONITOR->m_activeSpecialWorkspace : PMONITOR->m_activeWorkspace, + dir, PLASTWINDOW, PLASTWINDOW->m_isFloating); + if (PWINDOWCANDIDATE) + switchToWindow(PWINDOWCANDIDATE); + + return {}; +} + +ActionResult Actions::focus(PHLWINDOW window) { + if (!window) + return {}; + + const auto PWORKSPACE = window->m_workspace; + if (!PWORKSPACE) + return actionError("Window has no workspace", eActionErrorLevel::WARNING, eActionErrorCode::INVALID_STATE); + + updateRelativeCursorCoords(); + + if (Desktop::focusState()->monitor() && Desktop::focusState()->monitor()->m_activeWorkspace != window->m_workspace && + Desktop::focusState()->monitor()->m_activeSpecialWorkspace != window->m_workspace) // NOLINTNEXTLINE + Actions::changeWorkspace(PWORKSPACE); + + Desktop::focusState()->fullWindowFocus(window, Desktop::FOCUS_REASON_KEYBIND, nullptr, false); + window->warpCursor(); + + return {}; +} + +ActionResult Actions::moveInDirection(Math::eDirection dir, std::optional w) { + auto window = xtract(w); + if (!window) + return {}; + + if (window->isFullscreen()) + return actionError("Can't move fullscreen window", eActionErrorLevel::WARNING, eActionErrorCode::INVALID_STATE); + + updateRelativeCursorCoords(); + + g_layoutManager->moveInDirection(window->layoutTarget(), dirToString(dir)); + window->warpCursor(); + + return {}; +} + +ActionResult Actions::swapInDirection(Math::eDirection dir, std::optional w) { + auto window = xtract(w); + if (!window) + return {}; + + if (window->isFullscreen()) + return actionError("Can't swap fullscreen window", eActionErrorLevel::WARNING, eActionErrorCode::INVALID_STATE); + + const auto PWINDOWTOCHANGETO = g_pCompositor->getWindowInDirection(window, dir); + + if (!PWINDOWTOCHANGETO || PWINDOWTOCHANGETO == window) + return actionError("No window to swap with in that direction", eActionErrorLevel::INFO, eActionErrorCode::NOT_FOUND); + + updateRelativeCursorCoords(); + g_layoutManager->switchTargets(window->layoutTarget(), PWINDOWTOCHANGETO->layoutTarget(), true); + window->warpCursor(); + + return {}; +} + +ActionResult Actions::swapWith(PHLWINDOW other, std::optional w) { + auto window = xtract(w); + if (!window) + return {}; + + if (!other) + return actionError("No window to swap with", eActionErrorLevel::INFO, eActionErrorCode::NOT_FOUND); + + if (other == window) + return actionError("Can't swap a window with itself", eActionErrorLevel::WARNING, eActionErrorCode::INVALID_STATE); + + if (window->isFullscreen() || other->isFullscreen()) + return actionError("Can't swap fullscreen window", eActionErrorLevel::WARNING, eActionErrorCode::INVALID_STATE); + + updateRelativeCursorCoords(); + g_layoutManager->switchTargets(window->layoutTarget(), other->layoutTarget(), true); + window->warpCursor(); + + return {}; +} + +ActionResult Actions::focusCurrentOrLast() { + const auto& HISTORY = Desktop::History::windowTracker()->fullHistory(); + + if (HISTORY.size() <= 1) + return actionError("History too short", eActionErrorLevel::INFO, eActionErrorCode::NOT_FOUND); + + const auto PWINDOWPREV = HISTORY[HISTORY.size() - 2].lock(); + + if (!PWINDOWPREV) + return actionError("Window not found", eActionErrorLevel::INFO, eActionErrorCode::NOT_FOUND); + + switchToWindow(PWINDOWPREV); + + return {}; +} + +ActionResult Actions::focusUrgentOrLast() { + const auto& HISTORY = Desktop::History::windowTracker()->fullHistory(); + const auto PWINDOWURGENT = g_pCompositor->getUrgentWindow(); + const auto PWINDOWPREV = Desktop::focusState()->window() ? (HISTORY.size() < 2 ? nullptr : HISTORY[1].lock()) : (HISTORY.empty() ? nullptr : HISTORY[0].lock()); + + if (!PWINDOWURGENT && !PWINDOWPREV) + return actionError("Window not found", eActionErrorLevel::INFO, eActionErrorCode::NOT_FOUND); + + switchToWindow(PWINDOWURGENT ? PWINDOWURGENT : PWINDOWPREV); + + return {}; +} + +ActionResult Actions::center(std::optional w) { + auto window = xtract(w); + if (!window || !window->m_isFloating || window->isFullscreen()) + return actionError("No floating window found", eActionErrorLevel::INFO, eActionErrorCode::NOT_FOUND); + + const auto PMONITOR = window->m_monitor.lock(); + + window->layoutTarget()->setPositionGlobal(CBox{PMONITOR->logicalBoxMinusReserved().middle() - window->m_realSize->goal() / 2.F, window->layoutTarget()->position().size()}); + + return {}; +} + +ActionResult Actions::moveCursorToCorner(int corner, std::optional w) { + auto window = xtract(w); + if (!window) + return {}; + + if (corner < 0 || corner > 3) + return actionError("Corner must be 0 - 3", eActionErrorLevel::ERROR, eActionErrorCode::INVALID_ARGUMENT); + + switch (corner) { + case 0: g_pCompositor->warpCursorTo({window->m_realPosition->value().x, window->m_realPosition->value().y + window->m_realSize->value().y}, true); break; + case 1: + g_pCompositor->warpCursorTo({window->m_realPosition->value().x + window->m_realSize->value().x, window->m_realPosition->value().y + window->m_realSize->value().y}, + true); + break; + case 2: g_pCompositor->warpCursorTo({window->m_realPosition->value().x + window->m_realSize->value().x, window->m_realPosition->value().y}, true); break; + case 3: g_pCompositor->warpCursorTo({window->m_realPosition->value().x, window->m_realPosition->value().y}, true); break; + default: break; + } + + return {}; +} + +ActionResult Actions::resize(const Vector2D& size, bool relative, std::optional w) { + auto window = xtract(w); + if (!window) + return {}; + + if (window->isFullscreen()) + return actionError("Window is fullscreen", eActionErrorLevel::WARNING, eActionErrorCode::INVALID_STATE); + + if (!relative && (size.x < 1 || size.y < 1)) + return actionError("Invalid size", eActionErrorLevel::ERROR, eActionErrorCode::INVALID_ARGUMENT); + + const auto delta = relative ? size : size - window->m_realSize->goal(); + + g_layoutManager->resizeTarget(delta, window->layoutTarget()); + + if (window->m_realSize->goal().x > 1 && window->m_realSize->goal().y > 1) + window->setHidden(false); + + return {}; +} + +ActionResult Actions::move(const Vector2D& pos, bool relative, std::optional w) { + auto window = xtract(w); + if (!window) + return {}; + + if (window->isFullscreen()) + return actionError("Window is fullscreen", eActionErrorLevel::WARNING, eActionErrorCode::INVALID_STATE); + + const auto delta = relative ? pos : pos - window->m_realPosition->goal(); + + g_layoutManager->moveTarget(delta, window->layoutTarget()); + + return {}; +} + +ActionResult Actions::tag(const std::string& tagStr, std::optional w) { + auto window = xtract(w); + if (!window) + return {}; + + if (window->m_ruleApplicator->m_tagKeeper.applyTag(tagStr)) { + window->m_ruleApplicator->propertiesChanged(Desktop::Rule::RULE_PROP_TAG); + window->updateDecorationValues(); + } + + return {}; +} + +ActionResult Actions::swapNext(const bool next, std::optional w) { + auto window = xtract(w); + if (!window) + return {}; + + const auto PLASTCYCLED = + validMapped(window->m_lastCycledWindow) && window->m_lastCycledWindow->m_workspace == window->m_workspace ? window->m_lastCycledWindow.lock() : nullptr; + + auto toSwap = g_pCompositor->getWindowCycle(PLASTCYCLED ? PLASTCYCLED : window, true, std::nullopt, false, !next); + + if (toSwap == window) + toSwap = g_pCompositor->getWindowCycle(window, true, std::nullopt, false, !next); + + if (!toSwap) + return actionError("No window to swap with", eActionErrorLevel::INFO, eActionErrorCode::NOT_FOUND); + + g_layoutManager->switchTargets(window->layoutTarget(), toSwap->layoutTarget(), false); + window->m_lastCycledWindow = toSwap; + Desktop::focusState()->fullWindowFocus(window, Desktop::FOCUS_REASON_KEYBIND); + + return {}; +} + +ActionResult Actions::alterZOrder(const std::string& mode, std::optional w) { + auto window = xtract(w); + if (!window) + return {}; + + if (mode == "top") + g_pCompositor->changeWindowZOrder(window, true); + else if (mode == "bottom") + g_pCompositor->changeWindowZOrder(window, false); + else + return std::unexpected(std::format("Bad z-order position: {}", mode)); + + g_pInputManager->simulateMouseMovement(); + + return {}; +} + +template +static void parsePropTrivial(Desktop::Types::COverridableVar& prop, const std::string& s) { + static_assert(std::is_same_v || std::is_same_v || std::is_same_v || std::is_same_v || std::is_same_v, + "Invalid type passed to parsePropTrivial"); + + if (s == "unset") { + prop.unset(Desktop::Types::PRIORITY_SET_PROP); + return; + } + + try { + if constexpr (std::is_same_v) { + if (s == "toggle") + prop.increment(true, Desktop::Types::PRIORITY_SET_PROP); + else + prop = Desktop::Types::COverridableVar(truthy(s), Desktop::Types::PRIORITY_SET_PROP); + } else if constexpr (std::is_same_v || std::is_same_v) { + if (s.starts_with("relative")) { + const auto VAL = std::stoi(s.substr(s.find(' ') + 1)); + prop.increment(VAL, Desktop::Types::PRIORITY_SET_PROP); + } else + prop = Desktop::Types::COverridableVar(std::stoull(s), Desktop::Types::PRIORITY_SET_PROP); + } else if constexpr (std::is_same_v) { + if (s.starts_with("relative")) { + const auto VAL = std::stof(s.substr(s.find(' ') + 1)); + prop.increment(VAL, Desktop::Types::PRIORITY_SET_PROP); + } else + prop = Desktop::Types::COverridableVar(std::stof(s), Desktop::Types::PRIORITY_SET_PROP); + } else if constexpr (std::is_same_v) + prop = Desktop::Types::COverridableVar(s, Desktop::Types::PRIORITY_SET_PROP); + } catch (...) { Log::logger->log(Log::ERR, "Hyprctl: parsePropTrivial: failed to parse setprop for {}", s); } +} + +ActionResult Actions::setProp(const std::string& PROP, const std::string& VAL, std::optional w) { + auto PWINDOW = xtract(w); + if (!PWINDOW) + return {}; + + try { + if (PROP == "max_size") { + const auto SIZE = PWINDOW->calculateExpression(VAL); + if (!SIZE) { + Log::logger->log(Log::ERR, "failed to parse {} as an expression", VAL); + throw "failed to parse expression"; + } + PWINDOW->m_ruleApplicator->maxSizeOverride(Desktop::Types::COverridableVar(*SIZE, Desktop::Types::PRIORITY_SET_PROP)); + PWINDOW->clampWindowSize(std::nullopt, PWINDOW->m_ruleApplicator->maxSize().value()); + PWINDOW->setHidden(false); + } else if (PROP == "min_size") { + const auto SIZE = PWINDOW->calculateExpression(VAL); + if (!SIZE) { + Log::logger->log(Log::ERR, "failed to parse {} as an expression", VAL); + throw "failed to parse expression"; + } + PWINDOW->m_ruleApplicator->minSizeOverride(Desktop::Types::COverridableVar(*SIZE, Desktop::Types::PRIORITY_SET_PROP)); + PWINDOW->clampWindowSize(PWINDOW->m_ruleApplicator->minSize().value(), std::nullopt); + PWINDOW->setHidden(false); + } else if (PROP == "active_border_color" || PROP == "inactive_border_color") { + Config::CGradientValueData colorData = {}; + + Hyprutils::String::CVarList2 vars(VAL, 0, 's', true); + + if (vars.size() > 1) { + for (int i = 1; i < sc(vars.size()); ++i) { + const auto TOKEN = vars[i]; + if (TOKEN.ends_with("deg")) + colorData.m_angle = std::stoi(std::string(TOKEN.substr(0, TOKEN.size() - 3))) * (PI / 180.0); + else + configStringToInt(std::string(TOKEN)).and_then([&colorData](const auto& e) { + colorData.m_colors.push_back(e); + return std::invoke_result_t(1); + }); + } + } else if (VAL != "-1") + configStringToInt(VAL).and_then([&colorData](const auto& e) { + colorData.m_colors.push_back(e); + return std::invoke_result_t(1); + }); + + colorData.updateColorsOk(); + + if (PROP == "active_border_color") + PWINDOW->m_ruleApplicator->activeBorderColorOverride(Desktop::Types::COverridableVar(colorData, Desktop::Types::PRIORITY_SET_PROP)); + else + PWINDOW->m_ruleApplicator->inactiveBorderColorOverride(Desktop::Types::COverridableVar(colorData, Desktop::Types::PRIORITY_SET_PROP)); + } else if (PROP == "opacity") { + PWINDOW->m_ruleApplicator->alphaOverride(Desktop::Types::COverridableVar( + Desktop::Types::SAlphaValue{std::stof(VAL), PWINDOW->m_ruleApplicator->alpha().valueOrDefault().overridden}, Desktop::Types::PRIORITY_SET_PROP)); + } else if (PROP == "opacity_inactive") { + PWINDOW->m_ruleApplicator->alphaInactiveOverride(Desktop::Types::COverridableVar( + Desktop::Types::SAlphaValue{std::stof(VAL), PWINDOW->m_ruleApplicator->alphaInactive().valueOrDefault().overridden}, Desktop::Types::PRIORITY_SET_PROP)); + } else if (PROP == "opacity_fullscreen") { + PWINDOW->m_ruleApplicator->alphaFullscreenOverride(Desktop::Types::COverridableVar( + Desktop::Types::SAlphaValue{std::stof(VAL), PWINDOW->m_ruleApplicator->alphaFullscreen().valueOrDefault().overridden}, Desktop::Types::PRIORITY_SET_PROP)); + } else if (PROP == "opacity_override") { + PWINDOW->m_ruleApplicator->alphaOverride(Desktop::Types::COverridableVar( + Desktop::Types::SAlphaValue{PWINDOW->m_ruleApplicator->alpha().valueOrDefault().alpha, sc(configStringToInt(VAL).value_or(0))}, + Desktop::Types::PRIORITY_SET_PROP)); + } else if (PROP == "opacity_inactive_override") { + PWINDOW->m_ruleApplicator->alphaInactiveOverride(Desktop::Types::COverridableVar( + Desktop::Types::SAlphaValue{PWINDOW->m_ruleApplicator->alphaInactive().valueOrDefault().alpha, sc(configStringToInt(VAL).value_or(0))}, + Desktop::Types::PRIORITY_SET_PROP)); + } else if (PROP == "opacity_fullscreen_override") { + PWINDOW->m_ruleApplicator->alphaFullscreenOverride(Desktop::Types::COverridableVar( + Desktop::Types::SAlphaValue{PWINDOW->m_ruleApplicator->alphaFullscreen().valueOrDefault().alpha, sc(configStringToInt(VAL).value_or(0))}, + Desktop::Types::PRIORITY_SET_PROP)); + } else if (PROP == "allows_input") + parsePropTrivial(PWINDOW->m_ruleApplicator->allowsInput(), VAL); + else if (PROP == "decorate") + parsePropTrivial(PWINDOW->m_ruleApplicator->decorate(), VAL); + else if (PROP == "focus_on_activate") + parsePropTrivial(PWINDOW->m_ruleApplicator->focusOnActivate(), VAL); + else if (PROP == "keep_aspect_ratio") + parsePropTrivial(PWINDOW->m_ruleApplicator->keepAspectRatio(), VAL); + else if (PROP == "nearest_neighbor") + parsePropTrivial(PWINDOW->m_ruleApplicator->nearestNeighbor(), VAL); + else if (PROP == "no_anim") + parsePropTrivial(PWINDOW->m_ruleApplicator->noAnim(), VAL); + else if (PROP == "no_blur") + parsePropTrivial(PWINDOW->m_ruleApplicator->noBlur(), VAL); + else if (PROP == "no_dim") + parsePropTrivial(PWINDOW->m_ruleApplicator->noDim(), VAL); + else if (PROP == "no_focus") + parsePropTrivial(PWINDOW->m_ruleApplicator->noFocus(), VAL); + else if (PROP == "no_max_size") + parsePropTrivial(PWINDOW->m_ruleApplicator->noMaxSize(), VAL); + else if (PROP == "no_shadow") + parsePropTrivial(PWINDOW->m_ruleApplicator->noShadow(), VAL); + else if (PROP == "no_shortcuts_inhibit") + parsePropTrivial(PWINDOW->m_ruleApplicator->noShortcutsInhibit(), VAL); + else if (PROP == "dim_around") + parsePropTrivial(PWINDOW->m_ruleApplicator->dimAround(), VAL); + else if (PROP == "opaque") + parsePropTrivial(PWINDOW->m_ruleApplicator->opaque(), VAL); + else if (PROP == "force_rgbx") + parsePropTrivial(PWINDOW->m_ruleApplicator->RGBX(), VAL); + else if (PROP == "sync_fullscreen") + parsePropTrivial(PWINDOW->m_ruleApplicator->syncFullscreen(), VAL); + else if (PROP == "immediate") + parsePropTrivial(PWINDOW->m_ruleApplicator->tearing(), VAL); + else if (PROP == "xray") + parsePropTrivial(PWINDOW->m_ruleApplicator->xray(), VAL); + else if (PROP == "render_unfocused") + parsePropTrivial(PWINDOW->m_ruleApplicator->renderUnfocused(), VAL); + else if (PROP == "no_follow_mouse") + parsePropTrivial(PWINDOW->m_ruleApplicator->noFollowMouse(), VAL); + else if (PROP == "no_screen_share") + parsePropTrivial(PWINDOW->m_ruleApplicator->noScreenShare(), VAL); + else if (PROP == "no_vrr") + parsePropTrivial(PWINDOW->m_ruleApplicator->noVRR(), VAL); + else if (PROP == "persistent_size") + parsePropTrivial(PWINDOW->m_ruleApplicator->persistentSize(), VAL); + else if (PROP == "stay_focused") + parsePropTrivial(PWINDOW->m_ruleApplicator->stayFocused(), VAL); + else if (PROP == "idle_inhibit") + parsePropTrivial(PWINDOW->m_ruleApplicator->idleInhibitMode(), VAL); + else if (PROP == "border_size") + parsePropTrivial(PWINDOW->m_ruleApplicator->borderSize(), VAL); + else if (PROP == "rounding") + parsePropTrivial(PWINDOW->m_ruleApplicator->rounding(), VAL); + else if (PROP == "rounding_power") + parsePropTrivial(PWINDOW->m_ruleApplicator->roundingPower(), VAL); + else if (PROP == "scroll_mouse") + parsePropTrivial(PWINDOW->m_ruleApplicator->scrollMouse(), VAL); + else if (PROP == "scroll_touchpad") + parsePropTrivial(PWINDOW->m_ruleApplicator->scrollTouchpad(), VAL); + else if (PROP == "animation") + parsePropTrivial(PWINDOW->m_ruleApplicator->animationStyle(), VAL); + else + return actionError("Invalid prop name", eActionErrorLevel::ERROR, eActionErrorCode::INVALID_ARGUMENT); + + } catch (std::exception& e) { return std::unexpected(std::format("Error parsing prop value: {}", std::string(e.what()))); } + + g_pCompositor->updateAllWindowsAnimatedDecorationValues(); + + if (PROP == "no_vrr") + Config::monitorRuleMgr()->ensureVRR(); + + for (auto const& m : g_pCompositor->m_monitors) { + if (m->m_activeWorkspace) + m->m_activeWorkspace->m_space->recalculate(); + } + + return {}; +} + +ActionResult Actions::toggleGroup(std::optional w) { + auto window = xtract(w); + if (!window) + return {}; + + if (window->isFullscreen()) + g_pCompositor->setWindowFullscreenInternal(window, FSMODE_NONE); + + if (!window->m_group) + window->m_group = Desktop::View::CGroup::create({window}); + else + window->m_group->destroy(); + + return {}; +} + +ActionResult Actions::changeGroupActive(bool forward, std::optional w) { + auto window = xtract(w); + if (!window) + return {}; + + if (!window->m_group) + return actionError("Window is not in a group", eActionErrorLevel::WARNING, eActionErrorCode::INVALID_STATE); + + if (window->m_group->size() == 1) + return actionError("Only one window in group", eActionErrorLevel::INFO, eActionErrorCode::INVALID_STATE); + + window->m_group->moveCurrent(forward); + + return {}; +} + +ActionResult Actions::setGroupActive(int index, std::optional w) { + auto window = xtract(w); + if (!window) + return {}; + + if (!window->m_group) + return actionError("Window is not in a group", eActionErrorLevel::WARNING, eActionErrorCode::INVALID_STATE); + + if (window->m_group->size() == 1) + return actionError("Only one window in group", eActionErrorLevel::INFO, eActionErrorCode::INVALID_STATE); + + if (index <= 0) + window->m_group->setCurrent(window->m_group->size() - 1); + else if (sc(index) > window->m_group->size()) + return actionError("Index out of range", eActionErrorLevel::ERROR, eActionErrorCode::INVALID_ARGUMENT); + else + window->m_group->setCurrent(index - 1); + + return {}; +} + +ActionResult Actions::changeWorkspace(PHLWORKSPACE ws) { + if (!ws) + return actionError("Invalid workspace", eActionErrorLevel::WARNING, eActionErrorCode::NO_TARGET); + + static auto PHIDESPECIALONWORKSPACECHANGE = CConfigValue("binds:hide_special_on_workspace_change"); + static auto PWORKSPACECENTERON = CConfigValue("binds:workspace_center_on"); + + const auto PMONITOR = Desktop::focusState()->monitor(); + if (!PMONITOR) + return actionError("No focused monitor", eActionErrorLevel::WARNING, eActionErrorCode::INVALID_STATE); + + if (ws->m_isSpecialWorkspace) { + PMONITOR->setSpecialWorkspace(ws); + g_pInputManager->simulateMouseMovement(); + return {}; + } + + g_pInputManager->unconstrainMouse(); + g_pInputManager->m_emptyFocusCursorSet = false; + g_pInputManager->releaseAllMouseButtons(); + + const auto PMONITORWORKSPACEOWNER = PMONITOR == ws->m_monitor ? PMONITOR : ws->m_monitor.lock(); + if (!PMONITORWORKSPACEOWNER) + return actionError("Workspace has no monitor", eActionErrorLevel::WARNING, eActionErrorCode::INVALID_STATE); + + updateRelativeCursorCoords(); + + Desktop::focusState()->rawMonitorFocus(PMONITORWORKSPACEOWNER); + + if (*PHIDESPECIALONWORKSPACECHANGE) + PMONITORWORKSPACEOWNER->setSpecialWorkspace(nullptr); + PMONITORWORKSPACEOWNER->changeWorkspace(ws, false, true); + + if (PMONITOR != PMONITORWORKSPACEOWNER) { + Vector2D middle = PMONITORWORKSPACEOWNER->middle(); + if (const auto PLAST = ws->getLastFocusedWindow(); PLAST) { + Desktop::focusState()->fullWindowFocus(PLAST, Desktop::FOCUS_REASON_KEYBIND); + if (*PWORKSPACECENTERON == 1) + middle = PLAST->middle(); + } + g_pCompositor->warpCursorTo(middle); + } + + if (!g_pInputManager->m_lastFocusOnLS) { + if (Desktop::focusState()->surface()) + g_pInputManager->sendMotionEventsToFocused(); + else + g_pInputManager->simulateMouseMovement(); + } + + const static auto PWARPONWORKSPACECHANGE = CConfigValue("cursor:warp_on_change_workspace"); + + if (*PWARPONWORKSPACECHANGE > 0) { + auto PLAST = ws->getLastFocusedWindow(); + auto HLSurface = Desktop::View::CWLSurface::fromResource(g_pSeatManager->m_state.pointerFocus.lock()); + + if (PLAST && (!HLSurface || HLSurface->view()->type() == Desktop::View::VIEW_TYPE_WINDOW)) + PLAST->warpCursor(*PWARPONWORKSPACECHANGE == 2); + } + + return {}; +} + +static PHLWORKSPACE resolveWorkspaceForChange(const std::string& args) { + static auto PBACKANDFORTH = CConfigValue("binds:workspace_back_and_forth"); + + const auto PMONITOR = Desktop::focusState()->monitor(); + if (!PMONITOR) + return nullptr; + + const auto PCURRENTWORKSPACE = PMONITOR->m_activeWorkspace; + const bool EXPLICITPREVIOUS = args.contains("previous"); + + // handle "previous" workspace + if (args.starts_with("previous")) { + const bool PER_MON = args.contains("_per_monitor"); + const SWorkspaceIDName PPREVWS = PER_MON ? Desktop::History::workspaceTracker()->previousWorkspaceIDName(PCURRENTWORKSPACE, PMONITOR) : + Desktop::History::workspaceTracker()->previousWorkspaceIDName(PCURRENTWORKSPACE); + if (PPREVWS.id == -1 || PPREVWS.id == PCURRENTWORKSPACE->m_id) + return nullptr; + + auto ws = g_pCompositor->getWorkspaceByID(PPREVWS.id); + if (!ws) + ws = g_pCompositor->createNewWorkspace(PPREVWS.id, PMONITOR->m_id, PPREVWS.name.empty() ? std::to_string(PPREVWS.id) : PPREVWS.name); + return ws; + } + + const auto& [workspaceToChangeTo, workspaceName, isAutoID] = getWorkspaceIDNameFromString(args); + if (workspaceToChangeTo == WORKSPACE_INVALID || workspaceToChangeTo == WORKSPACE_NOT_CHANGED) + return nullptr; + + // back_and_forth: if switching to current workspace, go to previous + if (workspaceToChangeTo == PCURRENTWORKSPACE->m_id && (*PBACKANDFORTH || EXPLICITPREVIOUS)) { + const SWorkspaceIDName PPREVWS = args.contains("_per_monitor") ? Desktop::History::workspaceTracker()->previousWorkspaceIDName(PCURRENTWORKSPACE, PMONITOR) : + Desktop::History::workspaceTracker()->previousWorkspaceIDName(PCURRENTWORKSPACE); + if (PPREVWS.id == -1) + return nullptr; + + auto ws = g_pCompositor->getWorkspaceByID(PPREVWS.id); + if (!ws) + ws = g_pCompositor->createNewWorkspace(PPREVWS.id, PMONITOR->m_id, PPREVWS.name.empty() ? std::to_string(PPREVWS.id) : PPREVWS.name); + return ws; + } + + auto ws = g_pCompositor->getWorkspaceByID(workspaceToChangeTo); + if (!ws) + ws = g_pCompositor->createNewWorkspace(workspaceToChangeTo, PMONITOR->m_id, workspaceName); + return ws; +} + +ActionResult Actions::changeWorkspace(const std::string& ws) { + auto p = resolveWorkspaceForChange(ws); + if (!p) + return actionError("Bad workspace", eActionErrorLevel::WARNING, eActionErrorCode::NO_TARGET); + return Actions::changeWorkspace(p); +} + +ActionResult Actions::renameWorkspace(PHLWORKSPACE ws, const std::string& s) { + if (!ws) + return actionError("Bad workspace", eActionErrorLevel::WARNING, eActionErrorCode::NO_TARGET); + + ws->rename(s); + + return {}; +} + +ActionResult Actions::moveToMonitor(PHLWORKSPACE ws, PHLMONITOR mon) { + if (!ws) + return actionError("Bad workspace", eActionErrorLevel::WARNING, eActionErrorCode::NO_TARGET); + if (!mon) + return actionError("Bad monitor", eActionErrorLevel::WARNING, eActionErrorCode::NO_TARGET); + + g_pCompositor->moveWorkspaceToMonitor(ws, mon); + + return {}; +} + +ActionResult Actions::changeWorkspaceOnCurrentMonitor(PHLWORKSPACE ws) { + if (!ws) + return actionError("Bad workspace", eActionErrorLevel::WARNING, eActionErrorCode::NO_TARGET); + + const auto PCURRMONITOR = Desktop::focusState()->monitor(); + if (!PCURRMONITOR) + return actionError("No focused monitor", eActionErrorLevel::WARNING, eActionErrorCode::INVALID_STATE); + + if (ws->m_monitor != PCURRMONITOR) { + const auto POLDMONITOR = ws->m_monitor.lock(); + if (!POLDMONITOR) + return actionError("Workspace has no monitor", eActionErrorLevel::WARNING, eActionErrorCode::INVALID_STATE); + + if (POLDMONITOR->activeWorkspaceID() == ws->m_id) { + g_pCompositor->swapActiveWorkspaces(POLDMONITOR, PCURRMONITOR); + return {}; + } else { + g_pCompositor->moveWorkspaceToMonitor(ws, PCURRMONITOR, true); + } + } + + return changeWorkspace(ws); +} + +ActionResult Actions::toggleSpecial(PHLWORKSPACE special) { + if (!special || !special->m_isSpecialWorkspace) + return actionError("Bad special workspace", eActionErrorLevel::WARNING, eActionErrorCode::NO_TARGET); + + const auto PMONITOR = Desktop::focusState()->monitor(); + if (!PMONITOR) + return actionError("No focused monitor", eActionErrorLevel::WARNING, eActionErrorCode::INVALID_STATE); + + bool requestedWorkspaceIsAlreadyOpen = false; + auto specialOpenOnMonitor = PMONITOR->activeSpecialWorkspaceID(); + + for (auto const& m : g_pCompositor->m_monitors) { + if (m->activeSpecialWorkspaceID() == special->m_id) { + requestedWorkspaceIsAlreadyOpen = true; + break; + } + } + + updateRelativeCursorCoords(); + + PHLWORKSPACEREF focusedWorkspace; + + if (requestedWorkspaceIsAlreadyOpen && specialOpenOnMonitor == special->m_id) { + PMONITOR->setSpecialWorkspace(nullptr); + focusedWorkspace = PMONITOR->m_activeWorkspace; + } else { + PMONITOR->setSpecialWorkspace(special); + focusedWorkspace = special; + } + + const static auto PWARPONTOGGLESPECIAL = CConfigValue("cursor:warp_on_toggle_special"); + + if (*PWARPONTOGGLESPECIAL > 0) { + auto PLAST = focusedWorkspace->getLastFocusedWindow(); + auto HLSurface = Desktop::View::CWLSurface::fromResource(g_pSeatManager->m_state.pointerFocus.lock()); + + if (PLAST && (!HLSurface || HLSurface->view()->type() == Desktop::View::VIEW_TYPE_WINDOW)) + PLAST->warpCursor(*PWARPONTOGGLESPECIAL == 2); + } + + return {}; +} + +ActionResult Actions::focusMonitor(PHLMONITOR mon) { + if (!mon) + return actionError("Bad monitor", eActionErrorLevel::WARNING, eActionErrorCode::NO_TARGET); + + tryMoveFocusToMonitor(mon); + + return {}; +} + +ActionResult Actions::swapActiveWorkspaces(PHLMONITOR mon1, PHLMONITOR mon2) { + if (!mon1 || !mon2) + return actionError("Bad monitor", eActionErrorLevel::WARNING, eActionErrorCode::NO_TARGET); + + if (mon1 == mon2) + return {}; + + g_pCompositor->swapActiveWorkspaces(mon1, mon2); + + return {}; +} + +ActionResult Actions::layoutMessage(const std::string& msg) { + auto ret = g_layoutManager->layoutMsg(msg); + if (!ret) + return std::unexpected(ret.error()); + return {}; +} + +ActionResult Actions::moveCursor(const Vector2D& pos) { + g_pCompositor->warpCursorTo(pos, true); + g_pInputManager->simulateMouseMovement(); + return {}; +} + +ActionResult Actions::exit() { + Event::bus()->m_events.exit.emit(); + + if (g_pCompositor->m_finalRequests) + return {}; + + g_pCompositor->stopCompositor(); + return {}; +} + +ActionResult Actions::forceRendererReload() { + bool overAgain = false; + + for (auto const& m : g_pCompositor->m_monitors) { + if (!m->m_output) + continue; + + auto rule = Config::monitorRuleMgr()->get(m); + if (!m->applyMonitorRule(std::move(rule), true)) { + overAgain = true; + break; + } + } + + if (overAgain) // NOLINTNEXTLINE + forceRendererReload(); + + return {}; +} + +ActionResult Actions::toggleSwallow() { + PHLWINDOWREF pWindow = Desktop::focusState()->window(); + + if (!valid(pWindow) || !valid(pWindow->m_swallowed)) + return {}; + + if (pWindow->m_swallowed->m_currentlySwallowed) { + pWindow->m_swallowed->m_currentlySwallowed = false; + pWindow->m_swallowed->setHidden(false); + g_layoutManager->newTarget(pWindow->m_swallowed->layoutTarget(), pWindow->m_workspace->m_space); + } else { + pWindow->m_swallowed->m_currentlySwallowed = true; + pWindow->m_swallowed->setHidden(true); + g_layoutManager->removeTarget(pWindow->m_swallowed->layoutTarget()); + } + + return {}; +} + +ActionResult Actions::dpms(eTogglableAction action, std::optional mon) { + for (auto const& m : g_pCompositor->m_realMonitors) { + if (!m->m_enabled) + continue; + + if (mon.has_value() && m != mon.value()) + continue; + + bool enable; + switch (action) { + case TOGGLE_ACTION_TOGGLE: enable = !m->m_dpmsStatus; break; + case TOGGLE_ACTION_ENABLE: enable = true; break; + case TOGGLE_ACTION_DISABLE: enable = false; break; + } + + m->setDPMS(enable); + g_pCompositor->m_dpmsStateOn = enable; + } + + g_pPointerManager->recheckEnteredOutputs(); + + return {}; +} + +ActionResult Actions::forceIdle(float seconds) { + PROTO::idle->setTimers(seconds * 1000.0); + return {}; +} + +ActionResult Actions::event(const std::string& data) { + g_pEventManager->postEvent(SHyprIPCEvent{.event = "custom", .data = data}); + return {}; +} + +ActionResult Actions::lockGroups(eTogglableAction action) { + switch (action) { + case TOGGLE_ACTION_TOGGLE: g_pKeybindManager->m_groupsLocked = !g_pKeybindManager->m_groupsLocked; break; + case TOGGLE_ACTION_ENABLE: g_pKeybindManager->m_groupsLocked = true; break; + case TOGGLE_ACTION_DISABLE: g_pKeybindManager->m_groupsLocked = false; break; + } + + g_pEventManager->postEvent(SHyprIPCEvent{.event = "lockgroups", .data = g_pKeybindManager->m_groupsLocked ? "1" : "0"}); + g_pCompositor->updateAllWindowsAnimatedDecorationValues(); + + return {}; +} + +ActionResult Actions::lockActiveGroup(eTogglableAction action) { + const auto PWINDOW = Desktop::focusState()->window(); + if (!PWINDOW) + return actionError("No window found", eActionErrorLevel::INFO, eActionErrorCode::NO_TARGET); + + if (!PWINDOW->m_group) + return actionError("Window not in a group", eActionErrorLevel::WARNING, eActionErrorCode::INVALID_STATE); + + switch (action) { + case TOGGLE_ACTION_TOGGLE: PWINDOW->m_group->setLocked(!PWINDOW->m_group->locked()); break; + case TOGGLE_ACTION_ENABLE: PWINDOW->m_group->setLocked(true); break; + case TOGGLE_ACTION_DISABLE: PWINDOW->m_group->setLocked(false); break; + } + + PWINDOW->updateDecorationValues(); + + return {}; +} + +static void moveWindowIntoGroupHelper(PHLWINDOW pWindow, PHLWINDOW pWindowInDirection) { + if (!pWindowInDirection->m_group || pWindowInDirection->m_group->denied()) + return; + + updateRelativeCursorCoords(); + + if (pWindow->m_monitor != pWindowInDirection->m_monitor) { + pWindow->moveToWorkspace(pWindowInDirection->m_workspace); + pWindow->m_monitor = pWindowInDirection->m_monitor; + } + + if (pWindow->m_group) + pWindow->m_group->remove(pWindow); + + pWindowInDirection->m_group->add(pWindow); + pWindowInDirection->m_group->setCurrent(pWindow); + pWindow->updateWindowDecos(); + Desktop::focusState()->fullWindowFocus(pWindow, Desktop::FOCUS_REASON_KEYBIND); + pWindow->warpCursor(); + + g_pEventManager->postEvent(SHyprIPCEvent{.event = "moveintogroup", .data = std::format("{:x}", rc(pWindow.get()))}); +} + +static void moveWindowOutOfGroupHelper(PHLWINDOW pWindow, Math::eDirection direction = Math::DIRECTION_DEFAULT) { + static auto BFOCUSREMOVEDWINDOW = CConfigValue("group:focus_removed_window"); + + if (!pWindow->m_group) + return; + + WP group = pWindow->m_group; + + pWindow->m_group->remove(pWindow, direction); + + if (*BFOCUSREMOVEDWINDOW || !group) { + Desktop::focusState()->fullWindowFocus(pWindow, Desktop::FOCUS_REASON_KEYBIND); + pWindow->warpCursor(); + } else { + Desktop::focusState()->fullWindowFocus(group->current(), Desktop::FOCUS_REASON_KEYBIND); + group->current()->warpCursor(); + } + + g_pEventManager->postEvent(SHyprIPCEvent{.event = "moveoutofgroup", .data = std::format("{:x}", rc(pWindow.get()))}); +} + +ActionResult Actions::moveIntoGroup(Math::eDirection direction, std::optional w) { + static auto PIGNOREGROUPLOCK = CConfigValue("binds:ignore_group_lock"); + + if (!*PIGNOREGROUPLOCK && g_pKeybindManager->m_groupsLocked) + return {}; + + auto window = xtract(w); + if (!window) + return {}; + + auto PWINDOWINDIR = g_pCompositor->getWindowInDirection(window, direction); + + if (!PWINDOWINDIR || !PWINDOWINDIR->m_group) + return {}; + + if (!*PIGNOREGROUPLOCK && (PWINDOWINDIR->m_group->locked() || (window->m_group && window->m_group->locked()))) + return {}; + + moveWindowIntoGroupHelper(window, PWINDOWINDIR); + + return {}; +} + +ActionResult Actions::moveOutOfGroup(Math::eDirection direction, std::optional w) { + static auto PIGNOREGROUPLOCK = CConfigValue("binds:ignore_group_lock"); + + if (!*PIGNOREGROUPLOCK && g_pKeybindManager->m_groupsLocked) + return actionError("Groups locked", eActionErrorLevel::INFO, eActionErrorCode::INVALID_STATE); + + auto window = xtract(w); + if (!window) + return {}; + + if (!window->m_group) + return actionError("Window not in a group", eActionErrorLevel::WARNING, eActionErrorCode::INVALID_STATE); + + moveWindowOutOfGroupHelper(window, direction); + + return {}; +} + +ActionResult Actions::moveGroupWindow(bool forward) { + const auto PLASTWINDOW = Desktop::focusState()->window(); + if (!PLASTWINDOW) + return actionError("No window found", eActionErrorLevel::INFO, eActionErrorCode::NO_TARGET); + + if (!PLASTWINDOW->m_group) + return actionError("Window not in a group", eActionErrorLevel::WARNING, eActionErrorCode::INVALID_STATE); + + if (forward) + PLASTWINDOW->m_group->swapWithNext(); + else + PLASTWINDOW->m_group->swapWithLast(); + + return {}; +} + +ActionResult Actions::moveWindowOrGroup(Math::eDirection direction, std::optional w) { + static auto PIGNOREGROUPLOCK = CConfigValue("binds:ignore_group_lock"); + + auto window = xtract(w); + if (!window) + return {}; + + if (window->isFullscreen()) + return {}; + + if (!*PIGNOREGROUPLOCK && g_pKeybindManager->m_groupsLocked) { + g_layoutManager->moveInDirection(window->layoutTarget(), dirToString(direction)); + return {}; + } + + const auto PWINDOWINDIR = g_pCompositor->getWindowInDirection(window, direction); + + const bool ISWINDOWGROUP = window->m_group; + const bool ISWINDOWGROUPLOCKED = ISWINDOWGROUP && window->m_group->locked(); + const bool ISWINDOWGROUPSINGLE = ISWINDOWGROUP && window->m_group->size() == 1; + const bool ISWINDOWGROUPDENIED = ISWINDOWGROUP && window->m_group->denied(); + + updateRelativeCursorCoords(); + + if (PWINDOWINDIR && PWINDOWINDIR->m_group) { + if (!*PIGNOREGROUPLOCK && (PWINDOWINDIR->m_group->locked() || ISWINDOWGROUPLOCKED || ISWINDOWGROUPDENIED)) { + g_layoutManager->moveInDirection(window->layoutTarget(), dirToString(direction)); + window->warpCursor(); + } else + moveWindowIntoGroupHelper(window, PWINDOWINDIR); + } else if (PWINDOWINDIR) { + if ((!*PIGNOREGROUPLOCK && ISWINDOWGROUPLOCKED) || !ISWINDOWGROUP || (ISWINDOWGROUPSINGLE && window->m_groupRules & Desktop::View::GROUP_SET_ALWAYS)) { + g_layoutManager->moveInDirection(window->layoutTarget(), dirToString(direction)); + window->warpCursor(); + } else + moveWindowOutOfGroupHelper(window, direction); + } else if ((*PIGNOREGROUPLOCK || !ISWINDOWGROUPLOCKED) && ISWINDOWGROUP) { + moveWindowOutOfGroupHelper(window, direction); + } else if (!PWINDOWINDIR && !ISWINDOWGROUP) { + g_layoutManager->moveInDirection(window->layoutTarget(), dirToString(direction)); + window->warpCursor(); + } + + window->updateDecorationValues(); + + return {}; +} + +ActionResult Actions::denyWindowFromGroup(eTogglableAction action) { + const auto PWINDOW = Desktop::focusState()->window(); + if (!PWINDOW || !PWINDOW->m_group) + return {}; + + switch (action) { + case TOGGLE_ACTION_TOGGLE: PWINDOW->m_group->setDenied(!PWINDOW->m_group->denied()); break; + case TOGGLE_ACTION_ENABLE: PWINDOW->m_group->setDenied(true); break; + case TOGGLE_ACTION_DISABLE: PWINDOW->m_group->setDenied(false); break; + } + + PWINDOW->updateDecorationValues(); + + return {}; +} + +ActionResult Actions::pass(std::optional w) { + auto window = xtract(w); + if (!window) + return {}; + + if (!g_pSeatManager->m_keyboard) + return actionError("No keyboard connected", eActionErrorLevel::INFO, eActionErrorCode::NO_TARGET); + + const auto& S = *Config::Actions::state(); + const auto XWTOXW = window->m_isX11 && Desktop::focusState()->window() && Desktop::focusState()->window()->m_isX11; + const auto LASTMOUSESURF = g_pSeatManager->m_state.pointerFocus.lock(); + const auto LASTKBSURF = g_pSeatManager->m_state.keyboardFocus.lock(); + + if (!XWTOXW) { + if (S.m_lastCode != 0) + g_pSeatManager->setKeyboardFocus(window->wlSurface()->resource()); + else + g_pSeatManager->setPointerFocus(window->wlSurface()->resource(), {1, 1}); + } + + g_pSeatManager->sendKeyboardMods(g_pInputManager->getModsFromAllKBs(), 0, 0, 0); + + if (S.m_passPressed == 1) { + if (S.m_lastCode != 0) + g_pSeatManager->sendKeyboardKey(S.m_timeLastMs, S.m_lastCode - 8, WL_KEYBOARD_KEY_STATE_PRESSED); + else + g_pSeatManager->sendPointerButton(S.m_timeLastMs, S.m_lastMouseCode, WL_POINTER_BUTTON_STATE_PRESSED); + } else if (S.m_passPressed == 0) { + if (S.m_lastCode != 0) + g_pSeatManager->sendKeyboardKey(S.m_timeLastMs, S.m_lastCode - 8, WL_KEYBOARD_KEY_STATE_RELEASED); + else + g_pSeatManager->sendPointerButton(S.m_timeLastMs, S.m_lastMouseCode, WL_POINTER_BUTTON_STATE_RELEASED); + } else { + if (S.m_lastCode != 0) { + g_pSeatManager->sendKeyboardKey(S.m_timeLastMs, S.m_lastCode - 8, WL_KEYBOARD_KEY_STATE_PRESSED); + g_pSeatManager->sendKeyboardKey(S.m_timeLastMs, S.m_lastCode - 8, WL_KEYBOARD_KEY_STATE_RELEASED); + } else { + g_pSeatManager->sendPointerButton(S.m_timeLastMs, S.m_lastMouseCode, WL_POINTER_BUTTON_STATE_PRESSED); + g_pSeatManager->sendPointerButton(S.m_timeLastMs, S.m_lastMouseCode, WL_POINTER_BUTTON_STATE_RELEASED); + } + } + + if (XWTOXW) + return {}; + + if (window->m_isX11) { + if (S.m_lastCode != 0) { + g_pSeatManager->m_state.keyboardFocus.reset(); + g_pSeatManager->m_state.keyboardFocusResource.reset(); + } else { + g_pSeatManager->m_state.pointerFocus.reset(); + g_pSeatManager->m_state.pointerFocusResource.reset(); + } + } + + const auto SL = window->m_realPosition->goal() - g_pInputManager->getMouseCoordsInternal(); + + if (S.m_lastCode != 0) + g_pSeatManager->setKeyboardFocus(LASTKBSURF); + else + g_pSeatManager->setPointerFocus(LASTMOUSESURF, SL); + + return {}; +} + +ActionResult Actions::pass(uint32_t modMask, uint32_t key, std::optional w) { + auto window = xtract(w); + + const auto& S = *Config::Actions::state(); + const bool isMouse = key >= 272 && key < 0x160; // mouse button range + const auto LASTSURFACE = Desktop::focusState()->surface(); + + if (window) { + if (!g_pSeatManager->m_keyboard) + return actionError("No keyboard connected", eActionErrorLevel::INFO, eActionErrorCode::NO_TARGET); + + if (!isMouse) + g_pSeatManager->setKeyboardFocus(window->wlSurface()->resource()); + else + g_pSeatManager->setPointerFocus(window->wlSurface()->resource(), {1, 1}); + + // if wl -> xwl, activate destination + if (window->m_isX11 && Desktop::focusState()->window() && !Desktop::focusState()->window()->m_isX11) + g_pXWaylandManager->activateSurface(window->wlSurface()->resource(), true); + // if xwl -> xwl, send to current + if (window->m_isX11 && Desktop::focusState()->window() && Desktop::focusState()->window()->m_isX11) + window = nullptr; + } + + g_pSeatManager->sendKeyboardMods(modMask, 0, 0, 0); + + if (S.m_passPressed == 1) { + if (!isMouse) + g_pSeatManager->sendKeyboardKey(S.m_timeLastMs, key - 8, WL_KEYBOARD_KEY_STATE_PRESSED); + else + g_pSeatManager->sendPointerButton(S.m_timeLastMs, key, WL_POINTER_BUTTON_STATE_PRESSED); + } else if (S.m_passPressed == 0) { + if (!isMouse) + g_pSeatManager->sendKeyboardKey(S.m_timeLastMs, key - 8, WL_KEYBOARD_KEY_STATE_RELEASED); + else + g_pSeatManager->sendPointerButton(S.m_timeLastMs, key, WL_POINTER_BUTTON_STATE_RELEASED); + } else { + if (!isMouse) { + g_pSeatManager->sendKeyboardKey(S.m_timeLastMs, key - 8, WL_KEYBOARD_KEY_STATE_PRESSED); + g_pSeatManager->sendKeyboardKey(S.m_timeLastMs, key - 8, WL_KEYBOARD_KEY_STATE_RELEASED); + } else { + g_pSeatManager->sendPointerButton(S.m_timeLastMs, key, WL_POINTER_BUTTON_STATE_PRESSED); + g_pSeatManager->sendPointerButton(S.m_timeLastMs, key, WL_POINTER_BUTTON_STATE_RELEASED); + } + } + + g_pSeatManager->sendKeyboardMods(0, 0, 0, 0); + + if (!window) + return {}; + + if (window->m_isX11) { + if (!isMouse) { + g_pSeatManager->m_state.keyboardFocus.reset(); + g_pSeatManager->m_state.keyboardFocusResource.reset(); + } else { + g_pSeatManager->m_state.pointerFocus.reset(); + g_pSeatManager->m_state.pointerFocusResource.reset(); + } + } + + const auto SL = window->m_realPosition->goal() - g_pInputManager->getMouseCoordsInternal(); + + if (!isMouse) + g_pSeatManager->setKeyboardFocus(LASTSURFACE); + else + g_pSeatManager->setPointerFocus(LASTSURFACE, SL); + + return {}; +} + +ActionResult Actions::sendKeyState(uint32_t modMask, uint32_t key, uint32_t keyState, std::optional w) { + // keyState: 0 = up, 1 = down, 2 = repeat (down+down) + const int oldPassPressed = Config::Actions::state()->m_passPressed; + + if (keyState == 1 || keyState == 2) + Config::Actions::state()->m_passPressed = 1; + else + Config::Actions::state()->m_passPressed = 0; + + auto result = Actions::pass(modMask, key, w); + + if (keyState == 2 && result.has_value()) + result = Actions::pass(modMask, key, w); + + Config::Actions::state()->m_passPressed = oldPassPressed; + + return result; +} + +ActionResult Actions::global(const std::string& action) { + const auto COLONPOS = action.find_first_of(':'); + if (COLONPOS == std::string::npos) + return {}; + + const auto APPID = action.substr(0, COLONPOS); + const auto NAME = action.substr(COLONPOS + 1); + + if (NAME.empty()) + return {}; + + if (!PROTO::globalShortcuts->isTaken(APPID, NAME)) + return {}; + + PROTO::globalShortcuts->sendGlobalShortcutEvent(APPID, NAME, Config::Actions::state()->m_passPressed); + + return {}; +} + +ActionResult Actions::mouse(const std::string& action) { + const bool PRESSED = Config::Actions::state()->m_passPressed == 1; + + if (!PRESSED) + return SActionResult{.passEvent = CKeybindManager::changeMouseBindMode(MBIND_INVALID).passEvent}; + + if (action == "movewindow") + return SActionResult{.passEvent = CKeybindManager::changeMouseBindMode(MBIND_MOVE).passEvent}; + + // resizewindow with optional ratio mode + try { + const auto SPACEPOS = action.find(' '); + if (SPACEPOS != std::string::npos) { + switch (std::stoi(action.substr(SPACEPOS + 1))) { + case 1: return SActionResult{.passEvent = CKeybindManager::changeMouseBindMode(MBIND_RESIZE_FORCE_RATIO).passEvent}; + case 2: return SActionResult{.passEvent = CKeybindManager::changeMouseBindMode(MBIND_RESIZE_BLOCK_RATIO).passEvent}; + default: break; + } + } + } catch (...) { /* stoi failed, fall through to default resize */ + } + + return SActionResult{.passEvent = CKeybindManager::changeMouseBindMode(MBIND_RESIZE).passEvent}; +} + +ActionResult Actions::setSubmap(const std::string& submap) { + if (submap == "reset" || submap.empty()) { + Config::Actions::state()->m_currentSubmap = ""; + g_pEventManager->postEvent(SHyprIPCEvent{.event = "submap", .data = ""}); + Event::bus()->m_events.keybinds.submap.emit(std::string("")); + return {}; + } + + for (const auto& k : g_pKeybindManager->m_keybinds) { + if (k->submap.name == submap) { + Config::Actions::state()->m_currentSubmap = submap; + g_pEventManager->postEvent(SHyprIPCEvent{.event = "submap", .data = submap}); + Event::bus()->m_events.keybinds.submap.emit(submap); + return {}; + } + } + + return std::unexpected(std::format("Cannot set submap {}, submap doesn't exist (wasn't registered!)", submap)); +} + +ActionResult Actions::cycleNext(const bool next, std::optional onlyTiled, std::optional onlyFloating, std::optional w) { + auto window = xtract(w); + + if (!window) { + const auto PWS = Desktop::focusState()->monitor()->m_activeWorkspace; + if (PWS && PWS->getWindows() > 0) { + const auto PFIRST = PWS->getFirstWindow(); + switchToWindow(PFIRST); + } + return {}; + } + + // If requesting tiled-only and we're on a tiled window, try layout message for supported layouts + if (onlyTiled.value_or(false) && !window->m_isFloating) { + if (const auto SPACE = window->layoutTarget()->space(); SPACE) { + constexpr const std::array LAYOUTS_WITH_CYCLE_NEXT = { + &typeid(Layout::Tiled::CMonocleAlgorithm), + &typeid(Layout::Tiled::CMasterAlgorithm), + }; + + if (std::ranges::contains(LAYOUTS_WITH_CYCLE_NEXT, &typeid(*SPACE->algorithm()->tiledAlgo().get()))) { + // NOLINTNEXTLINE + Actions::layoutMessage(!next ? "cyclenext, b" : "cyclenext"); + return {}; + } + } + } + + std::optional floatStatus = {}; + if (onlyFloating.value_or(false)) + floatStatus = true; + + const auto& cycled = g_pCompositor->getWindowCycle(window, true, floatStatus, false, !next, window->m_workspace && window->m_workspace->m_hasFullscreenWindow); + + switchToWindow(cycled); + + return {}; +} + +ActionResult Actions::moveIntoOrCreateGroup(Math::eDirection dir, std::optional w) { + static auto PIGNOREGROUPLOCK = CConfigValue("binds:ignore_group_lock"); + + if (!*PIGNOREGROUPLOCK && g_pKeybindManager->m_groupsLocked) + return {}; + + if (dir == Math::DIRECTION_DEFAULT) { + Log::logger->log(Log::ERR, "Cannot move into or create group in direction {}, unsupported direction. Supported: l,r,u/t,d/b", dirToString(dir)); + return std::unexpected(std::format("Cannot move into or create group in direction {}, unsupported direction. Supported: l,r,u/t,d/b", dirToString(dir))); + } + + const auto PWINDOW = xtract(w); + + if (!PWINDOW) + return {}; + + auto PWINDOWINDIR = g_pCompositor->getWindowInDirection(PWINDOW, dir); + + if (!PWINDOWINDIR) + return {}; + + if (!PWINDOWINDIR->m_group) { + if (PWINDOWINDIR->isFullscreen()) + return {}; + + PWINDOWINDIR->m_group = Desktop::View::CGroup::create({PWINDOWINDIR}); + } + + const auto GROUP = PWINDOWINDIR->m_group; + + if (!*PIGNOREGROUPLOCK && (GROUP->locked() || (PWINDOW->m_group && PWINDOW->m_group->locked()))) + return {}; + + moveWindowIntoGroupHelper(PWINDOW, PWINDOWINDIR); + + return {}; +} diff --git a/src/config/shared/actions/ConfigActions.hpp b/src/config/shared/actions/ConfigActions.hpp new file mode 100644 index 000000000..0f16aff74 --- /dev/null +++ b/src/config/shared/actions/ConfigActions.hpp @@ -0,0 +1,113 @@ +#pragma once + +#include +#include +#include + +#include "../../../desktop/DesktopTypes.hpp" +#include "../../../desktop/Workspace.hpp" +#include "../../../helpers/math/Direction.hpp" +#include "../ConfigErrors.hpp" + +namespace Config::Actions { + struct SActionResult { + bool passEvent = false; + }; + + using eActionErrorLevel = Config::eConfigErrorLevel; + using eActionErrorCode = Config::eConfigErrorCode; + using SActionError = Config::SConfigError; + using Config::toString; + + inline std::unexpected actionError(std::string message, eActionErrorLevel level = eActionErrorLevel::ERROR, eActionErrorCode code = eActionErrorCode::UNKNOWN) { + return Config::configError(std::move(message), level, code); + } + + enum eTogglableAction : uint8_t { + TOGGLE_ACTION_TOGGLE = 0, + TOGGLE_ACTION_ENABLE, + TOGGLE_ACTION_DISABLE, + }; + + using ActionResult = std::expected; + + ActionResult closeWindow(std::optional window = std::nullopt /* Active */); + ActionResult killWindow(std::optional window = std::nullopt /* Active */); + ActionResult signalWindow(int sig, std::optional window = std::nullopt /* Active */); + ActionResult floatWindow(eTogglableAction action, std::optional window = std::nullopt /* Active */); + ActionResult pseudoWindow(eTogglableAction action, std::optional window = std::nullopt /* Active */); + ActionResult pinWindow(eTogglableAction action, std::optional window = std::nullopt /* Active */); + ActionResult fullscreenWindow(eFullscreenMode mode, std::optional window = std::nullopt /* Active */); + ActionResult fullscreenWindow(eFullscreenMode internalMode, eFullscreenMode clientMode, std::optional window = std::nullopt /* Active */); + ActionResult moveToWorkspace(PHLWORKSPACE ws, bool silent, std::optional window = std::nullopt /* Active */); + ActionResult moveFocus(Math::eDirection dir); + ActionResult focus(PHLWINDOW window); + ActionResult moveInDirection(Math::eDirection dir, std::optional window = std::nullopt /* Active */); + ActionResult swapInDirection(Math::eDirection dir, std::optional window = std::nullopt /* Active */); + ActionResult swapWith(PHLWINDOW other, std::optional window = std::nullopt /* Active */); + ActionResult focusCurrentOrLast(); + ActionResult focusUrgentOrLast(); + ActionResult center(std::optional window = std::nullopt /* Active */); + ActionResult moveCursorToCorner(int corner, std::optional window = std::nullopt /* Active */); + ActionResult resize(const Vector2D& size, bool relative = false, std::optional window = std::nullopt /* Active */); + ActionResult move(const Vector2D& pos, bool relative = false, std::optional window = std::nullopt /* Active */); + ActionResult cycleNext(const bool next, std::optional onlyTiled, std::optional onlyFloating, std::optional window = std::nullopt /* Active */); + ActionResult tag(const std::string& tag, std::optional window = std::nullopt /* Active */); + ActionResult pass(std::optional window = std::nullopt /* Active */); + ActionResult pass(uint32_t modMask, uint32_t key, std::optional window = std::nullopt /* Active */); + ActionResult sendKeyState(uint32_t modMask, uint32_t key, uint32_t state, std::optional window = std::nullopt /* Active */); + ActionResult swapNext(const bool next, std::optional window = std::nullopt /* Active */); + ActionResult alterZOrder(const std::string& mode, std::optional window = std::nullopt /* Active */); + ActionResult setProp(const std::string& prop, const std::string& val, std::optional window = std::nullopt /* Active */); + + ActionResult toggleGroup(std::optional window = std::nullopt /* Active */); + ActionResult changeGroupActive(bool forward = true, std::optional window = std::nullopt /* Active */); + ActionResult setGroupActive(int index, std::optional window = std::nullopt /* Active */); + + ActionResult changeWorkspace(PHLWORKSPACE ws); + ActionResult changeWorkspace(const std::string& ws); + ActionResult renameWorkspace(PHLWORKSPACE ws, const std::string& s); + ActionResult moveToMonitor(PHLWORKSPACE ws, PHLMONITOR mon); + ActionResult changeWorkspaceOnCurrentMonitor(PHLWORKSPACE ws); + ActionResult toggleSpecial(PHLWORKSPACE special); + + ActionResult focusMonitor(PHLMONITOR mon); + ActionResult swapActiveWorkspaces(PHLMONITOR mon1, PHLMONITOR mon2); + + ActionResult layoutMessage(const std::string& msg); + + ActionResult moveCursor(const Vector2D& pos); + ActionResult exit(); + ActionResult forceRendererReload(); + ActionResult toggleSwallow(); + ActionResult setSubmap(const std::string& submap); + ActionResult dpms(eTogglableAction action, std::optional mon); + ActionResult forceIdle(float seconds); + ActionResult global(const std::string& action); + ActionResult event(const std::string& data); + + ActionResult mouse(const std::string& action); + + ActionResult lockGroups(eTogglableAction action); + ActionResult lockActiveGroup(eTogglableAction action); + ActionResult moveIntoGroup(Math::eDirection direction, std::optional window = std::nullopt /* Active */); + ActionResult moveOutOfGroup(Math::eDirection direction, std::optional window = std::nullopt /* Active */); + ActionResult moveGroupWindow(bool forward = true); + ActionResult moveWindowOrGroup(Math::eDirection direction, std::optional window = std::nullopt /* Active */); + ActionResult denyWindowFromGroup(eTogglableAction action); + ActionResult moveIntoOrCreateGroup(Math::eDirection dir, std::optional window = std::nullopt /* Active */); + + class CActionState { + public: + CActionState() = default; + ~CActionState() = default; + + int m_passPressed = -1; // -1 = dynamic (press+release), 0 = released, 1 = pressed + uint32_t m_lastCode = 0; // last keycode (keyboard event), 0 if last was mouse + uint32_t m_lastMouseCode = 0; // last mouse button code, 0 if last was keyboard + uint32_t m_timeLastMs = 0; // timestamp of last key/mouse event + std::string m_currentSubmap = ""; // current keybind submap name + }; + + UP& state(); +}; diff --git a/src/config/shared/complex/ComplexDataType.hpp b/src/config/shared/complex/ComplexDataType.hpp index 84b4c5038..cc83bee9c 100644 --- a/src/config/shared/complex/ComplexDataType.hpp +++ b/src/config/shared/complex/ComplexDataType.hpp @@ -17,6 +17,6 @@ namespace Config { virtual eConfigValueDataTypes getDataType() = 0; - virtual std::string toString() = 0; + virtual std::string toString() const = 0; }; } \ No newline at end of file diff --git a/src/config/shared/complex/ComplexDataTypes.hpp b/src/config/shared/complex/ComplexDataTypes.hpp index 536b0433a..35cfd4936 100644 --- a/src/config/shared/complex/ComplexDataTypes.hpp +++ b/src/config/shared/complex/ComplexDataTypes.hpp @@ -66,7 +66,7 @@ namespace Config { return true; } - virtual std::string toString() { + virtual std::string toString() const { std::string result; for (auto& c : m_colors) { result += std::format("{:x} ", c.getAsHex()); @@ -130,7 +130,7 @@ namespace Config { return CVD_TYPE_CSS_VALUE; } - virtual std::string toString() { + virtual std::string toString() const { return std::format("{} {} {} {}", m_top, m_right, m_bottom, m_left); } }; @@ -141,6 +141,9 @@ namespace Config { CFontWeightConfigValueData(const char* weight) { parseWeight(weight); } + CFontWeightConfigValueData(int64_t def) : m_value(def) { + ; + } int64_t m_value = 400; // default to normal weight @@ -148,20 +151,19 @@ namespace Config { return CVD_TYPE_FONT_WEIGHT; } - virtual std::string toString() { + virtual std::string toString() const { return std::format("{}", m_value); } + inline static const auto WEIGHTS = std::map{ + {"thin", 100}, {"ultralight", 200}, {"light", 300}, {"semilight", 350}, {"book", 380}, {"normal", 400}, + {"medium", 500}, {"semibold", 600}, {"bold", 700}, {"ultrabold", 800}, {"heavy", 900}, {"ultraheavy", 1000}, + }; + void parseWeight(const std::string& strWeight) { auto lcWeight{strWeight}; std::ranges::transform(strWeight, lcWeight.begin(), ::tolower); - // values taken from Pango weight enums - const auto WEIGHTS = std::map{ - {"thin", 100}, {"ultralight", 200}, {"light", 300}, {"semilight", 350}, {"book", 380}, {"normal", 400}, - {"medium", 500}, {"semibold", 600}, {"bold", 700}, {"ultrabold", 800}, {"heavy", 900}, {"ultraheavy", 1000}, - }; - auto weight = WEIGHTS.find(lcWeight); if (weight != WEIGHTS.end()) m_value = weight->second; diff --git a/src/config/shared/inotify/ConfigWatcher.cpp b/src/config/shared/inotify/ConfigWatcher.cpp index 02cedbd69..c10039e48 100644 --- a/src/config/shared/inotify/ConfigWatcher.cpp +++ b/src/config/shared/inotify/ConfigWatcher.cpp @@ -39,7 +39,7 @@ CFileDescriptor& CConfigWatcher::getInotifyFD() { } void CConfigWatcher::update() { - static const auto PDISABLEAUTORELOAD = CConfigValue("misc:disable_autoreload"); + static const auto PDISABLEAUTORELOAD = CConfigValue("misc:disable_autoreload"); setWatchList(*PDISABLEAUTORELOAD ? std::vector{} : Config::mgr()->getConfigPaths()); } @@ -60,7 +60,7 @@ void CConfigWatcher::setWatchList(const std::vector& paths) { // add new paths for (const auto& path : paths) { m_watches.emplace_back(SInotifyWatch{ - .wd = inotify_add_watch(m_inotifyFd.get(), path.c_str(), IN_MODIFY | IN_DONT_FOLLOW), + .wd = inotify_add_watch(m_inotifyFd.get(), path.c_str(), IN_CLOSE_WRITE | IN_DONT_FOLLOW), .file = path, }); @@ -69,7 +69,7 @@ void CConfigWatcher::setWatchList(const std::vector& paths) { const auto IS_SYMLINK = std::filesystem::is_symlink(path, ec2); if (!ec && !ec2 && IS_SYMLINK) { m_watches.emplace_back(SInotifyWatch{ - .wd = inotify_add_watch(m_inotifyFd.get(), CANONICAL.c_str(), IN_MODIFY), + .wd = inotify_add_watch(m_inotifyFd.get(), CANONICAL.c_str(), IN_CLOSE_WRITE), .file = path, }); } diff --git a/src/config/shared/monitor/MonitorRuleManager.cpp b/src/config/shared/monitor/MonitorRuleManager.cpp index 3c68f65b7..9c6354740 100644 --- a/src/config/shared/monitor/MonitorRuleManager.cpp +++ b/src/config/shared/monitor/MonitorRuleManager.cpp @@ -33,6 +33,8 @@ void CMonitorRuleManager::clear() { void CMonitorRuleManager::add(CMonitorRule&& x) { std::erase_if(m_rules, [&x](const auto& e) { return e.m_name == x.m_name; }); m_rules.emplace_back(std::move(x)); + + scheduleReload(); } CMonitorRule CMonitorRuleManager::get(const PHLMONITOR PMONITOR) { @@ -156,7 +158,7 @@ void CMonitorRuleManager::ensureMonitorStatus() { } void CMonitorRuleManager::ensureVRR(PHLMONITOR pMonitor) { - static auto PVRR = CConfigValue("misc:vrr"); + static auto PVRR = CConfigValue("misc:vrr"); static auto ensureVRRForDisplay = [&](PHLMONITOR m) -> void { if (!m->m_output || m->m_createdByUser) diff --git a/src/config/shared/workspace/WorkspaceRule.hpp b/src/config/shared/workspace/WorkspaceRule.hpp index ec4c04931..5d63e4349 100644 --- a/src/config/shared/workspace/WorkspaceRule.hpp +++ b/src/config/shared/workspace/WorkspaceRule.hpp @@ -22,6 +22,7 @@ namespace Config { // merge other into us void mergeLeft(const CWorkspaceRule& other); + bool m_enabled = true; std::string m_monitor = ""; std::string m_workspaceString = ""; std::string m_workspaceName = ""; diff --git a/src/config/shared/workspace/WorkspaceRuleManager.cpp b/src/config/shared/workspace/WorkspaceRuleManager.cpp index ca45873af..4c2c2b909 100644 --- a/src/config/shared/workspace/WorkspaceRuleManager.cpp +++ b/src/config/shared/workspace/WorkspaceRuleManager.cpp @@ -22,7 +22,7 @@ void CWorkspaceRuleManager::add(CWorkspaceRule&& x) { } void CWorkspaceRuleManager::replaceOrAdd(CWorkspaceRule&& x) { - auto it = std::ranges::find_if(m_rules, [&x](const auto& r) { return r.m_workspaceString == x.m_workspaceString; }); + auto it = std::ranges::find_if(m_rules, [&x](const auto& r) { return r.m_enabled && r.m_workspaceString == x.m_workspaceString; }); if (it == m_rules.end()) m_rules.emplace_back(std::move(x)); else @@ -34,6 +34,9 @@ std::optional CWorkspaceRuleManager::getWorkspaceRuleFor(PHLWORK CWorkspaceRule mergedRule; for (auto const& rule : m_rules) { + if (!rule.m_enabled) + continue; + if (!workspace->matchesStaticSelector(rule.m_workspaceString)) continue; @@ -49,6 +52,9 @@ std::optional CWorkspaceRuleManager::getWorkspaceRuleFor(PHLWORK std::string CWorkspaceRuleManager::getDefaultWorkspaceFor(const std::string& name) { for (auto other = m_rules.begin(); other != m_rules.end(); ++other) { + if (!other->m_enabled) + continue; + if (other->m_isDefault) { if (other->m_monitor == name) return other->m_workspaceString; @@ -72,6 +78,8 @@ PHLMONITOR CWorkspaceRuleManager::getBoundMonitorForWS(const std::string& wsname std::string CWorkspaceRuleManager::getBoundMonitorStringForWS(const std::string& wsname) { for (auto const& wr : m_rules) { + if (!wr.m_enabled) + continue; const auto WSNAME = wr.m_workspaceName.starts_with("name:") ? wr.m_workspaceName.substr(5) : wr.m_workspaceName; if (WSNAME == wsname) return wr.m_monitor; diff --git a/src/config/supplementary/ConfigDescriptions.cpp b/src/config/supplementary/ConfigDescriptions.cpp deleted file mode 100644 index d0a1b4673..000000000 --- a/src/config/supplementary/ConfigDescriptions.cpp +++ /dev/null @@ -1,122 +0,0 @@ -#include "ConfigDescriptions.hpp" - -// FIXME: this NO NO NO!!!! -#include "../ConfigManager.hpp" -#include "../shared/complex/ComplexDataType.hpp" -#include "../../helpers/MiscFunctions.hpp" - -#include -#include - -using namespace Config::Supplementary; - -std::string SConfigOptionDescription::jsonify() const { - auto parseData = [this]() -> std::string { - return std::visit( - [this](auto&& val) { - const auto CONFIG_VAL = Config::mgr()->getConfigValue(value); - if (!CONFIG_VAL.dataptr) { - Log::logger->log(Log::ERR, "invalid SConfigOptionDescription: no config option {} exists", value); - return std::string{""}; - } - const char* const EXPLICIT = CONFIG_VAL.setByUser ? "true" : "false"; - - std::string currentValue = "undefined"; - - if (typeid(Config::INTEGER) == std::type_index(*CONFIG_VAL.type)) - currentValue = std::format("{}", **rc(CONFIG_VAL.dataptr)); - else if (typeid(Config::FLOAT) == std::type_index(*CONFIG_VAL.type)) - currentValue = std::format("{:.2f}", **rc(CONFIG_VAL.dataptr)); - else if (typeid(Hyprlang::STRING) == std::type_index(*CONFIG_VAL.type)) - currentValue = std::format("\"{}\"", *rc(CONFIG_VAL.dataptr)); - else if (typeid(Config::STRING) == std::type_index(*CONFIG_VAL.type)) - currentValue = std::format("\"{}\"", **rc(CONFIG_VAL.dataptr)); - else if (typeid(Config::VEC2) == std::type_index(*CONFIG_VAL.type)) { - const auto V = **rc(CONFIG_VAL.dataptr); - currentValue = std::format("\"{}, {}\"", V.x, V.y); - } else if (typeid(Hyprlang::VEC2) == std::type_index(*CONFIG_VAL.type)) { - const auto V = **rc(CONFIG_VAL.dataptr); - currentValue = std::format("\"{}, {}\"", V.x, V.y); - } else if (typeid(Config::IComplexConfigValue*) == std::type_index(*CONFIG_VAL.type)) { - const auto DATA = *rc(CONFIG_VAL.dataptr); - currentValue = std::format("\"{}\"", DATA->toString()); - } else if (typeid(void*) == std::type_index(*CONFIG_VAL.type)) { - // legacy hyprlang value - const auto DATA = *rc(CONFIG_VAL.dataptr); - const auto DATA2 = rc(DATA->getData()); - currentValue = std::format("\"{}\"", DATA2->toString()); - } - - try { - using T = std::decay_t; - if constexpr (std::is_same_v) { - return std::format(R"#( "value": "{}", - "current": {}, - "explicit": {})#", - val.value, currentValue, EXPLICIT); - } else if constexpr (std::is_same_v) { - return std::format(R"#( "value": {}, - "min": {}, - "max": {}, - "current": {}, - "explicit": {})#", - val.value, val.min, val.max, currentValue, EXPLICIT); - } else if constexpr (std::is_same_v) { - return std::format(R"#( "value": {}, - "min": {}, - "max": {}, - "current": {}, - "explicit": {})#", - val.value, val.min, val.max, currentValue, EXPLICIT); - } else if constexpr (std::is_same_v) { - return std::format(R"#( "value": "{}", - "current": {}, - "explicit": {})#", - val.color.getAsHex(), currentValue, EXPLICIT); - } else if constexpr (std::is_same_v) { - return std::format(R"#( "value": {}, - "current": {}, - "explicit": {})#", - val.value, currentValue, EXPLICIT); - } else if constexpr (std::is_same_v) { - return std::format(R"#( "value": "{}", - "firstIndex": {}, - "current": {}, - "explicit": {})#", - val.choices, val.firstIndex, currentValue, EXPLICIT); - } else if constexpr (std::is_same_v) { - return std::format(R"#( "x": {}, - "y": {}, - "min_x": {}, - "min_y": {}, - "max_x": {}, - "max_y": {}, - "current": {}, - "explicit": {})#", - val.vec.x, val.vec.y, val.min.x, val.min.y, val.max.x, val.max.y, currentValue, EXPLICIT); - } else if constexpr (std::is_same_v) { - return std::format(R"#( "value": "{}", - "current": {}, - "explicit": {})#", - val.gradient, currentValue, EXPLICIT); - } - - } catch (std::bad_any_cast& e) { Log::logger->log(Log::ERR, "Bad any_cast on value {} in descriptions", value); } - return std::string{""}; - }, - data); - }; - - std::string json = std::format(R"#({{ - "value": "{}", - "description": "{}", - "type": {}, - "flags": {}, - "data": {{ - {} - }} -}})#", - value, escapeJSONStrings(description), sc(type), sc(flags), parseData()); - - return json; -} \ No newline at end of file diff --git a/src/config/supplementary/ConfigDescriptions.hpp b/src/config/supplementary/ConfigDescriptions.hpp deleted file mode 100644 index 862cc1a93..000000000 --- a/src/config/supplementary/ConfigDescriptions.hpp +++ /dev/null @@ -1,2289 +0,0 @@ -#pragma once - -#include -#include -#include -#include -#include - -#include "../../helpers/math/Math.hpp" -#include "../../helpers/Color.hpp" - -namespace Config::Supplementary { - - enum eConfigOptionType : uint8_t { - CONFIG_OPTION_BOOL = 0, - CONFIG_OPTION_INT = 1, /* e.g. 0/1/2*/ - CONFIG_OPTION_FLOAT = 2, - CONFIG_OPTION_STRING_SHORT = 3, /* e.g. "auto" */ - CONFIG_OPTION_STRING_LONG = 4, /* e.g. a command */ - CONFIG_OPTION_COLOR = 5, - CONFIG_OPTION_CHOICE = 6, /* e.g. "one", "two", "three" */ - CONFIG_OPTION_GRADIENT = 7, - CONFIG_OPTION_VECTOR = 8, - }; - - enum eConfigOptionFlags : uint8_t { - CONFIG_OPTION_FLAG_PERCENTAGE = (1 << 0), - }; - - struct SConfigOptionDescription { - - struct SBoolData { - bool value = false; - }; - - struct SRangeData { - int value = 0, min = 0, max = 2; - }; - - struct SFloatData { - float value = 0, min = 0, max = 100; - }; - - struct SStringData { - std::string value; - }; - - struct SColorData { - CHyprColor color; - }; - - struct SChoiceData { - int firstIndex = 0; - std::string choices; // comma-separated - }; - - struct SGradientData { - std::string gradient; - }; - - struct SVectorData { - Vector2D vec, min, max; - }; - - std::string value; // e.g. general:gaps_in - std::string description; - std::string specialCategory; // if value is special (e.g. device:abc) value will be abc and special device - bool specialKey = false; - eConfigOptionType type = CONFIG_OPTION_BOOL; - uint32_t flags = 0; // eConfigOptionFlags - - std::string jsonify() const; - - // - std::variant data; - }; - - inline static const std::vector CONFIG_OPTIONS = { - - /* - * general: - */ - - SConfigOptionDescription{ - .value = "general:border_size", - .description = "size of the border around windows", - .type = CONFIG_OPTION_INT, - .data = SConfigOptionDescription::SRangeData{1, 0, 20}, - }, - SConfigOptionDescription{ - .value = "general:gaps_in", - .description = "gaps between windows\n\nsupports css style gaps (top, right, bottom, left -> 5 10 15 20)", - .type = CONFIG_OPTION_STRING_SHORT, - .data = SConfigOptionDescription::SStringData{"5"}, - }, - SConfigOptionDescription{ - .value = "general:gaps_out", - .description = "gaps between windows and monitor edges\n\nsupports css style gaps (top, right, bottom, left -> 5 10 15 20)", - .type = CONFIG_OPTION_STRING_SHORT, - .data = SConfigOptionDescription::SStringData{"20"}, - }, - SConfigOptionDescription{ - .value = "general:float_gaps", - .description = "gaps between windows and monitor edges for floating windows\n\nsupports css style gaps (top, right, bottom, left -> 5 10 15 20). \n-1 means default " - "gaps_in/gaps_out\n0 means no gaps", - .type = CONFIG_OPTION_STRING_SHORT, - .data = SConfigOptionDescription::SStringData{"0"}, - }, - SConfigOptionDescription{ - .value = "general:gaps_workspaces", - .description = "gaps between workspaces. Stacks with gaps_out.", - .type = CONFIG_OPTION_INT, - .data = SConfigOptionDescription::SRangeData{0, 0, 100}, - }, - SConfigOptionDescription{ - .value = "general:col.inactive_border", - .description = "border color for inactive windows", - .type = CONFIG_OPTION_GRADIENT, - .data = SConfigOptionDescription::SGradientData{"0xff444444"}, - }, - SConfigOptionDescription{ - .value = "general:col.active_border", - .description = "border color for the active window", - .type = CONFIG_OPTION_GRADIENT, - .data = SConfigOptionDescription::SGradientData{"0xffffffff"}, - }, - SConfigOptionDescription{ - .value = "general:col.nogroup_border", - .description = "inactive border color for window that cannot be added to a group (see denywindowfromgroup dispatcher)", - .type = CONFIG_OPTION_GRADIENT, - .data = SConfigOptionDescription::SGradientData{"0xffffaaff"}, - }, - SConfigOptionDescription{ - .value = "general:col.nogroup_border_active", - .description = "active border color for window that cannot be added to a group", - .type = CONFIG_OPTION_GRADIENT, - .data = SConfigOptionDescription::SGradientData{"0xffff00ff"}, - }, - SConfigOptionDescription{ - .value = "general:layout", - .description = "which layout to use. [dwindle/master]", - .type = CONFIG_OPTION_STRING_SHORT, - .data = SConfigOptionDescription::SStringData{"dwindle"}, - }, - SConfigOptionDescription{ - .value = "general:no_focus_fallback", - .description = "if true, will not fall back to the next available window when moving focus in a direction where no window was found", - .type = CONFIG_OPTION_BOOL, - .data = SConfigOptionDescription::SBoolData{false}, - }, - SConfigOptionDescription{ - .value = "general:resize_on_border", - .description = "enables resizing windows by clicking and dragging on borders and gaps", - .type = CONFIG_OPTION_BOOL, - .data = SConfigOptionDescription::SBoolData{false}, - }, - SConfigOptionDescription{ - .value = "general:extend_border_grab_area", - .description = "extends the area around the border where you can click and drag on, only used when general:resize_on_border is on.", - .type = CONFIG_OPTION_INT, - .data = SConfigOptionDescription::SRangeData{15, 0, 100}, - }, - SConfigOptionDescription{ - .value = "general:hover_icon_on_border", - .description = "show a cursor icon when hovering over borders, only used when general:resize_on_border is on.", - .type = CONFIG_OPTION_BOOL, - .data = SConfigOptionDescription::SBoolData{true}, - }, - SConfigOptionDescription{ - .value = "general:allow_tearing", - .description = "master switch for allowing tearing to occur. See the Tearing page.", - .type = CONFIG_OPTION_BOOL, - .data = SConfigOptionDescription::SBoolData{false}, - }, - SConfigOptionDescription{ - .value = "general:resize_corner", - .description = "force floating windows to use a specific corner when being resized (1-4 going clockwise from top left, 0 to disable)", - .type = CONFIG_OPTION_INT, - .data = SConfigOptionDescription::SRangeData{0, 0, 4}, - }, - SConfigOptionDescription{ - .value = "general:snap:enabled", - .description = "enable snapping for floating windows", - .type = CONFIG_OPTION_BOOL, - .data = SConfigOptionDescription::SBoolData{false}, - }, - SConfigOptionDescription{ - .value = "general:snap:window_gap", - .description = "minimum gap in pixels between windows before snapping", - .type = CONFIG_OPTION_INT, - .data = SConfigOptionDescription::SRangeData{10, 0, 100}, - }, - SConfigOptionDescription{ - .value = "general:snap:monitor_gap", - .description = "minimum gap in pixels between window and monitor edges before snapping", - .type = CONFIG_OPTION_INT, - .data = SConfigOptionDescription::SRangeData{10, 0, 100}, - }, - SConfigOptionDescription{ - .value = "general:snap:border_overlap", - .description = "if true, windows snap such that only one border's worth of space is between them", - .type = CONFIG_OPTION_BOOL, - .data = SConfigOptionDescription::SBoolData{false}, - }, - SConfigOptionDescription{ - .value = "general:snap:respect_gaps", - .description = "if true, snapping will respect gaps between windows", - .type = CONFIG_OPTION_BOOL, - .data = SConfigOptionDescription::SBoolData{false}, - }, - SConfigOptionDescription{ - .value = "general:modal_parent_blocking", - .description = "if true, parent windows of modals will not be interactive.", - .type = CONFIG_OPTION_BOOL, - .data = SConfigOptionDescription::SBoolData{true}, - }, - SConfigOptionDescription{ - .value = "general:locale", - .description = "overrides the system locale", - .type = CONFIG_OPTION_STRING_SHORT, - .data = SConfigOptionDescription::SStringData{""}, - }, - - /* - * decoration: - */ - - SConfigOptionDescription{ - .value = "decoration:rounding", - .description = "rounded corners' radius (in layout px)", - .type = CONFIG_OPTION_INT, - .data = SConfigOptionDescription::SRangeData{0, 0, 20}, - }, - SConfigOptionDescription{ - .value = "decoration:rounding_power", - .description = "rounding power of corners (2 is a circle)", - .type = CONFIG_OPTION_FLOAT, - .data = SConfigOptionDescription::SFloatData{2, 2, 10}, - }, - SConfigOptionDescription{ - .value = "decoration:active_opacity", - .description = "opacity of active windows. [0.0 - 1.0]", - .type = CONFIG_OPTION_FLOAT, - .data = SConfigOptionDescription::SFloatData{1, 0, 1}, - }, - SConfigOptionDescription{ - .value = "decoration:inactive_opacity", - .description = "opacity of inactive windows. [0.0 - 1.0]", - .type = CONFIG_OPTION_FLOAT, - .data = SConfigOptionDescription::SFloatData{1, 0, 1}, - }, - SConfigOptionDescription{ - .value = "decoration:fullscreen_opacity", - .description = "opacity of fullscreen windows. [0.0 - 1.0]", - .type = CONFIG_OPTION_FLOAT, - .data = SConfigOptionDescription::SFloatData{1, 0, 1}, - }, - SConfigOptionDescription{ - .value = "decoration:shadow:enabled", - .description = "enable drop shadows on windows", - .type = CONFIG_OPTION_BOOL, - .data = SConfigOptionDescription::SBoolData{true}, - }, - SConfigOptionDescription{ - .value = "decoration:shadow:range", - .description = "Shadow range (size) in layout px", - .type = CONFIG_OPTION_INT, - .data = SConfigOptionDescription::SRangeData{4, 0, 100}, - }, - SConfigOptionDescription{ - .value = "decoration:shadow:render_power", - .description = "in what power to render the falloff (more power, the faster the falloff) [1 - 4]", - .type = CONFIG_OPTION_INT, - .data = SConfigOptionDescription::SRangeData{3, 1, 4}, - }, - SConfigOptionDescription{ - .value = "decoration:shadow:sharp", - .description = "whether the shadow should be sharp or not. Akin to an infinitely high render power.", - .type = CONFIG_OPTION_BOOL, - .data = SConfigOptionDescription::SBoolData{false}, - }, - SConfigOptionDescription{ - .value = "decoration:shadow:color", - .description = "shadow's color. Alpha dictates shadow's opacity.", - .type = CONFIG_OPTION_COLOR, - .data = SConfigOptionDescription::SColorData{0xee1a1a1a}, - }, - SConfigOptionDescription{ - .value = "decoration:shadow:color_inactive", - .description = "inactive shadow color. (if not set, will fall back to col.shadow)", - .type = CONFIG_OPTION_COLOR, - .data = SConfigOptionDescription::SColorData{}, //TODO: UNSET? - }, - SConfigOptionDescription{ - .value = "decoration:shadow:offset", - .description = "shadow's rendering offset.", - .type = CONFIG_OPTION_VECTOR, - .data = SConfigOptionDescription::SVectorData{{}, {-250, -250}, {250, 250}}, - }, - SConfigOptionDescription{ - .value = "decoration:shadow:scale", - .description = "shadow's scale. [0.0 - 1.0]", - .type = CONFIG_OPTION_FLOAT, - .data = SConfigOptionDescription::SFloatData{1, 0, 1}, - }, - SConfigOptionDescription{ - .value = "decoration:glow:enabled", - .description = "enable inner glow on windows", - .type = CONFIG_OPTION_BOOL, - .data = SConfigOptionDescription::SBoolData{false}, - }, - SConfigOptionDescription{ - .value = "decoration:glow:range", - .description = "glow range (size) in layout px", - .type = CONFIG_OPTION_INT, - .data = SConfigOptionDescription::SRangeData{10, 0, 100}, - }, - SConfigOptionDescription{ - .value = "decoration:glow:render_power", - .description = "in what power to render the falloff (more power, the faster the falloff) [1 - 4]", - .type = CONFIG_OPTION_INT, - .data = SConfigOptionDescription::SRangeData{3, 1, 4}, - }, - SConfigOptionDescription{ - .value = "decoration:glow:color", - .description = "glow's color. Alpha dictates glow's opacity.", - .type = CONFIG_OPTION_COLOR, - .data = SConfigOptionDescription::SColorData{0xee33ccff}, - }, - SConfigOptionDescription{ - .value = "decoration:glow:color_inactive", - .description = "inactive glow color. (if not set, will fall back to decoration:glow:color)", - .type = CONFIG_OPTION_COLOR, - .data = SConfigOptionDescription::SColorData{0x0033ccff}, - }, - SConfigOptionDescription{ - .value = "decoration:dim_modal", - .description = "enables dimming of parents of modal windows", - .type = CONFIG_OPTION_BOOL, - .data = SConfigOptionDescription::SBoolData{true}, - }, - SConfigOptionDescription{ - .value = "decoration:dim_inactive", - .description = "enables dimming of inactive windows", - .type = CONFIG_OPTION_BOOL, - .data = SConfigOptionDescription::SBoolData{false}, - }, - SConfigOptionDescription{ - .value = "decoration:dim_strength", - .description = "how much inactive windows should be dimmed [0.0 - 1.0]", - .type = CONFIG_OPTION_FLOAT, - .data = SConfigOptionDescription::SFloatData{0.5, 0, 1}, - }, - SConfigOptionDescription{ - .value = "decoration:dim_special", - .description = "how much to dim the rest of the screen by when a special workspace is open. [0.0 - 1.0]", - .type = CONFIG_OPTION_FLOAT, - .data = SConfigOptionDescription::SFloatData{0.2, 0, 1}, - }, - SConfigOptionDescription{ - .value = "decoration:dim_around", - .description = "how much the dimaround window rule should dim by. [0.0 - 1.0]", - .type = CONFIG_OPTION_FLOAT, - .data = SConfigOptionDescription::SFloatData{0.4, 0, 1}, - }, - SConfigOptionDescription{ - .value = "decoration:screen_shader", - .description = "screen_shader a path to a custom shader to be applied at the end of rendering. See examples/screenShader.frag for an example.", - .type = CONFIG_OPTION_STRING_LONG, - .data = SConfigOptionDescription::SStringData{""}, //##TODO UNSET? - }, - SConfigOptionDescription{ - .value = "decoration:border_part_of_window", - .description = "whether the border should be treated as a part of the window.", - .type = CONFIG_OPTION_BOOL, - .data = SConfigOptionDescription::SBoolData{true}, - }, - - /* - * blur: - */ - - SConfigOptionDescription{ - .value = "decoration:blur:enabled", - .description = "enable kawase window background blur", - .type = CONFIG_OPTION_BOOL, - .data = SConfigOptionDescription::SBoolData{true}, - }, - SConfigOptionDescription{ - .value = "decoration:blur:size", - .description = "blur size (distance)", - .type = CONFIG_OPTION_INT, - .data = SConfigOptionDescription::SRangeData{8, 0, 100}, - }, - SConfigOptionDescription{ - .value = "decoration:blur:passes", - .description = "the amount of passes to perform", - .type = CONFIG_OPTION_INT, - .data = SConfigOptionDescription::SRangeData{1, 0, 10}, - }, - SConfigOptionDescription{ - .value = "decoration:blur:ignore_opacity", - .description = "make the blur layer ignore the opacity of the window", - .type = CONFIG_OPTION_BOOL, - .data = SConfigOptionDescription::SBoolData{true}, - }, - SConfigOptionDescription{ - .value = "decoration:blur:new_optimizations", - .description = "whether to enable further optimizations to the blur. Recommended to leave on, as it will massively improve performance.", - .type = CONFIG_OPTION_BOOL, - .data = SConfigOptionDescription::SBoolData{true}, - }, - SConfigOptionDescription{ - .value = "decoration:blur:xray", - .description = - "if enabled, floating windows will ignore tiled windows in their blur. Only available if blur_new_optimizations is true. Will reduce overhead on floating " - "blur significantly.", - .type = CONFIG_OPTION_BOOL, - .data = SConfigOptionDescription::SBoolData{false}, - }, - SConfigOptionDescription{ - .value = "decoration:blur:noise", - .description = "how much noise to apply. [0.0 - 1.0]", - .type = CONFIG_OPTION_FLOAT, - .data = SConfigOptionDescription::SFloatData{0.0117, 0, 1}, - }, - SConfigOptionDescription{ - .value = "decoration:blur:contrast", - .description = "contrast modulation for blur. [0.0 - 2.0]", - .type = CONFIG_OPTION_FLOAT, - .data = SConfigOptionDescription::SFloatData{0.8916, 0, 2}, - }, - SConfigOptionDescription{ - .value = "decoration:blur:brightness", - .description = "brightness modulation for blur. [0.0 - 2.0]", - .type = CONFIG_OPTION_FLOAT, - .data = SConfigOptionDescription::SFloatData{0.8172, 0, 2}, - }, - SConfigOptionDescription{ - .value = "decoration:blur:vibrancy", - .description = "Increase saturation of blurred colors. [0.0 - 1.0]", - .type = CONFIG_OPTION_FLOAT, - .data = SConfigOptionDescription::SFloatData{0.1696, 0, 1}, - }, - SConfigOptionDescription{ - .value = "decoration:blur:vibrancy_darkness", - .description = "How strong the effect of vibrancy is on dark areas . [0.0 - 1.0]", - .type = CONFIG_OPTION_FLOAT, - .data = SConfigOptionDescription::SFloatData{0, 0, 1}, - }, - SConfigOptionDescription{ - .value = "decoration:blur:special", - .description = "whether to blur behind the special workspace (note: expensive)", - .type = CONFIG_OPTION_BOOL, - .data = SConfigOptionDescription::SBoolData{false}, - }, - SConfigOptionDescription{ - .value = "decoration:blur:popups", - .description = "whether to blur popups (e.g. right-click menus)", - .type = CONFIG_OPTION_BOOL, - .data = SConfigOptionDescription::SBoolData{false}, - }, - SConfigOptionDescription{ - .value = "decoration:blur:popups_ignorealpha", - .description = "works like ignorealpha in layer rules. If pixel opacity is below set value, will not blur. [0.0 - 1.0]", - .type = CONFIG_OPTION_FLOAT, - .data = SConfigOptionDescription::SFloatData{0.2, 0, 1}, - }, - SConfigOptionDescription{ - .value = "decoration:blur:input_methods", - .description = "whether to blur input methods (e.g. fcitx5)", - .type = CONFIG_OPTION_BOOL, - .data = SConfigOptionDescription::SBoolData{false}, - }, - SConfigOptionDescription{ - .value = "decoration:blur:input_methods_ignorealpha", - .description = "works like ignorealpha in layer rules. If pixel opacity is below set value, will not blur. [0.0 - 1.0]", - .type = CONFIG_OPTION_FLOAT, - .data = SConfigOptionDescription::SFloatData{0.2, 0, 1}, - }, - - /* - * animations: - */ - - SConfigOptionDescription{ - .value = "animations:enabled", - .description = "enable animations", - .type = CONFIG_OPTION_BOOL, - .data = SConfigOptionDescription::SBoolData{true}, - }, - SConfigOptionDescription{ - .value = "animations:workspace_wraparound", - .description = "changes the direction of slide animations between the first and last workspaces", - .type = CONFIG_OPTION_BOOL, - .data = SConfigOptionDescription::SBoolData{true}, - }, - - /* - * input: - */ - - SConfigOptionDescription{ - .value = "input:kb_model", - .description = "Appropriate XKB keymap parameter. See the note below.", - .type = CONFIG_OPTION_STRING_SHORT, - .data = SConfigOptionDescription::SStringData{STRVAL_EMPTY}, - }, - SConfigOptionDescription{ - .value = "input:kb_layout", - .description = "Appropriate XKB keymap parameter", - .type = CONFIG_OPTION_STRING_SHORT, - .data = SConfigOptionDescription::SStringData{"us"}, - }, - SConfigOptionDescription{ - .value = "input:kb_variant", - .description = "Appropriate XKB keymap parameter", - .type = CONFIG_OPTION_STRING_SHORT, - .data = SConfigOptionDescription::SStringData{""}, - }, - SConfigOptionDescription{ - .value = "input:kb_options", - .description = "Appropriate XKB keymap parameter", - .type = CONFIG_OPTION_STRING_SHORT, - .data = SConfigOptionDescription::SStringData{""}, - }, - SConfigOptionDescription{ - .value = "input:kb_rules", - .description = "Appropriate XKB keymap parameter", - .type = CONFIG_OPTION_STRING_SHORT, - .data = SConfigOptionDescription::SStringData{""}, - }, - SConfigOptionDescription{ - .value = "input:kb_file", - .description = "Appropriate XKB keymap file", - .type = CONFIG_OPTION_STRING_LONG, - .data = SConfigOptionDescription::SStringData{""}, //##TODO UNSET? - }, - SConfigOptionDescription{ - .value = "input:numlock_by_default", - .description = "Engage numlock by default.", - .type = CONFIG_OPTION_BOOL, - .data = SConfigOptionDescription::SBoolData{false}, - }, - SConfigOptionDescription{ - .value = "input:resolve_binds_by_sym", - .description = "Determines how keybinds act when multiple layouts are used. If false, keybinds will always act as if the first specified layout is active. If true, " - "keybinds specified by symbols are activated when you type the respective symbol with the current layout.", - .type = CONFIG_OPTION_BOOL, - .data = SConfigOptionDescription::SBoolData{false}, - }, - SConfigOptionDescription{ - .value = "input:repeat_rate", - .description = "The repeat rate for held-down keys, in repeats per second.", - .type = CONFIG_OPTION_INT, - .data = SConfigOptionDescription::SRangeData{25, 0, 200}, - }, - SConfigOptionDescription{ - .value = "input:repeat_delay", - .description = "Delay before a held-down key is repeated, in milliseconds.", - .type = CONFIG_OPTION_INT, - .data = SConfigOptionDescription::SRangeData{600, 0, 2000}, - }, - SConfigOptionDescription{ - .value = "input:sensitivity", - .description = "Sets the mouse input sensitivity. Value is clamped to the range -1.0 to 1.0.", - .type = CONFIG_OPTION_FLOAT, - .data = SConfigOptionDescription::SFloatData{0, -1, 1}, - }, - SConfigOptionDescription{ - .value = "input:accel_profile", - .description = "Sets the cursor acceleration profile. Can be one of adaptive, flat. Can also be custom, see below. Leave empty to use libinput's default mode for your " - "input device. [adaptive/flat/custom]", - .type = CONFIG_OPTION_STRING_SHORT, - .data = SConfigOptionDescription::SStringData{""}, //##TODO UNSET? - }, - SConfigOptionDescription{ - .value = "input:force_no_accel", - .description = - "Force no cursor acceleration. This bypasses most of your pointer settings to get as raw of a signal as possible. Enabling this is not recommended due to " - "potential cursor desynchronization.", - .type = CONFIG_OPTION_BOOL, - .data = SConfigOptionDescription::SBoolData{false}, - }, - SConfigOptionDescription{ - .value = "input:rotation", - .description = "Sets the rotation of a device in degrees clockwise off the logical neutral position. Value is clamped to the range 0 to 359.", - .type = CONFIG_OPTION_INT, - .data = SConfigOptionDescription::SRangeData{0, 0, 359}, - }, - SConfigOptionDescription{ - .value = "input:left_handed", - .description = "Switches RMB and LMB", - .type = CONFIG_OPTION_BOOL, - .data = SConfigOptionDescription::SBoolData{false}, - }, - SConfigOptionDescription{ - .value = "input:scroll_points", - .description = - "Sets the scroll acceleration profile, when accel_profile is set to custom. Has to be in the form . Leave empty to have a flat scroll curve.", - .type = CONFIG_OPTION_STRING_SHORT, - .data = SConfigOptionDescription::SStringData{""}, //##TODO UNSET? - }, - SConfigOptionDescription{ - .value = "input:scroll_method", - .description = "Sets the scroll method. Can be one of 2fg (2 fingers), edge, on_button_down, no_scroll. [2fg/edge/on_button_down/no_scroll]", - .type = CONFIG_OPTION_STRING_SHORT, - .data = SConfigOptionDescription::SStringData{""}, //##TODO UNSET? - }, - SConfigOptionDescription{ - .value = "input:scroll_button", - .description = "Sets the scroll button. Has to be an int, cannot be a string. Check wev if you have any doubts regarding the ID. 0 means default.", - .type = CONFIG_OPTION_INT, - .data = SConfigOptionDescription::SRangeData{0, 0, 300}, - }, - SConfigOptionDescription{ - .value = "input:scroll_button_lock", - .description = - "If the scroll button lock is enabled, the button does not need to be held down. Pressing and releasing the button toggles the button lock, which logically " - "holds the button down or releases it. While the button is logically held down, motion events are converted to scroll events.", - .type = CONFIG_OPTION_BOOL, - .data = SConfigOptionDescription::SBoolData{false}, - }, - SConfigOptionDescription{ - .value = "input:scroll_factor", - .description = "Multiplier added to scroll movement for external mice. Note that there is a separate setting for touchpad scroll_factor.", - .type = CONFIG_OPTION_FLOAT, - .data = SConfigOptionDescription::SFloatData{1, 0, 2}, - }, - SConfigOptionDescription{ - .value = "input:natural_scroll", - .description = "Inverts scrolling direction. When enabled, scrolling moves content directly, rather than manipulating a scrollbar.", - .type = CONFIG_OPTION_BOOL, - .data = SConfigOptionDescription::SBoolData{false}, - }, - SConfigOptionDescription{ - .value = "input:follow_mouse", - .description = "Specify if and how cursor movement should affect window focus. See the note below. [0/1/2/3]", - .type = CONFIG_OPTION_INT, - .data = SConfigOptionDescription::SRangeData{1, 0, 3}, - }, - SConfigOptionDescription{ - .value = "input:follow_mouse_shrink", - .description = - "Shrinks the inactive window hitboxes used for focus detection by the specified number of pixels. This creates a dead zone in gaps between windows where " - "moving the cursor will not change focus. Works only with follow_mouse = 1.", - .type = CONFIG_OPTION_INT, - .data = SConfigOptionDescription::SRangeData{0, 0, 300}, - }, - SConfigOptionDescription{ - .value = "input:follow_mouse_threshold", - .description = "The smallest distance in logical pixels the mouse needs to travel for the window under it to get focused. Works only with follow_mouse = 1.", - .type = CONFIG_OPTION_FLOAT, - .data = SConfigOptionDescription::SFloatData{}, - }, - SConfigOptionDescription{ - .value = "input:focus_on_close", - .description = - "Controls the window focus behavior when a window is closed. When set to 0, focus will shift to the next window candidate. When set to 1, focus will shift " - "to the window under the cursor. When set to 2, focus will shift to the most recently used/active window.", - .type = CONFIG_OPTION_CHOICE, - .data = SConfigOptionDescription::SChoiceData{0, "next,cursor,mru"}, - }, - SConfigOptionDescription{ - .value = "input:mouse_refocus", - .description = "if disabled, mouse focus won't switch to the hovered window unless the mouse crosses a window boundary when follow_mouse=1.", - .type = CONFIG_OPTION_BOOL, - .data = SConfigOptionDescription::SBoolData{true}, - }, - SConfigOptionDescription{ - .value = "input:float_switch_override_focus", - .description = - "If enabled (1 or 2), focus will change to the window under the cursor when changing from tiled-to-floating and vice versa. If 2, focus will also follow " - "mouse on float-to-float switches.", - .type = CONFIG_OPTION_INT, - .data = SConfigOptionDescription::SRangeData{1, 0, 2}, - }, - SConfigOptionDescription{ - .value = "input:special_fallthrough", - .description = "if enabled, having only floating windows in the special workspace will not block focusing windows in the regular workspace.", - .type = CONFIG_OPTION_BOOL, - .data = SConfigOptionDescription::SBoolData{false}, - }, - SConfigOptionDescription{ - .value = "input:off_window_axis_events", - .description = - "Handles axis events around (gaps/border for tiled, dragarea/border for floated) a focused window. 0 ignores axis events 1 sends out-of-bound coordinates 2 " - "fakes pointer coordinates to the closest point inside the window 3 warps the cursor to the closest point inside the window", - .type = CONFIG_OPTION_INT, - .data = SConfigOptionDescription::SRangeData{1, 0, 3}, - }, - SConfigOptionDescription{ - .value = "input:emulate_discrete_scroll", - .description = - "Emulates discrete scrolling from high resolution scrolling events. 0 disables it, 1 enables handling of non-standard events only, and 2 force enables all " - "scroll wheel events to be handled", - .type = CONFIG_OPTION_INT, - .data = SConfigOptionDescription::SRangeData{1, 0, 2}, - }, - - /* - * input:touchpad: - */ - - SConfigOptionDescription{ - .value = "input:touchpad:disable_while_typing", - .description = "Disable the touchpad while typing.", - .type = CONFIG_OPTION_BOOL, - .data = SConfigOptionDescription::SBoolData{true}, - }, - SConfigOptionDescription{ - .value = "input:touchpad:natural_scroll", - .description = "Inverts scrolling direction. When enabled, scrolling moves content directly, rather than manipulating a scrollbar.", - .type = CONFIG_OPTION_BOOL, - .data = SConfigOptionDescription::SBoolData{false}, - }, - SConfigOptionDescription{ - .value = "input:touchpad:scroll_factor", - .description = "Multiplier applied to the amount of scroll movement.", - .type = CONFIG_OPTION_FLOAT, - .data = SConfigOptionDescription::SFloatData{1, 0, 2}, - }, - SConfigOptionDescription{ - .value = "input:touchpad:middle_button_emulation", - .description = "Sending LMB and RMB simultaneously will be interpreted as a middle click. This disables any touchpad area that would normally send a middle click " - "based on location.", - .type = CONFIG_OPTION_BOOL, - .data = SConfigOptionDescription::SBoolData{false}, - }, - SConfigOptionDescription{ - .value = "input:touchpad:tap_button_map", - .description = "Sets the tap button mapping for touchpad button emulation. Can be one of lrm (default) or lmr (Left, Middle, Right Buttons). [lrm/lmr]", - .type = CONFIG_OPTION_STRING_SHORT, - .data = SConfigOptionDescription::SStringData{""}, //##TODO UNSET? - }, - SConfigOptionDescription{ - .value = "input:touchpad:clickfinger_behavior", - .description = "Button presses with 1, 2, or 3 fingers will be mapped to LMB, RMB, and MMB respectively. This disables interpretation of clicks based on location on " - "the touchpad.", - .type = CONFIG_OPTION_BOOL, - .data = SConfigOptionDescription::SBoolData{false}, - }, - SConfigOptionDescription{ - .value = "input:touchpad:tap-to-click", - .description = "Tapping on the touchpad with 1, 2, or 3 fingers will send LMB, RMB, and MMB respectively.", - .type = CONFIG_OPTION_BOOL, - .data = SConfigOptionDescription::SBoolData{true}, - }, - SConfigOptionDescription{ - .value = "input:touchpad:drag_lock", - .description = "When enabled, lifting the finger off while dragging will not drop the dragged item. 0 -> disabled, 1 -> enabled with timeout, 2 -> enabled sticky." - "dragging will not drop the dragged item.", - .type = CONFIG_OPTION_INT, - .data = SConfigOptionDescription::SRangeData{0, 0, 2}, - }, - SConfigOptionDescription{ - .value = "input:touchpad:tap-and-drag", - .description = "Sets the tap and drag mode for the touchpad", - .type = CONFIG_OPTION_BOOL, - .data = SConfigOptionDescription::SBoolData{false}, - }, - SConfigOptionDescription{ - .value = "input:touchpad:flip_x", - .description = "Inverts the horizontal movement of the touchpad", - .type = CONFIG_OPTION_BOOL, - .data = SConfigOptionDescription::SBoolData{false}, - }, - SConfigOptionDescription{ - .value = "input:touchpad:flip_y", - .description = "Inverts the vertical movement of the touchpad", - .type = CONFIG_OPTION_BOOL, - .data = SConfigOptionDescription::SBoolData{false}, - }, - SConfigOptionDescription{ - .value = "input:touchpad:drag_3fg", - .description = "Three Finger Drag 0 -> disabled, 1 -> 3 finger, 2 -> 4 finger", - .type = CONFIG_OPTION_INT, - .data = SConfigOptionDescription::SRangeData{0, 0, 2}, - }, - - /* - * input:touchdevice: - */ - - SConfigOptionDescription{ - .value = "input:touchdevice:transform", - .description = "Transform the input from touchdevices. The possible transformations are the same as those of the monitors", - .type = CONFIG_OPTION_INT, - .data = SConfigOptionDescription::SRangeData{0, 0, 6}, // ##TODO RANGE? - }, - SConfigOptionDescription{ - .value = "input:touchdevice:output", - .description = "The monitor to bind touch devices. The default is auto-detection. To stop auto-detection, use an empty string or the [[Empty]] value.", - .type = CONFIG_OPTION_STRING_SHORT, - .data = SConfigOptionDescription::SStringData{""}, //##TODO UNSET? - }, - SConfigOptionDescription{ - .value = "input:touchdevice:enabled", - .description = "Whether input is enabled for touch devices.", - .type = CONFIG_OPTION_BOOL, - .data = SConfigOptionDescription::SBoolData{true}, - }, - - /* - * input:virtualkeyboard: - */ - - SConfigOptionDescription{ - .value = "input:virtualkeyboard:share_states", - .description = "Unify key down states and modifier states with other keyboards. 0 -> no, 1 -> yes, 2 -> yes unless IME client", - .type = CONFIG_OPTION_INT, - .data = SConfigOptionDescription::SRangeData{2, 0, 2}, - }, - SConfigOptionDescription{ - .value = "input:virtualkeyboard:release_pressed_on_close", - .description = "Release all pressed keys by virtual keyboard on close.", - .type = CONFIG_OPTION_BOOL, - .data = SConfigOptionDescription::SBoolData{false}, - }, - - /* - * input:tablet: - */ - - SConfigOptionDescription{ - .value = "input:tablet:transform", - .description = "transform the input from tablets. The possible transformations are the same as those of the monitors", - .type = CONFIG_OPTION_INT, - .data = SConfigOptionDescription::SRangeData{0, 0, 6}, // ##TODO RANGE? - }, - SConfigOptionDescription{ - .value = "input:tablet:output", - .description = "the monitor to bind tablets. Can be current or a monitor name. Leave empty to map across all monitors.", - .type = CONFIG_OPTION_STRING_SHORT, - .data = SConfigOptionDescription::SStringData{""}, //##TODO UNSET? - }, - SConfigOptionDescription{ - .value = "input:tablet:region_position", - .description = "position of the mapped region in monitor layout relative to the top left corner of the bound monitor or all monitors.", - .type = CONFIG_OPTION_VECTOR, - .data = SConfigOptionDescription::SVectorData{{}, {-20000, -20000}, {20000, 20000}}, - }, - SConfigOptionDescription{ - .value = "input:tablet:absolute_region_position", - .description = "whether to treat the region_position as an absolute position in monitor layout. Only applies when output is empty.", - .type = CONFIG_OPTION_BOOL, - .data = SConfigOptionDescription::SBoolData{false}, - }, - SConfigOptionDescription{ - .value = "input:tablet:region_size", - .description = "size of the mapped region. When this variable is set, tablet input will be mapped to the region. [0, 0] or invalid size means unset.", - .type = CONFIG_OPTION_VECTOR, - .data = SConfigOptionDescription::SVectorData{{}, {-100, -100}, {4000, 4000}}, - }, - SConfigOptionDescription{ - .value = "input:tablet:relative_input", - .description = "whether the input should be relative", - .type = CONFIG_OPTION_BOOL, - .data = SConfigOptionDescription::SBoolData{false}, - }, - SConfigOptionDescription{ - .value = "input:tablet:left_handed", - .description = "if enabled, the tablet will be rotated 180 degrees", - .type = CONFIG_OPTION_BOOL, - .data = SConfigOptionDescription::SBoolData{false}, - }, - SConfigOptionDescription{ - .value = "input:tablet:active_area_size", - .description = "size of tablet's active area in mm", - .type = CONFIG_OPTION_VECTOR, - .data = SConfigOptionDescription::SVectorData{{}, {}, {500, 500}}, - }, - SConfigOptionDescription{ - .value = "input:tablet:active_area_position", - .description = "position of the active area in mm", - .type = CONFIG_OPTION_VECTOR, - .data = SConfigOptionDescription::SVectorData{{}, {}, {500, 500}}, - }, - - /* ##TODO - * - * PER DEVICE SETTINGS? - * - * */ - - /* - * gestures: - */ - - SConfigOptionDescription{ - .value = "gestures:workspace_swipe_distance", - .description = "in px, the distance of the touchpad gesture", - .type = CONFIG_OPTION_INT, - .data = SConfigOptionDescription::SRangeData{300, 0, 2000}, //##TODO RANGE? - }, - SConfigOptionDescription{ - .value = "gestures:workspace_swipe_touch", - .description = "enable workspace swiping from the edge of a touchscreen", - .type = CONFIG_OPTION_BOOL, - .data = SConfigOptionDescription::SBoolData{false}, - }, - SConfigOptionDescription{ - .value = "gestures:workspace_swipe_invert", - .description = "invert the direction (touchpad only)", - .type = CONFIG_OPTION_BOOL, - .data = SConfigOptionDescription::SBoolData{true}, - }, - SConfigOptionDescription{ - .value = "gestures:workspace_swipe_touch_invert", - .description = "invert the direction (touchscreen only)", - .type = CONFIG_OPTION_BOOL, - .data = SConfigOptionDescription::SBoolData{false}, - }, - SConfigOptionDescription{ - .value = "gestures:workspace_swipe_min_speed_to_force", - .description = "minimum speed in px per timepoint to force the change ignoring cancel_ratio. Setting to 0 will disable this mechanic.", - .type = CONFIG_OPTION_INT, - .data = SConfigOptionDescription::SRangeData{30, 0, 200}, //##TODO RANGE? - }, - SConfigOptionDescription{ - .value = "gestures:workspace_swipe_cancel_ratio", - .description = "how much the swipe has to proceed in order to commence it. (0.7 -> if > 0.7 * distance, switch, if less, revert) [0.0 - 1.0]", - .type = CONFIG_OPTION_FLOAT, - .data = SConfigOptionDescription::SFloatData{0.5, 0, 1}, - }, - SConfigOptionDescription{ - .value = "gestures:workspace_swipe_create_new", - .description = "whether a swipe right on the last workspace should create a new one.", - .type = CONFIG_OPTION_BOOL, - .data = SConfigOptionDescription::SBoolData{true}, - }, - SConfigOptionDescription{ - .value = "gestures:workspace_swipe_direction_lock", - .description = "if enabled, switching direction will be locked when you swipe past the direction_lock_threshold (touchpad only).", - .type = CONFIG_OPTION_BOOL, - .data = SConfigOptionDescription::SBoolData{true}, - }, - SConfigOptionDescription{ - .value = "gestures:workspace_swipe_direction_lock_threshold", - .description = "in px, the distance to swipe before direction lock activates (touchpad only).", - .type = CONFIG_OPTION_INT, - .data = SConfigOptionDescription::SRangeData{10, 0, 200}, //##TODO RANGE? - }, - SConfigOptionDescription{ - .value = "gestures:workspace_swipe_forever", - .description = "if enabled, swiping will not clamp at the neighboring workspaces but continue to the further ones.", - .type = CONFIG_OPTION_BOOL, - .data = SConfigOptionDescription::SBoolData{false}, - }, - SConfigOptionDescription{ - .value = "gestures:workspace_swipe_use_r", - .description = "if enabled, swiping will use the r prefix instead of the m prefix for finding workspaces.", - .type = CONFIG_OPTION_BOOL, - .data = SConfigOptionDescription::SBoolData{false}, - }, - SConfigOptionDescription{ - .value = "gestures:close_max_timeout", - .description = "Timeout for closing windows with the close gesture, in ms.", - .type = CONFIG_OPTION_INT, - .data = SConfigOptionDescription::SRangeData{1000, 10, 2000}, - }, - - /* - * group: - */ - - SConfigOptionDescription{ - .value = "group:insert_after_current", - .description = "whether new windows in a group spawn after current or at group tail", - .type = CONFIG_OPTION_BOOL, - .data = SConfigOptionDescription::SBoolData{true}, - }, - SConfigOptionDescription{ - .value = "group:focus_removed_window", - .description = "whether Hyprland should focus on the window that has just been moved out of the group", - .type = CONFIG_OPTION_BOOL, - .data = SConfigOptionDescription::SBoolData{true}, - }, - SConfigOptionDescription{ - .value = "group:merge_groups_on_drag", - .description = "whether window groups can be dragged into other groups", - .type = CONFIG_OPTION_BOOL, - .data = SConfigOptionDescription::SBoolData{true}, - }, - SConfigOptionDescription{ - .value = "group:merge_groups_on_groupbar", - .description = "whether one group will be merged with another when dragged into its groupbar", - .type = CONFIG_OPTION_BOOL, - .data = SConfigOptionDescription::SBoolData{true}, - }, - SConfigOptionDescription{ - .value = "group:col.border_active", - .description = "border color for inactive windows", - .type = CONFIG_OPTION_GRADIENT, - .data = SConfigOptionDescription::SGradientData{"0x66ffff00"}, - }, - SConfigOptionDescription{ - .value = "group:col.border_inactive", - .description = "border color for the active window", - .type = CONFIG_OPTION_GRADIENT, - .data = SConfigOptionDescription::SGradientData{"0x66777700"}, - }, - SConfigOptionDescription{ - .value = "group:col.border_locked_inactive", - .description = "inactive border color for window that cannot be added to a group (see denywindowfromgroup dispatcher)", - .type = CONFIG_OPTION_GRADIENT, - .data = SConfigOptionDescription::SGradientData{"0x66ff5500"}, - }, - SConfigOptionDescription{ - .value = "group:col.border_locked_active", - .description = "active border color for window that cannot be added to a group", - .type = CONFIG_OPTION_GRADIENT, - .data = SConfigOptionDescription::SGradientData{"0x66775500"}, - }, - SConfigOptionDescription{ - .value = "group:auto_group", - .description = "automatically group new windows", - .type = CONFIG_OPTION_BOOL, - .data = SConfigOptionDescription::SBoolData{true}, - }, - SConfigOptionDescription{ - .value = "group:drag_into_group", - .description = "whether dragging a window into a unlocked group will merge them. Options: 0 (disabled), 1 (enabled), 2 (only when dragging into the groupbar)", - .type = CONFIG_OPTION_CHOICE, - .data = SConfigOptionDescription::SChoiceData{0, "disabled,enabled,only when dragging into the groupbar"}, - }, - SConfigOptionDescription{ - .value = "group:merge_floated_into_tiled_on_groupbar", - .description = "whether dragging a floating window into a tiled window groupbar will merge them", - .type = CONFIG_OPTION_BOOL, - .data = SConfigOptionDescription::SBoolData{false}, - }, - SConfigOptionDescription{ - .value = "group:group_on_movetoworkspace", - .description = "whether using movetoworkspace[silent] will merge the window into the workspace's solitary unlocked group", - .type = CONFIG_OPTION_BOOL, - .data = SConfigOptionDescription::SBoolData{false}, - }, - - /* - * group:groupbar: - */ - - SConfigOptionDescription{ - .value = "group:groupbar:enabled", - .description = "enables groupbars", - .type = CONFIG_OPTION_BOOL, - .data = SConfigOptionDescription::SBoolData{true}, - }, - SConfigOptionDescription{ - .value = "group:groupbar:font_family", - .description = "font used to display groupbar titles, use misc:font_family if not specified", - .type = CONFIG_OPTION_STRING_SHORT, - .data = SConfigOptionDescription::SStringData{STRVAL_EMPTY}, //##TODO UNSET? - }, - SConfigOptionDescription{ - .value = "group:groupbar:font_weight_active", - .description = "weight of the font used to display active groupbar titles", - .type = CONFIG_OPTION_STRING_SHORT, - .data = SConfigOptionDescription::SStringData{"normal"}, - }, - SConfigOptionDescription{ - .value = "group:groupbar:font_weight_inactive", - .description = "weight of the font used to display inactive groupbar titles", - .type = CONFIG_OPTION_STRING_SHORT, - .data = SConfigOptionDescription::SStringData{"normal"}, - }, - SConfigOptionDescription{ - .value = "group:groupbar:font_size", - .description = "font size of groupbar title", - .type = CONFIG_OPTION_INT, - .data = SConfigOptionDescription::SRangeData{8, 2, 64}, - }, - SConfigOptionDescription{ - .value = "group:groupbar:gradients", - .description = "enables gradients", - .type = CONFIG_OPTION_BOOL, - .data = SConfigOptionDescription::SBoolData{false}, - }, - SConfigOptionDescription{ - .value = "group:groupbar:height", - .description = "height of the groupbar", - .type = CONFIG_OPTION_INT, - .data = SConfigOptionDescription::SRangeData{14, 1, 64}, - }, - SConfigOptionDescription{ - .value = "group:groupbar:indicator_gap", - .description = "height of the gap between the groupbar indicator and title", - .type = CONFIG_OPTION_INT, - .data = SConfigOptionDescription::SRangeData{0, 0, 64}, - }, - SConfigOptionDescription{ - .value = "group:groupbar:indicator_height", - .description = "height of the groupbar indicator", - .type = CONFIG_OPTION_INT, - .data = SConfigOptionDescription::SRangeData{3, 1, 64}, - }, - SConfigOptionDescription{ - .value = "group:groupbar:stacked", - .description = "render the groupbar as a vertical stack", - .type = CONFIG_OPTION_BOOL, - .data = SConfigOptionDescription::SBoolData{false}, - }, - SConfigOptionDescription{ - .value = "group:groupbar:priority", - .description = "sets the decoration priority for groupbars", - .type = CONFIG_OPTION_INT, - .data = SConfigOptionDescription::SRangeData{3, 0, 6}, //##TODO RANGE? - }, - SConfigOptionDescription{ - .value = "group:groupbar:render_titles", - .description = "whether to render titles in the group bar decoration", - .type = CONFIG_OPTION_BOOL, - .data = SConfigOptionDescription::SBoolData{true}, - }, - SConfigOptionDescription{ - .value = "group:groupbar:scrolling", - .description = "whether scrolling in the groupbar changes group active window", - .type = CONFIG_OPTION_BOOL, - .data = SConfigOptionDescription::SBoolData{true}, - }, - SConfigOptionDescription{ - .value = "group:groupbar:rounding", - .description = "how much to round the groupbar", - .type = CONFIG_OPTION_INT, - .data = SConfigOptionDescription::SRangeData{1, 0, 20}, - }, - SConfigOptionDescription{ - .value = "group:groupbar:rounding_power", - .description = "rounding power of groupbar corners (2 is a circle)", - .type = CONFIG_OPTION_FLOAT, - .data = SConfigOptionDescription::SFloatData{2, 2, 10}, - }, - SConfigOptionDescription{ - .value = "group:groupbar:gradient_rounding", - .description = "how much to round the groupbar gradient", - .type = CONFIG_OPTION_INT, - .data = SConfigOptionDescription::SRangeData{1, 0, 20}, - }, - SConfigOptionDescription{ - .value = "group:groupbar:gradient_rounding_power", - .description = "rounding power of groupbar gradient corners (2 is a circle)", - .type = CONFIG_OPTION_FLOAT, - .data = SConfigOptionDescription::SFloatData{2, 2, 10}, - }, - SConfigOptionDescription{ - .value = "group:groupbar:round_only_edges", - .description = "if yes, will only round at the groupbar edges", - .type = CONFIG_OPTION_BOOL, - .data = SConfigOptionDescription::SBoolData{true}, - }, - SConfigOptionDescription{ - .value = "group:groupbar:gradient_round_only_edges", - .description = "if yes, will only round at the groupbar gradient edges", - .type = CONFIG_OPTION_BOOL, - .data = SConfigOptionDescription::SBoolData{true}, - }, - SConfigOptionDescription{ - .value = "group:groupbar:text_color", - .description = "color for window titles in the groupbar", - .type = CONFIG_OPTION_COLOR, - .data = SConfigOptionDescription::SColorData{0xffffffff}, - }, - SConfigOptionDescription{ - .value = "group:groupbar:text_color_inactive", - .description = "color for inactive windows' titles in the groupbar (if unset, defaults to text_color)", - .type = CONFIG_OPTION_COLOR, - .data = SConfigOptionDescription::SColorData{}, //TODO: UNSET? - }, - SConfigOptionDescription{ - .value = "group:groupbar:text_color_locked_active", - .description = "color for the active window's title in a locked group (if unset, defaults to text_color)", - .type = CONFIG_OPTION_COLOR, - .data = SConfigOptionDescription::SColorData{}, //TODO: UNSET? - }, - SConfigOptionDescription{ - .value = "group:groupbar:text_color_locked_inactive", - .description = "color for inactive windows' titles in locked groups (if unset, defaults to text_color_inactive)", - .type = CONFIG_OPTION_COLOR, - .data = SConfigOptionDescription::SColorData{}, //TODO: UNSET? - }, - SConfigOptionDescription{ - .value = "group:groupbar:col.active", - .description = "active group border color", - .type = CONFIG_OPTION_COLOR, - .data = SConfigOptionDescription::SColorData{0x66ffff00}, - }, - SConfigOptionDescription{ - .value = "group:groupbar:col.inactive", - .description = "inactive (out of focus) group border color", - .type = CONFIG_OPTION_COLOR, - .data = SConfigOptionDescription::SColorData{0x66777700}, - }, - SConfigOptionDescription{ - .value = "group:groupbar:col.locked_active", - .description = "active locked group border color", - .type = CONFIG_OPTION_COLOR, - .data = SConfigOptionDescription::SColorData{0x66ff5500}, - }, - SConfigOptionDescription{ - .value = "group:groupbar:col.locked_inactive", - .description = "controls the group bar text color", - .type = CONFIG_OPTION_COLOR, - .data = SConfigOptionDescription::SColorData{0x66775500}, - }, - SConfigOptionDescription{ - .value = "group:groupbar:gaps_out", - .description = "gap between gradients and window", - .type = CONFIG_OPTION_INT, - .data = SConfigOptionDescription::SRangeData{2, 0, 20}, - }, - SConfigOptionDescription{ - .value = "group:groupbar:gaps_in", - .description = "gap between gradients", - .type = CONFIG_OPTION_INT, - .data = SConfigOptionDescription::SRangeData{2, 0, 20}, - }, - SConfigOptionDescription{ - .value = "group:groupbar:keep_upper_gap", - .description = "keep an upper gap above gradient", - .type = CONFIG_OPTION_BOOL, - .data = SConfigOptionDescription::SBoolData{true}, - }, - SConfigOptionDescription{ - .value = "group:groupbar:text_offset", - .description = "set an offset for a text", - .type = CONFIG_OPTION_BOOL, - .data = SConfigOptionDescription::SRangeData{0, -20, 20}, - }, - SConfigOptionDescription{ - .value = "group:groupbar:text_padding", - .description = "set horizontal padding for a text", - .type = CONFIG_OPTION_BOOL, - .data = SConfigOptionDescription::SRangeData{0, 0, 22}, - }, - SConfigOptionDescription{ - .value = "group:groupbar:blur", - .description = "enable background blur for groupbars", - .type = CONFIG_OPTION_BOOL, - .data = SConfigOptionDescription::SBoolData{false}, - }, - - /* - * misc: - */ - - SConfigOptionDescription{ - .value = "misc:disable_hyprland_logo", - .description = "disables the random Hyprland logo / anime girl background. :(", - .type = CONFIG_OPTION_BOOL, - .data = SConfigOptionDescription::SBoolData{false}, - }, - SConfigOptionDescription{ - .value = "misc:disable_splash_rendering", - .description = "disables the Hyprland splash rendering. (requires a monitor reload to take effect)", - .type = CONFIG_OPTION_BOOL, - .data = SConfigOptionDescription::SBoolData{false}, - }, - SConfigOptionDescription{ - .value = "misc:col.splash", - .description = "Changes the color of the splash text (requires a monitor reload to take effect).", - .type = CONFIG_OPTION_COLOR, - .data = SConfigOptionDescription::SColorData{0xffffffff}, - }, - SConfigOptionDescription{ - .value = "misc:font_family", - .description = "Set the global default font to render the text including debug fps/notification, config error messages and etc., selected from system fonts.", - .type = CONFIG_OPTION_STRING_SHORT, - .data = SConfigOptionDescription::SStringData{"Sans"}, - }, - SConfigOptionDescription{ - .value = "misc:splash_font_family", - .description = "Changes the font used to render the splash text, selected from system fonts (requires a monitor reload to take effect).", - .type = CONFIG_OPTION_STRING_SHORT, - .data = SConfigOptionDescription::SStringData{STRVAL_EMPTY}, //##TODO UNSET? - }, - SConfigOptionDescription{ - .value = "misc:force_default_wallpaper", - .description = "Enforce any of the 3 default wallpapers. Setting this to 0 or 1 disables the anime background. -1 means “random”. [-1/0/1/2]", - .type = CONFIG_OPTION_INT, - .data = SConfigOptionDescription::SRangeData{-1, -1, 2}, - }, - SConfigOptionDescription{ - .value = "misc:vrr", - .description = " controls the VRR (Adaptive Sync) of your monitors. 0 - off, 1 - on, 2 - fullscreen only, 3 - fullscreen with game or video content type [0/1/2/3]", - .type = CONFIG_OPTION_INT, - .data = SConfigOptionDescription::SRangeData{.value = 0, .min = 0, .max = 3}, - }, - SConfigOptionDescription{ - .value = "misc:mouse_move_enables_dpms", - .description = "If DPMS is set to off, wake up the monitors if the mouse move", - .type = CONFIG_OPTION_BOOL, - .data = SConfigOptionDescription::SBoolData{false}, - }, - SConfigOptionDescription{ - .value = "misc:key_press_enables_dpms", - .description = "If DPMS is set to off, wake up the monitors if a key is pressed.", - .type = CONFIG_OPTION_BOOL, - .data = SConfigOptionDescription::SBoolData{false}, - }, - SConfigOptionDescription{ - .value = "misc:name_vk_after_proc", - .description = "Name virtual keyboards after the processes that create them. E.g. /usr/bin/fcitx5 will have hl-virtual-keyboard-fcitx5.", - .type = CONFIG_OPTION_BOOL, - .data = SConfigOptionDescription::SBoolData{true}, - }, - SConfigOptionDescription{ - .value = "misc:always_follow_on_dnd", - .description = "Will make mouse focus follow the mouse when drag and dropping. Recommended to leave it enabled, especially for people using focus follows mouse at 0.", - .type = CONFIG_OPTION_BOOL, - .data = SConfigOptionDescription::SBoolData{true}, - }, - SConfigOptionDescription{ - .value = "misc:layers_hog_keyboard_focus", - .description = "If true, will make keyboard-interactive layers keep their focus on mouse move (e.g. wofi, bemenu)", - .type = CONFIG_OPTION_BOOL, - .data = SConfigOptionDescription::SBoolData{true}, - }, - SConfigOptionDescription{ - .value = "misc:animate_manual_resizes", - .description = "If true, will animate manual window resizes/moves", - .type = CONFIG_OPTION_BOOL, - .data = SConfigOptionDescription::SBoolData{false}, - }, - SConfigOptionDescription{ - .value = "misc:animate_mouse_windowdragging", - .description = "If true, will animate windows being dragged by mouse, note that this can cause weird behavior on some curves", - .type = CONFIG_OPTION_BOOL, - .data = SConfigOptionDescription::SBoolData{false}, - }, - SConfigOptionDescription{ - .value = "misc:disable_autoreload", - .description = "If true, the config will not reload automatically on save, and instead needs to be reloaded with hyprctl reload. Might save on battery.", - .type = CONFIG_OPTION_BOOL, - .data = SConfigOptionDescription::SBoolData{false}, - }, - SConfigOptionDescription{ - .value = "misc:enable_swallow", - .description = "Enable window swallowing", - .type = CONFIG_OPTION_BOOL, - .data = SConfigOptionDescription::SBoolData{false}, - }, - SConfigOptionDescription{ - .value = "misc:swallow_regex", - .description = "The class regex to be used for windows that should be swallowed (usually, a terminal). To know more about the list of regex which can be used use this " - "cheatsheet.", - .type = CONFIG_OPTION_STRING_SHORT, - .data = SConfigOptionDescription::SStringData{""}, //##TODO UNSET? - }, - SConfigOptionDescription{ - .value = "misc:swallow_exception_regex", - .description = - "The title regex to be used for windows that should not be swallowed by the windows specified in swallow_regex (e.g. wev). The regex is matched against the " - "parent (e.g. Kitty) window’s title on the assumption that it changes to whatever process it’s running.", - .type = CONFIG_OPTION_STRING_SHORT, - .data = SConfigOptionDescription::SStringData{""}, //##TODO UNSET? - }, - SConfigOptionDescription{ - .value = "misc:focus_on_activate", - .description = "Whether Hyprland should focus an app that requests to be focused (an activate request)", - .type = CONFIG_OPTION_BOOL, - .data = SConfigOptionDescription::SBoolData{false}, - }, - SConfigOptionDescription{ - .value = "misc:mouse_move_focuses_monitor", - .description = "Whether mouse moving into a different monitor should focus it", - .type = CONFIG_OPTION_BOOL, - .data = SConfigOptionDescription::SBoolData{true}, - }, - SConfigOptionDescription{ - .value = "misc:allow_session_lock_restore", - .description = "if true, will allow you to restart a lockscreen app in case it crashes (red screen of death)", - .type = CONFIG_OPTION_BOOL, - .data = SConfigOptionDescription::SBoolData{false}, - }, - SConfigOptionDescription{ - .value = "misc:session_lock_xray", - .description = "keep rendering workspaces below your lockscreen", - .type = CONFIG_OPTION_BOOL, - .data = SConfigOptionDescription::SBoolData{false}, - }, - SConfigOptionDescription{ - .value = "misc:background_color", - .description = "change the background color. (requires enabled disable_hyprland_logo)", - .type = CONFIG_OPTION_COLOR, - .data = SConfigOptionDescription::SColorData{0x111111}, - }, - SConfigOptionDescription{ - .value = "misc:close_special_on_empty", - .description = "close the special workspace if the last window is removed", - .type = CONFIG_OPTION_BOOL, - .data = SConfigOptionDescription::SBoolData{true}, - }, - SConfigOptionDescription{ - .value = "misc:on_focus_under_fullscreen", - .description = "if there is a fullscreen or maximized window, decide whether a tiled window requested to focus should replace it, stay behind or disable the " - "fullscreen/maximized state. 0 - ignore focus request (keep focus on fullscreen window), 1 - takes over, 2 - unfullscreen/unmaximize [0/1/2]", - .type = CONFIG_OPTION_INT, - .data = SConfigOptionDescription::SRangeData{2, 0, 2}, - }, - SConfigOptionDescription{ - .value = "misc:exit_window_retains_fullscreen", - .description = "if true, closing a fullscreen window makes the next focused window fullscreen", - .type = CONFIG_OPTION_BOOL, - .data = SConfigOptionDescription::SBoolData{false}, - }, - SConfigOptionDescription{ - .value = "misc:initial_workspace_tracking", - .description = "if enabled, windows will open on the workspace they were invoked on. 0 - disabled, 1 - single-shot, 2 - persistent (all children too)", - .type = CONFIG_OPTION_INT, - .data = SConfigOptionDescription::SRangeData{1, 0, 2}, - }, - SConfigOptionDescription{ - .value = "misc:middle_click_paste", - .description = "whether to enable middle-click-paste (aka primary selection)", - .type = CONFIG_OPTION_BOOL, - .data = SConfigOptionDescription::SBoolData{true}, - }, - SConfigOptionDescription{ - .value = "misc:render_unfocused_fps", - .description = "the maximum limit for renderunfocused windows' fps in the background", - .type = CONFIG_OPTION_INT, - .data = SConfigOptionDescription::SRangeData{15, 1, 120}, - }, - SConfigOptionDescription{ - .value = "misc:disable_xdg_env_checks", - .description = "disable the warning if XDG environment is externally managed", - .type = CONFIG_OPTION_BOOL, - .data = SConfigOptionDescription::SBoolData{false}, - }, - SConfigOptionDescription{ - .value = "misc:disable_hyprland_guiutils_check", - .description = "disable the warning if hyprland-guiutils is missing", - .type = CONFIG_OPTION_BOOL, - .data = SConfigOptionDescription::SBoolData{false}, - }, - SConfigOptionDescription{ - .value = "misc:disable_watchdog_warning", - .description = "whether to disable the warning about not using start-hyprland.", - .type = CONFIG_OPTION_BOOL, - .data = SConfigOptionDescription::SBoolData{false}, - }, - SConfigOptionDescription{ - .value = "misc:lockdead_screen_delay", - .description = "the delay in ms after the lockdead screen appears if the lock screen did not appear after a lock event occurred.", - .type = CONFIG_OPTION_INT, - .data = SConfigOptionDescription::SRangeData{1000, 0, 5000}, - }, - SConfigOptionDescription{ - .value = "misc:enable_anr_dialog", - .description = "whether to enable the ANR (app not responding) dialog when your apps hang", - .type = CONFIG_OPTION_BOOL, - .data = SConfigOptionDescription::SBoolData{true}, - }, - SConfigOptionDescription{ - .value = "misc:anr_missed_pings", - .description = "number of missed pings before showing the ANR dialog", - .type = CONFIG_OPTION_INT, - .data = SConfigOptionDescription::SRangeData{5, 1, 20}, - }, - SConfigOptionDescription{ - .value = "misc:screencopy_force_8b", - .description = "forces 8 bit screencopy (fixes apps that don't understand 10bit)", - .type = CONFIG_OPTION_BOOL, - .data = SConfigOptionDescription::SBoolData{true}, - }, - SConfigOptionDescription{ - .value = "misc:disable_scale_notification", - .description = "disables notification popup when a monitor fails to set a suitable scale and falls back to suggested", - .type = CONFIG_OPTION_BOOL, - .data = SConfigOptionDescription::SBoolData{false}, - }, - SConfigOptionDescription{ - .value = "misc:size_limits_tiled", - .description = "whether to apply minsize and maxsize rules to tiled windows", - .type = CONFIG_OPTION_BOOL, - .data = SConfigOptionDescription::SBoolData{false}, - }, - - /* - * binds: - */ - - SConfigOptionDescription{ - .value = "binds:pass_mouse_when_bound", - .description = "if disabled, will not pass the mouse events to apps / dragging windows around if a keybind has been triggered.", - .type = CONFIG_OPTION_BOOL, - .data = SConfigOptionDescription::SBoolData{false}, - }, - SConfigOptionDescription{ - .value = "binds:scroll_event_delay", - .description = "in ms, how many ms to wait after a scroll event to allow passing another one for the binds.", - .type = CONFIG_OPTION_INT, - .data = SConfigOptionDescription::SRangeData{300, 0, 2000}, - }, - SConfigOptionDescription{ - .value = "binds:workspace_back_and_forth", - .description = "If enabled, an attempt to switch to the currently focused workspace will instead switch to the previous workspace. Akin to i3’s auto_back_and_forth.", - .type = CONFIG_OPTION_BOOL, - .data = SConfigOptionDescription::SBoolData{false}, - }, - SConfigOptionDescription{ - .value = "binds:hide_special_on_workspace_change", - .description = - "If enabled, changing the active workspace (including to itself) will hide the special workspace on the monitor where the newly active workspace resides.", - .type = CONFIG_OPTION_BOOL, - .data = SConfigOptionDescription::SBoolData{false}, - }, - SConfigOptionDescription{ - .value = "binds:allow_workspace_cycles", - .description = - "If enabled, workspaces don’t forget their previous workspace, so cycles can be created by switching to the first workspace in a sequence, then endlessly " - "going to the previous workspace.", - .type = CONFIG_OPTION_BOOL, - .data = SConfigOptionDescription::SBoolData{false}, - }, - SConfigOptionDescription{ - .value = "binds:workspace_center_on", - .description = "Whether switching workspaces should center the cursor on the workspace (0) or on the last active window for that workspace (1)", - .type = CONFIG_OPTION_INT, - .data = SConfigOptionDescription::SRangeData{0, 0, 1}, - }, - SConfigOptionDescription{ - .value = "binds:focus_preferred_method", - .description = "sets the preferred focus finding method when using focuswindow/movewindow/etc with a direction. 0 - history (recent have priority), 1 - length (longer " - "shared edges have priority)", - .type = CONFIG_OPTION_INT, - .data = SConfigOptionDescription::SRangeData{0, 0, 1}, - }, - SConfigOptionDescription{ - .value = "binds:ignore_group_lock", - .description = "If enabled, dispatchers like moveintogroup, moveoutofgroup and movewindoworgroup will ignore lock per group.", - .type = CONFIG_OPTION_BOOL, - .data = SConfigOptionDescription::SBoolData{false}, - }, - SConfigOptionDescription{ - .value = "binds:movefocus_cycles_fullscreen", - .description = "If enabled, when on a fullscreen window, movefocus will cycle fullscreen, if not, it will move the focus in a direction.", - .type = CONFIG_OPTION_BOOL, - .data = SConfigOptionDescription::SBoolData{false}, - }, - SConfigOptionDescription{ - .value = "binds:movefocus_cycles_groupfirst", - .description = - "If enabled, when in a grouped window, movefocus will cycle windows in the groups first, then at each ends of tabs, it'll move on to other windows/groups", - .type = CONFIG_OPTION_BOOL, - .data = SConfigOptionDescription::SBoolData{false}, - }, - SConfigOptionDescription{ - .value = "binds:disable_keybind_grabbing", - .description = "If enabled, apps that request keybinds to be disabled (e.g. VMs) will not be able to do so.", - .type = CONFIG_OPTION_BOOL, - .data = SConfigOptionDescription::SBoolData{false}, - }, - SConfigOptionDescription{ - .value = "binds:window_direction_monitor_fallback", - .description = "If enabled, moving a window or focus over the edge of a monitor with a direction will move it to the next monitor in that direction.", - .type = CONFIG_OPTION_BOOL, - .data = SConfigOptionDescription::SBoolData{true}, - }, - SConfigOptionDescription{ - .value = "binds:allow_pin_fullscreen", - .description = "Allows fullscreen to pinned windows, and restore their pinned status afterwards", - .type = CONFIG_OPTION_BOOL, - .data = SConfigOptionDescription::SBoolData{false}, - }, - SConfigOptionDescription{ - .value = "binds:drag_threshold", - .description = "Movement threshold in pixels for window dragging and c/g bind flags. 0 to disable and grab on mousedown.", - .type = CONFIG_OPTION_INT, - .data = SConfigOptionDescription::SRangeData{0, 0, INT_MAX}, - }, - - /* - * xwayland: - */ - - SConfigOptionDescription{ - .value = "xwayland:enabled", - .description = "allow running applications using X11", - .type = CONFIG_OPTION_BOOL, - .data = SConfigOptionDescription::SBoolData{true}, - }, - SConfigOptionDescription{ - .value = "xwayland:use_nearest_neighbor", - .description = "uses the nearest neighbor filtering for xwayland apps, making them pixelated rather than blurry", - .type = CONFIG_OPTION_BOOL, - .data = SConfigOptionDescription::SBoolData{true}, - }, - SConfigOptionDescription{ - .value = "xwayland:force_zero_scaling", - .description = "forces a scale of 1 on xwayland windows on scaled displays.", - .type = CONFIG_OPTION_BOOL, - .data = SConfigOptionDescription::SBoolData{false}, - }, - SConfigOptionDescription{ - .value = "xwayland:create_abstract_socket", - .description = "Create the abstract Unix domain socket for XWayland", - .type = CONFIG_OPTION_BOOL, - .data = SConfigOptionDescription::SBoolData{false}, - }, - - /* - * opengl: - */ - - SConfigOptionDescription{ - .value = "opengl:nvidia_anti_flicker", - .description = "reduces flickering on nvidia at the cost of possible frame drops on lower-end GPUs. On non-nvidia, this is ignored.", - .type = CONFIG_OPTION_BOOL, - .data = SConfigOptionDescription::SBoolData{true}, - }, - - /* - * render: - */ - - SConfigOptionDescription{ - .value = "render:direct_scanout", - .description = "Enables direct scanout. Direct scanout attempts to reduce lag when there is only one fullscreen application on a screen (e.g. game). It is also " - "recommended to set this to false if the fullscreen application shows graphical glitches. 0 - off, 1 - on, 2 - auto (on with content type 'game')", - .type = CONFIG_OPTION_INT, - .data = SConfigOptionDescription::SRangeData{.value = 0, .min = 0, .max = 2}, - }, - SConfigOptionDescription{ - .value = "render:expand_undersized_textures", - .description = "Whether to expand textures that have not yet resized to be larger, or to just stretch them instead.", - .type = CONFIG_OPTION_BOOL, - .data = SConfigOptionDescription::SBoolData{true}, - }, - SConfigOptionDescription{ - .value = "render:xp_mode", - .description = "Disable back buffer and bottom layer rendering.", - .type = CONFIG_OPTION_BOOL, - .data = SConfigOptionDescription::SBoolData{true}, - }, - SConfigOptionDescription{ - .value = "render:ctm_animation", - .description = "Whether to enable a fade animation for CTM changes (hyprsunset). 2 means 'auto' (Yes on everything but Nvidia).", - .type = CONFIG_OPTION_INT, - .data = SConfigOptionDescription::SRangeData{2, 0, 2}, - }, - SConfigOptionDescription{ - .value = "render:cm_enabled", - .description = "Enable Color Management pipelines (requires restart to fully take effect)", - .type = CONFIG_OPTION_BOOL, - .data = SConfigOptionDescription::SBoolData{true}, - }, - SConfigOptionDescription{ - .value = "render:send_content_type", - .description = "Report content type to allow monitor profile autoswitch (may result in a black screen during the switch)", - .type = CONFIG_OPTION_BOOL, - .data = SConfigOptionDescription::SBoolData{true}, - }, - SConfigOptionDescription{ - .value = "render:cm_auto_hdr", - .description = "Auto-switch to hdr mode when fullscreen app is in hdr, 0 - off, 1 - hdr, 2 - hdredid", - .type = CONFIG_OPTION_INT, - .data = SConfigOptionDescription::SRangeData{.value = 1, .min = 0, .max = 2}, - }, - SConfigOptionDescription{ - .value = "render:new_render_scheduling", - .description = "enable new render scheduling, which should improve FPS on underpowered devices. This does not add latency when your PC can keep up.", - .type = CONFIG_OPTION_BOOL, - .data = SConfigOptionDescription::SBoolData{false}, - }, - SConfigOptionDescription{ - .value = "render:non_shader_cm", - .description = "Enable CM without shader. 0 - disable, 1 - whenever possible, 2 - DS and passthrough only, 3 - disable and ignore CM issues", - .type = CONFIG_OPTION_CHOICE, - .data = SConfigOptionDescription::SChoiceData{0, "disable,always,ondemand,ignore"}, - }, - SConfigOptionDescription{ - .value = "render:non_shader_cm_interop", - .description = "non_shader_cm interaction with ctm proto (hyprsunset and similar). 0 - disable, 1 - enable, 2 - auto (enabled for unknown content type)", - .type = CONFIG_OPTION_CHOICE, - .data = SConfigOptionDescription::SRangeData{.value = 2, .min = 0, .max = 2}, - }, - SConfigOptionDescription{ - .value = "render:cm_sdr_eotf", - .description = - "Default transfer function for displaying SDR apps. default - Use default value (Gamma 2.2), gamma22 - Treat unspecified as Gamma 2.2, gamma22force - Treat " - "unspecified and sRGB as Gamma 2.2, srgb - Treat unspecified as sRGB", - .type = CONFIG_OPTION_STRING_SHORT, - .data = SConfigOptionDescription::SStringData{"default"}, - }, - SConfigOptionDescription{ - .value = "render:commit_timing_enabled", - .description = "Enable commit timing proto. Requires restart", - .type = CONFIG_OPTION_BOOL, - .data = SConfigOptionDescription::SBoolData{true}, - }, - SConfigOptionDescription{ - .value = "render:icc_vcgt_enabled", - .description = "Enable sending VCGT ramps to KMS with ICC profiles", - .type = CONFIG_OPTION_BOOL, - .data = SConfigOptionDescription::SBoolData{true}, - }, - { - .value = "render:use_shader_blur_blend", - .description = "Use experimental blurred bg blending", - .type = CONFIG_OPTION_BOOL, - .data = SConfigOptionDescription::SBoolData{false}, - }, - { - .value = "render:use_fp16", - .description = "Use experimental internal FP16 buffer. 0 - disabled, 1 - on, 2 - auto (enabled in HDR mode)", - .type = CONFIG_OPTION_INT, - .data = SConfigOptionDescription::SRangeData{.value = 2, .min = 0, .max = 2}, - }, - { - .value = "render:keep_unmodified_copy", - .description = "Keep umodified SDR frame copy for sreensharing. 0 - disabled, 1 - on, 2 - auto (enabled in HDR with SDR modifiers)", - .type = CONFIG_OPTION_INT, - .data = SConfigOptionDescription::SRangeData{.value = 2, .min = 0, .max = 2}, - }, - - /* - * cursor: - */ - - SConfigOptionDescription{ - .value = "cursor:invisible", - .description = "don't render cursors", - .type = CONFIG_OPTION_BOOL, - .data = SConfigOptionDescription::SBoolData{false}, - }, - SConfigOptionDescription{ - .value = "cursor:no_hardware_cursors", - .description = "disables hardware cursors. Auto = disable when multi-gpu on nvidia", - .type = CONFIG_OPTION_CHOICE, - .data = SConfigOptionDescription::SChoiceData{0, "Disabled,Enabled,Auto"}, - }, - SConfigOptionDescription{ - .value = "cursor:no_break_fs_vrr", - .description = - "disables scheduling new frames on cursor movement for fullscreen apps with VRR enabled to avoid framerate spikes (may require no_hardware_cursors = true) " - "0 - off, 1 - on, 2 - auto (on with content type 'game')", - .type = CONFIG_OPTION_INT, - .data = SConfigOptionDescription::SRangeData{.value = 2, .min = 0, .max = 2}, - }, - SConfigOptionDescription{ - .value = "cursor:min_refresh_rate", - .description = "minimum refresh rate for cursor movement when no_break_fs_vrr is active. Set to minimum supported refresh rate or higher", - .type = CONFIG_OPTION_INT, - .data = SConfigOptionDescription::SRangeData{24, 10, 500}, - }, - SConfigOptionDescription{ - .value = "cursor:hotspot_padding", - .description = "the padding, in logical px, between screen edges and the cursor", - .type = CONFIG_OPTION_INT, - .data = SConfigOptionDescription::SRangeData{1, 0, 20}, - }, - SConfigOptionDescription{ - .value = "cursor:inactive_timeout", - .description = "in seconds, after how many seconds of cursor’s inactivity to hide it. Set to 0 for never.", - .type = CONFIG_OPTION_INT, - .data = SConfigOptionDescription::SRangeData{0, 0, 20}, - }, - SConfigOptionDescription{ - .value = "cursor:no_warps", - .description = "if true, will not warp the cursor in many cases (focusing, keybinds, etc)", - .type = CONFIG_OPTION_BOOL, - .data = SConfigOptionDescription::SBoolData{false}, - }, - SConfigOptionDescription{ - .value = "cursor:persistent_warps", - .description = "When a window is refocused, the cursor returns to its last position relative to that window, rather than to the centre.", - .type = CONFIG_OPTION_BOOL, - .data = SConfigOptionDescription::SBoolData{false}, - }, - SConfigOptionDescription{ - .value = "cursor:warp_on_change_workspace", - .description = - "Move the cursor to the last focused window after changing the workspace. Options: 0 (Disabled), 1 (Enabled), 2 (Force - ignores cursor:no_warps option)", - .type = CONFIG_OPTION_CHOICE, - .data = SConfigOptionDescription::SChoiceData{0, "Disabled,Enabled,Force"}, - }, - SConfigOptionDescription{ - .value = "cursor:warp_on_toggle_special", - .description = "Move the cursor to the last focused window when toggling a special workspace. Options: 0 (Disabled), 1 (Enabled), " - "2 (Force - ignores cursor:no_warps option)", - .type = CONFIG_OPTION_CHOICE, - .data = SConfigOptionDescription::SChoiceData{0, "Disabled,Enabled,Force"}, - }, - SConfigOptionDescription{ - .value = "cursor:default_monitor", - .description = "the name of a default monitor for the cursor to be set to on startup (see hyprctl monitors for names)", - .type = CONFIG_OPTION_STRING_SHORT, - .data = SConfigOptionDescription::SStringData{""}, //##TODO UNSET? - }, - SConfigOptionDescription{ - .value = "cursor:zoom_factor", - .description = "the factor to zoom by around the cursor. Like a magnifying glass. Minimum 1.0 (meaning no zoom)", - .type = CONFIG_OPTION_FLOAT, - .data = SConfigOptionDescription::SFloatData{1, 1, 10}, - }, - SConfigOptionDescription{ - .value = "cursor:zoom_rigid", - .description = "whether the zoom should follow the cursor rigidly (cursor is always centered if it can be) or loosely", - .type = CONFIG_OPTION_BOOL, - .data = SConfigOptionDescription::SBoolData{false}, - }, - SConfigOptionDescription{ - .value = "cursor:zoom_disable_aa", - .description = "If enabled, when zooming, no antialiasing will be used (zoom will be pixelated)", - .type = CONFIG_OPTION_BOOL, - .data = SConfigOptionDescription::SBoolData{false}, - }, - SConfigOptionDescription{ - .value = "cursor:zoom_detached_camera", - .description = "Detaches the camera from the mouse when zoomed in, only ever moving to keep the mouse in view", - .type = CONFIG_OPTION_BOOL, - .data = SConfigOptionDescription::SBoolData{true}, - }, - SConfigOptionDescription{ - .value = "cursor:enable_hyprcursor", - .description = "whether to enable hyprcursor support", - .type = CONFIG_OPTION_BOOL, - .data = SConfigOptionDescription::SBoolData{true}, - }, - SConfigOptionDescription{ - .value = "cursor:hide_on_key_press", - .description = "Hides the cursor when you press any key until the mouse is moved.", - .type = CONFIG_OPTION_BOOL, - .data = SConfigOptionDescription::SBoolData{false}, - }, - SConfigOptionDescription{ - .value = "cursor:hide_on_touch", - .description = "Hides the cursor when the last input was a touch input until a mouse input is done.", - .type = CONFIG_OPTION_BOOL, - .data = SConfigOptionDescription::SBoolData{true}, - }, - SConfigOptionDescription{ - .value = "cursor:hide_on_tablet", - .description = "Hides the cursor when the last input was a tablet input until a mouse input is done.", - .type = CONFIG_OPTION_BOOL, - .data = SConfigOptionDescription::SBoolData{true}, - }, - SConfigOptionDescription{ - .value = "cursor:use_cpu_buffer", - .description = "Makes HW cursors use a CPU buffer. Required on Nvidia to have HW cursors. 0 - off, 1 - on, 2 - auto (nvidia only)", - .type = CONFIG_OPTION_INT, - .data = SConfigOptionDescription::SRangeData{.value = 2, .min = 0, .max = 2}, - }, - SConfigOptionDescription{ - .value = "cursor:sync_gsettings_theme", - .description = - "sync xcursor theme with gsettings, it applies cursor-theme and cursor-size on theme load to gsettings making most CSD gtk based clients use same xcursor " - "theme and size.", - .type = CONFIG_OPTION_BOOL, - .data = SConfigOptionDescription::SBoolData{true}, - }, - SConfigOptionDescription{ - .value = "cursor:warp_back_after_non_mouse_input", - .description = "warp the cursor back to where it was after using a non-mouse input to move it, and then returning back to mouse.", - .type = CONFIG_OPTION_BOOL, - .data = SConfigOptionDescription::SBoolData{false}, - }, - - /* - * ecosystem: - */ - SConfigOptionDescription{ - .value = "ecosystem:no_update_news", - .description = "disable the popup that shows up when you update hyprland to a new version.", - .type = CONFIG_OPTION_BOOL, - .data = SConfigOptionDescription::SBoolData{false}, - }, - SConfigOptionDescription{ - .value = "ecosystem:no_donation_nag", - .description = "disable the popup that shows up twice a year encouraging to donate.", - .type = CONFIG_OPTION_BOOL, - .data = SConfigOptionDescription::SBoolData{false}, - }, - SConfigOptionDescription{ - .value = "ecosystem:enforce_permissions", - .description = "whether to enable permission control (see https://wiki.hypr.land/Configuring/Permissions/).", - .type = CONFIG_OPTION_BOOL, - .data = SConfigOptionDescription::SBoolData{false}, - }, - - /* - * debug: - */ - - SConfigOptionDescription{ - .value = "debug:overlay", - .description = "print the debug performance overlay. Disable VFR for accurate results.", - .type = CONFIG_OPTION_BOOL, - .data = SConfigOptionDescription::SBoolData{false}, - }, - SConfigOptionDescription{ - .value = "debug:damage_blink", - .description = "disable logging to a file", - .type = CONFIG_OPTION_BOOL, - .data = SConfigOptionDescription::SBoolData{false}, - }, - SConfigOptionDescription{ - .value = "debug:vfr", - .description = "controls the VFR status of Hyprland. Do not turn off unless debugging.", - .type = CONFIG_OPTION_BOOL, - .data = SConfigOptionDescription::SBoolData{true}, - }, - SConfigOptionDescription{ - .value = "debug:gl_debugging", - .description = "enable OpenGL debugging and error checking, they hurt performance.", - .type = CONFIG_OPTION_BOOL, - .data = SConfigOptionDescription::SBoolData{false}, - }, - SConfigOptionDescription{ - .value = "debug:disable_logs", - .description = "disable logging to a file", - .type = CONFIG_OPTION_BOOL, - .data = SConfigOptionDescription::SBoolData{true}, - }, - SConfigOptionDescription{ - .value = "debug:disable_time", - .description = "disables time logging", - .type = CONFIG_OPTION_BOOL, - .data = SConfigOptionDescription::SBoolData{true}, - }, - SConfigOptionDescription{ - .value = "debug:damage_tracking", - .description = "redraw only the needed bits of the display. Do not change. (default: full - 2) monitor - 1, none - 0", - .type = CONFIG_OPTION_INT, - .data = SConfigOptionDescription::SRangeData{2, 0, 2}, - }, - SConfigOptionDescription{ - .value = "debug:enable_stdout_logs", - .description = "enables logging to stdout", - .type = CONFIG_OPTION_BOOL, - .data = SConfigOptionDescription::SBoolData{false}, - }, - SConfigOptionDescription{ - .value = "debug:manual_crash", - .description = "set to 1 and then back to 0 to crash Hyprland.", - .type = CONFIG_OPTION_INT, - .data = SConfigOptionDescription::SRangeData{0, 0, 1}, - }, - SConfigOptionDescription{ - .value = "debug:suppress_errors", - .description = "if true, do not display config file parsing errors.", - .type = CONFIG_OPTION_BOOL, - .data = SConfigOptionDescription::SBoolData{false}, - }, - SConfigOptionDescription{ - .value = "debug:disable_scale_checks", - .description = "disables verification of the scale factors. Will result in pixel alignment and rounding errors.", - .type = CONFIG_OPTION_BOOL, - .data = SConfigOptionDescription::SBoolData{false}, - }, - SConfigOptionDescription{ - .value = "debug:error_limit", - .description = "limits the number of displayed config file parsing errors.", - .type = CONFIG_OPTION_INT, - .data = SConfigOptionDescription::SRangeData{5, 0, 20}, - }, - SConfigOptionDescription{ - .value = "debug:error_position", - .description = "sets the position of the error bar. top - 0, bottom - 1", - .type = CONFIG_OPTION_INT, - .data = SConfigOptionDescription::SRangeData{0, 0, 1}, - }, - SConfigOptionDescription{ - .value = "debug:colored_stdout_logs", - .description = "enables colors in the stdout logs.", - .type = CONFIG_OPTION_BOOL, - .data = SConfigOptionDescription::SBoolData{true}, - }, - SConfigOptionDescription{ - .value = "debug:log_damage", - .description = "enables logging the damage.", - .type = CONFIG_OPTION_BOOL, - .data = SConfigOptionDescription::SBoolData{false}, - }, - SConfigOptionDescription{ - .value = "debug:pass", - .description = "enables render pass debugging.", - .type = CONFIG_OPTION_BOOL, - .data = SConfigOptionDescription::SBoolData{false}, - }, - SConfigOptionDescription{ - .value = "debug:full_cm_proto", - .description = "claims support for all cm proto features (requires restart)", - .type = CONFIG_OPTION_BOOL, - .data = SConfigOptionDescription::SBoolData{false}, - }, - SConfigOptionDescription{ - .value = "debug:ds_handle_same_buffer", - .description = "Special case for DS with unmodified buffer", - .type = CONFIG_OPTION_BOOL, - .data = SConfigOptionDescription::SBoolData{true}, - }, - SConfigOptionDescription{ - .value = "debug:ds_handle_same_buffer_fifo", - .description = "Special case for DS with unmodified buffer unlocks fifo", - .type = CONFIG_OPTION_BOOL, - .data = SConfigOptionDescription::SBoolData{true}, - }, - SConfigOptionDescription{ - .value = "debug:fifo_pending_workaround", - .description = "Fifo workaround for empty pending list", - .type = CONFIG_OPTION_BOOL, - .data = SConfigOptionDescription::SBoolData{false}, - }, - SConfigOptionDescription{ - .value = "debug:render_solitary_wo_damage", - .description = "Render solitary window with empty damage", - .type = CONFIG_OPTION_BOOL, - .data = SConfigOptionDescription::SBoolData{false}, - }, - SConfigOptionDescription{ - .value = "debug:invalidate_fp16", - .description = "Allow fp16 buffer invalidation. 0 - disable, 1 - enabled, 2 - disable on nvidia", - .type = CONFIG_OPTION_INT, - .data = SConfigOptionDescription::SRangeData{.value = 2, .min = 0, .max = 2}, - }, - - /* - * layout: - */ - SConfigOptionDescription{ - .value = "layout:single_window_aspect_ratio", - .description = "If specified, whenever only a single window is open, it will be coerced into the specified aspect ratio. Ignored if the y-value is zero.", - .type = CONFIG_OPTION_VECTOR, - .data = SConfigOptionDescription::SVectorData{{0, 0}, {0, 0}, {1000., 1000.}}, - }, - SConfigOptionDescription{ - .value = "layout:single_window_aspect_ratio_tolerance", - .description = "Minimum distance for single_window_aspect_ratio to take effect, in fractions of the monitor's size.", - .type = CONFIG_OPTION_FLOAT, - .data = SConfigOptionDescription::SFloatData{0.1f, 0.f, 1.f}, - }, - - /* - * dwindle: - */ - - SConfigOptionDescription{ - .value = "dwindle:pseudotile", - .description = "enable pseudotiling. Pseudotiled windows retain their floating size when tiled.", - .type = CONFIG_OPTION_BOOL, - .data = SConfigOptionDescription::SBoolData{false}, - }, - SConfigOptionDescription{ - .value = "dwindle:force_split", - .description = "0 -> split follows mouse, 1 -> always split to the left (new = left or top) 2 -> always split to the right (new = right or bottom)", - .type = CONFIG_OPTION_CHOICE, - .data = SConfigOptionDescription::SChoiceData{0, "follow mouse,left or top,right or bottom"}, - }, - SConfigOptionDescription{ - .value = "dwindle:preserve_split", - .description = "if enabled, the split (side/top) will not change regardless of what happens to the container.", - .type = CONFIG_OPTION_BOOL, - .data = SConfigOptionDescription::SBoolData{false}, - }, - SConfigOptionDescription{ - .value = "dwindle:smart_split", - .description = "if enabled, allows a more precise control over the window split direction based on the cursor's position. The window is conceptually divided into four " - "triangles, and cursor's triangle determines the split direction. This feature also turns on preserve_split.", - .type = CONFIG_OPTION_BOOL, - .data = SConfigOptionDescription::SBoolData{false}, - }, - SConfigOptionDescription{ - .value = "dwindle:smart_resizing", - .description = "if enabled, resizing direction will be determined by the mouse's position on the window (nearest to which corner). Else, it is based on the window's " - "tiling position.", - .type = CONFIG_OPTION_BOOL, - .data = SConfigOptionDescription::SBoolData{true}, - }, - SConfigOptionDescription{ - .value = "dwindle:permanent_direction_override", - .description = - "if enabled, makes the preselect direction persist until either this mode is turned off, another direction is specified, or a non-direction is specified " - "(anything other than l,r,u/t,d/b)", - .type = CONFIG_OPTION_BOOL, - .data = SConfigOptionDescription::SBoolData{false}, - }, - SConfigOptionDescription{ - .value = "dwindle:special_scale_factor", - .description = "specifies the scale factor of windows on the special workspace [0 - 1]", - .type = CONFIG_OPTION_FLOAT, - .data = SConfigOptionDescription::SFloatData{1, 0, 1}, - }, - SConfigOptionDescription{ - .value = "dwindle:split_width_multiplier", - .description = "specifies the auto-split width multiplier", - .type = CONFIG_OPTION_FLOAT, - .data = SConfigOptionDescription::SFloatData{1, 0.1, 3}, - }, - SConfigOptionDescription{ - .value = "dwindle:use_active_for_splits", - .description = "whether to prefer the active window or the mouse position for splits", - .type = CONFIG_OPTION_BOOL, - .data = SConfigOptionDescription::SBoolData{true}, - }, - SConfigOptionDescription{ - .value = "dwindle:default_split_ratio", - .description = "the default split ratio on window open. 1 means even 50/50 split. [0.1 - 1.9]", - .type = CONFIG_OPTION_FLOAT, - .data = SConfigOptionDescription::SFloatData{1, 0.1, 1.9}, - }, - SConfigOptionDescription{ - .value = "dwindle:split_bias", - .description = "specifies which window will receive the split ratio. 0 -> directional (the top or left window), 1 -> the current window", - .type = CONFIG_OPTION_CHOICE, - .data = SConfigOptionDescription::SChoiceData{0, "directional,current"}, - }, - SConfigOptionDescription{ - .value = "dwindle:precise_mouse_move", - .description = "if enabled, bindm movewindow will drop the window more precisely depending on where your mouse is. This feature also turns on preserve_split.", - .type = CONFIG_OPTION_BOOL, - .data = SConfigOptionDescription::SBoolData{false}, - }, - - /* - * master: - */ - - SConfigOptionDescription{ - .value = "master:allow_small_split", - .description = "enable adding additional master windows in a horizontal split style", - .type = CONFIG_OPTION_BOOL, - .data = SConfigOptionDescription::SBoolData{false}, - }, - SConfigOptionDescription{ - .value = "master:special_scale_factor", - .description = "the scale of the special workspace windows. [0.0 - 1.0]", - .type = CONFIG_OPTION_FLOAT, - .data = SConfigOptionDescription::SFloatData{1, 0, 1}, - }, - SConfigOptionDescription{ - .value = "master:mfact", - .description = - "the size as a percentage of the master window, for example `mfact = 0.70` would mean 70% of the screen will be the master window, and 30% the slave [0.0 - 1.0]", - .type = CONFIG_OPTION_FLOAT, - .data = SConfigOptionDescription::SFloatData{0.55, 0, 1}, - }, - SConfigOptionDescription{ - .value = "master:new_status", - .description = "`master`: new window becomes master; `slave`: new windows are added to slave stack; `inherit`: inherit from focused window", - .type = CONFIG_OPTION_STRING_SHORT, - .data = SConfigOptionDescription::SStringData{"slave"}, - }, - SConfigOptionDescription{ - .value = "master:new_on_top", - .description = "whether a newly open window should be on the top of the stack", - .type = CONFIG_OPTION_BOOL, - .data = SConfigOptionDescription::SBoolData{false}, - }, - SConfigOptionDescription{ - .value = "master:new_on_active", - .description = "`before`, `after`: place new window relative to the focused window; `none`: place new window according to the value of `new_on_top`. ", - .type = CONFIG_OPTION_STRING_SHORT, - .data = SConfigOptionDescription::SStringData{"none"}, - }, - SConfigOptionDescription{ - .value = "master:orientation", - .description = "default placement of the master area, can be left, right, top, bottom or center", - .type = CONFIG_OPTION_STRING_SHORT, - .data = SConfigOptionDescription::SStringData{"left"}, - }, - SConfigOptionDescription{ - .value = "master:slave_count_for_center_master", - .description = "when using orientation=center, make the master window centered only when at least this many slave windows are open. (Set 0 to always_center_master)", - .type = CONFIG_OPTION_INT, - .data = SConfigOptionDescription::SRangeData{2, 0, 10}, //##TODO RANGE? - }, - SConfigOptionDescription{.value = "master:center_master_fallback", - .description = "Set fallback for center master when slaves are less than slave_count_for_center_master, can be left ,right ,top ,bottom", - .type = CONFIG_OPTION_STRING_SHORT, - .data = SConfigOptionDescription::SStringData{"left"}}, - SConfigOptionDescription{ - .value = "master:center_ignores_reserved", - .description = "centers the master window on monitor ignoring reserved areas", - .type = CONFIG_OPTION_BOOL, - .data = SConfigOptionDescription::SBoolData{false}, - }, - SConfigOptionDescription{ - .value = "master:smart_resizing", - .description = "if enabled, resizing direction will be determined by the mouse's position on the window (nearest to which corner). Else, it is based on the window's " - "tiling position.", - .type = CONFIG_OPTION_BOOL, - .data = SConfigOptionDescription::SBoolData{true}, - }, - SConfigOptionDescription{ - .value = "master:drop_at_cursor", - .description = "when enabled, dragging and dropping windows will put them at the cursor position. Otherwise, when dropped at the stack side, they will go to the " - "top/bottom of the stack depending on new_on_top.", - .type = CONFIG_OPTION_BOOL, - .data = SConfigOptionDescription::SBoolData{true}, - }, - SConfigOptionDescription{ - .value = "master:always_keep_position", - .description = "whether to keep the master window in its configured position when there are no slave windows", - .type = CONFIG_OPTION_BOOL, - .data = SConfigOptionDescription::SBoolData{false}, - }, - - /* - * scrolling: - */ - - SConfigOptionDescription{ - .value = "scrolling:fullscreen_on_one_column", - .description = "when enabled, a single column on a workspace will always span the entire screen.", - .type = CONFIG_OPTION_BOOL, - .data = SConfigOptionDescription::SBoolData{true}, - }, - SConfigOptionDescription{ - .value = "scrolling:column_width", - .description = "the default width of a column, [0.1 - 1.0].", - .type = CONFIG_OPTION_FLOAT, - .data = SConfigOptionDescription::SFloatData{.value = 0.5, .min = 0.1, .max = 1.0}, - }, - SConfigOptionDescription{ - .value = "scrolling:focus_fit_method", - .description = "When a column is focused, what method should be used to bring it into view", - .type = CONFIG_OPTION_CHOICE, - .data = SConfigOptionDescription::SChoiceData{.firstIndex = 0, .choices = "center,fit"}, - }, - SConfigOptionDescription{ - .value = "scrolling:follow_focus", - .description = "when a window is focused, should the layout move to bring it into view automatically", - .type = CONFIG_OPTION_BOOL, - .data = SConfigOptionDescription::SBoolData{.value = true}, - }, - SConfigOptionDescription{ - .value = "scrolling:follow_min_visible", - .description = "when a window is focused, require that at least a given fraction of it is visible for focus to follow", - .type = CONFIG_OPTION_FLOAT, - .data = SConfigOptionDescription::SFloatData{.value = 0.4, .min = 0.0, .max = 1.0}, - }, - SConfigOptionDescription{ - .value = "scrolling:explicit_column_widths", - .description = "A comma-separated list of preconfigured widths for colresize +conf/-conf", - .type = CONFIG_OPTION_STRING_SHORT, - .data = SConfigOptionDescription::SStringData{"0.333, 0.5, 0.667, 1.0"}, - }, - SConfigOptionDescription{ - .value = "scrolling:direction", - .description = "Direction in which new windows appear and the layout scrolls", - .type = CONFIG_OPTION_CHOICE, - .data = SConfigOptionDescription::SChoiceData{.firstIndex = 0, .choices = "right,left,down,up"}, - }, - SConfigOptionDescription{ - .value = "scrolling:wrap_focus", - .description = "Determines if column focus wraps around when going before the first column or past the last column", - .type = CONFIG_OPTION_BOOL, - .data = SConfigOptionDescription::SBoolData{.value = true}, - }, - SConfigOptionDescription{ - .value = "scrolling:wrap_swapcol", - .description = "Determines if column movement wraps around when moving to before the first column or past the last column", - .type = CONFIG_OPTION_BOOL, - .data = SConfigOptionDescription::SBoolData{.value = true}, - }, - - /* - * Experimental - */ - - SConfigOptionDescription{ - .value = "experimental:wp_cm_1_2", - .description = "Allow wp-cm-v1 version 2", - .type = CONFIG_OPTION_BOOL, - .data = SConfigOptionDescription::SBoolData{false}, - }, - - /* - * Quirks - */ - - SConfigOptionDescription{ - .value = "quirks:prefer_hdr", - .description = "Prefer HDR mode. 0 - off, 1 - always, 2 - gamescope only", - .type = CONFIG_OPTION_INT, - .data = SConfigOptionDescription::SRangeData{.value = 0, .min = 0, .max = 2}, - }, - SConfigOptionDescription{ - .value = "quirks:skip_non_kms_dmabuf_formats", - .description = "Do not report dmabuf formats which cannot be imported into KMS", - .type = CONFIG_OPTION_BOOL, - .data = SConfigOptionDescription::SBoolData{true}, - }, - - }; -} \ No newline at end of file diff --git a/src/config/supplementary/executor/Executor.cpp b/src/config/supplementary/executor/Executor.cpp index 8c04df819..b5d3a0215 100644 --- a/src/config/supplementary/executor/Executor.cpp +++ b/src/config/supplementary/executor/Executor.cpp @@ -37,7 +37,14 @@ CExecutor::CExecutor() { m_firstExecDispatched = true; for (auto const& c : m_execOnce) { - c.withRules ? spawn(c.exec) : spawnRaw(c.exec); + auto res = c.withRules ? spawn(c.exec) : spawnRaw(c.exec); + + if (!res || !c.rule) + continue; + + const auto TOKEN = g_pTokenManager->registerNewToken(nullptr, std::chrono::seconds(1)); + + applyRuleToProc(c.rule, *res, TOKEN); } m_execOnce.clear(); // free some kb of memory :P @@ -61,18 +68,39 @@ CExecutor::CExecutor() { }); } -void CExecutor::addExecOnce(const SExecRequest& cmd) { - m_execOnce.emplace_back(cmd); +void CExecutor::applyRuleToProc(SP rule, int64_t pid, const std::string& token) { + rule->markAsExecRule(token, pid, false /* TODO: could be nice. */); + rule->registerMatch(Desktop::Rule::RULE_PROP_EXEC_TOKEN, token); + rule->registerMatch(Desktop::Rule::RULE_PROP_EXEC_PID, std::to_string(pid)); + Desktop::Rule::ruleEngine()->registerRule(std::move(rule)); + Log::logger->log(Log::DEBUG, "Applied rule arguments for exec, pid {}.", pid); } -void CExecutor::addExecShutdown(const SExecRequest& cmd) { - m_execShutdown.emplace_back(cmd); +void CExecutor::addExecOnce(SExecRequest&& cmd) { + m_execOnce.emplace_back(std::move(cmd)); +} + +void CExecutor::addExecShutdown(SExecRequest&& cmd) { + m_execShutdown.emplace_back(std::move(cmd)); } std::optional CExecutor::spawn(const std::string& args) { return spawnWithRules(args); } +std::optional CExecutor::spawn(const SExecRequest& args) { + auto res = spawn(args.exec); + + if (!args.rule) + return res; + + const auto TOKEN = g_pTokenManager->registerNewToken(nullptr, std::chrono::seconds(1)); + + applyRuleToProc(args.rule, *res, TOKEN); + + return res; +} + std::optional CExecutor::spawnRaw(const std::string& args) { return spawnRawProc(args); } @@ -95,7 +123,11 @@ std::optional CExecutor::spawnWithRules(std::string args, PHLWORKSPACE std::string execToken = ""; if (!RULES.empty()) { - auto rule = Desktop::Rule::CWindowRule::buildFromExecString(std::move(RULES)); + auto rule = Desktop::Rule::CWindowRule::buildFromExecString(std::move(RULES)); + if (!rule) { + Log::logger->log(Log::ERR, "Failed to parse exec rule: {}", rule.error()); + return std::nullopt; + } const auto TOKEN = g_pTokenManager->registerNewToken(nullptr, std::chrono::seconds(1)); @@ -104,11 +136,7 @@ std::optional CExecutor::spawnWithRules(std::string args, PHLWORKSPACE if (!PROC) return std::nullopt; - rule->markAsExecRule(TOKEN, *PROC, false /* TODO: could be nice. */); - rule->registerMatch(Desktop::Rule::RULE_PROP_EXEC_TOKEN, TOKEN); - rule->registerMatch(Desktop::Rule::RULE_PROP_EXEC_PID, std::to_string(*PROC)); - Desktop::Rule::ruleEngine()->registerRule(std::move(rule)); - Log::logger->log(Log::DEBUG, "Applied rule arguments for exec, pid {}.", *PROC); + applyRuleToProc(*rule, *PROC, TOKEN); return PROC; } @@ -117,7 +145,7 @@ std::optional CExecutor::spawnWithRules(std::string args, PHLWORKSPACE } static std::vector> getHyprlandLaunchEnv(PHLWORKSPACE pInitialWorkspace) { - static auto PINITIALWSTRACKING = CConfigValue("misc:initial_workspace_tracking"); + static auto PINITIALWSTRACKING = CConfigValue("misc:initial_workspace_tracking"); if (!*PINITIALWSTRACKING) return {}; diff --git a/src/config/supplementary/executor/Executor.hpp b/src/config/supplementary/executor/Executor.hpp index cc5f94a2a..899a2e6ea 100644 --- a/src/config/supplementary/executor/Executor.hpp +++ b/src/config/supplementary/executor/Executor.hpp @@ -7,11 +7,17 @@ #include "../../../helpers/signal/Signal.hpp" #include "../../../helpers/memory/Memory.hpp" #include "../../../desktop/DesktopTypes.hpp" +#include "../../../desktop/rule/windowRule/WindowRule.hpp" namespace Config::Supplementary { struct SExecRequest { std::string exec = ""; bool withRules = false; + + // if this rule is passed, don't put any match: on it, executor will do it + // for you + // also dont set withRules + SP rule; }; class CExecutor { @@ -19,10 +25,11 @@ namespace Config::Supplementary { CExecutor(); ~CExecutor() = default; - void addExecOnce(const SExecRequest& cmd); - void addExecShutdown(const SExecRequest& cmd); + void addExecOnce(SExecRequest&& cmd); + void addExecShutdown(SExecRequest&& cmd); std::optional spawn(const std::string& args); + std::optional spawn(const SExecRequest& args); std::optional spawnRaw(const std::string& args); std::optional spawnRawProc(const std::string&, PHLWORKSPACE pInitialWorkspace = nullptr, const std::string& execRuleToken = ""); @@ -31,6 +38,8 @@ namespace Config::Supplementary { private: std::vector m_execOnce, m_execShutdown; + void applyRuleToProc(SP rule, int64_t pid, const std::string& token); + struct { CHyprSignalListener init; CHyprSignalListener shutdown; diff --git a/src/config/supplementary/jeremy/Jeremy.cpp b/src/config/supplementary/jeremy/Jeremy.cpp index 5d3378208..e8be29146 100644 --- a/src/config/supplementary/jeremy/Jeremy.cpp +++ b/src/config/supplementary/jeremy/Jeremy.cpp @@ -5,28 +5,40 @@ #include #include +using namespace Config; using namespace Config::Supplementary; +using namespace Config::Supplementary::Jeremy; -std::expected Config::Supplementary::Jeremy::getMainConfigPath() { +std::expected Jeremy::getMainConfigPath() { static bool lastSafeMode = g_pCompositor->m_safeMode; - static auto getCfgPath = []() -> std::expected { + + static auto regularOrLuaIfAvail = [](std::filesystem::path p) -> std::filesystem::path { + std::error_code ec; + auto p2 = p; + p2.replace_extension(".lua"); + if (std::filesystem::exists(p2, ec) && !ec) + return p2; + return p; + }; + + static auto getCfgPath = []() -> std::expected { lastSafeMode = g_pCompositor->m_safeMode; if (g_pCompositor->m_safeMode) - return (std::filesystem::path{g_pCompositor->m_instancePath} / "recoverycfg.conf").string(); + return SConfigStateReply{(std::filesystem::path{g_pCompositor->m_instancePath} / "recoverycfg.conf").string(), CONFIG_TYPE_SPECIAL}; if (!g_pCompositor->m_explicitConfigPath.empty()) - return g_pCompositor->m_explicitConfigPath; + return SConfigStateReply{g_pCompositor->m_explicitConfigPath, CONFIG_TYPE_EXPLICIT}; if (const auto CFG_ENV = getenv("HYPRLAND_CONFIG"); CFG_ENV) - return CFG_ENV; + return SConfigStateReply{CFG_ENV, CONFIG_TYPE_EXPLICIT}; const auto PATHS = Hyprutils::Path::findConfig(ISDEBUG ? "hyprlandd" : "hyprland"); if (PATHS.first.has_value()) { - return PATHS.first.value(); + return SConfigStateReply{regularOrLuaIfAvail(PATHS.first.value()), CONFIG_TYPE_REGULAR}; } else if (PATHS.second.has_value()) { auto CONFIGPATH = Hyprutils::Path::fullConfigPath(PATHS.second.value(), ISDEBUG ? "hyprlandd" : "hyprland"); - return CONFIGPATH; + return SConfigStateReply{regularOrLuaIfAvail(CONFIGPATH), CONFIG_TYPE_REGULAR}; } else return std::unexpected("Neither HOME nor XDG_CONFIG_HOME are set in the environment. Could not find config in XDG_CONFIG_DIRS or /etc/xdg."); }; diff --git a/src/config/supplementary/jeremy/Jeremy.hpp b/src/config/supplementary/jeremy/Jeremy.hpp index cd77d727c..5ea82a035 100644 --- a/src/config/supplementary/jeremy/Jeremy.hpp +++ b/src/config/supplementary/jeremy/Jeremy.hpp @@ -2,7 +2,20 @@ #include #include +#include namespace Config::Supplementary::Jeremy { - std::expected getMainConfigPath(); + enum eConfigType : uint8_t { + CONFIG_TYPE_REGULAR = 0, + CONFIG_TYPE_EXPLICIT, + CONFIG_TYPE_SPECIAL, + CONFIG_TYPE_ERR, + }; + + struct SConfigStateReply { + std::string path; + eConfigType type = CONFIG_TYPE_REGULAR; + }; + + std::expected getMainConfigPath(); }; \ No newline at end of file diff --git a/src/config/supplementary/propRefresher/PropRefresher.cpp b/src/config/supplementary/propRefresher/PropRefresher.cpp new file mode 100644 index 000000000..909624a07 --- /dev/null +++ b/src/config/supplementary/propRefresher/PropRefresher.cpp @@ -0,0 +1,90 @@ +#include "PropRefresher.hpp" + +#include "../../../managers/eventLoop/EventLoopManager.hpp" +#include "../../../managers/input/InputManager.hpp" +#include "../../../render/Renderer.hpp" +#include "../../../Compositor.hpp" +#include "../../../layout/supplementary/WorkspaceAlgoMatcher.hpp" +#include "../../../layout/LayoutManager.hpp" +#include "../../../desktop/rule/Engine.hpp" + +#include "../../shared/monitor/MonitorRuleManager.hpp" + +using namespace Config; +using namespace Config::Supplementary; + +UP& Supplementary::refresher() { + static UP p = makeUnique(); + return p; +} + +void CPropRefresher::scheduleRefresh(PropRefreshBits prop) { + m_propsTripped |= prop; + + if (!m_scheduled) { + g_pEventLoopManager->doLater([this, weak = WP{refresher()}] { + if (!weak) + return; + + if (m_propsTripped & REFRESH_INPUT_DEVICES) { + g_pInputManager->setKeyboardLayout(); // update kb layout + g_pInputManager->setPointerConfigs(); // update mouse cfgs + g_pInputManager->setTouchDeviceConfigs(); // update touch device cfgs + g_pInputManager->setTabletConfigs(); // update tablets + } + + if (m_propsTripped & REFRESH_SCREEN_SHADER) + g_pHyprRenderer->m_reloadScreenShader = true; + + if (m_propsTripped & REFRESH_BLUR_FB) { + for (auto const& m : g_pCompositor->m_monitors) { + if (m) + m->m_blurFBDirty = true; + } + } + + if (m_propsTripped & REFRESH_WINDOW_STATES) { + Desktop::Rule::ruleEngine()->updateAllRules(); + + for (const auto& ws : g_pCompositor->getWorkspaces()) { + if (!ws) + continue; + + ws->updateWindows(); + ws->updateWindowData(); + ws->updateWindowDecos(); + } + + g_pCompositor->updateAllWindowsAnimatedDecorationValues(); + } + + if (m_propsTripped & REFRESH_MONITOR_STATES) { + Config::monitorRuleMgr()->scheduleReload(); + Config::monitorRuleMgr()->ensureVRR(); + + for (const auto& m : g_pCompositor->m_monitors) { + if (!m) + continue; + + g_layoutManager->recalculateMonitor(m); + } + + g_pCompositor->ensurePersistentWorkspacesPresent(); + } + + if (m_propsTripped & REFRESH_LAYOUTS) { + Layout::Supplementary::algoMatcher()->updateWorkspaceLayouts(); + + for (auto const& m : g_pCompositor->m_monitors) { + g_layoutManager->recalculateMonitor(m); + g_pHyprRenderer->damageMonitor(m); + } + } + + m_scheduled = false; + m_propsTripped = 0; + }); + + m_scheduled = true; + } +} \ No newline at end of file diff --git a/src/config/supplementary/propRefresher/PropRefresher.hpp b/src/config/supplementary/propRefresher/PropRefresher.hpp new file mode 100644 index 000000000..563b33a2f --- /dev/null +++ b/src/config/supplementary/propRefresher/PropRefresher.hpp @@ -0,0 +1,31 @@ +#pragma once + +#include +#include + +#include "../../../helpers/memory/Memory.hpp" + +namespace Config::Supplementary { + enum ePropRefreshProp : uint8_t { + REFRESH_LAYOUTS = (1 << 0), + REFRESH_INPUT_DEVICES = (1 << 1), + REFRESH_SCREEN_SHADER = (1 << 2), + REFRESH_BLUR_FB = (1 << 3), + REFRESH_RULES = (1 << 4), + REFRESH_WINDOW_STATES = (1 << 5) | REFRESH_RULES, + REFRESH_MONITOR_STATES = (1 << 6) | REFRESH_LAYOUTS, + }; + + using PropRefreshBits = std::underlying_type_t; + + class CPropRefresher { + public: + void scheduleRefresh(PropRefreshBits reason); + + private: + bool m_scheduled = false; + PropRefreshBits m_propsTripped; + }; + + UP& refresher(); +}; \ No newline at end of file diff --git a/src/config/values/ConfigValues.cpp b/src/config/values/ConfigValues.cpp new file mode 100644 index 000000000..0214ba86c --- /dev/null +++ b/src/config/values/ConfigValues.cpp @@ -0,0 +1,686 @@ +#include "ConfigValues.hpp" + +using namespace Config; +using namespace Config::Values; + +template +static std::string opt(std::optional x) { + if (x) + return std::format("{}", x.value()); + return "null"; +} + +template <> +std::string opt(std::optional x) { + if (x) { + std::string json = "["; + for (const auto& [k, v] : *x) { + json += std::format("{{ \"{}\": {} }},", k, v); + } + if (!json.empty()) + json.pop_back(); + json += "]"; + return json; + } + return "null"; +} + +static std::string jsonify(SP v) { + + if (auto x = dc(v.get()); x) { + return std::format( + R"#( + {{ + "name": "{}", + "description": "{}", + "default": {}, + "current": {} + }},)#", + x->name(), x->description(), x->defaultVal(), x->value()); + } + + if (auto x = dc(v.get()); x) { + return std::format( + R"#( + {{ + "name": "{}", + "description": "{}", + "default": {}, + "current": {}, + "min": {}, + "max": {}, + "map": {} + }},)#", + x->name(), x->description(), x->defaultVal(), x->value(), opt(x->m_min), opt(x->m_max), opt(x->m_map)); + } + + if (auto x = dc(v.get()); x) { + return std::format( + R"#( + {{ + "name": "{}", + "description": "{}", + "default": {}, + "current": {}, + "min": {}, + "max": {} + }},)#", + x->name(), x->description(), x->defaultVal(), x->value(), opt(x->m_min), opt(x->m_max)); + } + + if (auto x = dc(v.get()); x) { + return std::format( + R"#( + {{ + "name": "{}", + "description": "{}", + "default": "{}", + "current": "{}", + "min": {}, + "max": {} + }},)#", + x->name(), x->description(), x->defaultVal().toString(), x->value().toString(), opt(x->m_min), opt(x->m_max)); + } + + if (auto x = dc(v.get()); x) { + return std::format( + R"#( + {{ + "name": "{}", + "description": "{}", + "default": "{}", + "current": "{}" + }},)#", + x->name(), x->description(), x->defaultVal().toString(), x->value().toString()); + } + + if (auto x = dc(v.get()); x) { + return std::format( + R"#( + {{ + "name": "{}", + "description": "{}", + "default": "{}", + "current": "{}" + }},)#", + x->name(), x->description(), x->defaultVal().toString(), x->value().toString()); + } + + if (auto x = dc(v.get()); x) { + return std::format( + R"#( + {{ + "name": "{}", + "description": "{}", + "default": "{}", + "current": "{}" + }},)#", + x->name(), x->description(), x->defaultVal(), x->value()); + } + + if (auto x = dc(v.get()); x) { + return std::format( + R"#( + {{ + "name": "{}", + "description": "{}", + "default": [{}, {}], + "current": [{}, {}] + }},)#", + x->name(), x->description(), x->defaultVal().x, x->defaultVal().y, x->value().x, x->value().y); + } + + if (auto x = dc(v.get()); x) { + return std::format( + R"#( + {{ + "name": "{}", + "description": "{}", + "default": "{:x}", + "current": "{:x}" + }},)#", + x->name(), x->description(), x->defaultVal(), x->value()); + } + + Log::logger->log(Log::ERR, "values/jsonify: invalid value {}", v->name()); + return "{},"; +} + +std::string Values::getAsJson() { + std::string json = "[\n"; + for (const auto& v : CONFIG_VALUES) { + json += jsonify(v); + } + json.pop_back(); + json += "\n]"; + return json; +} + +std::vector> Values::getConfigValues() { +#define MS makeConfigValue + + return std::vector>{ + + /* + * general: + */ + + MS("general:border_size", "size of the border around windows", 1, {.min = 0, .max = 20, .refresh = Supplementary::REFRESH_WINDOW_STATES}), + MS("general:gaps_in", "gaps between windows", 5, {.refresh = Supplementary::REFRESH_LAYOUTS}), + MS("general:gaps_out", "gaps between windows and monitor edges", 20, {.refresh = Supplementary::REFRESH_LAYOUTS}), + MS("general:float_gaps", "gaps between windows and monitor edges for floating windows", 0, {.refresh = Supplementary::REFRESH_LAYOUTS}), + MS("general:gaps_workspaces", "gaps between workspaces. Stacks with gaps_out.", 0, {.min = 0, .max = 100, .refresh = Supplementary::REFRESH_LAYOUTS}), + MS("general:col.inactive_border", "border color for inactive windows", CHyprColor{0xff444444}), + MS("general:col.active_border", "border color for the active window", CHyprColor{0xffffffff}), + MS("general:col.nogroup_border", "inactive border color for window that cannot be added to a group", CHyprColor{0xffffaaff}), + MS("general:col.nogroup_border_active", "active border color for window that cannot be added to a group", CHyprColor{0xffff00ff}), + MS("general:layout", "which layout to use. [dwindle/master]", "dwindle", {.refresh = Supplementary::REFRESH_LAYOUTS}), + MS("general:no_focus_fallback", "if true, will not fall back to the next available window when moving focus in a direction where no window was found", false), + MS("general:resize_on_border", "enables resizing windows by clicking and dragging on borders and gaps", false), + MS("general:extend_border_grab_area", "extends the area around the border where you can click and drag on, only used when general:resize_on_border is on.", 15, + {.min = 0, .max = 100}), + MS("general:hover_icon_on_border", "show a cursor icon when hovering over borders, only used when general:resize_on_border is on.", true), + MS("general:allow_tearing", "master switch for allowing tearing to occur.", false), + MS("general:resize_corner", "force floating windows to use a specific corner when being resized (1-4 going clockwise from top left, 0 to disable)", 0, + {.min = 0, .max = 4, .map = OptionMap{{"disable", 0}, {"top_left", 1}, {"top_right", 2}, {"bottom_right", 3}, {"bottom_left", 4}}}), + MS("general:snap:enabled", "enable snapping for floating windows", false), + MS("general:snap:window_gap", "minimum gap in pixels between windows before snapping", 10, {.min = 0, .max = 100}), + MS("general:snap:monitor_gap", "minimum gap in pixels between window and monitor edges before snapping", 10, {.min = 0, .max = 100}), + MS("general:snap:border_overlap", "if true, windows snap such that only one border's worth of space is between them", false), + MS("general:snap:respect_gaps", "if true, snapping will respect gaps between windows", false), + MS("general:modal_parent_blocking", "if true, parent windows of modals will not be interactive.", true), + MS("general:locale", "overrides the system locale", ""), + + /* + * decoration: + */ + + MS("decoration:rounding", "rounded corners' radius (in layout px)", 0, {.min = 0, .max = 20}), + MS("decoration:rounding_power", "rounding power of corners (2 is a circle)", 2, {.min = 2, .max = 10}), + MS("decoration:active_opacity", "opacity of active windows.", 1, {.min = 0, .max = 1}), + MS("decoration:inactive_opacity", "opacity of inactive windows.", 1, {.min = 0, .max = 1}), + MS("decoration:fullscreen_opacity", "opacity of fullscreen windows.", 1, {.min = 0, .max = 1}), + MS("decoration:shadow:enabled", "enable drop shadows on windows", true), + MS("decoration:shadow:range", "Shadow range (size) in layout px", 4, {.min = 0, .max = 100}), + MS("decoration:shadow:render_power", "in what power to render the falloff (more power, the faster the falloff)", 3, {.min = 1, .max = 4}), + MS("decoration:shadow:sharp", "whether the shadow should be sharp or not.", false), + MS("decoration:shadow:color", "shadow's color. Alpha dictates shadow's opacity.", 0xee1a1a1a), + MS("decoration:shadow:color_inactive", "inactive shadow color. (if not set, will fall back to col.shadow)", -1), + MS("decoration:shadow:offset", "shadow's rendering offset.", Config::VEC2{}, {.validator = vec2Range(-250, -250, 250, 250)}), + MS("decoration:shadow:scale", "shadow's scale.", 1, {.min = 0, .max = 1}), + MS("decoration:glow:enabled", "enable inner glow on windows", false), + MS("decoration:glow:range", "glow range (size) in layout px", 10, {.min = 0, .max = 100}), + MS("decoration:glow:render_power", "in what power to render the falloff (more power, the faster the falloff)", 3, {.min = 1, .max = 4}), + MS("decoration:glow:color", "glow's color. Alpha dictates glow's opacity.", 0xee33ccff), + MS("decoration:glow:color_inactive", "inactive glow color. (if not set, will fall back to decoration:glow:color)", 0x0033ccff), + MS("decoration:dim_modal", "enables dimming of parents of modal windows", true), + MS("decoration:dim_inactive", "enables dimming of inactive windows", false), + MS("decoration:dim_strength", "how much inactive windows should be dimmed", 0.5, {.min = 0, .max = 1}), + MS("decoration:dim_special", "how much to dim the rest of the screen by when a special workspace is open.", 0.2, {.min = 0, .max = 1}), + MS("decoration:dim_around", "how much the dimaround window rule should dim by.", 0.4, {.min = 0, .max = 1}), + MS("decoration:screen_shader", "a path to a custom shader to be applied at the end of rendering.", STRVAL_EMPTY, {.refresh = Supplementary::REFRESH_SCREEN_SHADER}), + MS("decoration:border_part_of_window", "whether the border should be treated as a part of the window.", true), + + /* + * blur: + */ + + MS("decoration:blur:enabled", "enable kawase window background blur", true), + MS("decoration:blur:size", "blur size (distance)", 8, {.min = 0, .max = 100}), + MS("decoration:blur:passes", "the amount of passes to perform", 1, {.min = 0, .max = 10}), + MS("decoration:blur:ignore_opacity", "make the blur layer ignore the opacity of the window", true), + MS("decoration:blur:new_optimizations", "whether to enable further optimizations to the blur.", true), + MS("decoration:blur:xray", "if enabled, floating windows will ignore tiled windows in their blur.", false), + MS("decoration:blur:noise", "how much noise to apply.", 0.0117, {.min = 0, .max = 1}), + MS("decoration:blur:contrast", "contrast modulation for blur.", 0.8916, {.min = 0, .max = 2}), + MS("decoration:blur:brightness", "brightness modulation for blur.", 1, {.min = 0, .max = 2}), + MS("decoration:blur:vibrancy", "Increase saturation of blurred colors.", 0.1696, {.min = 0, .max = 1}), + MS("decoration:blur:vibrancy_darkness", "How strong the effect of vibrancy is on dark areas.", 0, {.min = 0, .max = 1}), + MS("decoration:blur:special", "whether to blur behind the special workspace (note: expensive)", false), + MS("decoration:blur:popups", "whether to blur popups (e.g. right-click menus)", false), + MS("decoration:blur:popups_ignorealpha", "works like ignorealpha in layer rules. If pixel opacity is below set value, will not blur.", 0.2, {.min = 0, .max = 1}), + MS("decoration:blur:input_methods", "whether to blur input methods (e.g. fcitx5)", false), + MS("decoration:blur:input_methods_ignorealpha", "works like ignorealpha in layer rules. If pixel opacity is below set value, will not blur.", 0.2, + {.min = 0, .max = 1}), + + /* + * animations: + */ + + MS("animations:enabled", "enable animations", true), + MS("animations:workspace_wraparound", "changes the direction of slide animations between the first and last workspaces", false), + + /* + * input: + */ + + MS("input:kb_model", "Appropriate XKB keymap parameter.", STRVAL_EMPTY, {.refresh = Supplementary::REFRESH_INPUT_DEVICES}), + MS("input:kb_layout", "Appropriate XKB keymap parameter", "us", {.refresh = Supplementary::REFRESH_INPUT_DEVICES}), + MS("input:kb_variant", "Appropriate XKB keymap parameter", STRVAL_EMPTY, {.refresh = Supplementary::REFRESH_INPUT_DEVICES}), + MS("input:kb_options", "Appropriate XKB keymap parameter", STRVAL_EMPTY, {.refresh = Supplementary::REFRESH_INPUT_DEVICES}), + MS("input:kb_rules", "Appropriate XKB keymap parameter", STRVAL_EMPTY, {.refresh = Supplementary::REFRESH_INPUT_DEVICES}), + MS("input:kb_file", "Appropriate XKB keymap file", STRVAL_EMPTY, {.refresh = Supplementary::REFRESH_INPUT_DEVICES}), + MS("input:numlock_by_default", "Engage numlock by default.", false, {.refresh = Supplementary::REFRESH_INPUT_DEVICES}), + MS("input:resolve_binds_by_sym", "Determines how keybinds act when multiple layouts are used.", false, {.refresh = Supplementary::REFRESH_INPUT_DEVICES}), + MS("input:repeat_rate", "The repeat rate for held-down keys, in repeats per second.", 25, {.min = 0, .max = 200, .refresh = Supplementary::REFRESH_INPUT_DEVICES}), + MS("input:repeat_delay", "Delay before a held-down key is repeated, in milliseconds.", 600, {.min = 0, .max = 2000}), + MS("input:sensitivity", "Sets the mouse input sensitivity. Value is clamped to the range -1.0 to 1.0.", 0, + {.min = -1, .max = 1, .refresh = Supplementary::REFRESH_INPUT_DEVICES}), + MS("input:accel_profile", "Sets the cursor acceleration profile. [adaptive/flat/custom]", STRVAL_EMPTY, + {.validator = strChoice({"adaptive", "flat", "custom"}), .refresh = Supplementary::REFRESH_INPUT_DEVICES}), + MS("input:force_no_accel", "Force no cursor acceleration.", false, {.refresh = Supplementary::REFRESH_INPUT_DEVICES}), + MS("input:rotation", "Sets the rotation of a device in degrees clockwise. Value is clamped to the range 0 to 359.", 0, + {.min = 0, .max = 359, .refresh = Supplementary::REFRESH_INPUT_DEVICES}), + MS("input:left_handed", "Switches RMB and LMB", false, {.refresh = Supplementary::REFRESH_INPUT_DEVICES}), + MS("input:scroll_points", "Sets the scroll acceleration profile, when accel_profile is set to custom.", STRVAL_EMPTY, + {.refresh = Supplementary::REFRESH_INPUT_DEVICES}), + MS("input:scroll_method", "Sets the scroll method. [2fg/edge/on_button_down/no_scroll]", STRVAL_EMPTY, + {.validator = strChoice({"2fg", "edge", "on_button_down", "no_scroll"}), .refresh = Supplementary::REFRESH_INPUT_DEVICES}), + MS("input:scroll_button", "Sets the scroll button. 0 means default.", 0, {.min = 0, .max = 300, .refresh = Supplementary::REFRESH_INPUT_DEVICES}), + MS("input:scroll_button_lock", "If the scroll button lock is enabled, the button does not need to be held down.", false, + {.refresh = Supplementary::REFRESH_INPUT_DEVICES}), + MS("input:scroll_factor", "Multiplier added to scroll movement for external mice.", 1, {.min = 0, .max = 2, .refresh = Supplementary::REFRESH_INPUT_DEVICES}), + MS("input:natural_scroll", "Inverts scrolling direction.", false, {.refresh = Supplementary::REFRESH_INPUT_DEVICES}), + MS("input:follow_mouse", "Specify if and how cursor movement should affect window focus.", 1, + {.min = 0, .max = 3, .map = OptionMap{{"disabled", 0}, {"follow", 1}, {"detached", 2}, {"separate", 3}}, .refresh = Supplementary::REFRESH_INPUT_DEVICES}), + MS("input:follow_mouse_threshold", "The smallest distance in logical pixels the mouse needs to travel for the window under it to get focused.", 0), + MS("input:focus_on_close", "Controls the window focus behavior when a window is closed.", 0, + {.min = 0, .max = 2, .map = OptionMap{{"next", 0}, {"cursor", 1}, {"mru", 2}}, .refresh = Supplementary::REFRESH_INPUT_DEVICES}), + MS("input:mouse_refocus", "if disabled, mouse focus won't switch to the hovered window unless the mouse crosses a window boundary when follow_mouse=1.", true), + MS("input:float_switch_override_focus", + "If enabled (1 or 2), focus will change to the window under the cursor when changing from tiled-to-floating and vice versa. If 2, focus will also follow mouse on " + "float-to-float switches.", + 1, {.min = 0, .max = 2, .refresh = Supplementary::REFRESH_INPUT_DEVICES}), + MS("input:special_fallthrough", "if enabled, having only floating windows in the special workspace will not block focusing windows in the regular workspace.", false), + MS("input:off_window_axis_events", "How to handle axis events around a focused window.", 1, + {.min = 0, .max = 3, .map = OptionMap{{"ignore", 0}, {"send", 1}, {"clamp", 2}, {"warp", 3}}, .refresh = Supplementary::REFRESH_INPUT_DEVICES}), + MS("input:emulate_discrete_scroll", "Emulates discrete scrolling from high resolution scrolling events.", 1, + {.min = 0, .max = 2, .map = OptionMap{{"disable", 0}, {"non_standard", 1}, {"force_all", 2}}, .refresh = Supplementary::REFRESH_INPUT_DEVICES}), + MS("input:follow_mouse_shrink", + "Shrinks the inactive window hitboxes used for focus detection by the specified number of pixels. This creates a dead zone in gaps between windows where moving " + "the cursor will not change focus. Works only with follow_mouse = 1.", + 0, {.min = 0, .max = 300, .refresh = Supplementary::REFRESH_INPUT_DEVICES}), + + /* + * input:touchpad: + */ + + MS("input:touchpad:disable_while_typing", "Disable the touchpad while typing.", true, {.refresh = Supplementary::REFRESH_INPUT_DEVICES}), + MS("input:touchpad:natural_scroll", "Inverts scrolling direction.", false, {.refresh = Supplementary::REFRESH_INPUT_DEVICES}), + MS("input:touchpad:scroll_factor", "Multiplier applied to the amount of scroll movement.", 1, {.min = 0, .max = 2, .refresh = Supplementary::REFRESH_INPUT_DEVICES}), + MS("input:touchpad:middle_button_emulation", "Sending LMB and RMB simultaneously will be interpreted as a middle click.", false, + {.refresh = Supplementary::REFRESH_INPUT_DEVICES}), + MS("input:touchpad:tap_button_map", "Sets the tap button mapping for touchpad button emulation. [lrm/lmr]", STRVAL_EMPTY, + {.validator = strChoice({"lrm", "lmr"}), .refresh = Supplementary::REFRESH_INPUT_DEVICES}), + MS("input:touchpad:clickfinger_behavior", "Button presses with 1, 2, or 3 fingers will be mapped to LMB, RMB, and MMB respectively.", false, + {.refresh = Supplementary::REFRESH_INPUT_DEVICES}), + MS("input:touchpad:tap-to-click", "Tapping on the touchpad with 1, 2, or 3 fingers will send LMB, RMB, and MMB respectively.", true, + {.refresh = Supplementary::REFRESH_INPUT_DEVICES}), + MS("input:touchpad:drag_lock", "When enabled, lifting the finger off while dragging will not drop the dragged item.", 0, + {.min = 0, .max = 2, .refresh = Supplementary::REFRESH_INPUT_DEVICES}), + MS("input:touchpad:tap-and-drag", "Sets the tap and drag mode for the touchpad", true, {.refresh = Supplementary::REFRESH_INPUT_DEVICES}), + MS("input:touchpad:flip_x", "Inverts the horizontal movement of the touchpad", false, {.refresh = Supplementary::REFRESH_INPUT_DEVICES}), + MS("input:touchpad:flip_y", "Inverts the vertical movement of the touchpad", false, {.refresh = Supplementary::REFRESH_INPUT_DEVICES}), + MS("input:touchpad:drag_3fg", "Whether to use 3 or 4 finger drag.", 0, + {.min = 0, .max = 2, .map = OptionMap{{"disable", 0}, {"3_finger", 1}, {"4_finger", 2}}, .refresh = Supplementary::REFRESH_INPUT_DEVICES}), + + /* + * input:touchdevice: + */ + + MS("input:touchdevice:transform", "Transform the input from touchdevices.", 0, {.min = 0, .max = 6, .refresh = Supplementary::REFRESH_INPUT_DEVICES}), + MS("input:touchdevice:output", "The monitor to bind touch devices.", "[[Auto]]", {.refresh = Supplementary::REFRESH_INPUT_DEVICES}), + MS("input:touchdevice:enabled", "Whether input is enabled for touch devices.", true, {.refresh = Supplementary::REFRESH_INPUT_DEVICES}), + + /* + * input:virtualkeyboard: + */ + + MS("input:virtualkeyboard:share_states", "Unify key down states and modifier states with other keyboards.", 2, + {.min = 0, .max = 2, .map = OptionMap{{"disable", 0}, {"enable", 1}, {"only_non_ime", 2}}, .refresh = Supplementary::REFRESH_INPUT_DEVICES}), + MS("input:virtualkeyboard:release_pressed_on_close", "Release all pressed keys by virtual keyboard on close.", false, + {.refresh = Supplementary::REFRESH_INPUT_DEVICES}), + + /* + * input:tablet: + */ + + MS("input:tablet:transform", "transform the input from tablets.", 0, {.min = 0, .max = 6, .refresh = Supplementary::REFRESH_INPUT_DEVICES}), + MS("input:tablet:output", "the monitor to bind tablets.", STRVAL_EMPTY, {.refresh = Supplementary::REFRESH_INPUT_DEVICES}), + MS("input:tablet:region_position", "position of the mapped region in monitor layout.", Config::VEC2{}, + {.validator = vec2Range(-20000, -20000, 20000, 20000), .refresh = Supplementary::REFRESH_INPUT_DEVICES}), + MS("input:tablet:absolute_region_position", "whether to treat the region_position as an absolute position in monitor layout.", false, + {.refresh = Supplementary::REFRESH_INPUT_DEVICES}), + MS("input:tablet:region_size", "size of the mapped region.", Config::VEC2{}, + {.validator = vec2Range(-100, -100, 4000, 4000), .refresh = Supplementary::REFRESH_INPUT_DEVICES}), + MS("input:tablet:relative_input", "whether the input should be relative", false, {.refresh = Supplementary::REFRESH_INPUT_DEVICES}), + MS("input:tablet:left_handed", "if enabled, the tablet will be rotated 180 degrees", false, {.refresh = Supplementary::REFRESH_INPUT_DEVICES}), + MS("input:tablet:active_area_size", "size of tablet's active area in mm", Config::VEC2{}, + {.validator = vec2Range(0, 0, 500, 500), .refresh = Supplementary::REFRESH_INPUT_DEVICES}), + MS("input:tablet:active_area_position", "position of the active area in mm", Config::VEC2{}, + {.validator = vec2Range(0, 0, 500, 500), .refresh = Supplementary::REFRESH_INPUT_DEVICES}), + + /* + * gestures: + */ + + MS("gestures:workspace_swipe_distance", "in px, the distance of the touchpad gesture", 300, {.min = 0, .max = 2000}), + MS("gestures:workspace_swipe_touch", "enable workspace swiping from the edge of a touchscreen", false), + MS("gestures:workspace_swipe_invert", "invert the direction (touchpad only)", true), + MS("gestures:workspace_swipe_touch_invert", "invert the direction (touchscreen only)", false), + MS("gestures:workspace_swipe_min_speed_to_force", "minimum speed in px per timepoint to force the change ignoring cancel_ratio.", 30, {.min = 0, .max = 200}), + MS("gestures:workspace_swipe_cancel_ratio", "how much the swipe has to proceed in order to commence it.", 0.5, {.min = 0, .max = 1}), + MS("gestures:workspace_swipe_create_new", "whether a swipe right on the last workspace should create a new one.", true), + MS("gestures:workspace_swipe_direction_lock", "if enabled, switching direction will be locked when you swipe past the direction_lock_threshold.", true), + MS("gestures:workspace_swipe_direction_lock_threshold", "in px, the distance to swipe before direction lock activates.", 10, {.min = 0, .max = 200}), + MS("gestures:workspace_swipe_forever", "if enabled, swiping will not clamp at the neighboring workspaces but continue to the further ones.", false), + MS("gestures:workspace_swipe_use_r", "if enabled, swiping will use the r prefix instead of the m prefix for finding workspaces.", false), + MS("gestures:close_max_timeout", "Timeout for closing windows with the close gesture, in ms.", 1000, {.min = 10, .max = 2000}), + + /* + * group: + */ + + MS("group:insert_after_current", "whether new windows in a group spawn after current or at group tail", true), + MS("group:focus_removed_window", "whether Hyprland should focus on the window that has just been moved out of the group", true), + MS("group:merge_groups_on_drag", "whether window groups can be dragged into other groups", true), + MS("group:merge_groups_on_groupbar", "whether one group will be merged with another when dragged into its groupbar", true), + MS("group:col.border_active", "active group border color", CHyprColor{0x66ffff00}), + MS("group:col.border_inactive", "inactive group border color", CHyprColor{0x66777700}), + MS("group:col.border_locked_inactive", "inactive locked group border color", CHyprColor{0x66ff5500}), + MS("group:col.border_locked_active", "active locked group border color", CHyprColor{0x66775500}), + MS("group:auto_group", "automatically group new windows", true), + MS("group:drag_into_group", "whether dragging a window into a unlocked group will merge them.", 1, + {.min = 0, .max = 2, .map = OptionMap{{"disabled", 0}, {"enabled", 1}, {"only when dragging into the groupbar", 2}}}), + MS("group:merge_floated_into_tiled_on_groupbar", "whether dragging a floating window into a tiled window groupbar will merge them", false), + MS("group:group_on_movetoworkspace", "whether using movetoworkspace[silent] will merge the window into the workspace's solitary unlocked group", false), + + /* + * group:groupbar: + */ + + MS("group:groupbar:enabled", "enables groupbars", true), + MS("group:groupbar:font_family", "font used to display groupbar titles", "[[EMPTY]]"), + MS("group:groupbar:font_weight_active", "weight of the font used to display active groupbar titles"), + MS("group:groupbar:font_weight_inactive", "weight of the font used to display inactive groupbar titles"), + MS("group:groupbar:font_size", "font size of groupbar title", 8, {.min = 2, .max = 64}), + MS("group:groupbar:gradients", "enables gradients", false), + MS("group:groupbar:height", "height of the groupbar", 14, {.min = 1, .max = 64}), + MS("group:groupbar:indicator_gap", "height of the gap between the groupbar indicator and title", 0, {.min = 0, .max = 64}), + MS("group:groupbar:indicator_height", "height of the groupbar indicator", 3, {.min = 1, .max = 64}), + MS("group:groupbar:stacked", "render the groupbar as a vertical stack", false), + MS("group:groupbar:priority", "sets the decoration priority for groupbars", 3, {.min = 0, .max = 6}), + MS("group:groupbar:render_titles", "whether to render titles in the group bar decoration", true), + MS("group:groupbar:scrolling", "whether scrolling in the groupbar changes group active window", true), + MS("group:groupbar:rounding", "how much to round the groupbar", 1, {.min = 0, .max = 20}), + MS("group:groupbar:rounding_power", "rounding power of groupbar corners (2 is a circle)", 2, {.min = 2, .max = 10}), + MS("group:groupbar:gradient_rounding", "how much to round the groupbar gradient", 2, {.min = 0, .max = 20}), + MS("group:groupbar:gradient_rounding_power", "rounding power of groupbar gradient corners (2 is a circle)", 2, {.min = 2, .max = 10}), + MS("group:groupbar:round_only_edges", "if yes, will only round at the groupbar edges", true), + MS("group:groupbar:gradient_round_only_edges", "if yes, will only round at the groupbar gradient edges", true), + MS("group:groupbar:text_color", "color for window titles in the groupbar", 0xffffffff), + MS("group:groupbar:text_color_inactive", "color for inactive windows' titles in the groupbar", -1), + MS("group:groupbar:text_color_locked_active", "color for the active window's title in a locked group", -1), + MS("group:groupbar:text_color_locked_inactive", "color for inactive windows' titles in locked groups", -1), + MS("group:groupbar:col.active", "active group border color", 0x66ffff00), + MS("group:groupbar:col.inactive", "inactive (out of focus) group border color", 0x66777700), + MS("group:groupbar:col.locked_active", "active locked group border color", 0x66ff5500), + MS("group:groupbar:col.locked_inactive", "inactive locked group border color", 0x66775500), + MS("group:groupbar:gaps_out", "gap between gradients and window", 2, {.min = 0, .max = 20}), + MS("group:groupbar:gaps_in", "gap between gradients", 2, {.min = 0, .max = 20}), + MS("group:groupbar:keep_upper_gap", "keep an upper gap above gradient", true), + MS("group:groupbar:text_offset", "set an offset for a text", 0, {.min = -20, .max = 20}), + MS("group:groupbar:text_padding", "set horizontal padding for a text", 0, {.min = 0, .max = 22}), + MS("group:groupbar:blur", "enable background blur for groupbars", false), + + /* + * misc: + */ + + MS("misc:disable_hyprland_logo", "disables the random Hyprland logo / anime girl background. :(", false), + MS("misc:disable_splash_rendering", "disables the Hyprland splash rendering.", false), + MS("misc:col.splash", "Changes the color of the splash text.", 0x55ffffff), + MS("misc:font_family", "Set the global default font to render the text.", "Sans"), + MS("misc:splash_font_family", "Changes the font used to render the splash text.", "[[EMPTY]]"), + MS("misc:force_default_wallpaper", "Force any of the 3 default wallpapers. [-1/0/1/2]", -1, {.min = -1, .max = 2}), + MS("misc:vrr", "controls the VRR (Adaptive Sync) of your monitors", 0, + {.min = 0, .max = 3, .map = OptionMap{{"off", 0}, {"on", 1}, {"fullscreen", 2}, {"fullscreen_game", 3}}}), + MS("misc:mouse_move_enables_dpms", "If DPMS is set to off, wake up the monitors if the mouse moves", false), + MS("misc:key_press_enables_dpms", "If DPMS is set to off, wake up the monitors if a key is pressed.", false), + MS("misc:name_vk_after_proc", "Name virtual keyboards after the processes that create them.", true), + MS("misc:always_follow_on_dnd", "Will make mouse focus follow the mouse when drag and dropping.", true), + MS("misc:layers_hog_keyboard_focus", "If true, will make keyboard-interactive layers keep their focus on mouse move.", true), + MS("misc:animate_manual_resizes", "If true, will animate manual window resizes/moves", false), + MS("misc:animate_mouse_windowdragging", "If true, will animate windows being dragged by mouse.", false), + MS("misc:disable_autoreload", "If true, the config will not reload automatically on save.", false), + MS("misc:enable_swallow", "Enable window swallowing", false), + MS("misc:swallow_regex", "The class regex to be used for windows that should be swallowed.", STRVAL_EMPTY), + MS("misc:swallow_exception_regex", "The title regex to be used for windows that should not be swallowed.", STRVAL_EMPTY), + MS("misc:focus_on_activate", "Whether Hyprland should focus an app that requests to be focused.", false), + MS("misc:mouse_move_focuses_monitor", "Whether mouse moving into a different monitor should focus it", true), + MS("misc:allow_session_lock_restore", "if true, will allow you to restart a lockscreen app in case it crashes.", false), + MS("misc:session_lock_xray", "keep rendering workspaces below your lockscreen", false), + MS("misc:background_color", "change the background color.", 0xff111111), + MS("misc:close_special_on_empty", "close the special workspace if the last window is removed", true), + MS("misc:on_focus_under_fullscreen", "if there is a fullscreen or maximized window, decide whether a tiled window requested to focus should replace it.", 2, + {.min = 0, .max = 2, .map = OptionMap{{"ignore", 0}, {"take_over", 1}, {"exit_fullscreen", 2}}}), + MS("misc:exit_window_retains_fullscreen", "if true, closing a fullscreen window makes the next focused window fullscreen", false), + MS("misc:initial_workspace_tracking", "if enabled, windows will open on the workspace they were invoked on.", 1, {.min = 0, .max = 2}), + MS("misc:middle_click_paste", "whether to enable middle-click-paste (aka primary selection)", true), + MS("misc:render_unfocused_fps", "the maximum limit for renderunfocused windows' fps in the background", 15, {.min = 1, .max = 120}), + MS("misc:disable_xdg_env_checks", "disable the warning if XDG environment is externally managed", false), + MS("misc:disable_hyprland_guiutils_check", "disable the warning if hyprland-guiutils is missing", false), + MS("misc:disable_watchdog_warning", "whether to disable the warning about not using start-hyprland.", false), + MS("misc:lockdead_screen_delay", "the delay in ms after the lockdead screen appears.", 1000, {.min = 0, .max = 5000}), + MS("misc:enable_anr_dialog", "whether to enable the ANR (app not responding) dialog when your apps hang", true), + MS("misc:anr_missed_pings", "number of missed pings before showing the ANR dialog", 5, {.min = 1, .max = 20}), + MS("misc:screencopy_force_8b", "forces 8 bit screencopy", true), + MS("misc:disable_scale_notification", "disables notification popup when a monitor fails to set a suitable scale", false), + MS("misc:size_limits_tiled", "whether to apply minsize and maxsize rules to tiled windows", false), + + /* + * binds: + */ + + MS("binds:pass_mouse_when_bound", "if disabled, will not pass the mouse events to apps / dragging windows around if a keybind has been triggered.", false), + MS("binds:scroll_event_delay", "in ms, how many ms to wait after a scroll event to allow passing another one for the binds.", 300, {.min = 0, .max = 2000}), + MS("binds:workspace_back_and_forth", "If enabled, an attempt to switch to the currently focused workspace will instead switch to the previous workspace.", false), + MS("binds:hide_special_on_workspace_change", "If enabled, changing the active workspace will hide the special workspace on the monitor.", false), + MS("binds:allow_workspace_cycles", "If enabled, workspaces don't forget their previous workspace.", false), + MS("binds:workspace_center_on", "Whether switching workspaces should center the cursor on the workspace (0) or on the last active window (1)", 1, + {.min = 0, .max = 1}), + MS("binds:focus_preferred_method", "sets the preferred focus finding method when using focuswindow/movewindow/etc with a direction.", 0, {.min = 0, .max = 1}), + MS("binds:ignore_group_lock", "If enabled, dispatchers like moveintogroup, moveoutofgroup and movewindoworgroup will ignore lock per group.", false), + MS("binds:movefocus_cycles_fullscreen", "If enabled, when on a fullscreen window, movefocus will cycle fullscreen.", false), + MS("binds:movefocus_cycles_groupfirst", "If enabled, when in a grouped window, movefocus will cycle windows in the groups first.", false), + MS("binds:disable_keybind_grabbing", "If enabled, apps that request keybinds to be disabled will not be able to do so.", false), + MS("binds:window_direction_monitor_fallback", "If enabled, moving a window or focus over the edge of a monitor with a direction will move it to the next monitor.", + true), + MS("binds:allow_pin_fullscreen", "Allows fullscreen to pinned windows, and restore their pinned status afterwards", false), + MS("binds:drag_threshold", "Movement threshold in pixels for window dragging and c/g bind flags. 0 to disable.", 0, + {.min = 0, .max = std::numeric_limits::max()}), + + /* + * xwayland: + */ + + MS("xwayland:enabled", "allow running applications using X11", true), + MS("xwayland:use_nearest_neighbor", "uses the nearest neighbor filtering for xwayland apps, making them pixelated rather than blurry", true), + MS("xwayland:force_zero_scaling", "forces a scale of 1 on xwayland windows on scaled displays.", false), + MS("xwayland:create_abstract_socket", "Create the abstract Unix domain socket for XWayland", false), + + /* + * opengl: + */ + + MS("opengl:nvidia_anti_flicker", "reduces flickering on nvidia at the cost of possible frame drops on lower-end GPUs.", true), + + /* + * render: + */ + + MS("render:direct_scanout", "Enables direct scanout.", 0, {.min = 0, .max = 2, .map = OptionMap{{"disable", 0}, {"enable", 1}, {"auto", 2}}}), + MS("render:expand_undersized_textures", "Whether to expand textures that have not yet resized to be larger.", true), + MS("render:xp_mode", "Disable back buffer and bottom layer rendering.", false), + MS("render:ctm_animation", "Whether to enable a fade animation for CTM changes.", 2, + {.min = 0, .max = 2, .map = OptionMap{{"disable", 0}, {"enable", 1}, {"auto", 2}}}), + MS("render:cm_enabled", "Enable Color Management pipelines (requires restart to fully take effect)", true), + MS("render:send_content_type", "Report content type to allow monitor profile autoswitch", true), + MS("render:cm_auto_hdr", "Auto-switch to hdr mode when fullscreen app is in hdr", 1, + {.min = 0, .max = 2, .map = OptionMap{{"disable", 0}, {"hdr", 1}, {"hdredid", 2}}}), + MS("render:new_render_scheduling", "enable new render scheduling, which should improve FPS on underpowered devices.", false), + MS("render:non_shader_cm", "Enable CM without shader.", 3, {.min = 0, .max = 3, .map = OptionMap{{"disable", 0}, {"always", 1}, {"ondemand", 2}, {"ignore", 3}}}), + MS("render:cm_sdr_eotf", "Default transfer function for displaying SDR apps.", "default"), + MS("render:commit_timing_enabled", "Enable commit timing proto. Requires restart", true), + MS("render:icc_vcgt_enabled", "Enable sending VCGT ramps to KMS with ICC profiles", true), + MS("render:use_shader_blur_blend", "Use experimental blurred bg blending", false), + MS("render:use_fp16", "Use experimental internal FP16 buffer.", 2, {.min = 0, .max = 2, .map = OptionMap{{"disable", 0}, {"enable", 1}, {"auto", 2}}}), + MS("render:keep_unmodified_copy", "Keep umodified SDR frame copy for sreensharing.", 2, + {.min = 0, .max = 2, .map = OptionMap{{"disable", 0}, {"enable", 1}, {"auto", 2}}}), + MS("render:non_shader_cm_interop", "non_shader_cm interaction with ctm proto (hyprsunset and similar).", 2, + {.min = 0, .max = 2, .map = OptionMap{{"disable", 0}, {"enable", 1}, {"auto", 2}}}), + + /* + * cursor: + */ + + MS("cursor:invisible", "don't render cursors", false), + MS("cursor:no_hardware_cursors", "disables hardware cursors.", 0, {.min = 0, .max = 2, .map = OptionMap{{"Disabled", 0}, {"Enabled", 1}, {"Auto", 2}}}), + MS("cursor:no_break_fs_vrr", "disables scheduling new frames on cursor movement for fullscreen apps with VRR enabled.", 2, + {.min = 0, .max = 2, .map = OptionMap{{"disable", 0}, {"enable", 1}, {"auto", 2}}}), + MS("cursor:min_refresh_rate", "minimum refresh rate for cursor movement when no_break_fs_vrr is active.", 24, {.min = 10, .max = 500}), + MS("cursor:hotspot_padding", "the padding, in logical px, between screen edges and the cursor", 0, {.min = 0, .max = 20}), + MS("cursor:inactive_timeout", "in seconds, after how many seconds of cursor's inactivity to hide it. Set to 0 for never.", 0, {.min = 0, .max = 20}), + MS("cursor:no_warps", "if true, will not warp the cursor in many cases", false), + MS("cursor:persistent_warps", "When a window is refocused, the cursor returns to its last position relative to that window.", false), + MS("cursor:warp_on_change_workspace", "Move the cursor to the last focused window after changing the workspace.", 0, + {.min = 0, .max = 2, .map = OptionMap{{"disable", 0}, {"enable", 1}, {"force", 2}}}), + MS("cursor:warp_on_toggle_special", "Move the cursor to the last focused window when toggling a special workspace.", 0, + {.min = 0, .max = 2, .map = OptionMap{{"disable", 0}, {"enable", 1}, {"force", 2}}}), + MS("cursor:default_monitor", "the name of a default monitor for the cursor to be set to on startup", STRVAL_EMPTY), + MS("cursor:zoom_factor", "the factor to zoom by around the cursor. 1 means no zoom.", 1, {.min = 1, .max = 10}), + MS("cursor:zoom_rigid", "whether the zoom should follow the cursor rigidly or loosely", false), + MS("cursor:zoom_disable_aa", "If enabled, when zooming, no antialiasing will be used", false), + MS("cursor:zoom_detached_camera", "Detaches the camera from the mouse when zoomed in", true), + MS("cursor:enable_hyprcursor", "whether to enable hyprcursor support", true), + MS("cursor:hide_on_key_press", "Hides the cursor when you press any key until the mouse is moved.", false), + MS("cursor:hide_on_touch", "Hides the cursor when the last input was a touch input until a mouse input is done.", true), + MS("cursor:hide_on_tablet", "Hides the cursor when the last input was a tablet input until a mouse input is done.", false), + MS("cursor:use_cpu_buffer", "Makes HW cursors use a CPU buffer.", 2, {.min = 0, .max = 2, .map = OptionMap{{"disable", 0}, {"enable", 1}, {"auto", 2}}}), + MS("cursor:sync_gsettings_theme", "sync xcursor theme with gsettings", true), + MS("cursor:warp_back_after_non_mouse_input", "warp the cursor back to where it was after using a non-mouse input to move it.", false), + + /* + * ecosystem: + */ + + MS("ecosystem:no_update_news", "disable the popup that shows up when you update hyprland to a new version.", false), + MS("ecosystem:no_donation_nag", "disable the popup that shows up twice a year encouraging to donate.", false), + MS("ecosystem:enforce_permissions", "whether to enable permission control.", false), + + /* + * debug: + */ + + MS("debug:overlay", "print the debug performance overlay.", false), + MS("debug:damage_blink", "flash damaged areas", false), + MS("debug:gl_debugging", "enable OpenGL debugging and error checking.", false), + MS("debug:disable_logs", "disable logging to a file", true), + MS("debug:disable_time", "disables time logging", true), + MS("debug:damage_tracking", "redraw only the needed bits of the display.", 2, {.min = 0, .max = 2, .map = OptionMap{{"disable", 0}, {"monitor", 1}, {"full", 2}}}), + MS("debug:enable_stdout_logs", "enables logging to stdout", false), + MS("debug:manual_crash", "set to 1 and then back to 0 to crash Hyprland.", 0, {.min = 0, .max = 1}), + MS("debug:suppress_errors", "if true, do not display config file parsing errors.", false), + MS("debug:disable_scale_checks", "disables verification of the scale factors.", false), + MS("debug:error_limit", "limits the number of displayed config file parsing errors.", 5, {.min = 0, .max = 20}), + MS("debug:error_position", "sets the position of the error bar.", 0, {.min = 0, .max = 1, .map = OptionMap{{"top", 0}, {"bottom", 1}}}), + MS("debug:colored_stdout_logs", "enables colors in the stdout logs.", true), + MS("debug:log_damage", "enables logging the damage.", false), + MS("debug:pass", "enables render pass debugging.", false), + MS("debug:full_cm_proto", "claims support for all cm proto features (requires restart)", false), + MS("debug:ds_handle_same_buffer", "Special case for DS with unmodified buffer", true), + MS("debug:ds_handle_same_buffer_fifo", "Special case for DS with unmodified buffer unlocks fifo", true), + MS("debug:fifo_pending_workaround", "Fifo workaround for empty pending list", false), + MS("debug:render_solitary_wo_damage", "Render solitary window with empty damage", false), + MS("debug:vfr", "controls the VFR status of Hyprland. Do not turn off unless debugging", true), + MS("debug:invalidate_fp16", "allow fp16 buffer invalidation.", 2, {.min = 0, .max = 2, .map = OptionMap{{"disable", 0}, {"enable", 1}, {"auto", 2}}}), + + /* + * layout: + */ + + MS("layout:single_window_aspect_ratio", "If specified, whenever only a single window is open, it will be coerced into the specified aspect ratio.", + Config::VEC2{0, 0}, {.validator = vec2Range(0, 0, 1000, 1000), .refresh = Supplementary::REFRESH_LAYOUTS}), + MS("layout:single_window_aspect_ratio_tolerance", "Minimum distance for single_window_aspect_ratio to take effect.", 0.1F, + {.min = 0.F, .max = 1.F, .refresh = Supplementary::REFRESH_LAYOUTS}), + + /* + * dwindle: + */ + + MS("dwindle:force_split", "force a split direction for new windows", 0, {.min = 0, .max = 2, .map = OptionMap{{"follow_mouse", 0}, {"left", 1}, {"right", 2}}}), + MS("dwindle:preserve_split", "if enabled, the split will not change regardless of what happens to the container.", false), + MS("dwindle:smart_split", "if enabled, allows a more precise control over the window split direction based on the cursor's position.", false), + MS("dwindle:smart_resizing", "if enabled, resizing direction will be determined by the mouse's position on the window.", true), + MS("dwindle:permanent_direction_override", "if enabled, makes the preselect direction persist.", false), + MS("dwindle:special_scale_factor", "specifies the scale factor of windows on the special workspace", 1, {.min = 0, .max = 1}), + MS("dwindle:split_width_multiplier", "specifies the auto-split width multiplier", 1, {.min = 0.1F, .max = 3}), + MS("dwindle:use_active_for_splits", "whether to prefer the active window or the mouse position for splits", true), + MS("dwindle:default_split_ratio", "the default split ratio on window open.", 1, {.min = 0.1F, .max = 1.9F}), + MS("dwindle:split_bias", "specifies which window will receive the split ratio.", 0, {.min = 0, .max = 1, .map = OptionMap{{"directional", 0}, {"current", 1}}}), + MS("dwindle:precise_mouse_move", "if enabled, bindm movewindow will drop the window more precisely depending on where your mouse is.", false), + + /* + * master: + */ + + MS("master:allow_small_split", "enable adding additional master windows in a horizontal split style", false), + MS("master:special_scale_factor", "the scale of the special workspace windows.", 1, {.min = 0, .max = 1}), + MS("master:mfact", "the size as a percentage of the master window.", 0.55, {.min = 0, .max = 1, .refresh = Supplementary::REFRESH_LAYOUTS}), + MS("master:new_status", "`master`: new window becomes master; `slave`: new windows are added to slave stack; `inherit`: inherit from focused window", "slave"), + MS("master:new_on_top", "whether a newly open window should be on the top of the stack", false), + MS("master:new_on_active", "`before`, `after`: place new window relative to the focused window; `none`: place new window according to new_on_top.", "none"), + MS("master:orientation", "default placement of the master area", "left", {.refresh = Supplementary::REFRESH_LAYOUTS}), + MS("master:slave_count_for_center_master", "when using orientation=center, make the master window centered only when at least this many slave windows are open.", 2, + {.min = 0, .max = 10, .refresh = Supplementary::REFRESH_LAYOUTS}), + MS("master:center_master_fallback", "Set fallback for center master when slaves are less than slave_count_for_center_master", "left", + {.refresh = Supplementary::REFRESH_LAYOUTS}), + MS("master:center_ignores_reserved", "centers the master window on monitor ignoring reserved areas", false, {.refresh = Supplementary::REFRESH_LAYOUTS}), + MS("master:smart_resizing", "if enabled, resizing direction will be determined by the mouse's position on the window.", true), + MS("master:drop_at_cursor", "when enabled, dragging and dropping windows will put them at the cursor position.", true), + MS("master:always_keep_position", "whether to keep the master window in its configured position when there are no slave windows", false, + {.refresh = Supplementary::REFRESH_LAYOUTS}), + + /* + * scrolling: + */ + + MS("scrolling:fullscreen_on_one_column", "when enabled, a single column on a workspace will always span the entire screen.", true, + {.refresh = Supplementary::REFRESH_LAYOUTS}), + MS("scrolling:column_width", "the default width of a column.", 0.5, {.min = 0.1, .max = 1.0}), + MS("scrolling:focus_fit_method", "When a column is focused, what method should be used to bring it into view", 1, + {.min = 0, .max = 1, .map = OptionMap{{"center", 0}, {"fit", 1}}}), + MS("scrolling:follow_focus", "when a window is focused, should the layout move to bring it into view automatically", true), + MS("scrolling:follow_min_visible", "when a window is focused, require that at least a given fraction of it is visible for focus to follow", 0.4, + {.min = 0.0, .max = 1.0}), + MS("scrolling:explicit_column_widths", "A comma-separated list of preconfigured widths for colresize +conf/-conf", "0.333, 0.5, 0.667, 1.0"), + MS("scrolling:direction", "Direction in which new windows appear and the layout scrolls", "right", {.refresh = Supplementary::REFRESH_LAYOUTS}), + MS("scrolling:wrap_focus", "Determines if column focus wraps around", true), + MS("scrolling:wrap_swapcol", "Determines if column movement wraps around", true), + + /* + * experimental: + */ + + MS("experimental:wp_cm_1_2", "Allow wp-cm-v1 version 2", false), + + /* + * quirks: + */ + + MS("quirks:prefer_hdr", "Prefer HDR mode.", 0, {.min = 0, .max = 2, .map = OptionMap{{"disable", 0}, {"enable", 1}, {"gamescope_only", 2}}}), + MS("quirks:skip_non_kms_dmabuf_formats", "Do not report dmabuf formats which cannot be imported into KMS", false), + }; + +#undef MS +} diff --git a/src/config/values/ConfigValues.hpp b/src/config/values/ConfigValues.hpp new file mode 100644 index 000000000..812ee7d99 --- /dev/null +++ b/src/config/values/ConfigValues.hpp @@ -0,0 +1,177 @@ +#pragma once + +#include +#include +#include +#include +#include + +#include "../../helpers/memory/Memory.hpp" +#include "../../helpers/Color.hpp" + +#include "./types/IValue.hpp" +#include "./types/BoolValue.hpp" +#include "./types/ColorValue.hpp" +#include "./types/CssGapValue.hpp" +#include "./types/FloatValue.hpp" +#include "./types/FontWeightValue.hpp" +#include "./types/GradientValue.hpp" +#include "./types/IntValue.hpp" +#include "./types/StringValue.hpp" +#include "./types/Vec2Value.hpp" + +namespace Config::Values { + + inline auto vec2Range(float minX, float minY, float maxX, float maxY) { + return [=](const Config::VEC2& v) -> std::expected { + if (v.x < minX || v.x > maxX || v.y < minY || v.y > maxY) + return std::unexpected(std::format("value out of range [{}, {}] - [{}, {}]", minX, minY, maxX, maxY)); + return {}; + }; + } + + inline auto strChoice(std::vector rawVec) { + return [vec = std::move(rawVec)](const Config::STRING& v) -> std::expected { + if (!std::ranges::contains(vec, v)) { + std::string allowed = ""; + for (const auto& e : vec) { + allowed += std::format("\"{}\", ", e); + } + + allowed = allowed.substr(0, allowed.size() - 2); + + return std::unexpected(std::format("bad value \"{}\", allowed values are: {}", v, allowed)); + } + return {}; + }; + } + + using Bool = CBoolValue; + using Color = CColorValue; + using CssGap = CCssGapValue; + using Float = CFloatValue; + using FontWeight = CFontWeightValue; + using Gradient = CGradientValue; + using Int = CIntValue; + using String = CStringValue; + using Vec2 = CVec2Value; + + template + struct SValueOptions; + + template <> + struct SValueOptions { + using type = SBoolValueOptions; + }; + + template <> + struct SValueOptions { + using type = SColorValueOptions; + }; + + template <> + struct SValueOptions { + using type = SCssGapValueOptions; + }; + + template <> + struct SValueOptions { + using type = SFloatValueOptions; + }; + + template <> + struct SValueOptions { + using type = SFontWeightValueOptions; + }; + + template <> + struct SValueOptions { + using type = SGradientValueOptions; + }; + + template <> + struct SValueOptions { + using type = SIntValueOptions; + }; + + template <> + struct SValueOptions { + using type = SStringValueOptions; + }; + + template <> + struct SValueOptions { + using type = SVec2ValueOptions; + }; + + template + using valueOptions_t = typename SValueOptions::type; + + template + SP makeConfigValue(const char* name, const char* description, Def&& def, valueOptions_t options) { + return makeShared(name, description, std::forward(def), std::move(options)); + } + + template + SP makeConfigValue(Args&&... args) { + return makeShared(std::forward(args)...); + } + + using OptionMap = std::unordered_map; + + // Device config values follow the parents. They have the same name, + // and restrictions, except without the category. + inline const std::vector CONFIG_DEVICE_VALUE_NAMES = { + "input:sensitivity", + "input:accel_profile", + "input:rotation", + "input:kb_file", + "input:kb_layout", + "input:kb_variant", + "input:kb_options", + "input:kb_rules", + "input:kb_model", + "input:repeat_rate", + "input:repeat_delay", + "input:natural_scroll", + "input:touchpad:natural_scroll", + "input:touchpad:tap_button_map", + "input:numlock_by_default", + "input:resolve_binds_by_sym", + "input:touchpad:disable_while_typing", + "input:touchpad:clickfinger_behavior", + "input:touchpad:middle_button_emulation", + "input:touchpad:tap-to-click", + "input:touchpad:tap-and-drag", + "input:touchpad:drag_lock", + "input:left_handed", + "input:tablet:left_handed", + "input:scroll_method", + "input:scroll_button", + "input:scroll_button_lock", + "input:scroll_points", + "input:scroll_factor", + "input:touchpad:scroll_factor", + "input:touchdevice:transform", + "input:tablet:transform", + "input:touchdevice:output", + "input:tablet:output", + "input:touchdevice:enabled", + "input:tablet:region_position", + "input:tablet:absolute_region_position", + "input:tablet:region_size", + "input:tablet:relative_input", + "input:tablet:active_area_position", + "input:tablet:active_area_size", + "input:touchpad:flip_x", + "input:touchpad:flip_y", + "input:touchpad:drag_3fg", + "input:virtualkeyboard:share_states", + "input:virtualkeyboard:release_pressed_on_close", + }; + + std::vector> getConfigValues(); + inline const std::vector> CONFIG_VALUES = getConfigValues(); + + std::string getAsJson(); +}; diff --git a/src/config/values/types/BoolValue.cpp b/src/config/values/types/BoolValue.cpp new file mode 100644 index 000000000..12b09acf2 --- /dev/null +++ b/src/config/values/types/BoolValue.cpp @@ -0,0 +1,27 @@ +#include "BoolValue.hpp" + +using namespace Config::Values; + +CBoolValue::CBoolValue(const char* name, const char* description, bool def, SBoolValueOptions&& options) : IValue(options.refresh), m_default(def) { + m_name = name; + m_description = description; +} + +const std::type_info* CBoolValue::underlying() const { + return &typeid(decltype(m_default)); +} + +bool CBoolValue::value() const { + if (!m_val.good()) + return false; + + return *m_val; +} + +void CBoolValue::commence() { + m_val = CConfigValue(m_name); +} + +Config::BOOL CBoolValue::defaultVal() const { + return m_default; +} diff --git a/src/config/values/types/BoolValue.hpp b/src/config/values/types/BoolValue.hpp new file mode 100644 index 000000000..7ce264e03 --- /dev/null +++ b/src/config/values/types/BoolValue.hpp @@ -0,0 +1,28 @@ +#pragma once + +#include "IValue.hpp" + +#include "../../ConfigValue.hpp" + +namespace Config::Values { + struct SBoolValueOptions { + Supplementary::PropRefreshBits refresh = 0; + }; + + class CBoolValue : public IValue { + public: + CBoolValue(const char* name, const char* description, Config::BOOL def, SBoolValueOptions&& options = {}); + + virtual ~CBoolValue() = default; + + virtual const std::type_info* underlying() const override; + virtual void commence() override; + + Config::BOOL value() const; + Config::BOOL defaultVal() const; + + private: + CConfigValue m_val; + bool m_default = false; + }; +} diff --git a/src/config/values/types/ColorValue.cpp b/src/config/values/types/ColorValue.cpp new file mode 100644 index 000000000..dfc5aaa10 --- /dev/null +++ b/src/config/values/types/ColorValue.cpp @@ -0,0 +1,27 @@ +#include "ColorValue.hpp" + +using namespace Config::Values; + +CColorValue::CColorValue(const char* name, const char* description, Config::INTEGER def, SColorValueOptions&& options) : IValue(options.refresh), m_default(def) { + m_name = name; + m_description = description; +} + +const std::type_info* CColorValue::underlying() const { + return &typeid(decltype(m_default)); +} + +void CColorValue::commence() { + m_val = CConfigValue(m_name); +} + +Config::INTEGER CColorValue::value() const { + if (!m_val.good()) + return m_default; + + return *m_val; +} + +Config::INTEGER CColorValue::defaultVal() const { + return m_default; +} diff --git a/src/config/values/types/ColorValue.hpp b/src/config/values/types/ColorValue.hpp new file mode 100644 index 000000000..6b607b8cb --- /dev/null +++ b/src/config/values/types/ColorValue.hpp @@ -0,0 +1,28 @@ +#pragma once + +#include "IValue.hpp" + +#include "../../ConfigValue.hpp" + +namespace Config::Values { + struct SColorValueOptions { + Supplementary::PropRefreshBits refresh = 0; + }; + + class CColorValue : public IValue { + public: + CColorValue(const char* name, const char* description, Config::INTEGER def, SColorValueOptions&& options = {}); + + virtual ~CColorValue() = default; + + virtual const std::type_info* underlying() const override; + virtual void commence() override; + + Config::INTEGER value() const; + Config::INTEGER defaultVal() const; + + private: + CConfigValue m_val; + Config::INTEGER m_default = 0; + }; +} diff --git a/src/config/values/types/CssGapValue.cpp b/src/config/values/types/CssGapValue.cpp new file mode 100644 index 000000000..34145203c --- /dev/null +++ b/src/config/values/types/CssGapValue.cpp @@ -0,0 +1,28 @@ +#include "CssGapValue.hpp" + +using namespace Config::Values; + +CCssGapValue::CCssGapValue(const char* name, const char* description, Config::INTEGER def, SCssGapValueOptions&& options) : + IValue(options.refresh), m_min(options.min), m_max(options.max), m_default(def) { + m_name = name; + m_description = description; +} + +const std::type_info* CCssGapValue::underlying() const { + return &typeid(decltype(m_default)); +} + +void CCssGapValue::commence() { + m_val = CConfigValue(m_name); +} + +const Config::CCssGapData& CCssGapValue::value() const { + if (!m_val.good()) + return m_default; + + return *dc(m_val.ptr()); +} + +const Config::CCssGapData& CCssGapValue::defaultVal() const { + return m_default; +} diff --git a/src/config/values/types/CssGapValue.hpp b/src/config/values/types/CssGapValue.hpp new file mode 100644 index 000000000..838336f1b --- /dev/null +++ b/src/config/values/types/CssGapValue.hpp @@ -0,0 +1,35 @@ +#pragma once + +#include "IValue.hpp" + +#include "../../ConfigValue.hpp" +#include "../../shared/complex/ComplexDataTypes.hpp" + +#include + +namespace Config::Values { + struct SCssGapValueOptions { + std::optional min = std::nullopt; + std::optional max = std::nullopt; + Supplementary::PropRefreshBits refresh = 0; + }; + + class CCssGapValue : public IValue { + public: + CCssGapValue(const char* name, const char* description, Config::INTEGER def, SCssGapValueOptions&& options = {}); + + virtual ~CCssGapValue() = default; + + virtual const std::type_info* underlying() const override; + virtual void commence() override; + + const Config::CCssGapData& value() const; + const Config::CCssGapData& defaultVal() const; + + const std::optional m_min, m_max; + + private: + CConfigValue m_val; + Config::CCssGapData m_default; + }; +} diff --git a/src/config/values/types/FloatValue.cpp b/src/config/values/types/FloatValue.cpp new file mode 100644 index 000000000..38261eb51 --- /dev/null +++ b/src/config/values/types/FloatValue.cpp @@ -0,0 +1,28 @@ +#include "FloatValue.hpp" + +using namespace Config::Values; + +CFloatValue::CFloatValue(const char* name, const char* description, Config::FLOAT def, SFloatValueOptions&& options) : + IValue(options.refresh), m_min(options.min), m_max(options.max), m_default(def) { + m_name = name; + m_description = description; +} + +const std::type_info* CFloatValue::underlying() const { + return &typeid(decltype(m_default)); +} + +void CFloatValue::commence() { + m_val = CConfigValue(m_name); +} + +Config::FLOAT CFloatValue::value() const { + if (!m_val.good()) + return false; + + return *m_val; +} + +Config::FLOAT CFloatValue::defaultVal() const { + return m_default; +} diff --git a/src/config/values/types/FloatValue.hpp b/src/config/values/types/FloatValue.hpp new file mode 100644 index 000000000..de0376ad7 --- /dev/null +++ b/src/config/values/types/FloatValue.hpp @@ -0,0 +1,32 @@ +#pragma once + +#include "IValue.hpp" + +#include "../../ConfigValue.hpp" + +namespace Config::Values { + struct SFloatValueOptions { + std::optional min = std::nullopt; + std::optional max = std::nullopt; + Supplementary::PropRefreshBits refresh = 0; + }; + + class CFloatValue : public IValue { + public: + CFloatValue(const char* name, const char* description, Config::FLOAT def, SFloatValueOptions&& options = {}); + + virtual ~CFloatValue() = default; + + virtual const std::type_info* underlying() const override; + virtual void commence() override; + + Config::FLOAT value() const; + Config::FLOAT defaultVal() const; + + const std::optional m_min, m_max; + + private: + CConfigValue m_val; + Config::FLOAT m_default = 0.F; + }; +} diff --git a/src/config/values/types/FontWeightValue.cpp b/src/config/values/types/FontWeightValue.cpp new file mode 100644 index 000000000..5d86ea4cb --- /dev/null +++ b/src/config/values/types/FontWeightValue.cpp @@ -0,0 +1,27 @@ +#include "FontWeightValue.hpp" + +using namespace Config::Values; + +CFontWeightValue::CFontWeightValue(const char* name, const char* description, Config::INTEGER def, SFontWeightValueOptions&& options) : IValue(options.refresh), m_default(def) { + m_name = name; + m_description = description; +} + +const std::type_info* CFontWeightValue::underlying() const { + return &typeid(Config::IComplexConfigValue*); +} + +void CFontWeightValue::commence() { + m_val = CConfigValue(m_name); +} + +const Config::CFontWeightConfigValueData& CFontWeightValue::value() const { + if (!m_val.good()) + return m_default; + + return *dc(m_val.ptr()); +} + +const Config::CFontWeightConfigValueData& CFontWeightValue::defaultVal() const { + return m_default; +} diff --git a/src/config/values/types/FontWeightValue.hpp b/src/config/values/types/FontWeightValue.hpp new file mode 100644 index 000000000..427bba6fa --- /dev/null +++ b/src/config/values/types/FontWeightValue.hpp @@ -0,0 +1,29 @@ +#pragma once + +#include "IValue.hpp" + +#include "../../ConfigValue.hpp" +#include "../../shared/complex/ComplexDataTypes.hpp" + +namespace Config::Values { + struct SFontWeightValueOptions { + Supplementary::PropRefreshBits refresh = 0; + }; + + class CFontWeightValue : public IValue { + public: + CFontWeightValue(const char* name, const char* description, Config::INTEGER def = 400, SFontWeightValueOptions&& options = {}); + + virtual ~CFontWeightValue() = default; + + virtual const std::type_info* underlying() const override; + virtual void commence() override; + + const Config::CFontWeightConfigValueData& value() const; + const Config::CFontWeightConfigValueData& defaultVal() const; + + private: + CConfigValue m_val; + Config::CFontWeightConfigValueData m_default; + }; +} diff --git a/src/config/values/types/GradientValue.cpp b/src/config/values/types/GradientValue.cpp new file mode 100644 index 000000000..2e35cde99 --- /dev/null +++ b/src/config/values/types/GradientValue.cpp @@ -0,0 +1,27 @@ +#include "GradientValue.hpp" + +using namespace Config::Values; + +CGradientValue::CGradientValue(const char* name, const char* description, CHyprColor def, SGradientValueOptions&& options) : IValue(options.refresh), m_default(def) { + m_name = name; + m_description = description; +} + +const std::type_info* CGradientValue::underlying() const { + return &typeid(decltype(m_default)); +} + +void CGradientValue::commence() { + m_val = CConfigValue(m_name); +} + +const Config::CGradientValueData& CGradientValue::value() const { + if (!m_val.good()) + return m_default; + + return *dc(m_val.ptr()); +} + +const Config::CGradientValueData& CGradientValue::defaultVal() const { + return m_default; +} diff --git a/src/config/values/types/GradientValue.hpp b/src/config/values/types/GradientValue.hpp new file mode 100644 index 000000000..cdb2f01dc --- /dev/null +++ b/src/config/values/types/GradientValue.hpp @@ -0,0 +1,29 @@ +#pragma once + +#include "IValue.hpp" + +#include "../../ConfigValue.hpp" +#include "../../shared/complex/ComplexDataTypes.hpp" + +namespace Config::Values { + struct SGradientValueOptions { + Supplementary::PropRefreshBits refresh = 0; + }; + + class CGradientValue : public IValue { + public: + CGradientValue(const char* name, const char* description, CHyprColor def, SGradientValueOptions&& options = {}); + + virtual ~CGradientValue() = default; + + virtual const std::type_info* underlying() const override; + virtual void commence() override; + + const Config::CGradientValueData& value() const; + const Config::CGradientValueData& defaultVal() const; + + private: + CConfigValue m_val; + Config::CGradientValueData m_default; + }; +} diff --git a/src/config/values/types/IValue.cpp b/src/config/values/types/IValue.cpp new file mode 100644 index 000000000..847e4f062 --- /dev/null +++ b/src/config/values/types/IValue.cpp @@ -0,0 +1,20 @@ +#include "IValue.hpp" + +using namespace Config::Values; +using namespace Config; + +IValue::IValue(Supplementary::PropRefreshBits refreshProps) : m_refreshProps(refreshProps) { + ; +} + +const char* IValue::name() const { + return m_name; +} + +const char* IValue::description() const { + return m_description; +} + +Supplementary::PropRefreshBits IValue::refreshBits() const { + return m_refreshProps; +} diff --git a/src/config/values/types/IValue.hpp b/src/config/values/types/IValue.hpp new file mode 100644 index 000000000..ff06f13dd --- /dev/null +++ b/src/config/values/types/IValue.hpp @@ -0,0 +1,26 @@ +#pragma once + +#include +#include + +#include "../../supplementary/propRefresher/PropRefresher.hpp" + +namespace Config::Values { + class IValue { + protected: + IValue(Supplementary::PropRefreshBits refreshProps); + + const char * m_name = nullptr, *m_description = nullptr; + + const Supplementary::PropRefreshBits m_refreshProps = 0; + + public: + virtual ~IValue() = default; + + virtual const std::type_info* underlying() const = 0; + virtual const char* name() const; + virtual const char* description() const; + virtual Supplementary::PropRefreshBits refreshBits() const; + virtual void commence() = 0; + }; +}; diff --git a/src/config/values/types/IntValue.cpp b/src/config/values/types/IntValue.cpp new file mode 100644 index 000000000..b7f13aaee --- /dev/null +++ b/src/config/values/types/IntValue.cpp @@ -0,0 +1,28 @@ +#include "IntValue.hpp" + +using namespace Config::Values; + +CIntValue::CIntValue(const char* name, const char* description, Config::INTEGER def, SIntValueOptions&& options) : + IValue(options.refresh), m_min(options.min), m_max(options.max), m_map(std::move(options.map)), m_default(def) { + m_name = name; + m_description = description; +} + +const std::type_info* CIntValue::underlying() const { + return &typeid(decltype(m_default)); +} + +void CIntValue::commence() { + m_val = CConfigValue(m_name); +} + +Config::INTEGER CIntValue::value() const { + if (!m_val.good()) + return m_default; + + return *m_val; +} + +Config::INTEGER CIntValue::defaultVal() const { + return m_default; +} diff --git a/src/config/values/types/IntValue.hpp b/src/config/values/types/IntValue.hpp new file mode 100644 index 000000000..ba87d1c39 --- /dev/null +++ b/src/config/values/types/IntValue.hpp @@ -0,0 +1,38 @@ +#pragma once + +#include "IValue.hpp" + +#include "../../ConfigValue.hpp" + +#include +#include +#include + +namespace Config::Values { + struct SIntValueOptions { + std::optional min = std::nullopt; + std::optional max = std::nullopt; + std::optional> map = std::nullopt; + Supplementary::PropRefreshBits refresh = 0; + }; + + class CIntValue : public IValue { + public: + CIntValue(const char* name, const char* description, Config::INTEGER def, SIntValueOptions&& options = {}); + + virtual ~CIntValue() = default; + + virtual const std::type_info* underlying() const override; + virtual void commence() override; + + Config::INTEGER value() const; + Config::INTEGER defaultVal() const; + + const std::optional m_min, m_max; + const std::optional> m_map; + + private: + CConfigValue m_val; + Config::INTEGER m_default = 0; + }; +} \ No newline at end of file diff --git a/src/config/values/types/StringValue.cpp b/src/config/values/types/StringValue.cpp new file mode 100644 index 000000000..d591ec966 --- /dev/null +++ b/src/config/values/types/StringValue.cpp @@ -0,0 +1,28 @@ +#include "StringValue.hpp" + +using namespace Config::Values; + +CStringValue::CStringValue(const char* name, const char* description, Config::STRING def, SStringValueOptions&& options) : + IValue(options.refresh), m_validator(std::move(options.validator)), m_default(def) { + m_name = name; + m_description = description; +} + +const std::type_info* CStringValue::underlying() const { + return &typeid(decltype(m_default)); +} + +void CStringValue::commence() { + m_val = CConfigValue(m_name); +} + +Config::STRING CStringValue::value() const { + if (!m_val.good()) + return m_default; + + return *m_val; +} + +Config::STRING CStringValue::defaultVal() const { + return m_default; +} diff --git a/src/config/values/types/StringValue.hpp b/src/config/values/types/StringValue.hpp new file mode 100644 index 000000000..d2bba5688 --- /dev/null +++ b/src/config/values/types/StringValue.hpp @@ -0,0 +1,34 @@ +#pragma once + +#include "IValue.hpp" + +#include "../../ConfigValue.hpp" + +#include +#include +#include + +namespace Config::Values { + struct SStringValueOptions { + std::optional(const Config::STRING&)>> validator = std::nullopt; + Supplementary::PropRefreshBits refresh = 0; + }; + + class CStringValue : public IValue { + public: + CStringValue(const char* name, const char* description, Config::STRING def, SStringValueOptions&& options = {}); + + virtual ~CStringValue() = default; + + virtual const std::type_info* underlying() const override; + virtual void commence() override; + + Config::STRING value() const; + Config::STRING defaultVal() const; + + private: + CConfigValue m_val; + std::optional(const std::string&)>> m_validator; + Config::STRING m_default = "[[EMPTY]]"; + }; +} diff --git a/src/config/values/types/Vec2Value.cpp b/src/config/values/types/Vec2Value.cpp new file mode 100644 index 000000000..9c14c2033 --- /dev/null +++ b/src/config/values/types/Vec2Value.cpp @@ -0,0 +1,28 @@ +#include "Vec2Value.hpp" + +using namespace Config::Values; + +CVec2Value::CVec2Value(const char* name, const char* description, Config::VEC2 def, SVec2ValueOptions&& options) : + IValue(options.refresh), m_validator(std::move(options.validator)), m_default(def) { + m_name = name; + m_description = description; +} + +const std::type_info* CVec2Value::underlying() const { + return &typeid(decltype(m_default)); +} + +void CVec2Value::commence() { + m_val = CConfigValue(m_name); +} + +Config::VEC2 CVec2Value::value() const { + if (!m_val.good()) + return m_default; + + return *m_val; +} + +Config::VEC2 CVec2Value::defaultVal() const { + return m_default; +} diff --git a/src/config/values/types/Vec2Value.hpp b/src/config/values/types/Vec2Value.hpp new file mode 100644 index 000000000..d79c2d203 --- /dev/null +++ b/src/config/values/types/Vec2Value.hpp @@ -0,0 +1,34 @@ +#pragma once + +#include "IValue.hpp" + +#include "../../ConfigValue.hpp" + +#include +#include +#include + +namespace Config::Values { + struct SVec2ValueOptions { + std::optional(const Config::VEC2&)>> validator = std::nullopt; + Supplementary::PropRefreshBits refresh = 0; + }; + + class CVec2Value : public IValue { + public: + CVec2Value(const char* name, const char* description, Config::VEC2 def, SVec2ValueOptions&& options = {}); + + virtual ~CVec2Value() = default; + + virtual const std::type_info* underlying() const override; + virtual void commence() override; + + Config::VEC2 value() const; + Config::VEC2 defaultVal() const; + + private: + CConfigValue m_val; + std::optional(const Config::VEC2&)>> m_validator; + Config::VEC2 m_default = {}; + }; +} diff --git a/src/debug/HyprCtl.cpp b/src/debug/HyprCtl.cpp index fd9cad75f..b334a26e3 100644 --- a/src/debug/HyprCtl.cpp +++ b/src/debug/HyprCtl.cpp @@ -34,13 +34,14 @@ using namespace Hyprutils::OS; #include "../config/shared/complex/ComplexDataTypes.hpp" #include "../config/legacy/ConfigManager.hpp" +#include "../config/lua/ConfigManager.hpp" #include "../config/ConfigValue.hpp" #include "../config/shared/complex/ComplexDataTypes.hpp" #include "../config/shared/inotify/ConfigWatcher.hpp" #include "../config/shared/workspace/WorkspaceRuleManager.hpp" #include "../config/shared/monitor/MonitorRuleManager.hpp" #include "../config/shared/animation/AnimationTree.hpp" -#include "../config/supplementary/ConfigDescriptions.hpp" +#include "../config/values/ConfigValues.hpp" #include "../managers/CursorManager.hpp" #include "../errorOverlay/Overlay.hpp" #include "../devices/IPointer.hpp" @@ -381,6 +382,8 @@ std::string CHyprCtl::getWindowData(PHLWINDOW w, eHyprCtlOutputFormat format) { "address": "0x{:x}", "mapped": {}, "hidden": {}, + "visible": {}, + "acceptsInput": {}, "at": [{}, {}], "size": [{}, {}], "workspace": {{ @@ -409,28 +412,31 @@ std::string CHyprCtl::getWindowData(PHLWINDOW w, eHyprCtlOutputFormat format) { "contentType": "{}", "stableId": "{:x}" }},)#", - rc(w.get()), (w->m_isMapped ? "true" : "false"), (w->isHidden() ? "true" : "false"), sc(w->m_realPosition->goal().x), - sc(w->m_realPosition->goal().y), sc(w->m_realSize->goal().x), sc(w->m_realSize->goal().y), w->m_workspace ? w->workspaceID() : WORKSPACE_INVALID, - escapeJSONStrings(!w->m_workspace ? "" : w->m_workspace->m_name), (sc(w->m_isFloating) == 1 ? "true" : "false"), w->monitorID(), escapeJSONStrings(w->m_class), - escapeJSONStrings(w->m_title), escapeJSONStrings(w->m_initialClass), escapeJSONStrings(w->m_initialTitle), w->getPID(), (sc(w->m_isX11) == 1 ? "true" : "false"), - (w->m_pinned ? "true" : "false"), sc(w->m_fullscreenState.internal), sc(w->m_fullscreenState.client), (w->m_createdOverFullscreen ? "true" : "false"), - getGroupedData(w, format), getTagsData(w, format), rc(w->m_swallowed.get()), getFocusHistoryID(w), - (g_pInputManager->isWindowInhibiting(w, false) ? "true" : "false"), escapeJSONStrings(w->xdgTag().value_or("")), escapeJSONStrings(w->xdgDescription().value_or("")), - escapeJSONStrings(NContentType::toString(w->getContentType())), w->m_stableID); + rc(w.get()), (w->m_isMapped ? "true" : "false"), (w->isHidden() ? "true" : "false"), (w->visible() ? "true" : "false"), + (w->acceptsInput() ? "true" : "false"), sc(w->m_realPosition->goal().x), sc(w->m_realPosition->goal().y), sc(w->m_realSize->goal().x), + sc(w->m_realSize->goal().y), w->m_workspace ? w->workspaceID() : WORKSPACE_INVALID, escapeJSONStrings(!w->m_workspace ? "" : w->m_workspace->m_name), + (sc(w->m_isFloating) == 1 ? "true" : "false"), w->monitorID(), escapeJSONStrings(w->m_class), escapeJSONStrings(w->m_title), escapeJSONStrings(w->m_initialClass), + escapeJSONStrings(w->m_initialTitle), w->getPID(), (sc(w->m_isX11) == 1 ? "true" : "false"), (w->m_pinned ? "true" : "false"), + sc(w->m_fullscreenState.internal), sc(w->m_fullscreenState.client), (w->m_createdOverFullscreen ? "true" : "false"), getGroupedData(w, format), + getTagsData(w, format), rc(w->m_swallowed.get()), getFocusHistoryID(w), (g_pInputManager->isWindowInhibiting(w, false) ? "true" : "false"), + escapeJSONStrings(w->xdgTag().value_or("")), escapeJSONStrings(w->xdgDescription().value_or("")), escapeJSONStrings(NContentType::toString(w->getContentType())), + w->m_stableID); } else { return std::format( - "Window {:x} -> {}:\n\tmapped: {}\n\thidden: {}\n\tat: {},{}\n\tsize: {},{}\n\tworkspace: {} ({})\n\tfloating: {}\n\tmonitor: {}\n\tclass: {}\n\ttitle: " + "Window {:x} -> {}:\n\tmapped: {}\n\thidden: {}\n\tvisible: {}\n\tacceptsInput: {}\n\tat: {},{}\n\tsize: {},{}\n\tworkspace: {} ({})\n\tfloating: {}\n\tmonitor: " + "{}\n\tclass: {}\n\ttitle: " "{}\n\tinitialClass: {}\n\tinitialTitle: {}\n\tpid: " "{}\n\txwayland: {}\n\tpinned: " "{}\n\tfullscreen: {}\n\tfullscreenClient: {}\n\toverFullscreen: {}\n\tgrouped: {}\n\ttags: {}\n\tswallowing: {:x}\n\tfocusHistoryID: {}\n\tinhibitingIdle: " "{}\n\txdgTag: " "{}\n\txdgDescription: {}\n\tcontentType: {}\n\tstableID: {:x}\n\n", - rc(w.get()), w->m_title, sc(w->m_isMapped), sc(w->isHidden()), sc(w->m_realPosition->goal().x), sc(w->m_realPosition->goal().y), - sc(w->m_realSize->goal().x), sc(w->m_realSize->goal().y), w->m_workspace ? w->workspaceID() : WORKSPACE_INVALID, - (!w->m_workspace ? "" : w->m_workspace->m_name), sc(w->m_isFloating), w->monitorID(), w->m_class, w->m_title, w->m_initialClass, w->m_initialTitle, w->getPID(), - sc(w->m_isX11), sc(w->m_pinned), sc(w->m_fullscreenState.internal), sc(w->m_fullscreenState.client), sc(w->m_createdOverFullscreen), - getGroupedData(w, format), getTagsData(w, format), rc(w->m_swallowed.get()), getFocusHistoryID(w), sc(g_pInputManager->isWindowInhibiting(w, false)), - w->xdgTag().value_or(""), w->xdgDescription().value_or(""), NContentType::toString(w->getContentType()), w->m_stableID); + rc(w.get()), w->m_title, sc(w->m_isMapped), sc(w->isHidden()), sc(w->visible()), sc(w->acceptsInput()), + sc(w->m_realPosition->goal().x), sc(w->m_realPosition->goal().y), sc(w->m_realSize->goal().x), sc(w->m_realSize->goal().y), + w->m_workspace ? w->workspaceID() : WORKSPACE_INVALID, (!w->m_workspace ? "" : w->m_workspace->m_name), sc(w->m_isFloating), w->monitorID(), w->m_class, + w->m_title, w->m_initialClass, w->m_initialTitle, w->getPID(), sc(w->m_isX11), sc(w->m_pinned), sc(w->m_fullscreenState.internal), + sc(w->m_fullscreenState.client), sc(w->m_createdOverFullscreen), getGroupedData(w, format), getTagsData(w, format), rc(w->m_swallowed.get()), + getFocusHistoryID(w), sc(g_pInputManager->isWindowInhibiting(w, false)), w->xdgTag().value_or(""), w->xdgDescription().value_or(""), + NContentType::toString(w->getContentType()), w->m_stableID); } } @@ -1009,11 +1015,13 @@ static std::string bindsRequest(eHyprCtlOutputFormat format, std::string request ret += "e"; if (kb->nonConsuming) ret += "n"; + if (kb->autoConsuming) + ret += "a"; if (kb->hasDescription) ret += "d"; ret += std::format("\n\tmodmask: {}\n\tsubmap: {}\n\tkey: {}\n\tkeycode: {}\n\tcatchall: {}\n\tdescription: {}\n\tdispatcher: {}\n\targ: {}\n\n", kb->modmask, - kb->submap.name, kb->key, kb->keycode, kb->catchAll, kb->description, kb->handler, kb->arg); + kb->submap.name, kb->key.empty() ? kb->displayKey : kb->key, kb->keycode, kb->catchAll, kb->description, kb->handler, kb->arg); } } else { // json @@ -1028,6 +1036,7 @@ static std::string bindsRequest(eHyprCtlOutputFormat format, std::string request "repeat": {}, "longPress": {}, "non_consuming": {}, + "auto_consuming": {}, "has_description": {}, "modmask": {}, "submap": "{}", @@ -1040,8 +1049,8 @@ static std::string bindsRequest(eHyprCtlOutputFormat format, std::string request "arg": "{}" }},)#", kb->locked ? "true" : "false", kb->mouse ? "true" : "false", kb->release ? "true" : "false", kb->repeat ? "true" : "false", kb->longPress ? "true" : "false", - kb->nonConsuming ? "true" : "false", kb->hasDescription ? "true" : "false", kb->modmask, escapeJSONStrings(kb->submap.name), kb->submapUniversal, - escapeJSONStrings(kb->key), kb->keycode, kb->catchAll ? "true" : "false", escapeJSONStrings(kb->description), escapeJSONStrings(kb->handler), + kb->nonConsuming ? "true" : "false", kb->autoConsuming ? "true" : "false", kb->hasDescription ? "true" : "false", kb->modmask, escapeJSONStrings(kb->submap.name), + kb->submapUniversal, escapeJSONStrings(kb->key), kb->keycode, kb->catchAll ? "true" : "false", escapeJSONStrings(kb->description), escapeJSONStrings(kb->handler), escapeJSONStrings(kb->arg)); } trimTrailingComma(ret); @@ -1246,10 +1255,39 @@ std::string systemInfoRequest(eHyprCtlOutputFormat format, std::string request) return result; } +static std::string evalRequest(eHyprCtlOutputFormat format, std::string request) { + if (Config::mgr()->type() != Config::CONFIG_LUA) + return "eval is only supported with the lua config manager"; + + auto luaMgr = dynamicPointerCast(WP(Config::mgr())); + + // strip the command name ("eval ") from the request + auto code = request.substr(request.find_first_of(' ') + 1); + + auto err = luaMgr->eval(code); + if (err) + return *err; + + return "ok"; +} + static std::string dispatchRequest(eHyprCtlOutputFormat format, std::string in) { // get rid of the dispatch keyword in = in.substr(in.find_first_of(' ') + 1); + if (Config::mgr()->type() == Config::CONFIG_LUA) { + // For lua, this is just a wrapper for `eval("hl.dispatch(in)") + std::string evalStr = std::format("return hl.dispatch({})", in); + auto luaMgr = dynamicPointerCast(WP(Config::mgr())); + auto ret = luaMgr->eval(evalStr).value_or("ok"); + + if (ret.starts_with("ok") || in.contains("(") /* this likely means the user is passing a valid lua dispatch string */) + return ret; + + // the user likely is trying to dispatch old hyprlang stuff via lua, let them know + return ret + "\n\n → Note: dispatch in lua is a shorthand for hl.dispatch(...), your syntax might need to be updated."; + } + const auto DISPATCHSTR = in.substr(0, in.find_first_of(' ')); auto DISPATCHARG = std::string(); @@ -1335,7 +1373,7 @@ static std::string dispatchKeyword(eHyprCtlOutputFormat format, std::string in) // decorations will probably need a repaint if (COMMAND.contains("decoration:") || COMMAND.contains("border") || COMMAND == "workspace" || COMMAND.contains("zoom_factor") || COMMAND == "source") { - static auto PZOOMFACTOR = CConfigValue("cursor:zoom_factor"); + static auto PZOOMFACTOR = CConfigValue("cursor:zoom_factor"); for (auto const& m : g_pCompositor->m_monitors) { *(m->m_cursorZoom) = *PZOOMFACTOR; g_pHyprRenderer->damageMonitor(m); @@ -1599,22 +1637,22 @@ static std::string dispatchGetProp(eHyprCtlOutputFormat format, std::string requ }; auto borderColorToString = [&](bool active) -> std::string { - static auto PACTIVECOL = CConfigValue("general:col.active_border"); - static auto PINACTIVECOL = CConfigValue("general:col.inactive_border"); - static auto PNOGROUPACTIVECOL = CConfigValue("general:col.nogroup_border_active"); - static auto PNOGROUPINACTIVECOL = CConfigValue("general:col.nogroup_border"); - static auto PGROUPACTIVECOL = CConfigValue("group:col.border_active"); - static auto PGROUPINACTIVECOL = CConfigValue("group:col.border_inactive"); - static auto PGROUPACTIVELOCKEDCOL = CConfigValue("group:col.border_locked_active"); - static auto PGROUPINACTIVELOCKEDCOL = CConfigValue("group:col.border_locked_inactive"); + static auto PACTIVECOL = CConfigValue("general:col.active_border"); + static auto PINACTIVECOL = CConfigValue("general:col.inactive_border"); + static auto PNOGROUPACTIVECOL = CConfigValue("general:col.nogroup_border_active"); + static auto PNOGROUPINACTIVECOL = CConfigValue("general:col.nogroup_border"); + static auto PGROUPACTIVECOL = CConfigValue("group:col.border_active"); + static auto PGROUPINACTIVECOL = CConfigValue("group:col.border_inactive"); + static auto PGROUPACTIVELOCKEDCOL = CConfigValue("group:col.border_locked_active"); + static auto PGROUPINACTIVELOCKEDCOL = CConfigValue("group:col.border_locked_inactive"); const bool GROUPLOCKED = PWINDOW->m_group ? PWINDOW->m_group->locked() : false; if (active) { - auto* const ACTIVECOL = (Config::CGradientValueData*)(PACTIVECOL.ptr())->getData(); - auto* const NOGROUPACTIVECOL = (Config::CGradientValueData*)(PNOGROUPACTIVECOL.ptr())->getData(); - auto* const GROUPACTIVECOL = (Config::CGradientValueData*)(PGROUPACTIVECOL.ptr())->getData(); - auto* const GROUPACTIVELOCKEDCOL = (Config::CGradientValueData*)(PGROUPACTIVELOCKEDCOL.ptr())->getData(); + auto* const ACTIVECOL = (Config::CGradientValueData*)(PACTIVECOL.ptr()); + auto* const NOGROUPACTIVECOL = (Config::CGradientValueData*)(PNOGROUPACTIVECOL.ptr()); + auto* const GROUPACTIVECOL = (Config::CGradientValueData*)(PGROUPACTIVECOL.ptr()); + auto* const GROUPACTIVELOCKEDCOL = (Config::CGradientValueData*)(PGROUPACTIVELOCKEDCOL.ptr()); const auto* const ACTIVECOLOR = !PWINDOW->m_group ? (!(PWINDOW->m_groupRules & Desktop::View::GROUP_DENY) ? ACTIVECOL : NOGROUPACTIVECOL) : (GROUPLOCKED ? GROUPACTIVELOCKEDCOL : GROUPACTIVECOL); @@ -1624,10 +1662,10 @@ static std::string dispatchGetProp(eHyprCtlOutputFormat format, std::string requ else return std::format(R"({{"{}": "{}"}})", PROP, borderColorString); } else { - auto* const INACTIVECOL = (Config::CGradientValueData*)(PINACTIVECOL.ptr())->getData(); - auto* const NOGROUPINACTIVECOL = (Config::CGradientValueData*)(PNOGROUPINACTIVECOL.ptr())->getData(); - auto* const GROUPINACTIVECOL = (Config::CGradientValueData*)(PGROUPINACTIVECOL.ptr())->getData(); - auto* const GROUPINACTIVELOCKEDCOL = (Config::CGradientValueData*)(PGROUPINACTIVELOCKEDCOL.ptr())->getData(); + auto* const INACTIVECOL = (Config::CGradientValueData*)(PINACTIVECOL.ptr()); + auto* const NOGROUPINACTIVECOL = (Config::CGradientValueData*)(PNOGROUPINACTIVECOL.ptr()); + auto* const GROUPINACTIVECOL = (Config::CGradientValueData*)(PGROUPINACTIVECOL.ptr()); + auto* const GROUPINACTIVELOCKEDCOL = (Config::CGradientValueData*)(PGROUPINACTIVELOCKEDCOL.ptr()); const auto* const INACTIVECOLOR = !PWINDOW->m_group ? (!(PWINDOW->m_groupRules & Desktop::View::GROUP_DENY) ? INACTIVECOL : NOGROUPINACTIVECOL) : (GROUPLOCKED ? GROUPINACTIVELOCKEDCOL : GROUPINACTIVECOL); @@ -1771,6 +1809,8 @@ static std::string dispatchGetOption(eHyprCtlOutputFormat format, std::string re return std::format("float: {:2f}\nset: {}", **rc(VAL), VAR.setByUser); else if (TYPE == typeid(Config::VEC2)) return std::format("vec2: [{}, {}]\nset: {}", (*rc(VAL))->x, (*rc(VAL))->y, VAR.setByUser); + else if (TYPE == typeid(Hyprlang::VEC2)) + return std::format("vec2: [{}, {}]\nset: {}", (*rc(VAL))->x, (*rc(VAL))->y, VAR.setByUser); else if (TYPE == typeid(Hyprlang::STRING)) return std::format("str: {}\nset: {}", *rc(VAL), VAR.setByUser); else if (TYPE == typeid(Config::STRING)) @@ -1785,6 +1825,9 @@ static std::string dispatchGetOption(eHyprCtlOutputFormat format, std::string re else if (TYPE == typeid(Config::VEC2)) return std::format(R"({{"option": "{}", "vec2": [{},{}], "set": {} }})", curitem, (*rc(VAL))->x, (*rc(VAL))->y, VAR.setByUser); + else if (TYPE == typeid(Hyprlang::VEC2)) + return std::format(R"({{"option": "{}", "vec2": [{},{}], "set": {} }})", curitem, (*rc(VAL))->x, (*rc(VAL))->y, + VAR.setByUser); else if (TYPE == typeid(Hyprlang::STRING)) return std::format(R"({{"option": "{}", "str": "{}", "set": {} }})", curitem, escapeJSONStrings(*rc(VAL)), VAR.setByUser); else if (TYPE == typeid(Config::STRING)) @@ -2043,18 +2086,7 @@ static std::string getIsLocked(eHyprCtlOutputFormat format, std::string request) } static std::string getDescriptions(eHyprCtlOutputFormat format, std::string request) { - std::string json = "["; - const auto& DESCS = Config::Supplementary::CONFIG_OPTIONS; - - for (const auto& d : DESCS) { - json += d.jsonify() + ",\n"; - } - - json.pop_back(); - json.pop_back(); - - json += "]\n"; - return json; + return Config::Values::getAsJson(); } static std::string submapRequest(eHyprCtlOutputFormat format, std::string request) { @@ -2065,6 +2097,43 @@ static std::string submapRequest(eHyprCtlOutputFormat format, std::string reques return format == FORMAT_JSON ? std::format("\"{}\"\n", escapeJSONStrings(submap)) : (submap + "\n"); } +static std::string statusRequest(eHyprCtlOutputFormat format, std::string request) { + Aquamarine::eBackendType backendType = Aquamarine::eBackendType::AQ_BACKEND_NULL; + + for (const auto& i : g_pCompositor->m_aqBackend->getImplementations()) { + if (i->type() == Aquamarine::eBackendType::AQ_BACKEND_NULL || i->type() == Aquamarine::eBackendType::AQ_BACKEND_HEADLESS) + continue; + + backendType = i->type(); + break; + } + + std::string backendStr; + + switch (backendType) { + case Aquamarine::AQ_BACKEND_DRM: backendStr = "drm"; break; + case Aquamarine::AQ_BACKEND_WAYLAND: backendStr = "wayland"; break; + default: backendStr = "error"; break; + } + + if (format == eHyprCtlOutputFormat::FORMAT_JSON) { + + return std::format(R"#( +{{ + "configProvider": "{}", + "backend": "{}" +}} +)#", + Config::typeToString(Config::mgr()->type()), backendStr); + } + + return std::format(R"#( +configProvider: {} +backend: {} +)#", + Config::typeToString(Config::mgr()->type()), backendStr); +} + static std::string reloadShaders(eHyprCtlOutputFormat format, std::string request) { CVarList vars(request, 0, ' '); @@ -2098,6 +2167,7 @@ CHyprCtl::CHyprCtl() { registerCommand(SHyprCtlCommand{"locked", true, getIsLocked}); registerCommand(SHyprCtlCommand{"descriptions", true, getDescriptions}); registerCommand(SHyprCtlCommand{"submap", true, submapRequest}); + registerCommand(SHyprCtlCommand{"status", true, statusRequest}); registerCommand(SHyprCtlCommand{.name = "reloadshaders", .exact = false, .fn = reloadShaders}); registerCommand(SHyprCtlCommand{"monitors", false, monitorsRequest}); @@ -2115,6 +2185,7 @@ CHyprCtl::CHyprCtl() { registerCommand(SHyprCtlCommand{"getoption", false, dispatchGetOption}); registerCommand(SHyprCtlCommand{"decorations", false, decorationRequest}); registerCommand(SHyprCtlCommand{"[[BATCH]]", false, dispatchBatch}); + registerCommand(SHyprCtlCommand{"eval", false, evalRequest}); startHyprCtlSocket(); } @@ -2200,6 +2271,7 @@ std::string CHyprCtl::getReply(std::string request) { if (reloadAll) { Config::monitorRuleMgr()->scheduleReload(); + Layout::Supplementary::algoMatcher()->updateWorkspaceLayouts(); g_pInputManager->setKeyboardLayout(); // update kb layout g_pInputManager->setPointerConfigs(); // update mouse cfgs diff --git a/src/debug/Overlay.cpp b/src/debug/Overlay.cpp index 694bb3207..3d5fdbc41 100644 --- a/src/debug/Overlay.cpp +++ b/src/debug/Overlay.cpp @@ -109,7 +109,7 @@ COverlay::COverlay() { } void CMonitorOverlay::renderData(PHLMONITOR pMonitor, float durationUs) { - static auto PDEBUGOVERLAY = CConfigValue("debug:overlay"); + static auto PDEBUGOVERLAY = CConfigValue("debug:overlay"); if (!*PDEBUGOVERLAY) return; @@ -125,7 +125,7 @@ void CMonitorOverlay::renderData(PHLMONITOR pMonitor, float durationUs) { } void CMonitorOverlay::renderDataNoOverlay(PHLMONITOR pMonitor, float durationUs) { - static auto PDEBUGOVERLAY = CConfigValue("debug:overlay"); + static auto PDEBUGOVERLAY = CConfigValue("debug:overlay"); if (!*PDEBUGOVERLAY) return; @@ -141,7 +141,7 @@ void CMonitorOverlay::renderDataNoOverlay(PHLMONITOR pMonitor, float durationUs) } void CMonitorOverlay::frameData(PHLMONITOR pMonitor) { - static auto PDEBUGOVERLAY = CConfigValue("debug:overlay"); + static auto PDEBUGOVERLAY = CConfigValue("debug:overlay"); if (!*PDEBUGOVERLAY) return; @@ -249,7 +249,7 @@ void CMonitorOverlay::rebuildCache() { const float IDEALFPS = std::max(1.F, PMONITOR->m_refreshRate); const float TICKTPS = ANIMATIONTICKMETRICS.avg <= 0.F ? 0.F : 1000.F / ANIMATIONTICKMETRICS.avg; - static auto FONTFAMILY = CConfigValue("misc:font_family"); + static auto FONTFAMILY = CConfigValue("misc:font_family"); CHyprColor fpsColor = CHyprColor{1.F, 0.2F, 0.2F, 1.F}; if (FPS > IDEALFPS * 0.95F) @@ -326,7 +326,7 @@ Vector2D CMonitorOverlay::size() const { } void COverlay::renderData(PHLMONITOR pMonitor, float durationUs) { - static auto PDEBUGOVERLAY = CConfigValue("debug:overlay"); + static auto PDEBUGOVERLAY = CConfigValue("debug:overlay"); if (!*PDEBUGOVERLAY) return; @@ -335,7 +335,7 @@ void COverlay::renderData(PHLMONITOR pMonitor, float durationUs) { } void COverlay::renderDataNoOverlay(PHLMONITOR pMonitor, float durationUs) { - static auto PDEBUGOVERLAY = CConfigValue("debug:overlay"); + static auto PDEBUGOVERLAY = CConfigValue("debug:overlay"); if (!*PDEBUGOVERLAY) return; @@ -344,7 +344,7 @@ void COverlay::renderDataNoOverlay(PHLMONITOR pMonitor, float durationUs) { } void COverlay::frameData(PHLMONITOR pMonitor) { - static auto PDEBUGOVERLAY = CConfigValue("debug:overlay"); + static auto PDEBUGOVERLAY = CConfigValue("debug:overlay"); if (!*PDEBUGOVERLAY) return; @@ -362,7 +362,7 @@ void COverlay::createWarningTexture(float maxW) { if (maxW == m_warningTextureMaxW) return; - static auto FONT = CConfigValue("misc:font_family"); + static auto FONT = CConfigValue("misc:font_family"); m_warningTexture = g_pHyprRenderer->renderText(Hyprgraphics::CTextResource::STextResourceData{ .text = "[!] FPS might be below your monitor's refresh rate if there are no content updates", diff --git a/src/debug/log/Logger.cpp b/src/debug/log/Logger.cpp index 1aca4d515..52ba475ce 100644 --- a/src/debug/log/Logger.cpp +++ b/src/debug/log/Logger.cpp @@ -43,10 +43,10 @@ void CLogger::initCallbacks() { } void CLogger::recheckCfg() { - static auto PDISABLELOGS = CConfigValue("debug:disable_logs"); - static auto PDISABLETIME = CConfigValue("debug:disable_time"); - static auto PENABLESTDOUT = CConfigValue("debug:enable_stdout_logs"); - static auto PENABLECOLOR = CConfigValue("debug:colored_stdout_logs"); + static auto PDISABLELOGS = CConfigValue("debug:disable_logs"); + static auto PDISABLETIME = CConfigValue("debug:disable_time"); + static auto PENABLESTDOUT = CConfigValue("debug:enable_stdout_logs"); + static auto PENABLECOLOR = CConfigValue("debug:colored_stdout_logs"); m_logger.setEnableStdout(!*PDISABLELOGS && *PENABLESTDOUT); m_logsEnabled = !*PDISABLELOGS; diff --git a/src/desktop/Workspace.cpp b/src/desktop/Workspace.cpp index cb2da6b3c..6d8e6cb49 100644 --- a/src/desktop/Workspace.cpp +++ b/src/desktop/Workspace.cpp @@ -426,7 +426,7 @@ int CWorkspace::getWindows(std::optional onlyTiled, std::optional on continue; if (onlyPinned.has_value() && (!t->window() || t->window()->m_pinned != onlyPinned.value())) continue; - if (onlyVisible.has_value() && (!t->window() || t->window()->isHidden() == onlyVisible.value())) + if (onlyVisible.has_value() && (!t->window() || t->window()->targetVisible() != onlyVisible.value())) continue; no++; } @@ -445,7 +445,7 @@ int CWorkspace::getGroups(std::optional onlyTiled, std::optional onl continue; if (onlyPinned.has_value() && HEAD->m_pinned != onlyPinned.value()) continue; - if (onlyVisible.has_value() && g->current()->isHidden() == onlyVisible.value()) + if (onlyVisible.has_value() && g->current()->targetVisible() != onlyVisible.value()) continue; no++; } @@ -454,7 +454,7 @@ int CWorkspace::getGroups(std::optional onlyTiled, std::optional onl PHLWINDOW CWorkspace::getFirstWindow() { for (auto const& w : g_pCompositor->m_windows) { - if (w->m_workspace == m_self && w->m_isMapped && !w->isHidden()) + if (w->m_workspace == m_self && w->m_isMapped && w->acceptsInput()) return w; } @@ -465,7 +465,7 @@ PHLWINDOW CWorkspace::getTopLeftWindow() { const auto PMONITOR = m_monitor.lock(); for (auto const& w : g_pCompositor->m_windows) { - if (w->m_workspace != m_self || !w->m_isMapped || w->isHidden()) + if (w->m_workspace != m_self || !w->m_isMapped || !w->acceptsInput()) continue; const auto WINDOWIDEALBB = w->getWindowIdealBoundingBoxIgnoreReserved(); diff --git a/src/desktop/Workspace.hpp b/src/desktop/Workspace.hpp index 81af38cd1..d778324de 100644 --- a/src/desktop/Workspace.hpp +++ b/src/desktop/Workspace.hpp @@ -37,8 +37,6 @@ class CWorkspace { bool m_hasFullscreenWindow = false; eFullscreenMode m_fullscreenMode = FSMODE_NONE; - wl_array m_wlrCoordinateArr; - // for animations PHLANIMVAR m_renderOffset; PHLANIMVAR m_alpha; @@ -54,10 +52,6 @@ class CWorkspace { // last window PHLWINDOWREF m_lastFocusedWindow; - // user-set - bool m_defaultFloating = false; - bool m_defaultPseudo = false; - // last monitor (used on reconnect) std::string m_lastMonitor = ""; diff --git a/src/desktop/history/WorkspaceHistoryTracker.cpp b/src/desktop/history/WorkspaceHistoryTracker.cpp index d4e8e5008..249586fac 100644 --- a/src/desktop/history/WorkspaceHistoryTracker.cpp +++ b/src/desktop/history/WorkspaceHistoryTracker.cpp @@ -37,7 +37,7 @@ void CWorkspaceHistoryTracker::track(PHLWORKSPACE ws) { if (!ws || !ws->m_monitor) return; - static auto PALLOWWORKSPACECYCLES = CConfigValue("binds:allow_workspace_cycles"); + static auto PALLOWWORKSPACECYCLES = CConfigValue("binds:allow_workspace_cycles"); if (!m_history.empty() && m_history.front().workspace == ws && !*PALLOWWORKSPACECYCLES) return; diff --git a/src/desktop/rule/Rule.cpp b/src/desktop/rule/Rule.cpp index 5e3141cdd..cfc1edf34 100644 --- a/src/desktop/rule/Rule.cpp +++ b/src/desktop/rule/Rule.cpp @@ -104,10 +104,22 @@ std::underlying_type_t IRule::getPropertiesMask() { return m_mask; } +void IRule::setEnabled(bool enable) { + m_enabled = enable; +} + +bool IRule::isEnabled() const { + return m_enabled; +} + bool IRule::has(eRuleProperty p) { return m_matchEngines.contains(p); } +bool IRule::canMatch() const { + return !m_matchEngines.empty() && m_enabled; +} + bool IRule::matches(eRuleProperty p, const std::string& s) { if (!has(p)) return false; diff --git a/src/desktop/rule/Rule.hpp b/src/desktop/rule/Rule.hpp index efd3cb39a..85c9b2c42 100644 --- a/src/desktop/rule/Rule.hpp +++ b/src/desktop/rule/Rule.hpp @@ -52,6 +52,8 @@ namespace Desktop::Rule { virtual std::underlying_type_t getPropertiesMask(); void registerMatch(eRuleProperty, const std::string&); + void setEnabled(bool enable); + bool isEnabled() const; void markAsExecRule(const std::string& token, uint64_t pid, bool persistent = false); bool isExecRule(); bool isExecPersistent(); @@ -66,13 +68,15 @@ namespace Desktop::Rule { bool matches(eRuleProperty, const std::string& s); bool matches(eRuleProperty, bool b); bool has(eRuleProperty); + bool canMatch() const; // std::unordered_map> m_matchEngines; private: - std::underlying_type_t m_mask = 0; - std::string m_name = ""; + std::underlying_type_t m_mask = 0; + std::string m_name = ""; + bool m_enabled = true; struct { bool isExecRule = false; diff --git a/src/desktop/rule/RuleWithEffects.hpp b/src/desktop/rule/RuleWithEffects.hpp new file mode 100644 index 000000000..17805e9be --- /dev/null +++ b/src/desktop/rule/RuleWithEffects.hpp @@ -0,0 +1,58 @@ +#pragma once + +#include "Rule.hpp" + +#include +#include +#include +#include +#include + +namespace Desktop::Rule { + template + class CRuleWithEffects : public IRule { + public: + using effectType = TEffect; + using storageType = TEffect::storageType; + using valueType = TEffect::valueType; + + eRuleType type() override { + return RULE_TYPE; + } + + std::expected addEffect(storageType e, const std::string& result) { + auto parsed = parseEffect(e, result); + if (!parsed) + return std::unexpected(parsed.error()); + + m_effects.emplace_back(TEffect{.key = e, .raw = result, .value = std::move(*parsed)}); + m_effectSet.emplace(e); + + return {}; + } + + const std::vector& effects() { + return m_effects; + } + + const std::unordered_set& effectsSet() { + return m_effectSet; + } + + protected: + CRuleWithEffects(const std::string& name = "") : IRule(name) {} + + virtual std::expected parseEffect(storageType e, const std::string& result) = 0; + + std::expected addParsedEffect(storageType e, valueType value, std::string raw = {}) { + m_effects.emplace_back(TEffect{.key = e, .raw = std::move(raw), .value = std::move(value)}); + m_effectSet.emplace(e); + + return {}; + } + + private: + std::vector m_effects; + std::unordered_set m_effectSet; + }; +} diff --git a/src/desktop/rule/layerRule/LayerRule.cpp b/src/desktop/rule/layerRule/LayerRule.cpp index eccd99142..11cdd7026 100644 --- a/src/desktop/rule/layerRule/LayerRule.cpp +++ b/src/desktop/rule/layerRule/LayerRule.cpp @@ -1,28 +1,100 @@ #include "LayerRule.hpp" #include "../../../debug/log/Logger.hpp" +#include "../../../helpers/MiscFunctions.hpp" #include "../../view/LayerSurface.hpp" +#include +#include +#include + using namespace Desktop; using namespace Desktop::Rule; +using namespace Hyprutils::String; -CLayerRule::CLayerRule(const std::string& name) : IRule(name) { +static const char* numericParseError(eNumericParseResult r) { + switch (r) { + case NUMERIC_PARSE_BAD: return "bad input"; + case NUMERIC_PARSE_GARBAGE: return "garbage input"; + case NUMERIC_PARSE_OUT_OF_RANGE: return "out of range"; + case NUMERIC_PARSE_OK: return "ok"; + default: return "error"; + } +} + +static std::expected parseInt(std::string_view effectName, const std::string& raw) { + auto parsed = strToNumber(raw); + if (!parsed) + return std::unexpected(std::format("{} rule \"{}\" failed with: {}", effectName, raw, numericParseError(parsed.error()))); + + return *parsed; +} + +static std::expected parseFloat(std::string_view effectName, const std::string& raw) { + auto parsed = strToNumber(raw); + if (!parsed) + return std::unexpected(std::format("{} rule \"{}\" failed with: {}", effectName, raw, numericParseError(parsed.error()))); + + return *parsed; +} + +static std::expected parseAboveLock(const std::string& raw) { + auto parsed = strToNumber(raw); + if (!parsed) + return std::unexpected(std::format("above_lock rule \"{}\" failed with: {}", raw, numericParseError(parsed.error()))); + + return std::clamp(*parsed, int64_t{0}, int64_t{2}); +} + +static std::expected parseLayerRuleEffect(CLayerRuleEffectContainer::storageType e, const std::string& raw) { + if (layerEffects()->isEffectDynamic(e)) + return std::string{raw}; + + const auto EFFECT_NAME = layerEffects()->get(e); + + switch (e) { + default: return std::unexpected(std::format("unknown layer rule effect {}", e)); + + case LAYER_RULE_EFFECT_NONE: return std::monostate{}; + + case LAYER_RULE_EFFECT_NO_ANIM: + case LAYER_RULE_EFFECT_BLUR: + case LAYER_RULE_EFFECT_BLUR_POPUPS: + case LAYER_RULE_EFFECT_DIM_AROUND: + case LAYER_RULE_EFFECT_XRAY: + case LAYER_RULE_EFFECT_NO_SCREEN_SHARE: return truthy(raw); + + case LAYER_RULE_EFFECT_ORDER: { + auto parsed = parseInt(EFFECT_NAME, raw); + if (!parsed) + return std::unexpected(parsed.error()); + return *parsed; + } + case LAYER_RULE_EFFECT_ABOVE_LOCK: { + auto parsed = parseAboveLock(raw); + if (!parsed) + return std::unexpected(parsed.error()); + return *parsed; + } + case LAYER_RULE_EFFECT_IGNORE_ALPHA: { + auto parsed = parseFloat(EFFECT_NAME, raw); + if (!parsed) + return std::unexpected(parsed.error()); + return std::clamp(*parsed, 0.F, 1.F); + } + case LAYER_RULE_EFFECT_ANIMATION: return std::string{raw}; + } +} + +CLayerRule::CLayerRule(const std::string& name) : CRuleWithEffects(name) { ; } -eRuleType CLayerRule::type() { - return RULE_TYPE_LAYER; -} - -void CLayerRule::addEffect(CLayerRule::storageType e, const std::string& result) { - m_effects.emplace_back(std::make_pair<>(e, result)); -} - -const std::vector>& CLayerRule::effects() { - return m_effects; +std::expected CLayerRule::parseEffect(CLayerRule::storageType e, const std::string& result) { + return parseLayerRuleEffect(e, result); } bool CLayerRule::matches(PHLLS ls) { - if (m_matchEngines.empty()) + if (!canMatch()) return false; for (const auto& [prop, engine] : m_matchEngines) { diff --git a/src/desktop/rule/layerRule/LayerRule.hpp b/src/desktop/rule/layerRule/LayerRule.hpp index 990796c12..8934d1c62 100644 --- a/src/desktop/rule/layerRule/LayerRule.hpp +++ b/src/desktop/rule/layerRule/LayerRule.hpp @@ -1,25 +1,35 @@ #pragma once -#include "../Rule.hpp" +#include "../RuleWithEffects.hpp" #include "../../DesktopTypes.hpp" #include "LayerRuleEffectContainer.hpp" +#include +#include + namespace Desktop::Rule { - class CLayerRule : public IRule { - public: + using LayerRuleEffectValue = std::variant; + + struct SLayerRuleEffect { using storageType = CLayerRuleEffectContainer::storageType; + using valueType = LayerRuleEffectValue; + + CLayerRuleEffectContainer::storageType key = LAYER_RULE_EFFECT_NONE; + std::string raw; + LayerRuleEffectValue value; + }; + + class CLayerRule : public CRuleWithEffects { + public: + using Base = CRuleWithEffects; + using storageType = Base::storageType; CLayerRule(const std::string& name = ""); virtual ~CLayerRule() = default; - virtual eRuleType type(); - - void addEffect(storageType e, const std::string& result); - const std::vector>& effects(); - - bool matches(PHLLS w); + bool matches(PHLLS w); private: - std::vector> m_effects; + std::expected parseEffect(storageType e, const std::string& result) override; }; }; diff --git a/src/desktop/rule/layerRule/LayerRuleApplicator.cpp b/src/desktop/rule/layerRule/LayerRuleApplicator.cpp index 8c157dae8..79ee80ae0 100644 --- a/src/desktop/rule/layerRule/LayerRuleApplicator.cpp +++ b/src/desktop/rule/layerRule/LayerRuleApplicator.cpp @@ -3,7 +3,6 @@ #include "../Engine.hpp" #include "../../view/LayerSurface.hpp" #include "../../types/OverridableVar.hpp" -#include "../../../helpers/MiscFunctions.hpp" #include "../../../event/EventBus.hpp" #include @@ -39,7 +38,11 @@ void CLayerRuleApplicator::resetProps(std::underlying_type_t prop } void CLayerRuleApplicator::applyDynamicRule(const SP& rule) { - for (const auto& [key, effect] : rule->effects()) { + for (const auto& effectData : rule->effects()) { + const auto key = effectData.key; + const auto& effect = effectData.raw; + const auto& value = effectData.value; + switch (key) { default: { if (key <= LAYER_RULE_EFFECT_LAST_STATIC) { @@ -68,58 +71,52 @@ void CLayerRuleApplicator::applyDynamicRule(const SP& rule) { break; } case LAYER_RULE_EFFECT_NO_ANIM: { - m_noanim.first.set(truthy(effect), Types::PRIORITY_WINDOW_RULE); + m_noanim.first.set(std::get(value), Types::PRIORITY_WINDOW_RULE); m_noanim.second |= rule->getPropertiesMask(); break; } case LAYER_RULE_EFFECT_BLUR: { - m_blur.first.set(truthy(effect), Types::PRIORITY_WINDOW_RULE); + m_blur.first.set(std::get(value), Types::PRIORITY_WINDOW_RULE); m_blur.second |= rule->getPropertiesMask(); break; } case LAYER_RULE_EFFECT_BLUR_POPUPS: { - m_blurPopups.first.set(truthy(effect), Types::PRIORITY_WINDOW_RULE); + m_blurPopups.first.set(std::get(value), Types::PRIORITY_WINDOW_RULE); m_blurPopups.second |= rule->getPropertiesMask(); break; } case LAYER_RULE_EFFECT_DIM_AROUND: { - m_dimAround.first.set(truthy(effect), Types::PRIORITY_WINDOW_RULE); + m_dimAround.first.set(std::get(value), Types::PRIORITY_WINDOW_RULE); m_dimAround.second |= rule->getPropertiesMask(); break; } case LAYER_RULE_EFFECT_XRAY: { - m_xray.first.set(truthy(effect), Types::PRIORITY_WINDOW_RULE); + m_xray.first.set(std::get(value), Types::PRIORITY_WINDOW_RULE); m_xray.second |= rule->getPropertiesMask(); break; } case LAYER_RULE_EFFECT_NO_SCREEN_SHARE: { - m_noScreenShare.first.set(truthy(effect), Types::PRIORITY_WINDOW_RULE); + m_noScreenShare.first.set(std::get(value), Types::PRIORITY_WINDOW_RULE); m_noScreenShare.second |= rule->getPropertiesMask(); break; } case LAYER_RULE_EFFECT_ORDER: { - try { - m_order.first.set(std::stoi(effect), Types::PRIORITY_WINDOW_RULE); - m_order.second |= rule->getPropertiesMask(); - } catch (...) { Log::logger->log(Log::ERR, "CLayerRuleApplicator::applyDynamicRule: invalid order {}", effect); } + m_order.first.set(std::get(value), Types::PRIORITY_WINDOW_RULE); + m_order.second |= rule->getPropertiesMask(); break; } case LAYER_RULE_EFFECT_ABOVE_LOCK: { - try { - m_aboveLock.first.set(std::clamp(std::stoull(effect), 0ULL, 2ULL), Types::PRIORITY_WINDOW_RULE); - m_aboveLock.second |= rule->getPropertiesMask(); - } catch (...) { Log::logger->log(Log::ERR, "CLayerRuleApplicator::applyDynamicRule: invalid order {}", effect); } + m_aboveLock.first.set(std::get(value), Types::PRIORITY_WINDOW_RULE); + m_aboveLock.second |= rule->getPropertiesMask(); break; } case LAYER_RULE_EFFECT_IGNORE_ALPHA: { - try { - m_ignoreAlpha.first.set(std::clamp(std::stof(effect), 0.F, 1.F), Types::PRIORITY_WINDOW_RULE); - m_ignoreAlpha.second |= rule->getPropertiesMask(); - } catch (...) { Log::logger->log(Log::ERR, "CLayerRuleApplicator::applyDynamicRule: invalid order {}", effect); } + m_ignoreAlpha.first.set(std::get(value), Types::PRIORITY_WINDOW_RULE); + m_ignoreAlpha.second |= rule->getPropertiesMask(); break; } case LAYER_RULE_EFFECT_ANIMATION: { - m_animationStyle.first.set(effect, Types::PRIORITY_WINDOW_RULE); + m_animationStyle.first.set(std::get(value), Types::PRIORITY_WINDOW_RULE); m_animationStyle.second |= rule->getPropertiesMask(); break; } diff --git a/src/desktop/rule/layerRule/LayerRuleApplicator.hpp b/src/desktop/rule/layerRule/LayerRuleApplicator.hpp index 5669ff870..b208aac67 100644 --- a/src/desktop/rule/layerRule/LayerRuleApplicator.hpp +++ b/src/desktop/rule/layerRule/LayerRuleApplicator.hpp @@ -54,10 +54,10 @@ namespace Desktop::Rule { DEFINE_PROP(bool, xray, false) DEFINE_PROP(bool, noScreenShare, false) - DEFINE_PROP(Hyprlang::INT, order, 0) - DEFINE_PROP(Hyprlang::INT, aboveLock, 0) + DEFINE_PROP(Config::INTEGER, order, 0) + DEFINE_PROP(Config::INTEGER, aboveLock, 0) - DEFINE_PROP(Hyprlang::FLOAT, ignoreAlpha, 0.F) + DEFINE_PROP(Config::FLOAT, ignoreAlpha, 0.F) DEFINE_PROP(std::string, animationStyle, std::string("")) diff --git a/src/desktop/rule/windowRule/WindowRule.cpp b/src/desktop/rule/windowRule/WindowRule.cpp index b53caedc1..10d39383b 100644 --- a/src/desktop/rule/windowRule/WindowRule.cpp +++ b/src/desktop/rule/windowRule/WindowRule.cpp @@ -1,35 +1,363 @@ #include "WindowRule.hpp" #include "../../view/Window.hpp" #include "../../../helpers/Monitor.hpp" +#include "../../../helpers/MiscFunctions.hpp" #include "../../../Compositor.hpp" #include "../../../managers/TokenManager.hpp" #include "../../../desktop/state/FocusState.hpp" +#include "../../../protocols/types/ContentType.hpp" +#include +#include +#include #include +#include +#include using namespace Desktop; using namespace Desktop::Rule; using namespace Hyprutils::String; -CWindowRule::CWindowRule(const std::string& name) : IRule(name) { +static const char* numericParseError(eNumericParseResult r) { + switch (r) { + case NUMERIC_PARSE_BAD: return "bad input"; + case NUMERIC_PARSE_GARBAGE: return "garbage input"; + case NUMERIC_PARSE_OUT_OF_RANGE: return "out of range"; + case NUMERIC_PARSE_OK: return "ok"; + default: return "error"; + } +} + +static std::expected parseInt(std::string_view effectName, const std::string& raw) { + auto parsed = strToNumber(raw); + if (!parsed) + return std::unexpected(std::format("{} rule \"{}\" failed with: {}", effectName, raw, numericParseError(parsed.error()))); + + return *parsed; +} + +static std::expected parseFloat(std::string_view effectName, const std::string& raw) { + auto parsed = strToNumber(raw); + if (!parsed) + return std::unexpected(std::format("{} rule \"{}\" failed with: {}", effectName, raw, numericParseError(parsed.error()))); + + return *parsed; +} + +static std::expected parseBorderColorToken(const std::string& raw, const std::string& token) { + auto parsed = configStringToInt(token); + if (!parsed) + return std::unexpected(std::format(R"(border_color rule "{}" has invalid color "{}": {})", raw, token, parsed.error())); + + return CHyprColor(*parsed); +} + +static std::expected parseFullscreenState(const std::string& raw) { + CVarList2 vars(std::string{raw}, 0, 's'); + + SFullscreenStateRule result; + + auto internal = strToNumber(vars[0]); + if (!internal) + return std::unexpected(std::format("fullscreen_state rule \"{}\" failed with: {}", raw, numericParseError(internal.error()))); + + result.internal = *internal; + + if (!vars[1].empty()) { + auto client = strToNumber(vars[1]); + if (!client) + return std::unexpected(std::format("fullscreen_state rule \"{}\" failed with: {}", raw, numericParseError(client.error()))); + + result.client = *client; + } + + return result; +} + +static std::expected parseIdleInhibitMode(const std::string& raw) { + if (raw == "none") + return IDLEINHIBIT_NONE; + if (raw == "always") + return IDLEINHIBIT_ALWAYS; + if (raw == "focus") + return IDLEINHIBIT_FOCUS; + if (raw == "fullscreen") + return IDLEINHIBIT_FULLSCREEN; + + return std::unexpected(std::format("idle_inhibit rule has unknown mode \"{}\"", raw)); +} + +static std::expected parseOpacityRule(const std::string& raw) { + try { + CVarList2 vars(std::string{raw}, 0, ' '); + + int opacityIDX = 0; + SOpacityRule result; + + for (const auto& r : vars) { + if (r == "opacity") + continue; + + if (r == "override") { + if (opacityIDX == 1) + result.alpha.overridden = true; + else if (opacityIDX == 2) + result.alphaInactive.overridden = true; + else if (opacityIDX == 3) + result.alphaFullscreen.overridden = true; + } else { + auto alpha = strToNumber(r); + if (!alpha) + return std::unexpected(std::format("opacity rule \"{}\" failed with: {}", raw, numericParseError(alpha.error()))); + + if (opacityIDX == 0) + result.alpha.alpha = *alpha; + else if (opacityIDX == 1) + result.alphaInactive.alpha = *alpha; + else if (opacityIDX == 2) + result.alphaFullscreen.alpha = *alpha; + else + throw std::runtime_error("more than 3 alpha values"); + + opacityIDX++; + } + } + + if (opacityIDX == 1) { + result.alphaInactive = result.alpha; + result.alphaFullscreen = result.alpha; + } + + return result; + } catch (std::exception& e) { return std::unexpected(std::format("opacity rule \"{}\" failed with: {}", raw, e.what())); } +} + +static std::expected parseBorderColorRule(const std::string& raw) { + try { + Config::CGradientValueData activeBorderGradient = {}; + Config::CGradientValueData inactiveBorderGradient = {}; + bool active = true; + CVarList colorsAndAngles = CVarList(trim(raw), 0, 's', true); + + if (colorsAndAngles.size() == 2 && !colorsAndAngles[1].contains("deg")) { + auto activeColor = parseBorderColorToken(raw, colorsAndAngles[0]); + if (!activeColor) + return std::unexpected(activeColor.error()); + + auto inactiveColor = parseBorderColorToken(raw, colorsAndAngles[1]); + if (!inactiveColor) + return std::unexpected(inactiveColor.error()); + + return SBorderColorRule{ + .active = Config::CGradientValueData(*activeColor), + .inactive = Config::CGradientValueData(*inactiveColor), + }; + } + + for (auto const& token : colorsAndAngles) { + if (active && token.contains("deg")) { + auto angle = strToNumber(token.substr(0, token.size() - 3)); + if (!angle) + return std::unexpected(std::format("border_color rule \"{}\" has invalid angle \"{}\": {}", raw, token, numericParseError(angle.error()))); + + activeBorderGradient.m_angle = *angle * (PI / 180.0); + active = false; + } else if (token.contains("deg")) { + auto angle = strToNumber(token.substr(0, token.size() - 3)); + if (!angle) + return std::unexpected(std::format("border_color rule \"{}\" has invalid angle \"{}\": {}", raw, token, numericParseError(angle.error()))); + + inactiveBorderGradient.m_angle = *angle * (PI / 180.0); + } else { + auto color = parseBorderColorToken(raw, token); + if (!color) + return std::unexpected(color.error()); + + if (active) + activeBorderGradient.m_colors.emplace_back(*color); + else + inactiveBorderGradient.m_colors.emplace_back(*color); + } + } + + activeBorderGradient.updateColorsOk(); + + if (activeBorderGradient.m_colors.size() > 10 || inactiveBorderGradient.m_colors.size() > 10) + return std::unexpected(std::format("border_color rule \"{}\" has more than 10 colors in one gradient", raw)); + if (activeBorderGradient.m_colors.empty()) + return std::unexpected(std::format("border_color rule \"{}\" has no colors", raw)); + + SBorderColorRule result{.active = activeBorderGradient}; + if (!inactiveBorderGradient.m_colors.empty()) + result.inactive = inactiveBorderGradient; + + return result; + } catch (std::exception& e) { return std::unexpected(std::format("border_color rule \"{}\" failed with: {}", raw, e.what())); } +} + +static std::vector parseStringList(const std::string& raw) { + std::vector result; + CVarList2 varlist(std::string{raw}, 0, 's'); + + for (const auto& e : varlist) { + result.emplace_back(e); + } + + return result; +} + +static std::expected parseWindowRuleEffect(CWindowRuleEffectContainer::storageType e, const std::string& raw) { + if (windowEffects()->isEffectDynamic(e)) + return std::string{raw}; + + const auto EFFECT_NAME = windowEffects()->get(e); + + switch (e) { + default: return std::unexpected(std::format("unknown window rule effect {}", e)); + + case WINDOW_RULE_EFFECT_NONE: return std::monostate{}; + + case WINDOW_RULE_EFFECT_FLOAT: + case WINDOW_RULE_EFFECT_TILE: + case WINDOW_RULE_EFFECT_FULLSCREEN: + case WINDOW_RULE_EFFECT_MAXIMIZE: + case WINDOW_RULE_EFFECT_CENTER: + case WINDOW_RULE_EFFECT_PSEUDO: + case WINDOW_RULE_EFFECT_NOINITIALFOCUS: + case WINDOW_RULE_EFFECT_PIN: + case WINDOW_RULE_EFFECT_PERSISTENT_SIZE: + case WINDOW_RULE_EFFECT_ALLOWS_INPUT: + case WINDOW_RULE_EFFECT_DIM_AROUND: + case WINDOW_RULE_EFFECT_DECORATE: + case WINDOW_RULE_EFFECT_FOCUS_ON_ACTIVATE: + case WINDOW_RULE_EFFECT_KEEP_ASPECT_RATIO: + case WINDOW_RULE_EFFECT_NEAREST_NEIGHBOR: + case WINDOW_RULE_EFFECT_NO_ANIM: + case WINDOW_RULE_EFFECT_NO_BLUR: + case WINDOW_RULE_EFFECT_NO_DIM: + case WINDOW_RULE_EFFECT_NO_FOCUS: + case WINDOW_RULE_EFFECT_NO_FOLLOW_MOUSE: + case WINDOW_RULE_EFFECT_NO_MAX_SIZE: + case WINDOW_RULE_EFFECT_NO_SHADOW: + case WINDOW_RULE_EFFECT_NO_SHORTCUTS_INHIBIT: + case WINDOW_RULE_EFFECT_OPAQUE: + case WINDOW_RULE_EFFECT_FORCE_RGBX: + case WINDOW_RULE_EFFECT_SYNC_FULLSCREEN: + case WINDOW_RULE_EFFECT_IMMEDIATE: + case WINDOW_RULE_EFFECT_XRAY: + case WINDOW_RULE_EFFECT_RENDER_UNFOCUSED: + case WINDOW_RULE_EFFECT_NO_SCREEN_SHARE: + case WINDOW_RULE_EFFECT_NO_VRR: + case WINDOW_RULE_EFFECT_STAY_FOCUSED: return truthy(raw); + + case WINDOW_RULE_EFFECT_FULLSCREENSTATE: { + auto parsed = parseFullscreenState(raw); + if (!parsed) + return std::unexpected(parsed.error()); + return *parsed; + } + + case WINDOW_RULE_EFFECT_MOVE: + case WINDOW_RULE_EFFECT_SIZE: + case WINDOW_RULE_EFFECT_MAX_SIZE: + case WINDOW_RULE_EFFECT_MIN_SIZE: { + auto parsed = Math::parseExpressionVec2(raw); + if (!parsed) + return std::unexpected(std::format("{} rule \"{}\" failed with: {}", EFFECT_NAME, raw, parsed.error())); + return *parsed; + } + + case WINDOW_RULE_EFFECT_MONITOR: + case WINDOW_RULE_EFFECT_WORKSPACE: + case WINDOW_RULE_EFFECT_GROUP: + case WINDOW_RULE_EFFECT_ANIMATION: + case WINDOW_RULE_EFFECT_TAG: return std::string{raw}; + + case WINDOW_RULE_EFFECT_SUPPRESSEVENT: return parseStringList(raw); + + case WINDOW_RULE_EFFECT_CONTENT: return sc(NContentType::fromString(raw)); + + case WINDOW_RULE_EFFECT_NOCLOSEFOR: + case WINDOW_RULE_EFFECT_BORDER_SIZE: { + auto parsed = parseInt(EFFECT_NAME, raw); + if (!parsed) + return std::unexpected(parsed.error()); + return *parsed; + } + + case WINDOW_RULE_EFFECT_ROUNDING: { + auto parsed = parseInt(EFFECT_NAME, raw); + if (!parsed) + return std::unexpected(parsed.error()); + if (*parsed < 0) + return std::unexpected(std::format("{} rule \"{}\" must be non-negative", EFFECT_NAME, raw)); + return *parsed; + } + + case WINDOW_RULE_EFFECT_SCROLLING_WIDTH: { + auto parsed = parseFloat(EFFECT_NAME, raw); + if (!parsed) + return std::unexpected(parsed.error()); + return *parsed; + } + + case WINDOW_RULE_EFFECT_ROUNDING_POWER: { + auto parsed = parseFloat(EFFECT_NAME, raw); + if (!parsed) + return std::unexpected(parsed.error()); + return std::clamp(*parsed, 1.F, 10.F); + } + case WINDOW_RULE_EFFECT_SCROLL_MOUSE: + case WINDOW_RULE_EFFECT_SCROLL_TOUCHPAD: { + auto parsed = parseFloat(EFFECT_NAME, raw); + if (!parsed) + return std::unexpected(parsed.error()); + return std::clamp(*parsed, 0.01F, 10.F); + } + + case WINDOW_RULE_EFFECT_BORDER_COLOR: { + auto parsed = parseBorderColorRule(raw); + if (!parsed) + return std::unexpected(parsed.error()); + return *parsed; + } + case WINDOW_RULE_EFFECT_IDLE_INHIBIT: { + auto parsed = parseIdleInhibitMode(raw); + if (!parsed) + return std::unexpected(parsed.error()); + return *parsed; + } + case WINDOW_RULE_EFFECT_OPACITY: { + auto parsed = parseOpacityRule(raw); + if (!parsed) + return std::unexpected(parsed.error()); + return *parsed; + } + } +} + +CWindowRule::CWindowRule(const std::string& name) : CRuleWithEffects(name) { ; } -eRuleType CWindowRule::type() { - return RULE_TYPE_WINDOW; +std::expected CWindowRule::addEffect(CWindowRule::storageType e, const Math::SExpressionVec2& expr) { + switch (e) { + case WINDOW_RULE_EFFECT_MOVE: + case WINDOW_RULE_EFFECT_SIZE: + case WINDOW_RULE_EFFECT_MAX_SIZE: + case WINDOW_RULE_EFFECT_MIN_SIZE: break; + default: return std::unexpected(std::format("{} is not an expression vec2 window rule effect", windowEffects()->get(e))); + } + + return addParsedEffect(e, expr, expr.toString()); } -void CWindowRule::addEffect(CWindowRule::storageType e, const std::string& result) { - m_effects.emplace_back(std::make_pair<>(e, result)); - m_effectSet.emplace(e); -} - -const std::vector>& CWindowRule::effects() { - return m_effects; +std::expected CWindowRule::parseEffect(CWindowRule::storageType e, const std::string& result) { + return parseWindowRuleEffect(e, result); } bool CWindowRule::matches(PHLWINDOW w, bool allowEnvLookup) { - if (m_matchEngines.empty()) + if (!canMatch()) return false; for (const auto& [prop, engine] : m_matchEngines) { @@ -131,7 +459,7 @@ bool CWindowRule::matches(PHLWINDOW w, bool allowEnvLookup) { return true; } -SP CWindowRule::buildFromExecString(std::string&& s) { +std::expected, std::string> CWindowRule::buildFromExecString(std::string&& s) { CVarList2 varlist(std::move(s), 0, ';'); SP wr = makeShared("__exec_rule"); @@ -146,7 +474,9 @@ SP CWindowRule::buildFromExecString(std::string&& s) { if (!EFFECT.has_value() || *EFFECT == WINDOW_RULE_EFFECT_NONE) continue; // invalid... - wr->addEffect(*EFFECT, std::string{el.substr(spacePos + 1)}); + auto res = wr->addEffect(*EFFECT, std::string{el.substr(spacePos + 1)}); + if (!res) + return std::unexpected(res.error()); continue; } @@ -157,12 +487,10 @@ SP CWindowRule::buildFromExecString(std::string&& s) { if (!EFFECT.has_value() || *EFFECT == WINDOW_RULE_EFFECT_NONE) continue; // invalid... - wr->addEffect(*EFFECT, std::string{"1"}); + auto res = wr->addEffect(*EFFECT, std::string{"1"}); + if (!res) + return std::unexpected(res.error()); } return wr; } - -const std::unordered_set& CWindowRule::effectsSet() { - return m_effectSet; -} diff --git a/src/desktop/rule/windowRule/WindowRule.hpp b/src/desktop/rule/windowRule/WindowRule.hpp index f76218281..3701ae35f 100644 --- a/src/desktop/rule/windowRule/WindowRule.hpp +++ b/src/desktop/rule/windowRule/WindowRule.hpp @@ -1,35 +1,70 @@ #pragma once -#include "../Rule.hpp" +#include "../RuleWithEffects.hpp" #include "../../DesktopTypes.hpp" +#include "../../types/OverridableVar.hpp" #include "WindowRuleEffectContainer.hpp" +#include "../../../config/shared/complex/ComplexDataTypes.hpp" +#include "../../../helpers/math/Expression.hpp" #include "../../../helpers/math/Math.hpp" +#include #include +#include namespace Desktop::Rule { constexpr const char* EXEC_RULE_ENV_NAME = "HL_EXEC_RULE_TOKEN"; - class CWindowRule : public IRule { - private: + struct SFullscreenStateRule { + int internal = 0; + std::optional client; + }; + + struct SOpacityRule { + Types::SAlphaValue alpha; + Types::SAlphaValue alphaInactive; + Types::SAlphaValue alphaFullscreen; + }; + + struct SBorderColorRule { + Config::CGradientValueData active; + std::optional inactive; + }; + + using WindowRuleEffectValue = + std::variant, SFullscreenStateRule, SOpacityRule, SBorderColorRule, Math::SExpressionVec2>; + + struct SWindowRuleEffect { using storageType = CWindowRuleEffectContainer::storageType; + using valueType = WindowRuleEffectValue; + + CWindowRuleEffectContainer::storageType key = WINDOW_RULE_EFFECT_NONE; + std::string raw; + WindowRuleEffectValue value; + }; + + class CWindowRule : public CRuleWithEffects { + private: + using Base = CRuleWithEffects; + using storageType = Base::storageType; public: CWindowRule(const std::string& name = ""); virtual ~CWindowRule() = default; - static SP buildFromExecString(std::string&&); + using Base::addEffect; - virtual eRuleType type(); + CWindowRule(const CWindowRule&) = default; + CWindowRule(CWindowRule&) = default; + CWindowRule(CWindowRule&&) = default; - void addEffect(storageType e, const std::string& result); - const std::vector>& effects(); - const std::unordered_set& effectsSet(); + static std::expected, std::string> buildFromExecString(std::string&&); - bool matches(PHLWINDOW w, bool allowEnvLookup = false); + std::expected addEffect(storageType e, const Math::SExpressionVec2& expr); + + bool matches(PHLWINDOW w, bool allowEnvLookup = false); private: - std::vector> m_effects; - std::unordered_set m_effectSet; + std::expected parseEffect(storageType e, const std::string& result) override; }; }; diff --git a/src/desktop/rule/windowRule/WindowRuleApplicator.cpp b/src/desktop/rule/windowRule/WindowRuleApplicator.cpp index 4af0d2ba1..c19c63389 100644 --- a/src/desktop/rule/windowRule/WindowRuleApplicator.cpp +++ b/src/desktop/rule/windowRule/WindowRuleApplicator.cpp @@ -6,33 +6,26 @@ #include "../../types/OverridableVar.hpp" #include "../../../event/EventBus.hpp" -#include -#include -#include #include -using namespace Hyprutils::String; - using namespace Desktop; using namespace Desktop::Rule; -namespace { - template - void resetRuleProp(std::pair, std::underlying_type_t>& prop, - std::underlying_type_t props, Desktop::Types::eOverridePriority prio, - std::unordered_set& effectsNuked, TEffect&& effect) { - auto& [value, propMask] = prop; +template +static void resetRuleProp(std::pair, std::underlying_type_t>& prop, + std::underlying_type_t props, Desktop::Types::eOverridePriority prio, + std::unordered_set& effectsNuked, TEffect&& effect) { + auto& [value, propMask] = prop; - if (!(propMask & props)) - return; + if (!(propMask & props)) + return; - if (prio == Desktop::Types::PRIORITY_WINDOW_RULE) { - effectsNuked.emplace(effect()); - propMask &= ~props; - } - - value.unset(prio); + if (prio == Desktop::Types::PRIORITY_WINDOW_RULE) { + effectsNuked.emplace(effect()); + propMask &= ~props; } + + value.unset(prio); } CWindowRuleApplicator::CWindowRuleApplicator(PHLWINDOW w) : m_window(w) { @@ -85,7 +78,11 @@ std::unordered_set CWindowRuleApplicato CWindowRuleApplicator::SRuleResult CWindowRuleApplicator::applyDynamicRule(const SP& rule) { SRuleResult result; - for (const auto& [key, effect] : rule->effects()) { + for (const auto& effectData : rule->effects()) { + const auto key = effectData.key; + const auto& effect = effectData.raw; + const auto& value = effectData.value; + switch (key) { default: { if (key <= WINDOW_RULE_EFFECT_LAST_STATIC) { @@ -115,133 +112,44 @@ CWindowRuleApplicator::SRuleResult CWindowRuleApplicator::applyDynamicRule(const break; } case WINDOW_RULE_EFFECT_ROUNDING: { - try { - m_rounding.first.set(std::stoull(effect), Types::PRIORITY_WINDOW_RULE); - m_rounding.second |= rule->getPropertiesMask(); - } catch (...) { Log::logger->log(Log::ERR, "CWindowRuleApplicator::applyDynamicRule: invalid rounding {}", effect); } + m_rounding.first.set(std::get(value), Types::PRIORITY_WINDOW_RULE); + m_rounding.second |= rule->getPropertiesMask(); break; } case WINDOW_RULE_EFFECT_ROUNDING_POWER: { - try { - m_roundingPower.first.set(std::clamp(std::stof(effect), 1.F, 10.F), Types::PRIORITY_WINDOW_RULE); - m_roundingPower.second |= rule->getPropertiesMask(); - } catch (...) { Log::logger->log(Log::ERR, "CWindowRuleApplicator::applyDynamicRule: invalid rounding_power {}", effect); } + m_roundingPower.first.set(std::get(value), Types::PRIORITY_WINDOW_RULE); + m_roundingPower.second |= rule->getPropertiesMask(); break; } case WINDOW_RULE_EFFECT_PERSISTENT_SIZE: { - m_persistentSize.first.set(truthy(effect), Types::PRIORITY_WINDOW_RULE); + m_persistentSize.first.set(std::get(value), Types::PRIORITY_WINDOW_RULE); m_persistentSize.second |= rule->getPropertiesMask(); break; } case WINDOW_RULE_EFFECT_ANIMATION: { - m_animationStyle.first.set(effect, Types::PRIORITY_WINDOW_RULE); + m_animationStyle.first.set(std::get(value), Types::PRIORITY_WINDOW_RULE); m_animationStyle.second |= rule->getPropertiesMask(); break; } case WINDOW_RULE_EFFECT_BORDER_COLOR: { - try { - // Each vector will only get used if it has at least one color - Config::CGradientValueData activeBorderGradient = {}; - Config::CGradientValueData inactiveBorderGradient = {}; - bool active = true; - CVarList colorsAndAngles = CVarList(trim(effect), 0, 's', true); - - // Basic form has only two colors, everything else can be parsed as a gradient - if (colorsAndAngles.size() == 2 && !colorsAndAngles[1].contains("deg")) { - m_activeBorderColor.first = - Types::COverridableVar(Config::CGradientValueData(CHyprColor(configStringToInt(colorsAndAngles[0]).value_or(0))), Types::PRIORITY_WINDOW_RULE); - m_inactiveBorderColor.first = - Types::COverridableVar(Config::CGradientValueData(CHyprColor(configStringToInt(colorsAndAngles[1]).value_or(0))), Types::PRIORITY_WINDOW_RULE); - m_activeBorderColor.second |= rule->getPropertiesMask(); - m_inactiveBorderColor.second |= rule->getPropertiesMask(); - break; - } - - for (auto const& token : colorsAndAngles) { - // The first angle, or an explicit "0deg", splits the two gradients - if (active && token.contains("deg")) { - activeBorderGradient.m_angle = std::stoi(token.substr(0, token.size() - 3)) * (PI / 180.0); - active = false; - } else if (token.contains("deg")) - inactiveBorderGradient.m_angle = std::stoi(token.substr(0, token.size() - 3)) * (PI / 180.0); - else if (active) - activeBorderGradient.m_colors.emplace_back(configStringToInt(token).value_or(0)); - else - inactiveBorderGradient.m_colors.emplace_back(configStringToInt(token).value_or(0)); - } - - activeBorderGradient.updateColorsOk(); - - // Includes sanity checks for the number of colors in each gradient - if (activeBorderGradient.m_colors.size() > 10 || inactiveBorderGradient.m_colors.size() > 10) - Log::logger->log(Log::WARN, "Bordercolor rule \"{}\" has more than 10 colors in one gradient, ignoring", effect); - else if (activeBorderGradient.m_colors.empty()) - Log::logger->log(Log::WARN, "Bordercolor rule \"{}\" has no colors, ignoring", effect); - else if (inactiveBorderGradient.m_colors.empty()) - m_activeBorderColor.first = Types::COverridableVar(activeBorderGradient, Types::PRIORITY_WINDOW_RULE); - else { - m_activeBorderColor.first = Types::COverridableVar(activeBorderGradient, Types::PRIORITY_WINDOW_RULE); - m_inactiveBorderColor.first = Types::COverridableVar(inactiveBorderGradient, Types::PRIORITY_WINDOW_RULE); - } - } catch (std::exception& e) { Log::logger->log(Log::ERR, "BorderColor rule \"{}\" failed with: {}", effect, e.what()); } + const auto& borderColor = std::get(value); + m_activeBorderColor.first = Types::COverridableVar(borderColor.active, Types::PRIORITY_WINDOW_RULE); + if (borderColor.inactive) + m_inactiveBorderColor.first = Types::COverridableVar(*borderColor.inactive, Types::PRIORITY_WINDOW_RULE); m_activeBorderColor.second = rule->getPropertiesMask(); m_inactiveBorderColor.second = rule->getPropertiesMask(); break; } case WINDOW_RULE_EFFECT_IDLE_INHIBIT: { - if (effect == "none") - m_idleInhibitMode.first.set(IDLEINHIBIT_NONE, Types::PRIORITY_WINDOW_RULE); - else if (effect == "always") - m_idleInhibitMode.first.set(IDLEINHIBIT_ALWAYS, Types::PRIORITY_WINDOW_RULE); - else if (effect == "focus") - m_idleInhibitMode.first.set(IDLEINHIBIT_FOCUS, Types::PRIORITY_WINDOW_RULE); - else if (effect == "fullscreen") - m_idleInhibitMode.first.set(IDLEINHIBIT_FULLSCREEN, Types::PRIORITY_WINDOW_RULE); - else - Log::logger->log(Log::ERR, "Rule idleinhibit: unknown mode {}", effect); + m_idleInhibitMode.first.set(std::get(value), Types::PRIORITY_WINDOW_RULE); m_idleInhibitMode.second = rule->getPropertiesMask(); break; } case WINDOW_RULE_EFFECT_OPACITY: { - try { - CVarList2 vars(std::string{effect}, 0, ' '); - - int opacityIDX = 0; - - for (const auto& r : vars) { - if (r == "opacity") - continue; - - if (r == "override") { - if (opacityIDX == 1) - m_alpha.first = Types::COverridableVar(Types::SAlphaValue{.alpha = m_alpha.first.value().alpha, .overridden = true}, Types::PRIORITY_WINDOW_RULE); - else if (opacityIDX == 2) - m_alphaInactive.first = - Types::COverridableVar(Types::SAlphaValue{.alpha = m_alphaInactive.first.value().alpha, .overridden = true}, Types::PRIORITY_WINDOW_RULE); - else if (opacityIDX == 3) - m_alphaFullscreen.first = - Types::COverridableVar(Types::SAlphaValue{.alpha = m_alphaFullscreen.first.value().alpha, .overridden = true}, Types::PRIORITY_WINDOW_RULE); - } else { - if (opacityIDX == 0) - m_alpha.first = Types::COverridableVar(Types::SAlphaValue{.alpha = std::stof(std::string{r}), .overridden = false}, Types::PRIORITY_WINDOW_RULE); - else if (opacityIDX == 1) - m_alphaInactive.first = - Types::COverridableVar(Types::SAlphaValue{.alpha = std::stof(std::string{r}), .overridden = false}, Types::PRIORITY_WINDOW_RULE); - else if (opacityIDX == 2) - m_alphaFullscreen.first = - Types::COverridableVar(Types::SAlphaValue{.alpha = std::stof(std::string{r}), .overridden = false}, Types::PRIORITY_WINDOW_RULE); - else - throw std::runtime_error("more than 3 alpha values"); - - opacityIDX++; - } - } - - if (opacityIDX == 1) { - m_alphaInactive.first = m_alpha.first; - m_alphaFullscreen.first = m_alpha.first; - } - } catch (std::exception& e) { Log::logger->log(Log::ERR, "Opacity rule \"{}\" failed with: {}", effect, e.what()); } + const auto& opacity = std::get(value); + m_alpha.first = Types::COverridableVar(opacity.alpha, Types::PRIORITY_WINDOW_RULE); + m_alphaInactive.first = Types::COverridableVar(opacity.alphaInactive, Types::PRIORITY_WINDOW_RULE); + m_alphaFullscreen.first = Types::COverridableVar(opacity.alphaFullscreen, Types::PRIORITY_WINDOW_RULE); m_alpha.second = rule->getPropertiesMask(); m_alphaInactive.second = rule->getPropertiesMask(); m_alphaFullscreen.second = rule->getPropertiesMask(); @@ -255,14 +163,18 @@ CWindowRuleApplicator::SRuleResult CWindowRuleApplicator::applyDynamicRule(const } case WINDOW_RULE_EFFECT_MAX_SIZE: { try { - static auto PCLAMP_TILED = CConfigValue("misc:size_limits_tiled"); + static auto PCLAMP_TILED = CConfigValue("misc:size_limits_tiled"); if (!m_window) break; - const auto VEC = m_window->calculateExpression(effect); + const auto& expr = std::get(value); + if (expr.empty()) + break; + + const auto VEC = m_window->calculateExpression(expr); if (!VEC) { - Log::logger->log(Log::ERR, "failed to parse {} as an expression", effect); + Log::logger->log(Log::ERR, "failed to parse {} as an expression", expr.toString()); break; } if (VEC->x < 1 || VEC->y < 1) { @@ -274,20 +186,24 @@ CWindowRuleApplicator::SRuleResult CWindowRuleApplicator::applyDynamicRule(const if (*PCLAMP_TILED || m_window->m_isFloating) m_window->clampWindowSize(std::nullopt, m_maxSize.first.value()); - } catch (std::exception& e) { Log::logger->log(Log::ERR, "maxsize rule \"{}\" failed with: {}", effect, e.what()); } + } catch (std::exception& e) { Log::logger->log(Log::ERR, "maxsize rule \"{}\" failed with: {}", std::get(value).toString(), e.what()); } m_maxSize.second = rule->getPropertiesMask(); break; } case WINDOW_RULE_EFFECT_MIN_SIZE: { try { - static auto PCLAMP_TILED = CConfigValue("misc:size_limits_tiled"); + static auto PCLAMP_TILED = CConfigValue("misc:size_limits_tiled"); if (!m_window) break; - const auto VEC = m_window->calculateExpression(effect); + const auto& expr = std::get(value); + if (expr.empty()) + break; + + const auto VEC = m_window->calculateExpression(expr); if (!VEC) { - Log::logger->log(Log::ERR, "failed to parse {} as an expression", effect); + Log::logger->log(Log::ERR, "failed to parse {} as an expression", expr.toString()); break; } @@ -299,147 +215,141 @@ CWindowRuleApplicator::SRuleResult CWindowRuleApplicator::applyDynamicRule(const m_minSize.first = Types::COverridableVar(*VEC, Types::PRIORITY_WINDOW_RULE); if (*PCLAMP_TILED || m_window->m_isFloating) m_window->clampWindowSize(m_minSize.first.value(), std::nullopt); - } catch (std::exception& e) { Log::logger->log(Log::ERR, "minsize rule \"{}\" failed with: {}", effect, e.what()); } + } catch (std::exception& e) { Log::logger->log(Log::ERR, "minsize rule \"{}\" failed with: {}", std::get(value).toString(), e.what()); } m_minSize.second = rule->getPropertiesMask(); break; } case WINDOW_RULE_EFFECT_BORDER_SIZE: { - try { - auto oldBorderSize = m_borderSize.first.valueOrDefault(); - m_borderSize.first.set(std::stoi(effect), Types::PRIORITY_WINDOW_RULE); - m_borderSize.second |= rule->getPropertiesMask(); - if (oldBorderSize != m_borderSize.first.valueOrDefault()) - result.needsRelayout = true; - } catch (...) { Log::logger->log(Log::ERR, "CWindowRuleApplicator::applyDynamicRule: invalid border_size {}", effect); } + auto oldBorderSize = m_borderSize.first.valueOrDefault(); + m_borderSize.first.set(std::get(value), Types::PRIORITY_WINDOW_RULE); + m_borderSize.second |= rule->getPropertiesMask(); + if (oldBorderSize != m_borderSize.first.valueOrDefault()) + result.needsRelayout = true; break; } case WINDOW_RULE_EFFECT_ALLOWS_INPUT: { - m_allowsInput.first.set(truthy(effect), Types::PRIORITY_WINDOW_RULE); + m_allowsInput.first.set(std::get(value), Types::PRIORITY_WINDOW_RULE); m_allowsInput.second |= rule->getPropertiesMask(); break; } case WINDOW_RULE_EFFECT_DIM_AROUND: { - m_dimAround.first.set(truthy(effect), Types::PRIORITY_WINDOW_RULE); + m_dimAround.first.set(std::get(value), Types::PRIORITY_WINDOW_RULE); m_dimAround.second |= rule->getPropertiesMask(); break; } case WINDOW_RULE_EFFECT_DECORATE: { - m_decorate.first.set(truthy(effect), Types::PRIORITY_WINDOW_RULE); + m_decorate.first.set(std::get(value), Types::PRIORITY_WINDOW_RULE); m_decorate.second |= rule->getPropertiesMask(); break; } case WINDOW_RULE_EFFECT_FOCUS_ON_ACTIVATE: { - m_focusOnActivate.first.set(truthy(effect), Types::PRIORITY_WINDOW_RULE); + m_focusOnActivate.first.set(std::get(value), Types::PRIORITY_WINDOW_RULE); m_focusOnActivate.second |= rule->getPropertiesMask(); break; } case WINDOW_RULE_EFFECT_KEEP_ASPECT_RATIO: { - m_keepAspectRatio.first.set(truthy(effect), Types::PRIORITY_WINDOW_RULE); + m_keepAspectRatio.first.set(std::get(value), Types::PRIORITY_WINDOW_RULE); m_keepAspectRatio.second |= rule->getPropertiesMask(); break; } case WINDOW_RULE_EFFECT_NEAREST_NEIGHBOR: { - m_nearestNeighbor.first.set(truthy(effect), Types::PRIORITY_WINDOW_RULE); + m_nearestNeighbor.first.set(std::get(value), Types::PRIORITY_WINDOW_RULE); m_nearestNeighbor.second |= rule->getPropertiesMask(); break; } case WINDOW_RULE_EFFECT_NO_ANIM: { - m_noAnim.first.set(truthy(effect), Types::PRIORITY_WINDOW_RULE); + m_noAnim.first.set(std::get(value), Types::PRIORITY_WINDOW_RULE); m_noAnim.second |= rule->getPropertiesMask(); break; } case WINDOW_RULE_EFFECT_NO_BLUR: { - m_noBlur.first.set(truthy(effect), Types::PRIORITY_WINDOW_RULE); + m_noBlur.first.set(std::get(value), Types::PRIORITY_WINDOW_RULE); m_noBlur.second |= rule->getPropertiesMask(); break; } case WINDOW_RULE_EFFECT_NO_DIM: { - m_noDim.first.set(truthy(effect), Types::PRIORITY_WINDOW_RULE); + m_noDim.first.set(std::get(value), Types::PRIORITY_WINDOW_RULE); m_noDim.second |= rule->getPropertiesMask(); break; } case WINDOW_RULE_EFFECT_NO_FOCUS: { - m_noFocus.first.set(truthy(effect), Types::PRIORITY_WINDOW_RULE); + m_noFocus.first.set(std::get(value), Types::PRIORITY_WINDOW_RULE); m_noFocus.second |= rule->getPropertiesMask(); break; } case WINDOW_RULE_EFFECT_NO_FOLLOW_MOUSE: { - m_noFollowMouse.first.set(truthy(effect), Types::PRIORITY_WINDOW_RULE); + m_noFollowMouse.first.set(std::get(value), Types::PRIORITY_WINDOW_RULE); m_noFollowMouse.second |= rule->getPropertiesMask(); break; } case WINDOW_RULE_EFFECT_NO_MAX_SIZE: { - m_noMaxSize.first.set(truthy(effect), Types::PRIORITY_WINDOW_RULE); + m_noMaxSize.first.set(std::get(value), Types::PRIORITY_WINDOW_RULE); m_noMaxSize.second |= rule->getPropertiesMask(); break; } case WINDOW_RULE_EFFECT_NO_SHADOW: { - m_noShadow.first.set(truthy(effect), Types::PRIORITY_WINDOW_RULE); + m_noShadow.first.set(std::get(value), Types::PRIORITY_WINDOW_RULE); m_noShadow.second |= rule->getPropertiesMask(); break; } case WINDOW_RULE_EFFECT_NO_SHORTCUTS_INHIBIT: { - m_noShortcutsInhibit.first.set(truthy(effect), Types::PRIORITY_WINDOW_RULE); + m_noShortcutsInhibit.first.set(std::get(value), Types::PRIORITY_WINDOW_RULE); m_noShortcutsInhibit.second |= rule->getPropertiesMask(); break; } case WINDOW_RULE_EFFECT_OPAQUE: { - m_opaque.first.set(truthy(effect), Types::PRIORITY_WINDOW_RULE); + m_opaque.first.set(std::get(value), Types::PRIORITY_WINDOW_RULE); m_opaque.second |= rule->getPropertiesMask(); break; } case WINDOW_RULE_EFFECT_FORCE_RGBX: { - m_RGBX.first.set(truthy(effect), Types::PRIORITY_WINDOW_RULE); + m_RGBX.first.set(std::get(value), Types::PRIORITY_WINDOW_RULE); m_RGBX.second |= rule->getPropertiesMask(); break; } case WINDOW_RULE_EFFECT_SYNC_FULLSCREEN: { - m_syncFullscreen.first.set(truthy(effect), Types::PRIORITY_WINDOW_RULE); + m_syncFullscreen.first.set(std::get(value), Types::PRIORITY_WINDOW_RULE); m_syncFullscreen.second |= rule->getPropertiesMask(); break; } case WINDOW_RULE_EFFECT_IMMEDIATE: { - m_tearing.first.set(truthy(effect), Types::PRIORITY_WINDOW_RULE); + m_tearing.first.set(std::get(value), Types::PRIORITY_WINDOW_RULE); m_tearing.second |= rule->getPropertiesMask(); break; } case WINDOW_RULE_EFFECT_XRAY: { - m_xray.first.set(truthy(effect), Types::PRIORITY_WINDOW_RULE); + m_xray.first.set(std::get(value), Types::PRIORITY_WINDOW_RULE); m_xray.second |= rule->getPropertiesMask(); break; } case WINDOW_RULE_EFFECT_RENDER_UNFOCUSED: { - m_renderUnfocused.first.set(truthy(effect), Types::PRIORITY_WINDOW_RULE); + m_renderUnfocused.first.set(std::get(value), Types::PRIORITY_WINDOW_RULE); m_renderUnfocused.second |= rule->getPropertiesMask(); break; } case WINDOW_RULE_EFFECT_NO_SCREEN_SHARE: { - m_noScreenShare.first.set(truthy(effect), Types::PRIORITY_WINDOW_RULE); + m_noScreenShare.first.set(std::get(value), Types::PRIORITY_WINDOW_RULE); m_noScreenShare.second |= rule->getPropertiesMask(); break; } case WINDOW_RULE_EFFECT_NO_VRR: { - m_noVRR.first.set(truthy(effect), Types::PRIORITY_WINDOW_RULE); + m_noVRR.first.set(std::get(value), Types::PRIORITY_WINDOW_RULE); m_noVRR.second |= rule->getPropertiesMask(); break; } case WINDOW_RULE_EFFECT_STAY_FOCUSED: { - m_stayFocused.first.set(truthy(effect), Types::PRIORITY_WINDOW_RULE); + m_stayFocused.first.set(std::get(value), Types::PRIORITY_WINDOW_RULE); m_stayFocused.second |= rule->getPropertiesMask(); break; } case WINDOW_RULE_EFFECT_SCROLL_MOUSE: { - try { - m_scrollMouse.first.set(std::clamp(std::stof(effect), 0.01F, 10.F), Types::PRIORITY_WINDOW_RULE); - m_scrollMouse.second |= rule->getPropertiesMask(); - } catch (...) { Log::logger->log(Log::ERR, "CWindowRuleApplicator::applyDynamicRule: invalid scroll_mouse {}", effect); } + m_scrollMouse.first.set(std::get(value), Types::PRIORITY_WINDOW_RULE); + m_scrollMouse.second |= rule->getPropertiesMask(); break; } case WINDOW_RULE_EFFECT_SCROLL_TOUCHPAD: { - try { - m_scrollTouchpad.first.set(std::clamp(std::stof(effect), 0.01F, 10.F), Types::PRIORITY_WINDOW_RULE); - m_scrollTouchpad.second |= rule->getPropertiesMask(); - } catch (...) { Log::logger->log(Log::ERR, "CWindowRuleApplicator::applyDynamicRule: invalid scroll_touchpad {}", effect); } + m_scrollTouchpad.first.set(std::get(value), Types::PRIORITY_WINDOW_RULE); + m_scrollTouchpad.second |= rule->getPropertiesMask(); break; } } @@ -448,7 +358,10 @@ CWindowRuleApplicator::SRuleResult CWindowRuleApplicator::applyDynamicRule(const } CWindowRuleApplicator::SRuleResult CWindowRuleApplicator::applyStaticRule(const SP& rule) { - for (const auto& [key, effect] : rule->effects()) { + for (const auto& effectData : rule->effects()) { + const auto key = effectData.key; + const auto& value = effectData.value; + switch (key) { default: { Log::logger->log(Log::TRACE, "CWindowRuleApplicator::applyStaticRule: Skipping effect {}, not static", sc>(key)); @@ -456,89 +369,90 @@ CWindowRuleApplicator::SRuleResult CWindowRuleApplicator::applyStaticRule(const } case WINDOW_RULE_EFFECT_FLOAT: { - static_.floating = truthy(effect); + static_.floating = std::get(value); break; } case WINDOW_RULE_EFFECT_TILE: { - static_.floating = !truthy(effect); + static_.floating = !std::get(value); break; } case WINDOW_RULE_EFFECT_FULLSCREEN: { - static_.fullscreen = truthy(effect); + static_.fullscreen = std::get(value); break; } case WINDOW_RULE_EFFECT_MAXIMIZE: { - static_.maximize = truthy(effect); + static_.maximize = std::get(value); break; } case WINDOW_RULE_EFFECT_FULLSCREENSTATE: { - CVarList2 vars(std::string{effect}, 0, 's'); - try { - static_.fullscreenStateInternal = std::stoi(std::string{vars[0]}); - if (!vars[1].empty()) - static_.fullscreenStateClient = std::stoi(std::string{vars[1]}); - } catch (...) { Log::logger->log(Log::ERR, "CWindowRuleApplicator::applyStaticRule: invalid fullscreen state {}", effect); } + const auto& fullscreenState = std::get(value); + static_.fullscreenStateInternal = fullscreenState.internal; + if (fullscreenState.client) + static_.fullscreenStateClient = *fullscreenState.client; break; } case WINDOW_RULE_EFFECT_MOVE: { static_.center = std::nullopt; - static_.position = effect; + const auto& expr = std::get(value); + if (expr.empty()) + static_.position.reset(); + else + static_.position = expr; break; } case WINDOW_RULE_EFFECT_SIZE: { - static_.size = effect; + const auto& expr = std::get(value); + if (expr.empty()) + static_.size.reset(); + else + static_.size = expr; break; } case WINDOW_RULE_EFFECT_CENTER: { - static_.position.clear(); - static_.center = truthy(effect); + static_.position.reset(); + static_.center = std::get(value); break; } case WINDOW_RULE_EFFECT_PSEUDO: { - static_.pseudo = truthy(effect); + static_.pseudo = std::get(value); break; } case WINDOW_RULE_EFFECT_MONITOR: { - static_.monitor = effect; + static_.monitor = std::get(value); break; } case WINDOW_RULE_EFFECT_WORKSPACE: { - static_.workspace = effect; + static_.workspace = std::get(value); break; } case WINDOW_RULE_EFFECT_NOINITIALFOCUS: { - static_.noInitialFocus = truthy(effect); + static_.noInitialFocus = std::get(value); break; } case WINDOW_RULE_EFFECT_PIN: { - static_.pin = truthy(effect); + static_.pin = std::get(value); break; } case WINDOW_RULE_EFFECT_GROUP: { - static_.group = effect; + static_.group = std::get(value); break; } case WINDOW_RULE_EFFECT_SUPPRESSEVENT: { - CVarList2 varlist(std::string{effect}, 0, 's'); - for (const auto& e : varlist) { + for (const auto& e : std::get>(value)) { static_.suppressEvent.emplace_back(e); } break; } case WINDOW_RULE_EFFECT_CONTENT: { - static_.content = NContentType::fromString(effect); + static_.content = std::get(value); break; } case WINDOW_RULE_EFFECT_NOCLOSEFOR: { - try { - static_.noCloseFor = std::stoi(effect); - } catch (...) { Log::logger->log(Log::ERR, "CWindowRuleApplicator::applyStaticRule: invalid no close for {}", effect); } + static_.noCloseFor = std::get(value); break; } case WINDOW_RULE_EFFECT_SCROLLING_WIDTH: { - try { - static_.scrollingWidth = std::stof(effect); - } catch (...) { Log::logger->log(Log::ERR, "CWindowRuleApplicator::applyStaticRule: invalid scrolling width {}", effect); } + static_.scrollingWidth = std::get(value); break; } } diff --git a/src/desktop/rule/windowRule/WindowRuleApplicator.hpp b/src/desktop/rule/windowRule/WindowRuleApplicator.hpp index 4227368f2..f5e436092 100644 --- a/src/desktop/rule/windowRule/WindowRuleApplicator.hpp +++ b/src/desktop/rule/windowRule/WindowRuleApplicator.hpp @@ -8,6 +8,7 @@ #include "../Rule.hpp" #include "../../types/OverridableVar.hpp" #include "../../../helpers/math/Math.hpp" +#include "../../../helpers/math/Expression.hpp" #include "../../../helpers/TagKeeper.hpp" #include "../../../config/shared/complex/ComplexDataTypes.hpp" @@ -38,25 +39,25 @@ namespace Desktop::Rule { // static props struct { - std::string monitor, workspace, group; + std::string monitor, workspace, group; - std::optional floating; - std::optional fullscreen; - std::optional maximize; - std::optional pseudo; - std::optional pin; - std::optional noInitialFocus; - std::optional center; + std::optional floating; + std::optional fullscreen; + std::optional maximize; + std::optional pseudo; + std::optional pin; + std::optional noInitialFocus; + std::optional center; - std::optional fullscreenStateClient; - std::optional fullscreenStateInternal; - std::optional content; - std::optional noCloseFor; + std::optional fullscreenStateClient; + std::optional fullscreenStateInternal; + std::optional content; + std::optional noCloseFor; - std::string size, position; - std::optional scrollingWidth; + std::optional size, position; + std::optional scrollingWidth; - std::vector suppressEvent; + std::vector suppressEvent; } static_; struct SCustomPropContainer { @@ -118,12 +119,12 @@ namespace Desktop::Rule { DEFINE_PROP(int, idleInhibitMode, false, WINDOW_RULE_EFFECT_IDLE_INHIBIT) - DEFINE_PROP(Hyprlang::INT, borderSize, {std::string("general:border_size") COMMA sc(0) COMMA std::nullopt}, WINDOW_RULE_EFFECT_BORDER_SIZE) - DEFINE_PROP(Hyprlang::INT, rounding, {std::string("decoration:rounding") COMMA sc(0) COMMA std::nullopt}, WINDOW_RULE_EFFECT_ROUNDING) + DEFINE_PROP(Config::INTEGER, borderSize, {std::string("general:border_size") COMMA sc(0) COMMA std::nullopt}, WINDOW_RULE_EFFECT_BORDER_SIZE) + DEFINE_PROP(Config::INTEGER, rounding, {std::string("decoration:rounding") COMMA sc(0) COMMA std::nullopt}, WINDOW_RULE_EFFECT_ROUNDING) - DEFINE_PROP(Hyprlang::FLOAT, roundingPower, {std::string("decoration:rounding_power")}, WINDOW_RULE_EFFECT_ROUNDING_POWER) - DEFINE_PROP(Hyprlang::FLOAT, scrollMouse, {std::string("input:scroll_factor")}, WINDOW_RULE_EFFECT_SCROLL_MOUSE) - DEFINE_PROP(Hyprlang::FLOAT, scrollTouchpad, {std::string("input:touchpad:scroll_factor")}, WINDOW_RULE_EFFECT_SCROLL_TOUCHPAD) + DEFINE_PROP(Config::FLOAT, roundingPower, {std::string("decoration:rounding_power")}, WINDOW_RULE_EFFECT_ROUNDING_POWER) + DEFINE_PROP(Config::FLOAT, scrollMouse, {std::string("input:scroll_factor")}, WINDOW_RULE_EFFECT_SCROLL_MOUSE) + DEFINE_PROP(Config::FLOAT, scrollTouchpad, {std::string("input:touchpad:scroll_factor")}, WINDOW_RULE_EFFECT_SCROLL_TOUCHPAD) DEFINE_PROP(std::string, animationStyle, std::string(""), WINDOW_RULE_EFFECT_ANIMATION) diff --git a/src/desktop/state/FocusState.cpp b/src/desktop/state/FocusState.cpp index c12987668..5db4bac5c 100644 --- a/src/desktop/state/FocusState.cpp +++ b/src/desktop/state/FocusState.cpp @@ -37,12 +37,13 @@ static SFullscreenWorkspaceFocusResult onFullscreenWorkspaceFocusWindow(PHLWINDO if (pWindow->m_isFloating) { // if the window is floating, just bring it to the top pWindow->m_createdOverFullscreen = true; + pWindow->updateFullscreenInputState(); g_pDesktopAnimationManager->setFullscreenFloatingFade(pWindow, 1.f); g_pHyprRenderer->damageWindow(pWindow); return {}; } - static auto PONFOCUSUNDERFS = CConfigValue("misc:on_focus_under_fullscreen"); + static auto PONFOCUSUNDERFS = CConfigValue("misc:on_focus_under_fullscreen"); switch (*PONFOCUSUNDERFS) { case 0: @@ -80,7 +81,7 @@ void CFocusState::fullWindowFocus(PHLWINDOW pWindow, eFocusReason reason, SP("general:modal_parent_blocking"); + static auto PMODALPARENTBLOCKING = CConfigValue("general:modal_parent_blocking"); if (*PMODALPARENTBLOCKING && pWindow && pWindow->m_xdgSurface && pWindow->m_xdgSurface->m_toplevel && pWindow->m_xdgSurface->m_toplevel->anyChildModal()) { Log::logger->log(Log::DEBUG, "Refusing focus to window shadowed by modal dialog"); @@ -91,8 +92,8 @@ void CFocusState::fullWindowFocus(PHLWINDOW pWindow, eFocusReason reason, SP surface) { - static auto PFOLLOWMOUSE = CConfigValue("input:follow_mouse"); - static auto PSPECIALFALLTHROUGH = CConfigValue("input:special_fallthrough"); + static auto PFOLLOWMOUSE = CConfigValue("input:follow_mouse"); + static auto PSPECIALFALLTHROUGH = CConfigValue("input:special_fallthrough"); if (!pWindow || !pWindow->priorityFocus()) { if (g_pSessionLockManager->isSessionLocked()) { diff --git a/src/desktop/types/MultiAnimatedVariable.hpp b/src/desktop/types/MultiAnimatedVariable.hpp new file mode 100644 index 000000000..564a2db36 --- /dev/null +++ b/src/desktop/types/MultiAnimatedVariable.hpp @@ -0,0 +1,184 @@ +#pragma once + +#include +#include + +#include "../../helpers/AnimatedVariable.hpp" + +namespace Desktop::Types { + + template + struct SAddOperation { + static constexpr T identity() { + return T{}; + } + + static T apply(const T& lhs, const T& rhs) { + return lhs + rhs; + } + }; + + template + struct SMultiplyOperation { + static constexpr T identity() { + return T{1}; + } + + static T apply(const T& lhs, const T& rhs) { + return lhs * rhs; + } + }; + + template > + class CMultiAnimatedVariableContainer { + public: + using value_type = VarType; + using key_type = Key; + using operation_type = Operation; + + static constexpr size_t size() { + return Count; + } + + PHLANIMVAR& get(const Key key) { + return m_vars.at(index(key)); + } + + const PHLANIMVAR& get(const Key key) const { + return m_vars.at(index(key)); + } + + PHLANIMVAR& operator[](const Key key) { + return get(key); + } + + const PHLANIMVAR& operator[](const Key key) const { + return get(key); + } + + PHLANIMVAR& raw(const size_t i) { + return m_vars.at(i); + } + + const PHLANIMVAR& raw(const size_t i) const { + return m_vars.at(i); + } + + std::array, Count>& all() { + return m_vars; + } + + const std::array, Count>& all() const { + return m_vars; + } + + template + void forEach(Fn&& fn) { + for (size_t i = 0; i < Count; ++i) + fn(static_cast(i), m_vars.at(i)); + } + + template + void forEach(Fn&& fn) const { + for (size_t i = 0; i < Count; ++i) + fn(static_cast(i), m_vars.at(i)); + } + + VarType getTotal() const { + return accumulate([](const auto& var) { return var->value(); }); + } + + VarType getTotalGoal() const { + return accumulate([](const auto& var) { return var->goal(); }); + } + + VarType getTotalBegun() const { + return accumulate([](const auto& var) { return var->begun(); }); + } + + VarType getTotalWithout(const Key key) const { + return accumulateExcept(key, [](const auto& var) { return var->value(); }); + } + + VarType getTotalGoalWithout(const Key key) const { + return accumulateExcept(key, [](const auto& var) { return var->goal(); }); + } + + VarType getTotalBegunWithout(const Key key) const { + return accumulateExcept(key, [](const auto& var) { return var->begun(); }); + } + + VarType value() const { + return getTotal(); + } + + VarType goal() const { + return getTotalGoal(); + } + + bool isBeingAnimated() const { + for (const auto& var : m_vars) { + if (var && var->isBeingAnimated()) + return true; + } + + return false; + } + + bool initialized() const { + for (const auto& var : m_vars) { + if (!var) + return false; + } + + return true; + } + + void warp(const bool endCallback = true) { + for (auto& var : m_vars) { + if (var) + var->warp(endCallback); + } + } + + private: + static constexpr size_t index(const Key key) { + return static_cast(key); + } + + template + VarType accumulate(Getter&& getter) const { + VarType result = Operation::identity(); + + for (const auto& var : m_vars) { + if (!var) + continue; + + result = Operation::apply(result, getter(var)); + } + + return result; + } + + template + VarType accumulateExcept(const Key key, Getter&& getter) const { + VarType result = Operation::identity(); + const size_t except = index(key); + + for (size_t i = 0; i < Count; ++i) { + if (i == except || !m_vars.at(i)) + continue; + + result = Operation::apply(result, getter(m_vars.at(i))); + } + + return result; + } + + std::array, Count> m_vars; + }; + + template > + using CMultiAVarContainer = CMultiAnimatedVariableContainer; + +} diff --git a/src/desktop/types/OverridableVar.hpp b/src/desktop/types/OverridableVar.hpp index 065da167a..df5ecd410 100644 --- a/src/desktop/types/OverridableVar.hpp +++ b/src/desktop/types/OverridableVar.hpp @@ -35,7 +35,7 @@ namespace Desktop::Types { return std::clamp(value, min.value_or(std::numeric_limits::min()), max.value_or(std::numeric_limits::max())); } - template || std::is_same_v || std::is_same_v> + template || std::is_same_v || std::is_same_v> class COverridableVar { public: COverridableVar(T const& value, eOverridePriority priority) { diff --git a/src/desktop/view/Group.cpp b/src/desktop/view/Group.cpp index 287276b22..f2b2a5642 100644 --- a/src/desktop/view/Group.cpp +++ b/src/desktop/view/Group.cpp @@ -84,7 +84,7 @@ bool CGroup::has(PHLWINDOW w) const { } void CGroup::add(PHLWINDOW w) { - static auto INSERT_AFTER_CURRENT = CConfigValue("group:insert_after_current"); + static auto INSERT_AFTER_CURRENT = CConfigValue("group:insert_after_current"); if (w->m_group) { if (w->m_group == m_self) @@ -142,7 +142,8 @@ void CGroup::remove(PHLWINDOW w, Math::eDirection dir) { w->m_group.reset(); removeWindowDecos(w); - w->setHidden(false); + w->setInputBlocked(INPUT_BLOCK_GROUP_INACTIVE, false); + *w->alpha(WINDOW_ALPHA_LAYOUT) = 1.F; const bool REMOVING_GROUP = m_windows.size() <= 1; @@ -280,12 +281,16 @@ void CGroup::updateWindowVisibility() { for (size_t i = 0; i < m_windows.size(); ++i) { if (i == m_current) { auto& x = m_windows.at(i); - x->setHidden(false); + x->setInputBlocked(INPUT_BLOCK_GROUP_INACTIVE, false); + *x->alpha(WINDOW_ALPHA_LAYOUT) = 1.F; x->m_ruleApplicator->propertiesChanged(Desktop::Rule::RULE_PROP_GROUP | Desktop::Rule::RULE_PROP_ON_WORKSPACE); x->updateWindowDecos(); x->updateDecorationValues(); - } else - m_windows.at(i)->setHidden(true); + } else { + auto& x = m_windows.at(i); + x->setInputBlocked(INPUT_BLOCK_GROUP_INACTIVE, true); + *x->alpha(WINDOW_ALPHA_LAYOUT) = 0.F; + } } m_target->recalc(); diff --git a/src/desktop/view/Popup.cpp b/src/desktop/view/Popup.cpp index 7791e9211..55af4d417 100644 --- a/src/desktop/view/Popup.cpp +++ b/src/desktop/view/Popup.cpp @@ -305,7 +305,7 @@ void CPopup::onCommit(bool ignoreSiblings) { if (PREV_SIZE != m_lastSize) invalidateTreeExtentsCache(); - static auto PLOGDAMAGE = CConfigValue("debug:log_damage"); + static auto PLOGDAMAGE = CConfigValue("debug:log_damage"); if (*PLOGDAMAGE) Log::logger->log(Log::DEBUG, "Refusing to commit damage from a subsurface of {} because it's invisible.", m_windowOwner.lock()); return; diff --git a/src/desktop/view/Subsurface.cpp b/src/desktop/view/Subsurface.cpp index c3c985dfc..8eef2b048 100644 --- a/src/desktop/view/Subsurface.cpp +++ b/src/desktop/view/Subsurface.cpp @@ -162,7 +162,7 @@ void CSubsurface::onCommit() { if (!m_windowParent.expired() && (!m_windowParent->m_isMapped || !m_windowParent->m_workspace->m_visible)) { m_lastSize = m_wlSurface->resource()->m_current.size; - static auto PLOGDAMAGE = CConfigValue("debug:log_damage"); + static auto PLOGDAMAGE = CConfigValue("debug:log_damage"); if (*PLOGDAMAGE) Log::logger->log(Log::DEBUG, "Refusing to commit damage from a subsurface of {} because it's invisible.", m_windowParent.lock()); return; diff --git a/src/desktop/view/Window.cpp b/src/desktop/view/Window.cpp index 6da62b340..b2f4821b8 100644 --- a/src/desktop/view/Window.cpp +++ b/src/desktop/view/Window.cpp @@ -25,6 +25,7 @@ #include "../../render/decorations/CHyprGroupBarDecoration.hpp" #include "../../render/decorations/CHyprBorderDecoration.hpp" #include "../../config/ConfigValue.hpp" +#include "../../config/shared/actions/ConfigActions.hpp" #include "../../config/ConfigManager.hpp" #include "../../config/shared/animation/AnimationTree.hpp" #include "../../config/shared/workspace/WorkspaceRuleManager.hpp" @@ -83,13 +84,17 @@ PHLWINDOW CWindow::create(SP surface) { g_pAnimationManager->createAnimation(0.f, pWindow->m_borderFadeAnimationProgress, Config::animationTree()->getAnimationPropertyConfig("border"), pWindow, AVARDAMAGE_BORDER); g_pAnimationManager->createAnimation(0.f, pWindow->m_borderAngleAnimationProgress, Config::animationTree()->getAnimationPropertyConfig("borderangle"), pWindow, AVARDAMAGE_BORDER); - g_pAnimationManager->createAnimation(1.f, pWindow->m_alpha, Config::animationTree()->getAnimationPropertyConfig("fadeIn"), pWindow, AVARDAMAGE_ENTIRE); - g_pAnimationManager->createAnimation(1.f, pWindow->m_activeInactiveAlpha, Config::animationTree()->getAnimationPropertyConfig("fadeSwitch"), pWindow, AVARDAMAGE_ENTIRE); + g_pAnimationManager->createAnimation(1.f, pWindow->alpha(WINDOW_ALPHA_FADE), Config::animationTree()->getAnimationPropertyConfig("fadeIn"), pWindow, AVARDAMAGE_ENTIRE); + g_pAnimationManager->createAnimation(1.f, pWindow->alpha(WINDOW_ALPHA_ACTIVE), Config::animationTree()->getAnimationPropertyConfig("fadeSwitch"), pWindow, AVARDAMAGE_ENTIRE); + g_pAnimationManager->createAnimation(1.f, pWindow->alpha(WINDOW_ALPHA_FULLSCREEN), Config::animationTree()->getAnimationPropertyConfig("fadeIn"), pWindow, AVARDAMAGE_ENTIRE); + g_pAnimationManager->createAnimation(1.f, pWindow->alpha(WINDOW_ALPHA_LAYOUT), Config::animationTree()->getAnimationPropertyConfig("fadeSwitch"), pWindow, AVARDAMAGE_ENTIRE); g_pAnimationManager->createAnimation(CHyprColor(), pWindow->m_realShadowColor, Config::animationTree()->getAnimationPropertyConfig("fadeShadow"), pWindow, AVARDAMAGE_SHADOW); g_pAnimationManager->createAnimation(CHyprColor(), pWindow->m_realGlowColor, Config::animationTree()->getAnimationPropertyConfig("fadeGlow"), pWindow, AVARDAMAGE_ENTIRE); g_pAnimationManager->createAnimation(0.f, pWindow->m_dimPercent, Config::animationTree()->getAnimationPropertyConfig("fadeDim"), pWindow, AVARDAMAGE_ENTIRE); - g_pAnimationManager->createAnimation(0.f, pWindow->m_movingToWorkspaceAlpha, Config::animationTree()->getAnimationPropertyConfig("fadeOut"), pWindow, AVARDAMAGE_ENTIRE); - g_pAnimationManager->createAnimation(0.f, pWindow->m_movingFromWorkspaceAlpha, Config::animationTree()->getAnimationPropertyConfig("fadeIn"), pWindow, AVARDAMAGE_ENTIRE); + g_pAnimationManager->createAnimation(1.f, pWindow->alpha(WINDOW_ALPHA_MOVE_TO_WORKSPACE), Config::animationTree()->getAnimationPropertyConfig("fadeOut"), pWindow, + AVARDAMAGE_ENTIRE); + g_pAnimationManager->createAnimation(1.f, pWindow->alpha(WINDOW_ALPHA_MOVE_FROM_WORKSPACE), Config::animationTree()->getAnimationPropertyConfig("fadeIn"), pWindow, + AVARDAMAGE_ENTIRE); g_pAnimationManager->createAnimation(0.f, pWindow->m_notRespondingTint, Config::animationTree()->getAnimationPropertyConfig("fade"), pWindow, AVARDAMAGE_ENTIRE); pWindow->addWindowDeco(makeUnique(pWindow)); @@ -113,13 +118,17 @@ PHLWINDOW CWindow::create(SP resource) { g_pAnimationManager->createAnimation(0.f, pWindow->m_borderFadeAnimationProgress, Config::animationTree()->getAnimationPropertyConfig("border"), pWindow, AVARDAMAGE_BORDER); g_pAnimationManager->createAnimation(0.f, pWindow->m_borderAngleAnimationProgress, Config::animationTree()->getAnimationPropertyConfig("borderangle"), pWindow, AVARDAMAGE_BORDER); - g_pAnimationManager->createAnimation(1.f, pWindow->m_alpha, Config::animationTree()->getAnimationPropertyConfig("fadeIn"), pWindow, AVARDAMAGE_ENTIRE); - g_pAnimationManager->createAnimation(1.f, pWindow->m_activeInactiveAlpha, Config::animationTree()->getAnimationPropertyConfig("fadeSwitch"), pWindow, AVARDAMAGE_ENTIRE); + g_pAnimationManager->createAnimation(1.f, pWindow->alpha(WINDOW_ALPHA_FADE), Config::animationTree()->getAnimationPropertyConfig("fadeIn"), pWindow, AVARDAMAGE_ENTIRE); + g_pAnimationManager->createAnimation(1.f, pWindow->alpha(WINDOW_ALPHA_ACTIVE), Config::animationTree()->getAnimationPropertyConfig("fadeSwitch"), pWindow, AVARDAMAGE_ENTIRE); + g_pAnimationManager->createAnimation(1.f, pWindow->alpha(WINDOW_ALPHA_FULLSCREEN), Config::animationTree()->getAnimationPropertyConfig("fadeIn"), pWindow, AVARDAMAGE_ENTIRE); + g_pAnimationManager->createAnimation(1.f, pWindow->alpha(WINDOW_ALPHA_LAYOUT), Config::animationTree()->getAnimationPropertyConfig("fadeSwitch"), pWindow, AVARDAMAGE_ENTIRE); g_pAnimationManager->createAnimation(CHyprColor(), pWindow->m_realShadowColor, Config::animationTree()->getAnimationPropertyConfig("fadeShadow"), pWindow, AVARDAMAGE_SHADOW); g_pAnimationManager->createAnimation(CHyprColor(), pWindow->m_realGlowColor, Config::animationTree()->getAnimationPropertyConfig("fadeGlow"), pWindow, AVARDAMAGE_ENTIRE); g_pAnimationManager->createAnimation(0.f, pWindow->m_dimPercent, Config::animationTree()->getAnimationPropertyConfig("fadeDim"), pWindow, AVARDAMAGE_ENTIRE); - g_pAnimationManager->createAnimation(0.f, pWindow->m_movingToWorkspaceAlpha, Config::animationTree()->getAnimationPropertyConfig("fadeOut"), pWindow, AVARDAMAGE_ENTIRE); - g_pAnimationManager->createAnimation(0.f, pWindow->m_movingFromWorkspaceAlpha, Config::animationTree()->getAnimationPropertyConfig("fadeIn"), pWindow, AVARDAMAGE_ENTIRE); + g_pAnimationManager->createAnimation(1.f, pWindow->alpha(WINDOW_ALPHA_MOVE_TO_WORKSPACE), Config::animationTree()->getAnimationPropertyConfig("fadeOut"), pWindow, + AVARDAMAGE_ENTIRE); + g_pAnimationManager->createAnimation(1.f, pWindow->alpha(WINDOW_ALPHA_MOVE_FROM_WORKSPACE), Config::animationTree()->getAnimationPropertyConfig("fadeIn"), pWindow, + AVARDAMAGE_ENTIRE); g_pAnimationManager->createAnimation(0.f, pWindow->m_notRespondingTint, Config::animationTree()->getAnimationPropertyConfig("fade"), pWindow, AVARDAMAGE_ENTIRE); pWindow->addWindowDeco(makeUnique(pWindow)); @@ -178,7 +187,7 @@ eViewType CWindow::type() const { } bool CWindow::visible() const { - return !m_hidden && ((m_isMapped && m_wlSurface && m_wlSurface->resource()) || (m_fadingOut && m_alpha->value() != 0.F)); + return !m_hidden && ((m_isMapped && m_wlSurface && m_wlSurface->resource()) || m_fadingOut) && visibleByAlpha(); } std::optional CWindow::logicalBox() const { @@ -470,7 +479,7 @@ void CWindow::moveToWorkspace(PHLWORKSPACE pWorkspace) { if (m_workspace == pWorkspace) return; - static auto PINITIALWSTRACKING = CConfigValue("misc:initial_workspace_tracking"); + static auto PINITIALWSTRACKING = CConfigValue("misc:initial_workspace_tracking"); if (!m_initialWorkspaceToken.empty()) { const auto TOKEN = g_pTokenManager->getToken(m_initialWorkspaceToken); @@ -488,18 +497,23 @@ void CWindow::moveToWorkspace(PHLWORKSPACE pWorkspace) { } } - static auto PCLOSEONLASTSPECIAL = CConfigValue("misc:close_special_on_empty"); + static auto PCLOSEONLASTSPECIAL = CConfigValue("misc:close_special_on_empty"); const auto OLDWORKSPACE = m_workspace; if (OLDWORKSPACE->isVisible()) { - m_movingToWorkspaceAlpha->setValueAndWarp(1.F); - *m_movingToWorkspaceAlpha = 0.F; - m_movingToWorkspaceAlpha->setCallbackOnEnd([this](auto) { m_monitorMovedFrom = -1; }); + alpha(WINDOW_ALPHA_MOVE_TO_WORKSPACE)->setValueAndWarp(1.F); + *alpha(WINDOW_ALPHA_MOVE_TO_WORKSPACE) = 0.F; + alpha(WINDOW_ALPHA_MOVE_TO_WORKSPACE)->setCallbackOnEnd([this](auto) { + alpha(WINDOW_ALPHA_MOVE_TO_WORKSPACE)->setValueAndWarp(1.F); + m_monitorMovedFrom = -1; + }); m_monitorMovedFrom = OLDWORKSPACE ? OLDWORKSPACE->monitorID() : -1; } m_workspace = pWorkspace; + updateFullscreenInputState(); + *alpha(WINDOW_ALPHA_FULLSCREEN) = isBlockedByFullscreen() ? 0.F : 1.F; setAnimationsToMove(); @@ -553,8 +567,8 @@ PHLWINDOW CWindow::x11TransientFor() { } void CWindow::onUnmap() { - static auto PCLOSEONLASTSPECIAL = CConfigValue("misc:close_special_on_empty"); - static auto PINITIALWSTRACKING = CConfigValue("misc:initial_workspace_tracking"); + static auto PCLOSEONLASTSPECIAL = CConfigValue("misc:close_special_on_empty"); + static auto PINITIALWSTRACKING = CConfigValue("misc:initial_workspace_tracking"); if (!m_initialWorkspaceToken.empty()) { const auto TOKEN = g_pTokenManager->getToken(m_initialWorkspaceToken); @@ -575,8 +589,11 @@ void CWindow::onUnmap() { // if the special workspace now has 0 windows, it will be closed, and this // window will no longer pass render checks, cuz the workspace will be nuked. // throw it into the main one for the fadeout. - if (m_workspace->m_isSpecialWorkspace && m_workspace->getWindows() == 0) - m_lastWorkspace = m_monitor->activeWorkspaceID(); + if (m_workspace->m_isSpecialWorkspace && m_workspace->getWindows() == 0) { + const auto PMONITOR = m_monitor.lock(); + if (PMONITOR) + m_lastWorkspace = PMONITOR->activeWorkspaceID(); + } if (*PCLOSEONLASTSPECIAL && m_workspace && m_workspace->getWindows() == 0 && onSpecialWorkspace()) { const auto PMONITOR = m_monitor.lock(); @@ -611,15 +628,18 @@ void CWindow::onMap() { m_realSize->resetAllCallbacks(); m_borderFadeAnimationProgress->resetAllCallbacks(); m_borderAngleAnimationProgress->resetAllCallbacks(); - m_activeInactiveAlpha->resetAllCallbacks(); - m_alpha->resetAllCallbacks(); + alpha(WINDOW_ALPHA_ACTIVE)->resetAllCallbacks(); + alpha(WINDOW_ALPHA_FADE)->resetAllCallbacks(); + alpha(WINDOW_ALPHA_FULLSCREEN)->resetAllCallbacks(); + alpha(WINDOW_ALPHA_LAYOUT)->resetAllCallbacks(); m_realShadowColor->resetAllCallbacks(); m_realGlowColor->resetAllCallbacks(); m_dimPercent->resetAllCallbacks(); - m_movingToWorkspaceAlpha->resetAllCallbacks(); - m_movingFromWorkspaceAlpha->resetAllCallbacks(); + alpha(WINDOW_ALPHA_MOVE_TO_WORKSPACE)->resetAllCallbacks(); + alpha(WINDOW_ALPHA_MOVE_FROM_WORKSPACE)->resetAllCallbacks(); - m_movingFromWorkspaceAlpha->setValueAndWarp(1.F); + alpha(WINDOW_ALPHA_MOVE_FROM_WORKSPACE)->setValueAndWarp(1.F); + alpha(WINDOW_ALPHA_MOVE_TO_WORKSPACE)->setValueAndWarp(1.F); if (m_borderAngleAnimationProgress->enabled()) { m_borderAngleAnimationProgress->setValueAndWarp(0.f); @@ -662,7 +682,7 @@ void CWindow::onMap() { } }); - m_movingFromWorkspaceAlpha->setValueAndWarp(1.F); + alpha(WINDOW_ALPHA_MOVE_FROM_WORKSPACE)->setValueAndWarp(1.F); m_reportedSize = m_pendingReportedSize; m_animatingIn = true; @@ -705,10 +725,115 @@ void CWindow::setHidden(bool hidden) { setSuspended(hidden); } -bool CWindow::isHidden() { +bool CWindow::isHidden() const { return m_hidden; } +void CWindow::setInputBlocked(eWindowInputBlockReason reason, bool blocked) { + if (reason == INPUT_BLOCK_NONE) + return; + + const auto MASK = sc(reason); + + if (blocked) + m_inputBlockReasons |= MASK; + else + m_inputBlockReasons &= ~MASK; + + if (blocked && Desktop::focusState()->window() == m_self) + Desktop::focusState()->window().reset(); +} + +bool CWindow::isInputBlocked() const { + return m_inputBlockReasons != INPUT_BLOCK_NONE; +} + +bool CWindow::isInputBlocked(eWindowInputBlockReason reason) const { + return (m_inputBlockReasons & sc(reason)) != 0; +} + +bool CWindow::isInputBlockedOnly(eWindowInputBlockReason reason) const { + return m_inputBlockReasons == sc(reason); +} + +bool CWindow::acceptsInput() const { + return !isHidden() && !isInputBlocked(); +} + +bool CWindow::isAllowedOverFullscreen() const { + if (isFullscreen() || m_pinned || m_createdOverFullscreen) + return true; + + if (!m_workspace) + return false; + + const auto FSWINDOW = m_workspace->getFullscreenWindow(); + return FSWINDOW && FSWINDOW->m_group && FSWINDOW->m_group->has(m_self.lock()); +} + +bool CWindow::isBlockedByFullscreen() const { + if (!m_workspace || !m_workspace->m_hasFullscreenWindow) + return false; + + return !isAllowedOverFullscreen(); +} + +bool CWindow::isFadingOutUnderFullscreen() const { + return isBlockedByFullscreen() && alpha(WINDOW_ALPHA_FULLSCREEN)->isBeingAnimated() && alphaValue(WINDOW_ALPHA_FULLSCREEN) > 0.F; +} + +bool CWindow::shouldRenderOverFullscreen() const { + return isAllowedOverFullscreen() || isFadingOutUnderFullscreen(); +} + +void CWindow::updateFullscreenInputState() { + setInputBlocked(INPUT_BLOCK_BELOW_FULLSCREEN, isBlockedByFullscreen()); +} + +PHLANIMVAR& CWindow::alpha(eWindowAlpha type) { + return m_alpha.get(type); +} + +const PHLANIMVAR& CWindow::alpha(eWindowAlpha type) const { + return m_alpha.get(type); +} + +float CWindow::alphaValue(eWindowAlpha type) const { + return alpha(type)->value(); +} + +float CWindow::alphaGoal(eWindowAlpha type) const { + return alpha(type)->goal(); +} + +float CWindow::alphaTotal() const { + return m_alpha.getTotal(); +} + +float CWindow::alphaTotalGoal() const { + return m_alpha.getTotalGoal(); +} + +float CWindow::alphaTotalWithout(eWindowAlpha type) const { + return m_alpha.getTotalWithout(type); +} + +float CWindow::effectiveAlpha() const { + return alphaTotal(); +} + +bool CWindow::visibleByAlpha() const { + return effectiveAlpha() != 0.F; +} + +bool CWindow::visibleByAlphaGoal() const { + return alphaTotalGoal() != 0.F; +} + +bool CWindow::targetVisible() const { + return !m_hidden && ((m_isMapped && m_wlSurface && m_wlSurface->resource()) || m_fadingOut) && visibleByAlphaGoal(); +} + // check if the point is "hidden" under a rounded corner of the window // it is assumed that the point is within the real window box (m_vRealPosition, m_vRealSize) // otherwise behaviour is undefined @@ -758,7 +883,7 @@ Vector2D CWindow::middle() { } bool CWindow::opaque() { - if (m_alpha->value() != 1.f || m_activeInactiveAlpha->value() != 1.f) + if (alphaValue(WINDOW_ALPHA_FADE) != 1.f || alphaValue(WINDOW_ALPHA_FULLSCREEN) != 1.f || alphaValue(WINDOW_ALPHA_ACTIVE) != 1.f) return false; const auto PWORKSPACE = m_workspace; @@ -785,8 +910,8 @@ bool CWindow::opaque() { } float CWindow::rounding() { - static auto PROUNDING = CConfigValue("decoration:rounding"); - static auto PROUNDINGPOWER = CConfigValue("decoration:rounding_power"); + static auto PROUNDING = CConfigValue("decoration:rounding"); + static auto PROUNDINGPOWER = CConfigValue("decoration:rounding_power"); float roundingPower = m_ruleApplicator->roundingPower().valueOr(*PROUNDINGPOWER); float rounding = m_ruleApplicator->rounding().valueOr(*PROUNDING) * (roundingPower / 2.0); /* Make perceived roundness consistent. */ @@ -795,7 +920,7 @@ float CWindow::rounding() { } float CWindow::roundingPower() { - static auto PROUNDINGPOWER = CConfigValue("decoration:rounding_power"); + static auto PROUNDINGPOWER = CConfigValue("decoration:rounding_power"); return m_ruleApplicator->roundingPower().valueOr(std::clamp(*PROUNDINGPOWER, 1.F, 10.F)); } @@ -808,13 +933,13 @@ void CWindow::updateWindowData() { void CWindow::updateWindowData(const Config::CWorkspaceRule& workspaceRule) { if (workspaceRule.m_noBorder.value_or(false)) - m_ruleApplicator->borderSize().matchOptional(std::optional(0), Desktop::Types::PRIORITY_WORKSPACE_RULE); + m_ruleApplicator->borderSize().matchOptional(std::optional(0), Desktop::Types::PRIORITY_WORKSPACE_RULE); else if (workspaceRule.m_borderSize) m_ruleApplicator->borderSize().matchOptional(workspaceRule.m_borderSize, Desktop::Types::PRIORITY_WORKSPACE_RULE); else m_ruleApplicator->borderSize().matchOptional(std::nullopt, Desktop::Types::PRIORITY_WORKSPACE_RULE); m_ruleApplicator->decorate().matchOptional(workspaceRule.m_decorate, Desktop::Types::PRIORITY_WORKSPACE_RULE); - m_ruleApplicator->rounding().matchOptional(workspaceRule.m_noRounding.value_or(false) ? std::optional(0) : std::nullopt, + m_ruleApplicator->rounding().matchOptional(workspaceRule.m_noRounding.value_or(false) ? std::optional(0) : std::nullopt, Desktop::Types::PRIORITY_WORKSPACE_RULE); m_ruleApplicator->noShadow().matchOptional(workspaceRule.m_noShadow, Desktop::Types::PRIORITY_WORKSPACE_RULE); @@ -831,7 +956,7 @@ int CWindow::getRealBorderSize() const { return 0; } - static auto PBORDERSIZE = CConfigValue("general:border_size"); + static auto PBORDERSIZE = CConfigValue("general:border_size"); m_cachedBorderSize = m_ruleApplicator->borderSize().valueOr(*PBORDERSIZE); m_borderSizeCacheDirty = false; @@ -839,12 +964,12 @@ int CWindow::getRealBorderSize() const { } float CWindow::getScrollMouse() { - static auto PINPUTSCROLLFACTOR = CConfigValue("input:scroll_factor"); + static auto PINPUTSCROLLFACTOR = CConfigValue("input:scroll_factor"); return m_ruleApplicator->scrollMouse().valueOr(*PINPUTSCROLLFACTOR); } float CWindow::getScrollTouchpad() { - static auto PTOUCHPADSCROLLFACTOR = CConfigValue("input:touchpad:scroll_factor"); + static auto PTOUCHPADSCROLLFACTOR = CConfigValue("input:touchpad:scroll_factor"); return m_ruleApplicator->scrollTouchpad().valueOr(*PTOUCHPADSCROLLFACTOR); } @@ -857,7 +982,7 @@ bool CWindow::isScrollTouchpadOverridden() { } bool CWindow::canBeTorn() { - static auto PTEARING = CConfigValue("general:allow_tearing"); + static auto PTEARING = CConfigValue("general:allow_tearing"); return m_ruleApplicator->tearing().valueOr(m_tearingHint) && *PTEARING; } @@ -963,7 +1088,7 @@ bool CWindow::clampWindowSize(const std::optional minSize, const std:: return changed; } -bool CWindow::isFullscreen() { +bool CWindow::isFullscreen() const { return m_fullscreenState.internal != FSMODE_NONE; } @@ -1009,7 +1134,7 @@ std::unordered_map CWindow::getEnv() { needle += 512; } #elif defined(__FreeBSD__) || defined(__NetBSD__) || defined(__OpenBSD__) || defined(__DragonFly__) - int mib[4] = {CTL_KERN, KERN_PROC, KERN_PROC_ENV, static_cast(PID)}; + int mib[4] = {CTL_KERN, KERN_PROC, KERN_PROC_ENV, sc(PID)}; size_t len = 0; if (sysctl(mib, 4, nullptr, &len, nullptr, 0) < 0 || len == 0) @@ -1045,7 +1170,7 @@ void CWindow::activate(bool force) { if (Desktop::focusState()->window() == m_self) return; - static auto PFOCUSONACTIVATE = CConfigValue("misc:focus_on_activate"); + static auto PFOCUSONACTIVATE = CConfigValue("misc:focus_on_activate"); m_isUrgent = true; @@ -1270,7 +1395,7 @@ void CWindow::onX11ConfigureRequest(CBox box) { } void CWindow::warpCursor(bool force) { - static auto PERSISTENTWARPS = CConfigValue("cursor:persistent_warps"); + static auto PERSISTENTWARPS = CConfigValue("cursor:persistent_warps"); const auto coords = m_relativeCursorCoordsOnLastWarp; m_relativeCursorCoordsOnLastWarp.x = -1; // reset m_vRelativeCursorCoordsOnLastWarp @@ -1283,7 +1408,7 @@ void CWindow::warpCursor(bool force) { PHLWINDOW CWindow::getSwallower() { static auto PSWALLOWREGEX = CConfigValue("misc:swallow_regex"); static auto PSWALLOWEXREGEX = CConfigValue("misc:swallow_exception_regex"); - static auto PSWALLOW = CConfigValue("misc:enable_swallow"); + static auto PSWALLOW = CConfigValue("misc:enable_swallow"); if (!*PSWALLOW || std::string{*PSWALLOWREGEX} == STRVAL_EMPTY || (*PSWALLOWREGEX).empty()) return nullptr; @@ -1299,7 +1424,7 @@ PHLWINDOW CWindow::getSwallower() { break; for (auto const& w : g_pCompositor->m_windows) { - if (!w->m_isMapped || w->isHidden()) + if (!w->m_isMapped || !w->acceptsInput()) continue; if (w->getPID() == currentPid) @@ -1347,7 +1472,7 @@ Vector2D CWindow::realToReportSize() { if (!m_isX11) return m_realSize->goal().clamp(Vector2D{0, 0}, Math::VECTOR2D_MAX); - static auto PXWLFORCESCALEZERO = CConfigValue("xwayland:force_zero_scaling"); + static auto PXWLFORCESCALEZERO = CConfigValue("xwayland:force_zero_scaling"); const auto REPORTSIZE = m_realSize->goal().clamp(Vector2D{1, 1}, Math::VECTOR2D_MAX); const auto PMONITOR = m_monitor.lock(); @@ -1367,7 +1492,7 @@ Vector2D CWindow::realToReportPosition() { } Vector2D CWindow::xwaylandSizeToReal(Vector2D size) { - static auto PXWLFORCESCALEZERO = CConfigValue("xwayland:force_zero_scaling"); + static auto PXWLFORCESCALEZERO = CConfigValue("xwayland:force_zero_scaling"); const auto PMONITOR = m_monitor.lock(); const auto SIZE = size.clamp(Vector2D{1, 1}, Math::VECTOR2D_MAX); @@ -1381,7 +1506,7 @@ Vector2D CWindow::xwaylandPositionToReal(Vector2D pos) { } void CWindow::updateX11SurfaceScale() { - static auto PXWLFORCESCALEZERO = CConfigValue("xwayland:force_zero_scaling"); + static auto PXWLFORCESCALEZERO = CConfigValue("xwayland:force_zero_scaling"); m_X11SurfaceScaledBy = 1.0f; if (m_isX11 && *PXWLFORCESCALEZERO) { @@ -1530,34 +1655,34 @@ Vector2D CWindow::getReportedSize() { void CWindow::updateDecorationValues() { m_borderSizeCacheDirty = true; - static auto PACTIVECOL = CConfigValue("general:col.active_border"); - static auto PINACTIVECOL = CConfigValue("general:col.inactive_border"); - static auto PNOGROUPACTIVECOL = CConfigValue("general:col.nogroup_border_active"); - static auto PNOGROUPINACTIVECOL = CConfigValue("general:col.nogroup_border"); - static auto PGROUPACTIVECOL = CConfigValue("group:col.border_active"); - static auto PGROUPINACTIVECOL = CConfigValue("group:col.border_inactive"); - static auto PGROUPACTIVELOCKEDCOL = CConfigValue("group:col.border_locked_active"); - static auto PGROUPINACTIVELOCKEDCOL = CConfigValue("group:col.border_locked_inactive"); - static auto PINACTIVEALPHA = CConfigValue("decoration:inactive_opacity"); - static auto PACTIVEALPHA = CConfigValue("decoration:active_opacity"); - static auto PFULLSCREENALPHA = CConfigValue("decoration:fullscreen_opacity"); - static auto PSHADOWCOL = CConfigValue("decoration:shadow:color"); - static auto PSHADOWCOLINACTIVE = CConfigValue("decoration:shadow:color_inactive"); - static auto PGLOW = CConfigValue("decoration:glow:enabled"); - static auto PGLOWCOL = CConfigValue("decoration:glow:color"); - static auto PGLOWCOLINACTIVE = CConfigValue("decoration:glow:color_inactive"); - static auto PDIMSTRENGTH = CConfigValue("decoration:dim_strength"); - static auto PDIMENABLED = CConfigValue("decoration:dim_inactive"); - static auto PDIMMODAL = CConfigValue("decoration:dim_modal"); + static auto PACTIVECOL = CConfigValue("general:col.active_border"); + static auto PINACTIVECOL = CConfigValue("general:col.inactive_border"); + static auto PNOGROUPACTIVECOL = CConfigValue("general:col.nogroup_border_active"); + static auto PNOGROUPINACTIVECOL = CConfigValue("general:col.nogroup_border"); + static auto PGROUPACTIVECOL = CConfigValue("group:col.border_active"); + static auto PGROUPINACTIVECOL = CConfigValue("group:col.border_inactive"); + static auto PGROUPACTIVELOCKEDCOL = CConfigValue("group:col.border_locked_active"); + static auto PGROUPINACTIVELOCKEDCOL = CConfigValue("group:col.border_locked_inactive"); + static auto PINACTIVEALPHA = CConfigValue("decoration:inactive_opacity"); + static auto PACTIVEALPHA = CConfigValue("decoration:active_opacity"); + static auto PFULLSCREENALPHA = CConfigValue("decoration:fullscreen_opacity"); + static auto PSHADOWCOL = CConfigValue("decoration:shadow:color"); + static auto PSHADOWCOLINACTIVE = CConfigValue("decoration:shadow:color_inactive"); + static auto PGLOW = CConfigValue("decoration:glow:enabled"); + static auto PGLOWCOL = CConfigValue("decoration:glow:color"); + static auto PGLOWCOLINACTIVE = CConfigValue("decoration:glow:color_inactive"); + static auto PDIMSTRENGTH = CConfigValue("decoration:dim_strength"); + static auto PDIMENABLED = CConfigValue("decoration:dim_inactive"); + static auto PDIMMODAL = CConfigValue("decoration:dim_modal"); - auto* const ACTIVECOL = sc((PACTIVECOL.ptr())->getData()); - auto* const INACTIVECOL = sc((PINACTIVECOL.ptr())->getData()); - auto* const NOGROUPACTIVECOL = sc((PNOGROUPACTIVECOL.ptr())->getData()); - auto* const NOGROUPINACTIVECOL = sc((PNOGROUPINACTIVECOL.ptr())->getData()); - auto* const GROUPACTIVECOL = sc((PGROUPACTIVECOL.ptr())->getData()); - auto* const GROUPINACTIVECOL = sc((PGROUPINACTIVECOL.ptr())->getData()); - auto* const GROUPACTIVELOCKEDCOL = sc((PGROUPACTIVELOCKEDCOL.ptr())->getData()); - auto* const GROUPINACTIVELOCKEDCOL = sc((PGROUPINACTIVELOCKEDCOL.ptr())->getData()); + auto* const ACTIVECOL = sc((PACTIVECOL.ptr())); + auto* const INACTIVECOL = sc((PINACTIVECOL.ptr())); + auto* const NOGROUPACTIVECOL = sc((PNOGROUPACTIVECOL.ptr())); + auto* const NOGROUPINACTIVECOL = sc((PNOGROUPINACTIVECOL.ptr())); + auto* const GROUPACTIVECOL = sc((PGROUPACTIVECOL.ptr())); + auto* const GROUPINACTIVECOL = sc((PGROUPINACTIVECOL.ptr())); + auto* const GROUPACTIVELOCKEDCOL = sc((PGROUPACTIVELOCKEDCOL.ptr())); + auto* const GROUPINACTIVELOCKEDCOL = sc((PGROUPINACTIVELOCKEDCOL.ptr())); auto setBorderColor = [&](Config::CGradientValueData grad) -> void { if (grad == m_realBorderColor) @@ -1589,12 +1714,12 @@ void CWindow::updateDecorationValues() { // opacity const auto PWORKSPACE = m_workspace; if (isEffectiveInternalFSMode(FSMODE_FULLSCREEN)) { - *m_activeInactiveAlpha = m_ruleApplicator->alphaFullscreen().valueOrDefault().applyAlpha(*PFULLSCREENALPHA); + *alpha(WINDOW_ALPHA_ACTIVE) = m_ruleApplicator->alphaFullscreen().valueOrDefault().applyAlpha(*PFULLSCREENALPHA); } else { if (m_self == Desktop::focusState()->window()) - *m_activeInactiveAlpha = m_ruleApplicator->alpha().valueOrDefault().applyAlpha(*PACTIVEALPHA); + *alpha(WINDOW_ALPHA_ACTIVE) = m_ruleApplicator->alpha().valueOrDefault().applyAlpha(*PACTIVEALPHA); else - *m_activeInactiveAlpha = m_ruleApplicator->alphaInactive().valueOrDefault().applyAlpha(*PINACTIVEALPHA); + *alpha(WINDOW_ALPHA_ACTIVE) = m_ruleApplicator->alphaInactive().valueOrDefault().applyAlpha(*PINACTIVEALPHA); } // dim @@ -1641,12 +1766,16 @@ std::optional CWindow::calculateSingleExpr(const std::string& s) { } std::optional CWindow::calculateExpression(const std::string& s) { - auto spacePos = s.find(' '); - if (spacePos == std::string::npos) + const auto parsed = Math::parseExpressionVec2(s); + if (!parsed) return std::nullopt; - const auto LHS = calculateSingleExpr(s.substr(0, spacePos)); - const auto RHS = calculateSingleExpr(s.substr(spacePos + 1)); + return calculateExpression(*parsed); +} + +std::optional CWindow::calculateExpression(const Math::SExpressionVec2& expr) { + const auto LHS = calculateSingleExpr(expr.x); + const auto RHS = calculateSingleExpr(expr.y); if (!LHS || !RHS) return std::nullopt; @@ -1666,12 +1795,12 @@ static void setVector2DAnimToMove(WP pav) { } void CWindow::mapWindow() { - static auto PINACTIVEALPHA = CConfigValue("decoration:inactive_opacity"); - static auto PACTIVEALPHA = CConfigValue("decoration:active_opacity"); - static auto PDIMSTRENGTH = CConfigValue("decoration:dim_strength"); - static auto PNEWTAKESOVERFS = CConfigValue("misc:on_focus_under_fullscreen"); - static auto PINITIALWSTRACKING = CConfigValue("misc:initial_workspace_tracking"); - static auto PAUTOGROUP = CConfigValue("group:auto_group"); + static auto PINACTIVEALPHA = CConfigValue("decoration:inactive_opacity"); + static auto PACTIVEALPHA = CConfigValue("decoration:active_opacity"); + static auto PDIMSTRENGTH = CConfigValue("decoration:dim_strength"); + static auto PNEWTAKESOVERFS = CConfigValue("misc:on_focus_under_fullscreen"); + static auto PINITIALWSTRACKING = CConfigValue("misc:initial_workspace_tracking"); + static auto PAUTOGROUP = CConfigValue("group:auto_group"); const auto LAST_FOCUS_WINDOW = Desktop::focusState()->window(); const bool IS_LAST_IN_FS = LAST_FOCUS_WINDOW ? LAST_FOCUS_WINDOW->m_fullscreenState.internal != FSMODE_NONE : false; @@ -1764,8 +1893,8 @@ void CWindow::mapWindow() { const auto PMONITORFROMID = m_monitor.lock(); - if (m_monitor != PMONITOR) { - g_pKeybindManager->m_dispatchers["focusmonitor"](std::to_string(monitorID())); + if (m_monitor != PMONITOR) { // NOLINTNEXTLINE + Config::Actions::focusMonitor(PMONITORFROMID); PMONITOR = PMONITORFROMID; } m_workspace = PMONITOR->m_activeSpecialWorkspace ? PMONITOR->m_activeSpecialWorkspace : PMONITOR->m_activeWorkspace; @@ -1930,8 +2059,8 @@ void CWindow::mapWindow() { if (!workspaceSilent) { if (pWorkspace->m_isSpecialWorkspace) pWorkspace->m_monitor->setSpecialWorkspace(pWorkspace); - else if (PMONITOR->activeWorkspaceID() != requestedWorkspaceID && !m_noInitialFocus) - g_pKeybindManager->m_dispatchers["workspace"](requestedWorkspaceName); + else if (PMONITOR->activeWorkspaceID() != requestedWorkspaceID && !m_noInitialFocus) // NOLINTNEXTLINE + Config::Actions::changeWorkspace(requestedWorkspaceName); PMONITOR = Desktop::focusState()->monitor(); } @@ -1949,8 +2078,8 @@ void CWindow::mapWindow() { const auto PMONITORFROMID = m_monitor.lock(); - if (m_monitor != PMONITOR) { - g_pKeybindManager->m_dispatchers["focusmonitor"](std::to_string(monitorID())); + if (m_monitor != PMONITOR) { // NOLINTNEXTLINE + Config::Actions::focusMonitor(PMONITORFROMID); PMONITOR = PMONITORFROMID; } m_workspace = PMONITOR->m_activeSpecialWorkspace ? PMONITOR->m_activeSpecialWorkspace : PMONITOR->m_activeWorkspace; @@ -1959,15 +2088,6 @@ void CWindow::mapWindow() { Log::logger->log(Log::DEBUG, "Requested monitor, applying to {:mw}", m_self.lock()); } - if (PWORKSPACE->m_defaultFloating) - m_isFloating = true; - - if (PWORKSPACE->m_defaultPseudo) { - CBox desiredGeometry = g_pXWaylandManager->getGeometryForWindow(m_self.lock()); - m_target->setPseudoSize(Vector2D{desiredGeometry.width, desiredGeometry.height}); - m_target->setPseudo(true); - } - // Verify window swallowing. Get the swallower before calling onWindowCreated(m_self.lock()) because getSwallower() wouldn't get it after if m_self.lock() gets auto grouped. const auto SWALLOWER = getSwallower(); m_swallowed = SWALLOWER; @@ -2007,10 +2127,10 @@ void CWindow::mapWindow() { } else { bool setPseudo = false; - if (!m_ruleApplicator->static_.size.empty()) { - const auto COMPUTED = calculateExpression(m_ruleApplicator->static_.size); + if (m_ruleApplicator->static_.size) { + const auto COMPUTED = calculateExpression(*m_ruleApplicator->static_.size); if (!COMPUTED) - Log::logger->log(Log::ERR, "failed to parse {} as an expression", m_ruleApplicator->static_.size); + Log::logger->log(Log::ERR, "failed to parse {} as an expression", m_ruleApplicator->static_.size->toString()); else { setPseudo = true; m_target->setPseudoSize(*COMPUTED); @@ -2057,10 +2177,10 @@ void CWindow::mapWindow() { } else Desktop::focusState()->fullWindowFocus(m_self.lock(), FOCUS_REASON_NEW_WINDOW); - m_activeInactiveAlpha->setValueAndWarp(*PACTIVEALPHA); + alpha(WINDOW_ALPHA_ACTIVE)->setValueAndWarp(*PACTIVEALPHA); m_dimPercent->setValueAndWarp(m_ruleApplicator->noDim().valueOrDefault() ? 0.f : *PDIMSTRENGTH); } else { - m_activeInactiveAlpha->setValueAndWarp(*PINACTIVEALPHA); + alpha(WINDOW_ALPHA_ACTIVE)->setValueAndWarp(*PINACTIVEALPHA); m_dimPercent->setValueAndWarp(0); } @@ -2130,7 +2250,7 @@ void CWindow::mapWindow() { updateDecorationValues(); // avoid this window being visible if (PWORKSPACE->m_hasFullscreenWindow && !isFullscreen() && !m_isFloating) - m_alpha->setValueAndWarp(0.f); + alpha(WINDOW_ALPHA_FULLSCREEN)->setValueAndWarp(0.f); g_pCompositor->setPreferredScaleForSurface(wlSurface()->resource(), PMONITOR->m_scale); g_pCompositor->setPreferredTransformForSurface(wlSurface()->resource(), PMONITOR->m_transform); @@ -2145,7 +2265,7 @@ void CWindow::mapWindow() { m_workspace->updateWindows(); if (PMONITOR && isX11OverrideRedirect()) { - static auto PXWLFORCESCALEZERO = CConfigValue("xwayland:force_zero_scaling"); + static auto PXWLFORCESCALEZERO = CConfigValue("xwayland:force_zero_scaling"); if (*PXWLFORCESCALEZERO) m_X11SurfaceScaledBy = PMONITOR->m_scale; } @@ -2154,7 +2274,7 @@ void CWindow::mapWindow() { void CWindow::unmapWindow() { Log::logger->log(Log::DEBUG, "{:c} unmapped", m_self.lock()); - static auto PEXITRETAINSFS = CConfigValue("misc:exit_window_retains_fullscreen"); + static auto PEXITRETAINSFS = CConfigValue("misc:exit_window_retains_fullscreen"); const auto CURRENTWINDOWFSSTATE = isFullscreen(); const auto CURRENTFSMODE = m_fullscreenState.internal; @@ -2252,7 +2372,7 @@ void CWindow::unmapWindow() { // refocus on a new window if needed if (wasLastWindow) { - static auto FOCUSONCLOSE = CConfigValue("input:focus_on_close"); + static auto FOCUSONCLOSE = CConfigValue("input:focus_on_close"); PHLWINDOW candidate = nextInGroup; if (!candidate) { @@ -2445,7 +2565,7 @@ void CWindow::unmanagedSetGeometry() { return; } - static auto PXWLFORCESCALEZERO = CConfigValue("xwayland:force_zero_scaling"); + static auto PXWLFORCESCALEZERO = CConfigValue("xwayland:force_zero_scaling"); const auto PMONITOR = m_monitor.lock(); const auto LOGICALPOS = g_pXWaylandManager->xwaylandToWaylandCoords(m_xwaylandSurface->m_geometry.pos(), PMONITOR); @@ -2526,7 +2646,7 @@ bool CWindow::canBeGroupedInto(SP group) { if (isX11OverrideRedirect()) return false; - static auto ALLOWGROUPMERGE = CConfigValue("group:merge_groups_on_drag"); + static auto ALLOWGROUPMERGE = CConfigValue("group:merge_groups_on_drag"); bool isGroup = m_group; bool disallowDragIntoGroup = g_layoutManager->dragController()->wasDraggingWindow() && isGroup && !sc(*ALLOWGROUPMERGE); return !g_pKeybindManager->m_groupsLocked // global group lock disengaged @@ -2537,3 +2657,8 @@ bool CWindow::canBeGroupedInto(SP group) { && !(m_groupRules & GROUP_BARRED && m_firstMap) // group rule doesn't prevent adding window && !disallowDragIntoGroup; // config allows groups to be merged } + +void CWindow::sendClose() { + if (m_isMapped) + g_pXWaylandManager->sendCloseWindow(m_self.lock()); +} diff --git a/src/desktop/view/Window.hpp b/src/desktop/view/Window.hpp index a72c626ba..32a7bcfe2 100644 --- a/src/desktop/view/Window.hpp +++ b/src/desktop/view/Window.hpp @@ -13,6 +13,7 @@ #include "../../render/decorations/IHyprWindowDecoration.hpp" #include "../../render/Transformer.hpp" #include "../DesktopTypes.hpp" +#include "../types/MultiAnimatedVariable.hpp" #include "Popup.hpp" #include "Subsurface.hpp" #include "WLSurface.hpp" @@ -77,6 +78,24 @@ namespace Desktop::View { SUPPRESS_FULLSCREEN_OUTPUT = 1 << 4, }; + enum eWindowAlpha : uint8_t { + WINDOW_ALPHA_FADE = 0, + WINDOW_ALPHA_ACTIVE, + WINDOW_ALPHA_FULLSCREEN, + WINDOW_ALPHA_LAYOUT, + WINDOW_ALPHA_MOVE_TO_WORKSPACE, + WINDOW_ALPHA_MOVE_FROM_WORKSPACE, + + WINDOW_ALPHA_LAST, + }; + + enum eWindowInputBlockReason : uint32_t { + INPUT_BLOCK_NONE = 0, + INPUT_BLOCK_GROUP_INACTIVE = 1 << 0, + INPUT_BLOCK_MONOCLE_INACTIVE = 1 << 1, + INPUT_BLOCK_BELOW_FULLSCREEN = 1 << 2, + }; + struct SWindowActiveEvent { PHLWINDOW window = nullptr; eFocusReason reason = sc(0) /* unknown */; @@ -194,13 +213,13 @@ namespace Desktop::View { mutable bool m_borderSizeCacheDirty = true; // Fade in-out - PHLANIMVAR m_alpha; - bool m_fadingOut = false; - bool m_readyToDelete = false; - Vector2D m_originalClosedPos; // these will be used for calculations later on in - Vector2D m_originalClosedSize; // drawing the closing animations - SBoxExtents m_originalClosedExtents; - bool m_animatingIn = false; + Desktop::Types::CMultiAVarContainer m_alpha; + bool m_fadingOut = false; + bool m_readyToDelete = false; + Vector2D m_originalClosedPos; // these will be used for calculations later on in + Vector2D m_originalClosedSize; // drawing the closing animations + SBoxExtents m_originalClosedExtents; + bool m_animatingIn = false; // For pinned (sticky) windows bool m_pinned = false; @@ -225,10 +244,6 @@ namespace Desktop::View { // Transformers std::vector> m_transformers; - // for alpha - PHLANIMVAR m_activeInactiveAlpha; - PHLANIMVAR m_movingFromWorkspaceAlpha; - // animated shadow color PHLANIMVAR m_realShadowColor; @@ -239,8 +254,7 @@ namespace Desktop::View { PHLANIMVAR m_dimPercent; // animate moving to an invisible workspace - int m_monitorMovedFrom = -1; // -1 means not moving - PHLANIMVAR m_movingToWorkspaceAlpha; + int m_monitorMovedFrom = -1; // -1 means not moving // swallowing PHLWINDOWREF m_swallowed; @@ -297,7 +311,28 @@ namespace Desktop::View { void onUnmap(); void onMap(); void setHidden(bool hidden); - bool isHidden(); + bool isHidden() const; + void setInputBlocked(eWindowInputBlockReason reason, bool blocked); + bool isInputBlocked() const; + bool isInputBlocked(eWindowInputBlockReason reason) const; + bool isInputBlockedOnly(eWindowInputBlockReason reason) const; + bool acceptsInput() const; + bool isAllowedOverFullscreen() const; + bool isBlockedByFullscreen() const; + bool isFadingOutUnderFullscreen() const; + bool shouldRenderOverFullscreen() const; + void updateFullscreenInputState(); + PHLANIMVAR& alpha(eWindowAlpha type); + const PHLANIMVAR& alpha(eWindowAlpha type) const; + float alphaValue(eWindowAlpha type) const; + float alphaGoal(eWindowAlpha type) const; + float alphaTotal() const; + float alphaTotalGoal() const; + float alphaTotalWithout(eWindowAlpha type) const; + float effectiveAlpha() const; + bool visibleByAlpha() const; + bool visibleByAlphaGoal() const; + bool targetVisible() const; void updateDecorationValues(); SBoxExtents getFullWindowReservedArea(); Vector2D middle(); @@ -313,7 +348,7 @@ namespace Desktop::View { void activate(bool force = false); int surfacesCount(); bool clampWindowSize(const std::optional minSize, const std::optional maxSize); - bool isFullscreen(); + bool isFullscreen() const; bool isEffectiveInternalFSMode(const eFullscreenMode) const; int getRealBorderSize() const; float getScrollMouse(); @@ -356,10 +391,12 @@ namespace Desktop::View { SP getSolitaryResource(); Vector2D getReportedSize(); std::optional calculateExpression(const std::string& s); + std::optional calculateExpression(const Math::SExpressionVec2& expr); std::optional minSize(); std::optional maxSize(); SP layoutTarget(); bool canBeGroupedInto(SP group); + void sendClose(); CBox getWindowMainSurfaceBox() const { return {m_realPosition->value().x, m_realPosition->value().y, m_realSize->value().x, m_realSize->value().y}; @@ -399,9 +436,10 @@ namespace Desktop::View { void unmanagedSetGeometry(); // For hidden windows and stuff - bool m_hidden = false; - bool m_suspended = false; - WORKSPACEID m_lastWorkspace = WORKSPACE_INVALID; + bool m_hidden = false; + bool m_suspended = false; + WORKSPACEID m_lastWorkspace = WORKSPACE_INVALID; + uint32_t m_inputBlockReasons = INPUT_BLOCK_NONE; }; inline bool valid(PHLWINDOW w) { diff --git a/src/errorOverlay/Overlay.cpp b/src/errorOverlay/Overlay.cpp index 928d78003..2fbdd4974 100644 --- a/src/errorOverlay/Overlay.cpp +++ b/src/errorOverlay/Overlay.cpp @@ -108,9 +108,9 @@ void COverlay::createQueued() { const float SCALE = PMONITOR->m_scale; const int FONTSIZE = std::clamp(sc(10.f * ((PMONITOR->m_pixelSize.x * SCALE) / 1920.f)), 8, 40); - static auto LINELIMIT = CConfigValue("debug:error_limit"); - static auto BAR_POSITION = CConfigValue("debug:error_position"); - static auto FONT_FAMILY = CConfigValue("misc:font_family"); + static auto LINELIMIT = CConfigValue("debug:error_limit"); + static auto BAR_POSITION = CConfigValue("debug:error_position"); + static auto FONT_FAMILY = CConfigValue("misc:font_family"); const bool TOPBAR = *BAR_POSITION == 0; const std::string visibleText = buildVisibleText(m_queued, *LINELIMIT); @@ -199,7 +199,7 @@ void COverlay::draw() { if (!PMONITOR) return; - static auto BAR_POSITION = CConfigValue("debug:error_position"); + static auto BAR_POSITION = CConfigValue("debug:error_position"); const bool TOPBAR = *BAR_POSITION == 0; const float barWidth = std::max(1.F, sc(PMONITOR->m_pixelSize.x) - m_outerPad * 2.F); diff --git a/src/helpers/AsyncDialogBox.cpp b/src/helpers/AsyncDialogBox.cpp index 8c2c7cd70..570592757 100644 --- a/src/helpers/AsyncDialogBox.cpp +++ b/src/helpers/AsyncDialogBox.cpp @@ -161,7 +161,12 @@ SP CAsyncDialogBox::lockSelf() { } void CAsyncDialogBox::setExecRule(std::string&& s) { - auto rule = Desktop::Rule::CWindowRule::buildFromExecString(std::move(s)); - m_execRuleToken = rule->execToken(); - Desktop::Rule::ruleEngine()->registerRule(std::move(rule)); + auto rule = Desktop::Rule::CWindowRule::buildFromExecString(std::move(s)); + if (!rule) { + Log::logger->log(Log::ERR, "CAsyncDialogBox: failed to parse exec rule: {}", rule.error()); + return; + } + + m_execRuleToken = (*rule)->execToken(); + Desktop::Rule::ruleEngine()->registerRule(std::move(*rule)); } diff --git a/src/helpers/MiscFunctions.cpp b/src/helpers/MiscFunctions.cpp index d056cb199..5e37f19fb 100644 --- a/src/helpers/MiscFunctions.cpp +++ b/src/helpers/MiscFunctions.cpp @@ -655,7 +655,7 @@ std::expected configStringToInt(const std::string& VALUE) a = std::round(std::stof(trim(rolling.substr(0, rolling.find(',')))) * 255.f); } catch (std::exception& e) { return std::unexpected("failed parsing " + VALUEWITHOUTFUNC); } - return a * sc(0x1000000) + *r * sc(0x10000) + *g * sc(0x100) + *b; + return a * sc(0x1000000) + *r * sc(0x10000) + *g * sc(0x100) + *b; } else if (VALUEWITHOUTFUNC.length() == 8) { const auto RGBA = parseHex(VALUEWITHOUTFUNC); @@ -683,7 +683,7 @@ std::expected configStringToInt(const std::string& VALUE) if (!r || !g || !b) return std::unexpected("failed parsing " + VALUEWITHOUTFUNC); - return sc(0xFF000000) + *r * sc(0x10000) + *g * sc(0x100) + *b; + return sc(0xFF000000) + *r * sc(0x10000) + *g * sc(0x100) + *b; } else if (VALUEWITHOUTFUNC.length() == 6) { auto r = parseHex(VALUEWITHOUTFUNC); return r ? *r + 0xFF000000 : r; @@ -935,7 +935,7 @@ std::string deviceNameToInternalString(const std::string& in) { case '\n': case ',': return '-'; - default: return static_cast(std::tolower(ch)); + default: return sc(std::tolower(ch)); } }); diff --git a/src/helpers/Monitor.cpp b/src/helpers/Monitor.cpp index b65cb5753..657a8c11b 100644 --- a/src/helpers/Monitor.cpp +++ b/src/helpers/Monitor.cpp @@ -65,7 +65,7 @@ using namespace Monitor; CMonitor::CMonitor(SP output_) : m_state(this), m_output(output_), m_imageDescription(getDefaultImageDescription()) { g_pAnimationManager->createAnimation(0.f, m_specialFade, Config::animationTree()->getAnimationPropertyConfig("specialWorkspaceIn"), AVARDAMAGE_NONE); m_specialFade->setUpdateCallback([this](auto) { g_pHyprRenderer->damageMonitor(m_self.lock()); }); - static auto PZOOMFACTOR = CConfigValue("cursor:zoom_factor"); + static auto PZOOMFACTOR = CConfigValue("cursor:zoom_factor"); g_pAnimationManager->createAnimation(*PZOOMFACTOR, m_cursorZoom, Config::animationTree()->getAnimationPropertyConfig("zoomFactor"), AVARDAMAGE_NONE); m_cursorZoom->setUpdateCallback([this](auto) { g_pHyprRenderer->damageMonitor(m_self.lock()); }); g_pAnimationManager->createAnimation(0.F, m_zoomAnimProgress, Config::animationTree()->getAnimationPropertyConfig("monitorAdded"), AVARDAMAGE_NONE); @@ -625,7 +625,7 @@ void CMonitor::applyCMType(NCMType::eCMType cmType, NTransferFunction::eTF cmSdr bool CMonitor::applyMonitorRule(Config::CMonitorRule&& pMonitorRule, bool force) { - static auto PDISABLESCALECHECKS = CConfigValue("debug:disable_scale_checks"); + static auto PDISABLESCALECHECKS = CConfigValue("debug:disable_scale_checks"); Log::logger->log(Log::DEBUG, "Applying monitor rule for {}", m_name); @@ -1036,7 +1036,7 @@ bool CMonitor::applyMonitorRule(Config::CMonitorRule&& pMonitorRule, bool force) } else { if (!autoScale) { Log::logger->log(Log::ERR, "Invalid scale passed to monitor, {} found suggestion {}", m_scale, searchScale); - static auto PDISABLENOTIFICATION = CConfigValue("misc:disable_scale_notification"); + static auto PDISABLENOTIFICATION = CConfigValue("misc:disable_scale_notification"); if (!*PDISABLENOTIFICATION) { Notification::overlay()->addNotification( I18n::i18nEngine()->localize(I18n::TXT_KEY_NOTIF_MONITOR_AUTO_SCALE, @@ -1127,8 +1127,8 @@ void CMonitor::addDamage(const CBox& box) { } bool CMonitor::shouldSkipScheduleFrameOnMouseEvent() { - static auto PNOBREAK = CConfigValue("cursor:no_break_fs_vrr"); - static auto PMINRR = CConfigValue("cursor:min_refresh_rate"); + static auto PNOBREAK = CConfigValue("cursor:no_break_fs_vrr"); + static auto PMINRR = CConfigValue("cursor:min_refresh_rate"); // skip scheduling extra frames for fullsreen apps with vrr const auto FS_WINDOW = getFullscreenWindow(); @@ -1336,7 +1336,7 @@ float CMonitor::getDefaultScale() { } static bool shouldWraparound(const WORKSPACEID id1, const WORKSPACEID id2) { - static auto PWORKSPACEWRAPAROUND = CConfigValue("animations:workspace_wraparound"); + static auto PWORKSPACEWRAPAROUND = CConfigValue("animations:workspace_wraparound"); if (!*PWORKSPACEWRAPAROUND) return false; @@ -1394,7 +1394,7 @@ void CMonitor::changeWorkspace(const PHLWORKSPACE& pWorkspace, bool internal, bo if (!noFocus && !Desktop::focusState()->monitor()->m_activeSpecialWorkspace && !(Desktop::focusState()->window() && Desktop::focusState()->window()->m_pinned && Desktop::focusState()->window()->m_monitor == m_self)) { - static auto PFOLLOWMOUSE = CConfigValue("input:follow_mouse"); + static auto PFOLLOWMOUSE = CConfigValue("input:follow_mouse"); auto pWindow = pWorkspace->m_hasFullscreenWindow ? pWorkspace->getFullscreenWindow() : pWorkspace->getLastFocusedWindow(); if (!pWindow) { @@ -1755,10 +1755,10 @@ uint32_t CMonitor::isSolitaryBlocked(bool full) { } for (auto const& w : g_pCompositor->m_windows) { - if (w == PCANDIDATE || (!w->m_isMapped && !w->m_fadingOut) || w->isHidden()) + if (w == PCANDIDATE || (!w->m_isMapped && !w->m_fadingOut) || !w->visible()) continue; - if (w->workspaceID() == PCANDIDATE->workspaceID() && w->m_isFloating && w->m_createdOverFullscreen && w->visibleOnMonitor(m_self.lock())) { + if (w->workspaceID() == PCANDIDATE->workspaceID() && w->m_isFloating && w->isAllowedOverFullscreen() && w->visibleOnMonitor(m_self.lock())) { reasons |= SC_FLOAT; if (!full) return reasons; @@ -1797,7 +1797,7 @@ void CMonitor::recheckSolitary() { uint8_t CMonitor::isTearingBlocked(bool full) { uint8_t reasons = 0; - static auto PTEARINGENABLED = CConfigValue("general:allow_tearing"); + static auto PTEARINGENABLED = CConfigValue("general:allow_tearing"); if (!m_tearingState.nextRenderTorn) { reasons |= TC_NOT_TORN; @@ -1852,8 +1852,8 @@ bool CMonitor::updateTearing() { uint16_t CMonitor::isDSBlocked(bool full) { uint16_t reasons = 0; - static auto PDIRECTSCANOUT = CConfigValue("render:direct_scanout"); - static auto PNONSHADER = CConfigValue("render:non_shader_cm"); + static auto PDIRECTSCANOUT = CConfigValue("render:direct_scanout"); + static auto PNONSHADER = CConfigValue("render:non_shader_cm"); const auto PWORKSPACE = m_activeWorkspace; // Fast reject for the hot render path; full=true callers still collect @@ -1942,8 +1942,8 @@ uint16_t CMonitor::isDSBlocked(bool full) { } bool CMonitor::attemptDirectScanout() { - static const auto PSAME = CConfigValue("debug:ds_handle_same_buffer"); - static const auto PSAMEFIFO = CConfigValue("debug:ds_handle_same_buffer_fifo"); + static const auto PSAME = CConfigValue("debug:ds_handle_same_buffer"); + static const auto PSAMEFIFO = CConfigValue("debug:ds_handle_same_buffer_fifo"); const auto blockedReason = isDSBlocked(); if (blockedReason) @@ -1991,7 +1991,8 @@ bool CMonitor::attemptDirectScanout() { if (m_lastScanout.expired()) m_prevDrmFormat = m_drmFormat; - const bool NEEDS_TEST = !m_lastScanout || m_drmFormat != params.format; // do not retest while it's active + const auto PREV_FORMAT = m_drmFormat; + const bool NEEDS_TEST = !m_lastScanout || m_drmFormat != params.format; // do not retest while it's active if (m_drmFormat != params.format) { m_output->state->setFormat(params.format); m_drmFormat = params.format; @@ -2004,6 +2005,10 @@ bool CMonitor::attemptDirectScanout() { if (NEEDS_TEST && !m_state.test()) { Log::logger->log(Log::TRACE, "attemptDirectScanout: failed basic test"); + if (m_drmFormat != PREV_FORMAT) { + m_output->state->setFormat(PREV_FORMAT); + m_drmFormat = PREV_FORMAT; + } return false; } @@ -2029,6 +2034,10 @@ bool CMonitor::attemptDirectScanout() { if (!ok) { Log::logger->log(Log::TRACE, "attemptDirectScanout: failed to scanout surface"); + if (m_drmFormat != PREV_FORMAT) { + m_output->state->setFormat(PREV_FORMAT); + m_drmFormat = PREV_FORMAT; + } m_lastScanout.reset(); return false; } @@ -2096,8 +2105,8 @@ bool CMonitor::isMultiGPU() { } bool CMonitor::shouldUseSoftwareCursors() { - static auto PNOHW = CConfigValue("cursor:no_hardware_cursors"); - static auto PINVISIBLE = CConfigValue("cursor:invisible"); + static auto PNOHW = CConfigValue("cursor:no_hardware_cursors"); + static auto PINVISIBLE = CConfigValue("cursor:invisible"); if (m_tearingState.activelyTearing) return true; @@ -2305,7 +2314,7 @@ NColorManagement::SImageDescription::SPCMasteringLuminances CMonitor::getMasteri } uint32_t CMonitor::getPreferredReadFormat() { - static const auto PFORCE8BIT = CConfigValue("misc:screencopy_force_8b"); + static const auto PFORCE8BIT = CConfigValue("misc:screencopy_force_8b"); auto monFmt = m_output->state->state().drmFormat; @@ -2336,7 +2345,7 @@ static bool isCompatibleTF(eTransferFunction sourceTF, eTransferFunction targetT // TODO support more drm properties bool CMonitor::canNoShaderCM(bool forDSmode) { - static auto PNONSHADER = CConfigValue("render:non_shader_cm"); + static auto PNONSHADER = CConfigValue("render:non_shader_cm"); if (*PNONSHADER == CM_NS_DISABLE) return false; diff --git a/src/helpers/MonitorFrameScheduler.cpp b/src/helpers/MonitorFrameScheduler.cpp index 58caeca5d..40c464177 100644 --- a/src/helpers/MonitorFrameScheduler.cpp +++ b/src/helpers/MonitorFrameScheduler.cpp @@ -11,7 +11,7 @@ CMonitorFrameScheduler::CMonitorFrameScheduler(PHLMONITOR m) : m_monitor(m) { } bool CMonitorFrameScheduler::newSchedulingEnabled() { - static auto PENABLENEW = CConfigValue("render:new_render_scheduling"); + static auto PENABLENEW = CConfigValue("render:new_render_scheduling"); return *PENABLENEW && g_pHyprRenderer->explicitSyncSupported() && m_monitor && !m_monitor->m_directScanoutIsActive; } diff --git a/src/helpers/MonitorZoomController.cpp b/src/helpers/MonitorZoomController.cpp index 3e1bab99a..c1b6cc6af 100644 --- a/src/helpers/MonitorZoomController.cpp +++ b/src/helpers/MonitorZoomController.cpp @@ -69,11 +69,11 @@ void CMonitorZoomController::zoomWithDetachedCamera(CBox& result, const Render:: } void CMonitorZoomController::applyZoomTransform(CBox& monbox, const Render::SRenderData& m_renderData) { - static auto PZOOMRIGID = CConfigValue("cursor:zoom_rigid"); - static auto PZOOMDETACHEDCAMERA = CConfigValue("cursor:zoom_detached_camera"); + static auto PZOOMRIGID = CConfigValue("cursor:zoom_rigid"); + static auto PZOOMDETACHEDCAMERA = CConfigValue("cursor:zoom_detached_camera"); const auto ZOOM = g_pHyprRenderer->m_renderData.mouseZoomFactor; - if (ZOOM == 1.0f) + if (ZOOM == 1.F) return; const auto m = m_renderData.pMonitor; diff --git a/src/helpers/TransferFunction.cpp b/src/helpers/TransferFunction.cpp index 074f4b19c..fa6fbb06b 100644 --- a/src/helpers/TransferFunction.cpp +++ b/src/helpers/TransferFunction.cpp @@ -30,7 +30,7 @@ eTF NTransferFunction::fromConfig(bool useICC) { if (useICC) return TF_SRGB; - static auto PSDREOTF = CConfigValue("render:cm_sdr_eotf"); + static auto PSDREOTF = CConfigValue("render:cm_sdr_eotf"); static auto sdrEOTF = NTransferFunction::fromString(*PSDREOTF); static auto P = Event::bus()->m_events.config.reloaded.listen([]() { sdrEOTF = NTransferFunction::fromString(*PSDREOTF); }); diff --git a/src/helpers/cm/ICC.cpp b/src/helpers/cm/ICC.cpp index 00140c624..69a67c1d9 100644 --- a/src/helpers/cm/ICC.cpp +++ b/src/helpers/cm/ICC.cpp @@ -91,7 +91,7 @@ static std::expected, std::string> readVCGT16(cmsHPR for (int c = 0; c < 3; ++c) { table.ch[c].resize(table.entries); for (uint16_t i = 0; i < table.entries; ++i) { - const uint8_t* p = raw.data() + tableOff + static_cast((c * table.entries + i) * 2); + const uint8_t* p = raw.data() + tableOff + sc((c * table.entries + i) * 2); table.ch[c][i] = bigEndianU16(p); // 0 ... 65535 } } @@ -232,7 +232,7 @@ static std::expected buildIcc3DLut(cmsHPROFILE profile, SImag } std::expected SImageDescription::fromICC(const std::filesystem::path& file) { - static auto PVCGTENABLED = CConfigValue("render:icc_vcgt_enabled"); + static auto PVCGTENABLED = CConfigValue("render:icc_vcgt_enabled"); std::error_code ec; if (!std::filesystem::exists(file, ec) || ec) diff --git a/src/helpers/math/Expression.cpp b/src/helpers/math/Expression.cpp index 3c0bee919..2289b85e9 100644 --- a/src/helpers/math/Expression.cpp +++ b/src/helpers/math/Expression.cpp @@ -2,8 +2,50 @@ #include "muParser.h" #include "../../debug/log/Logger.hpp" +#include +#include + using namespace Math; +static std::string_view trimExprView(std::string_view s) { + while (!s.empty() && std::isspace(static_cast(s.front()))) + s.remove_prefix(1); + + while (!s.empty() && std::isspace(static_cast(s.back()))) + s.remove_suffix(1); + + return s; +} + +bool SExpressionVec2::empty() const { + return x.empty() || y.empty(); +} + +std::string SExpressionVec2::toString() const { + if (empty()) + return ""; + + return std::format("{} {}", x, y); +} + +std::expected Math::parseExpressionVec2(std::string_view raw) { + raw = trimExprView(raw); + if (raw.empty()) + return SExpressionVec2{}; + + const auto spacePos = raw.find_first_of(" \t\n\r\f\v"); + if (spacePos == std::string_view::npos) + return std::unexpected("expression vec2 requires two expressions separated by whitespace"); + + auto lhs = trimExprView(raw.substr(0, spacePos)); + auto rhs = trimExprView(raw.substr(spacePos + 1)); + + if (lhs.empty() || rhs.empty()) + return std::unexpected("expression vec2 requires two non-empty expressions"); + + return SExpressionVec2{std::string{lhs}, std::string{rhs}}; +} + CExpression::CExpression() : m_parser(makeUnique()) { ; } diff --git a/src/helpers/math/Expression.hpp b/src/helpers/math/Expression.hpp index 1780e3eeb..631d78ad0 100644 --- a/src/helpers/math/Expression.hpp +++ b/src/helpers/math/Expression.hpp @@ -1,14 +1,26 @@ #pragma once #include "../memory/Memory.hpp" +#include #include #include +#include namespace mu { class Parser; }; namespace Math { + struct SExpressionVec2 { + std::string x; + std::string y; + + bool empty() const; + std::string toString() const; + }; + + std::expected parseExpressionVec2(std::string_view raw); + class CExpression { public: CExpression(); @@ -25,4 +37,4 @@ namespace Math { private: UP m_parser; }; -}; \ No newline at end of file +}; diff --git a/src/i18n/Engine.cpp b/src/i18n/Engine.cpp index 92280e760..d81bf9d9d 100644 --- a/src/i18n/Engine.cpp +++ b/src/i18n/Engine.cpp @@ -1354,8 +1354,8 @@ I18n::CI18nEngine::CI18nEngine() { huEngine->registerEntry("ar", TXT_KEY_SAFE_MODE_TITLE, "الوضع الآمن"); huEngine->registerEntry("ar", TXT_KEY_SAFE_MODE_DESCRIPTION, "شُغل Hyprland في الوضع الآمن، هذا يعني أن جلستك الأخيرة قد انهارت.\nالوضع الآمن يمنع تحميل إعداداتك، " - "يمكنك البحث عن وحل المشاكل في هذه البيئة، أو تحميل إعداداتك باستخدام الزر أدناه.\n اختصارات المفاتيح الافتراضية: الطرفية (Kitty) — SUPER+Q، مشغّل " - "الأوامر البسيط — SUPER+R، الخروج — SUPER+M.\n" + "يمكنك البحث عن وحل المشاكل في هذه البيئة، أو تحميل إعداداتك باستخدام الزر أدناه.\n اختصارات المفاتيح الافتراضية: الطرفية (Kitty) - SUPER+Q، مشغّل " + "الأوامر البسيط - SUPER+R، الخروج - SUPER+M.\n" "إعادة تشغيل Hyprland سيشغله في الوضع العادي"); huEngine->registerEntry("ar", TXT_KEY_SAFE_MODE_BUTTON_LOAD_CONFIG, "حمل ملف الإعدادات"); huEngine->registerEntry("ar", TXT_KEY_SAFE_MODE_BUTTON_OPEN_CRASH_REPORT_DIR, "افتح مجلد تقرير الانهيار"); diff --git a/src/layout/LayoutManager.cpp b/src/layout/LayoutManager.cpp index 1c7384266..f90cedf63 100644 --- a/src/layout/LayoutManager.cpp +++ b/src/layout/LayoutManager.cpp @@ -68,17 +68,17 @@ void CLayoutManager::setTargetGeom(const CBox& box, SP target) { target->space()->setTargetGeom(box, target); } -std::expected CLayoutManager::layoutMsg(const std::string_view& sv) { +Config::ErrorResult CLayoutManager::layoutMsg(const std::string_view& sv) { const auto MONITOR = Desktop::focusState()->monitor(); // forward to the active workspace if (!MONITOR) - return std::unexpected("No monitor, can't find ws to target"); + return Config::configError("No monitor, can't find ws to target", Config::eConfigErrorLevel::ERROR, Config::eConfigErrorCode::NO_TARGET); auto ws = MONITOR->m_activeSpecialWorkspace ? MONITOR->m_activeSpecialWorkspace : MONITOR->m_activeWorkspace; if (!ws) - return std::unexpected("No workspace, can't target"); + return Config::configError("No workspace, can't target", Config::eConfigErrorLevel::ERROR, Config::eConfigErrorCode::NO_TARGET); return ws->m_space->layoutMsg(sv); } @@ -187,13 +187,13 @@ void CLayoutManager::performSnap(Vector2D& sourcePos, Vector2D& sourceSize, SP("general:snap:window_gap"); - static auto SNAPMONITORGAP = CConfigValue("general:snap:monitor_gap"); - static auto SNAPBORDEROVERLAP = CConfigValue("general:snap:border_overlap"); - static auto SNAPRESPECTGAPS = CConfigValue("general:snap:respect_gaps"); + static auto SNAPWINDOWGAP = CConfigValue("general:snap:window_gap"); + static auto SNAPMONITORGAP = CConfigValue("general:snap:monitor_gap"); + static auto SNAPBORDEROVERLAP = CConfigValue("general:snap:border_overlap"); + static auto SNAPRESPECTGAPS = CConfigValue("general:snap:respect_gaps"); - static auto PGAPSIN = CConfigValue("general:gaps_in"); - static auto PGAPSOUT = CConfigValue("general:gaps_out"); + static auto PGAPSIN = CConfigValue("general:gaps_in"); + static auto PGAPSOUT = CConfigValue("general:gaps_out"); const auto GAPSNONE = Config::CCssGapData{0, 0, 0, 0}; const SnapFn SNAP = (MODE == MBIND_MOVE) ? snapMove : snapResize; @@ -212,12 +212,12 @@ void CLayoutManager::performSnap(Vector2D& sourcePos, Vector2D& sourceSize, SPworkspaceID(); const bool HASFULLSCREEN = DRAGGINGWINDOW->m_workspace && DRAGGINGWINDOW->m_workspace->m_hasFullscreenWindow; - const auto* GAPSIN = *SNAPRESPECTGAPS ? sc(PGAPSIN.ptr()->getData()) : &GAPSNONE; + const auto* GAPSIN = *SNAPRESPECTGAPS ? sc(PGAPSIN.ptr()) : &GAPSNONE; const double GAPSX = GAPSIN->m_left + GAPSIN->m_right; const double GAPSY = GAPSIN->m_top + GAPSIN->m_bottom; for (auto& other : g_pCompositor->m_windows) { - if ((HASFULLSCREEN && !other->m_createdOverFullscreen) || other == DRAGGINGWINDOW || other->workspaceID() != WSID || !other->m_isMapped || other->m_fadingOut || + if ((HASFULLSCREEN && !other->isAllowedOverFullscreen()) || other == DRAGGINGWINDOW || other->workspaceID() != WSID || !other->m_isMapped || other->m_fadingOut || other->isX11OverrideRedirect()) continue; @@ -275,7 +275,7 @@ void CLayoutManager::performSnap(Vector2D& sourcePos, Vector2D& sourceSize, SPm_monitor.lock(); - const auto* GAPSOUT = *SNAPRESPECTGAPS ? sc(PGAPSOUT.ptr()->getData()) : &GAPSNONE; + const auto* GAPSOUT = *SNAPRESPECTGAPS ? sc(PGAPSOUT.ptr()) : &GAPSNONE; const auto WORK_AREA = Desktop::CReservedArea{GAPSOUT->m_top, GAPSOUT->m_right, GAPSOUT->m_bottom, GAPSOUT->m_left}.apply(MON->logicalBoxMinusReserved()); SRange monX = {WORK_AREA.x, WORK_AREA.x + WORK_AREA.w}; diff --git a/src/layout/LayoutManager.hpp b/src/layout/LayoutManager.hpp index e99911d51..2660cf289 100644 --- a/src/layout/LayoutManager.hpp +++ b/src/layout/LayoutManager.hpp @@ -3,6 +3,7 @@ #include "../helpers/memory/Memory.hpp" #include "../helpers/math/Math.hpp" #include "../managers/input/InputManager.hpp" +#include "../config/shared/ConfigErrors.hpp" #include "supplementary/DragController.hpp" @@ -44,38 +45,38 @@ namespace Layout { CLayoutManager(); ~CLayoutManager() = default; - void newTarget(SP target, SP space); - void removeTarget(SP target); + void newTarget(SP target, SP space); + void removeTarget(SP target); - void changeFloatingMode(SP target); + void changeFloatingMode(SP target); - void beginDragTarget(SP target, eMouseBindMode mode); - void moveMouse(const Vector2D& mousePos); - void resizeTarget(const Vector2D& Δ, SP target, eRectCorner corner = CORNER_NONE); - void moveTarget(const Vector2D& Δ, SP target); - void setTargetGeom(const CBox& box, SP target); // floats only - void endDragTarget(); + void beginDragTarget(SP target, eMouseBindMode mode); + void moveMouse(const Vector2D& mousePos); + void resizeTarget(const Vector2D& Δ, SP target, eRectCorner corner = CORNER_NONE); + void moveTarget(const Vector2D& Δ, SP target); + void setTargetGeom(const CBox& box, SP target); // floats only + void endDragTarget(); - std::expected layoutMsg(const std::string_view& sv); + Config::ErrorResult layoutMsg(const std::string_view& sv); - void fullscreenRequestForTarget(SP target, eFullscreenMode currentEffectiveMode, eFullscreenMode effectiveMode); + void fullscreenRequestForTarget(SP target, eFullscreenMode currentEffectiveMode, eFullscreenMode effectiveMode); - void switchTargets(SP a, SP b, bool preserveFocus = true); + void switchTargets(SP a, SP b, bool preserveFocus = true); - void moveInDirection(SP target, const std::string& direction, bool silent = false); + void moveInDirection(SP target, const std::string& direction, bool silent = false); - SP getNextCandidate(SP space, SP from); + SP getNextCandidate(SP space, SP from); - bool isReachable(SP target); + bool isReachable(SP target); - void bringTargetToTop(SP target); + void bringTargetToTop(SP target); - std::optional predictSizeForNewTiledTarget(); + std::optional predictSizeForNewTiledTarget(); - void performSnap(Vector2D& sourcePos, Vector2D& sourceSize, SP target, eMouseBindMode mode, int corner, const Vector2D& beginSize); + void performSnap(Vector2D& sourcePos, Vector2D& sourceSize, SP target, eMouseBindMode mode, int corner, const Vector2D& beginSize); - void invalidateMonitorGeometries(PHLMONITOR); - void recalculateMonitor(PHLMONITOR); + void invalidateMonitorGeometries(PHLMONITOR); + void recalculateMonitor(PHLMONITOR); const UP& dragController(); @@ -84,4 +85,4 @@ namespace Layout { }; } -inline UP g_layoutManager; \ No newline at end of file +inline UP g_layoutManager; diff --git a/src/layout/algorithm/Algorithm.cpp b/src/layout/algorithm/Algorithm.cpp index eb737d34a..b22eb9bfc 100644 --- a/src/layout/algorithm/Algorithm.cpp +++ b/src/layout/algorithm/Algorithm.cpp @@ -132,7 +132,7 @@ void CAlgorithm::recenter(SP t) { m_floating->recenter(t); } -std::expected CAlgorithm::layoutMsg(const std::string_view& sv) { +Config::ErrorResult CAlgorithm::layoutMsg(const std::string_view& sv) { if (const auto ret = m_floating->layoutMsg(sv); !ret) return ret; return m_tiled->layoutMsg(sv); diff --git a/src/layout/algorithm/Algorithm.hpp b/src/layout/algorithm/Algorithm.hpp index 7df6c5c18..8b9e471d2 100644 --- a/src/layout/algorithm/Algorithm.hpp +++ b/src/layout/algorithm/Algorithm.hpp @@ -20,38 +20,38 @@ namespace Layout { static SP create(UP&& tiled, UP&& floating, SP space); ~CAlgorithm() = default; - void addTarget(SP target); - void moveTarget(SP target, std::optional focalPoint = std::nullopt, bool reposition = false); - void removeTarget(SP target); + void addTarget(SP target); + void moveTarget(SP target, std::optional focalPoint = std::nullopt, bool reposition = false); + void removeTarget(SP target); - void swapTargets(SP a, SP b); - void moveTargetInDirection(SP t, Math::eDirection dir, bool silent); + void swapTargets(SP a, SP b); + void moveTargetInDirection(SP t, Math::eDirection dir, bool silent); - SP getNextCandidate(SP old); + SP getNextCandidate(SP old); - void setFloating(SP target, bool floating, bool reposition = false); + void setFloating(SP target, bool floating, bool reposition = false); - std::expected layoutMsg(const std::string_view& sv); - std::optional predictSizeForNewTiledTarget(); + Config::ErrorResult layoutMsg(const std::string_view& sv); + std::optional predictSizeForNewTiledTarget(); - void recalculate(); - void recenter(SP t); + void recalculate(); + void recenter(SP t); - void resizeTarget(const Vector2D& Δ, SP target, eRectCorner corner = CORNER_NONE); - void moveTarget(const Vector2D& Δ, SP target); + void resizeTarget(const Vector2D& Δ, SP target, eRectCorner corner = CORNER_NONE); + void moveTarget(const Vector2D& Δ, SP target); - void setTargetGeom(const CBox& box, SP target); // only for float + void setTargetGeom(const CBox& box, SP target); // only for float - void updateFloatingAlgo(UP&& algo); - void updateTiledAlgo(UP&& algo); + void updateFloatingAlgo(UP&& algo); + void updateTiledAlgo(UP&& algo); - const UP& tiledAlgo() const; - const UP& floatingAlgo() const; + const UP& tiledAlgo() const; + const UP& floatingAlgo() const; - SP space() const; + SP space() const; - size_t tiledTargets() const; - size_t floatingTargets() const; + size_t tiledTargets() const; + size_t floatingTargets() const; private: CAlgorithm(UP&& tiled, UP&& floating, SP space); @@ -63,4 +63,4 @@ namespace Layout { std::vector> m_tiledTargets, m_floatingTargets; }; -} \ No newline at end of file +} diff --git a/src/layout/algorithm/ModeAlgorithm.cpp b/src/layout/algorithm/ModeAlgorithm.cpp index 2fea2b681..c029336f3 100644 --- a/src/layout/algorithm/ModeAlgorithm.cpp +++ b/src/layout/algorithm/ModeAlgorithm.cpp @@ -7,7 +7,7 @@ using namespace Layout; -std::expected IModeAlgorithm::layoutMsg(const std::string_view& sv) { +Config::ErrorResult IModeAlgorithm::layoutMsg(const std::string_view& sv) { return {}; } diff --git a/src/layout/algorithm/ModeAlgorithm.hpp b/src/layout/algorithm/ModeAlgorithm.hpp index 0fedc3da2..e53d4391e 100644 --- a/src/layout/algorithm/ModeAlgorithm.hpp +++ b/src/layout/algorithm/ModeAlgorithm.hpp @@ -39,7 +39,7 @@ namespace Layout { virtual void moveTargetInDirection(SP t, Math::eDirection dir, bool silent) = 0; // optional: handle layout messages - virtual std::expected layoutMsg(const std::string_view& sv); + virtual Config::ErrorResult layoutMsg(const std::string_view& sv); // optional: predict new window's size virtual std::optional predictSizeForNewTarget(); @@ -54,4 +54,4 @@ namespace Layout { friend class Layout::CAlgorithm; }; -} \ No newline at end of file +} diff --git a/src/layout/algorithm/floating/default/DefaultFloatingAlgorithm.cpp b/src/layout/algorithm/floating/default/DefaultFloatingAlgorithm.cpp index 0d069e4f3..f8f89fcbd 100644 --- a/src/layout/algorithm/floating/default/DefaultFloatingAlgorithm.cpp +++ b/src/layout/algorithm/floating/default/DefaultFloatingAlgorithm.cpp @@ -50,10 +50,10 @@ void CDefaultFloatingAlgorithm::newTarget(SP target) { // set this here so that expressions can use it. This could be wrong of course. WINDOW->m_realSize->setValueAndWarp(DESIRED_GEOM ? DESIRED_GEOM->size : DEFAULT_SIZE); - if (!WINDOW->m_ruleApplicator->static_.size.empty()) { - const auto COMPUTED = WINDOW->calculateExpression(WINDOW->m_ruleApplicator->static_.size); + if (WINDOW->m_ruleApplicator->static_.size) { + const auto COMPUTED = WINDOW->calculateExpression(*WINDOW->m_ruleApplicator->static_.size); if (!COMPUTED) - Log::logger->log(Log::ERR, "failed to parse {} as an expression", WINDOW->m_ruleApplicator->static_.size); + Log::logger->log(Log::ERR, "failed to parse {} as an expression", WINDOW->m_ruleApplicator->static_.size->toString()); else { windowGeometry.w = COMPUTED->x; windowGeometry.h = COMPUTED->y; @@ -63,10 +63,10 @@ void CDefaultFloatingAlgorithm::newTarget(SP target) { } } - if (!WINDOW->m_ruleApplicator->static_.position.empty()) { - const auto COMPUTED = WINDOW->calculateExpression(WINDOW->m_ruleApplicator->static_.position); + if (WINDOW->m_ruleApplicator->static_.position) { + const auto COMPUTED = WINDOW->calculateExpression(*WINDOW->m_ruleApplicator->static_.position); if (!COMPUTED) - Log::logger->log(Log::ERR, "failed to parse {} as an expression", WINDOW->m_ruleApplicator->static_.position); + Log::logger->log(Log::ERR, "failed to parse {} as an expression", WINDOW->m_ruleApplicator->static_.position->toString()); else { windowGeometry.x = COMPUTED->x + MONITOR_POS.x; windowGeometry.y = COMPUTED->y + MONITOR_POS.y; diff --git a/src/layout/algorithm/tiled/dwindle/DwindleAlgorithm.cpp b/src/layout/algorithm/tiled/dwindle/DwindleAlgorithm.cpp index baf767be0..eabb77674 100644 --- a/src/layout/algorithm/tiled/dwindle/DwindleAlgorithm.cpp +++ b/src/layout/algorithm/tiled/dwindle/DwindleAlgorithm.cpp @@ -18,58 +18,40 @@ using namespace Layout::Tiled; using namespace Hyprutils::String; -struct Layout::Tiled::SDwindleNodeData { - WP pParent; - bool isNode = false; - WP pTarget; - std::array, 2> children = {}; - WP self; - bool splitTop = false; // for preserve_split - CBox box = {0}; - float splitRatio = 1.f; - bool valid = true; - bool ignoreFullscreenChecks = false; +void SDwindleNodeData::recalcSizePosRecursive(bool force, bool horizontalOverride, bool verticalOverride) { + if (children[0]) { + static auto PSMARTSPLIT = CConfigValue("dwindle:smart_split"); + static auto PPRESERVESPLIT = CConfigValue("dwindle:preserve_split"); + static auto PFLMULT = CConfigValue("dwindle:split_width_multiplier"); + static auto PPRECISEMOUSEMOVE = CConfigValue("dwindle:precise_mouse_move"); - // For list lookup - bool operator==(const SDwindleNodeData& rhs) const { - return pTarget.lock() == rhs.pTarget.lock() && box == rhs.box && pParent == rhs.pParent && children[0] == rhs.children[0] && children[1] == rhs.children[1]; - } + if (*PPRESERVESPLIT == 0 && *PSMARTSPLIT == 0 && *PPRECISEMOUSEMOVE == 0) + splitTop = box.h * *PFLMULT > box.w; - void recalcSizePosRecursive(bool force = false, bool horizontalOverride = false, bool verticalOverride = false) { - if (children[0]) { - static auto PSMARTSPLIT = CConfigValue("dwindle:smart_split"); - static auto PPRESERVESPLIT = CConfigValue("dwindle:preserve_split"); - static auto PFLMULT = CConfigValue("dwindle:split_width_multiplier"); - static auto PPRECISEMOUSEMOVE = CConfigValue("dwindle:precise_mouse_move"); + if (verticalOverride) + splitTop = true; + else if (horizontalOverride) + splitTop = false; - if (*PPRESERVESPLIT == 0 && *PSMARTSPLIT == 0 && *PPRECISEMOUSEMOVE == 0) - splitTop = box.h * *PFLMULT > box.w; + const auto SPLITSIDE = !splitTop; - if (verticalOverride) - splitTop = true; - else if (horizontalOverride) - splitTop = false; + if (SPLITSIDE) { + // split left/right + const float FIRSTSIZE = box.w / 2.0 * splitRatio; + children[0]->box = CBox{box.x, box.y, FIRSTSIZE, box.h}.noNegativeSize(); + children[1]->box = CBox{box.x + FIRSTSIZE, box.y, box.w - FIRSTSIZE, box.h}.noNegativeSize(); + } else { + // split top/bottom + const float FIRSTSIZE = box.h / 2.0 * splitRatio; + children[0]->box = CBox{box.x, box.y, box.w, FIRSTSIZE}.noNegativeSize(); + children[1]->box = CBox{box.x, box.y + FIRSTSIZE, box.w, box.h - FIRSTSIZE}.noNegativeSize(); + } - const auto SPLITSIDE = !splitTop; - - if (SPLITSIDE) { - // split left/right - const float FIRSTSIZE = box.w / 2.0 * splitRatio; - children[0]->box = CBox{box.x, box.y, FIRSTSIZE, box.h}.noNegativeSize(); - children[1]->box = CBox{box.x + FIRSTSIZE, box.y, box.w - FIRSTSIZE, box.h}.noNegativeSize(); - } else { - // split top/bottom - const float FIRSTSIZE = box.h / 2.0 * splitRatio; - children[0]->box = CBox{box.x, box.y, box.w, FIRSTSIZE}.noNegativeSize(); - children[1]->box = CBox{box.x, box.y + FIRSTSIZE, box.w, box.h - FIRSTSIZE}.noNegativeSize(); - } - - children[0]->recalcSizePosRecursive(force); - children[1]->recalcSizePosRecursive(force); - } else - pTarget->setPositionGlobal(box); - } -}; + children[0]->recalcSizePosRecursive(force); + children[1]->recalcSizePosRecursive(force); + } else + pTarget->setPositionGlobal(box); +} void CDwindleAlgorithm::newTarget(SP target) { addTarget(target); @@ -84,8 +66,8 @@ void CDwindleAlgorithm::addTarget(SP target) { const auto PMONITOR = m_parent->space()->workspace()->m_monitor; const auto PWORKSPACE = m_parent->space()->workspace(); - static auto PUSEACTIVE = CConfigValue("dwindle:use_active_for_splits"); - static auto PDEFAULTSPLIT = CConfigValue("dwindle:default_split_ratio"); + static auto PUSEACTIVE = CConfigValue("dwindle:use_active_for_splits"); + static auto PDEFAULTSPLIT = CConfigValue("dwindle:default_split_ratio"); // Populate the node with our window's data PNODE->pTarget = target; @@ -154,17 +136,17 @@ void CDwindleAlgorithm::addTarget(SP target) { NEWPARENT->isNode = true; // it is a node NEWPARENT->splitRatio = std::clamp(*PDEFAULTSPLIT, 0.1F, 1.9F); - static auto PWIDTHMULTIPLIER = CConfigValue("dwindle:split_width_multiplier"); + static auto PWIDTHMULTIPLIER = CConfigValue("dwindle:split_width_multiplier"); // if cursor over first child, make it first, etc const auto SIDEBYSIDE = NEWPARENT->box.w > NEWPARENT->box.h * *PWIDTHMULTIPLIER; NEWPARENT->splitTop = !SIDEBYSIDE; - static auto PFORCESPLIT = CConfigValue("dwindle:force_split"); - static auto PERMANENTDIRECTIONOVERRIDE = CConfigValue("dwindle:permanent_direction_override"); - static auto PSMARTSPLIT = CConfigValue("dwindle:smart_split"); - static auto PSPLITBIAS = CConfigValue("dwindle:split_bias"); - static auto PPRECISEMOUSEMOVE = CConfigValue("dwindle:precise_mouse_move"); + static auto PFORCESPLIT = CConfigValue("dwindle:force_split"); + static auto PERMANENTDIRECTIONOVERRIDE = CConfigValue("dwindle:permanent_direction_override"); + static auto PSMARTSPLIT = CConfigValue("dwindle:smart_split"); + static auto PSPLITBIAS = CConfigValue("dwindle:split_bias"); + static auto PPRECISEMOUSEMOVE = CConfigValue("dwindle:precise_mouse_move"); bool horizontalOverride = false; bool verticalOverride = false; @@ -324,8 +306,8 @@ void CDwindleAlgorithm::resizeTarget(const Vector2D& Δ, SP target, eRe if (!PNODE) return; - static auto PANIMATE = CConfigValue("misc:animate_manual_resizes"); - static auto PSMARTRESIZING = CConfigValue("dwindle:smart_resizing"); + static auto PANIMATE = CConfigValue("misc:animate_manual_resizes"); + static auto PSMARTRESIZING = CConfigValue("dwindle:smart_resizing"); // get some data about our window const auto PMONITOR = m_parent->space()->workspace()->m_monitor; @@ -533,7 +515,7 @@ std::optional CDwindleAlgorithm::predictSizeForNewTarget() { CBox box = PNODE->box; - static auto PFLMULT = CConfigValue("dwindle:split_width_multiplier"); + static auto PFLMULT = CConfigValue("dwindle:split_width_multiplier"); bool splitTop = box.h * *PFLMULT > box.w; @@ -553,7 +535,7 @@ std::optional CDwindleAlgorithm::predictSizeForNewTarget() { } void CDwindleAlgorithm::moveTargetInDirection(SP t, Math::eDirection dir, bool silent) { - static auto PMONITORFALLBACK = CConfigValue("binds:window_direction_monitor_fallback"); + static auto PMONITORFALLBACK = CConfigValue("binds:window_direction_monitor_fallback"); const auto PNODE = getNodeFromTarget(t); const Vector2D originalPos = t->position().middle(); @@ -655,7 +637,7 @@ SP CDwindleAlgorithm::getMasterNode() { return nullptr; } -std::expected CDwindleAlgorithm::layoutMsg(const std::string_view& sv) { +Config::ErrorResult CDwindleAlgorithm::layoutMsg(const std::string_view& sv) { const auto ARGS = CVarList2(std::string{sv}, 0, ' '); const auto CURRENT_NODE = getNodeFromWindow(Desktop::focusState()->window()); @@ -663,12 +645,12 @@ std::expected CDwindleAlgorithm::layoutMsg(const std::string_ if (ARGS[0] == "togglesplit") { if (CURRENT_NODE) { if (!toggleSplit(CURRENT_NODE)) - return std::unexpected("can't togglesplit in the current workspace"); + return Config::configError("can't togglesplit in the current workspace", Config::eConfigErrorLevel::WARNING, Config::eConfigErrorCode::INVALID_STATE); } } else if (ARGS[0] == "swapsplit") { if (CURRENT_NODE) { if (!swapSplit(CURRENT_NODE)) - return std::unexpected("can't swapsplit in the current workspace"); + return Config::configError("can't swapsplit in the current workspace", Config::eConfigErrorLevel::WARNING, Config::eConfigErrorCode::INVALID_STATE); } } else if (ARGS[0] == "rotatesplit") { if (CURRENT_NODE) { @@ -678,7 +660,7 @@ std::expected CDwindleAlgorithm::layoutMsg(const std::string_ angle = std::stoi(std::string{ARGS[1]}); } catch (const std::exception& e) { Log::logger->log(Log::WARN, "Invalid angle argument for rotatesplit: {}", ARGS[1]); - return std::unexpected("Invalid angle argument"); + return Config::configError("Invalid angle argument", Config::eConfigErrorLevel::ERROR, Config::eConfigErrorCode::INVALID_ARGUMENT); } } rotateSplit(CURRENT_NODE, angle); @@ -693,13 +675,13 @@ std::expected CDwindleAlgorithm::layoutMsg(const std::string_ const auto STABLE = ARGS[2].empty() || ARGS[2] != "unstable"; if (!moveToRoot(node, STABLE)) - return std::unexpected("can't movetoroot in the current workspace"); + return Config::configError("can't movetoroot in the current workspace", Config::eConfigErrorLevel::WARNING, Config::eConfigErrorCode::INVALID_STATE); } else if (ARGS[0] == "preselect") { auto direction = ARGS[1]; if (direction.empty()) { Log::logger->log(Log::ERR, "Expected direction for preselect"); - return std::unexpected("No direction for preselect"); + return Config::configError("No direction for preselect", Config::eConfigErrorLevel::ERROR, Config::eConfigErrorCode::INVALID_ARGUMENT); } switch (direction.front()) { @@ -733,15 +715,15 @@ std::expected CDwindleAlgorithm::layoutMsg(const std::string_ bool exact = ARGS[2].starts_with("exact"); if (ratio.empty()) - return std::unexpected("splitratio requires an arg"); + return Config::configError("splitratio requires an arg", Config::eConfigErrorLevel::ERROR, Config::eConfigErrorCode::INVALID_ARGUMENT); auto delta = getPlusMinusKeywordResult(std::string{ratio}, 0.F); if (!CURRENT_NODE || !CURRENT_NODE->pParent) - return std::unexpected("cannot alter split ratio on no / single node"); + return Config::configError("cannot alter split ratio on no / single node", Config::eConfigErrorLevel::WARNING, Config::eConfigErrorCode::INVALID_STATE); if (!delta) - return std::unexpected(std::format("failed to parse \"{}\" as a delta", ratio)); + return Config::configError(std::format("failed to parse \"{}\" as a delta", ratio), Config::eConfigErrorLevel::ERROR, Config::eConfigErrorCode::INVALID_ARGUMENT); const float newRatio = exact ? *delta : CURRENT_NODE->pParent->splitRatio + *delta; CURRENT_NODE->pParent->splitRatio = std::clamp(newRatio, 0.1F, 1.9F); diff --git a/src/layout/algorithm/tiled/dwindle/DwindleAlgorithm.hpp b/src/layout/algorithm/tiled/dwindle/DwindleAlgorithm.hpp index 043f87384..300daa639 100644 --- a/src/layout/algorithm/tiled/dwindle/DwindleAlgorithm.hpp +++ b/src/layout/algorithm/tiled/dwindle/DwindleAlgorithm.hpp @@ -5,27 +5,47 @@ namespace Layout { } namespace Layout::Tiled { - struct SDwindleNodeData; + struct SDwindleNodeData { + WP pParent; + bool isNode = false; + WP pTarget; + std::array, 2> children = {}; + WP self; + bool splitTop = false; // for preserve_split + CBox box = {0}; + float splitRatio = 1.f; + bool valid = true; + bool ignoreFullscreenChecks = false; + + // For list lookup + bool operator==(const SDwindleNodeData& rhs) const { + return pTarget.lock() == rhs.pTarget.lock() && box == rhs.box && pParent == rhs.pParent && children[0] == rhs.children[0] && children[1] == rhs.children[1]; + } + + void recalcSizePosRecursive(bool force = false, bool horizontalOverride = false, bool verticalOverride = false); + }; class CDwindleAlgorithm : public ITiledAlgorithm { public: CDwindleAlgorithm() = default; virtual ~CDwindleAlgorithm() = default; - virtual void newTarget(SP target); - virtual void movedTarget(SP target, std::optional focalPoint = std::nullopt); - virtual void removeTarget(SP target); + virtual void newTarget(SP target); + virtual void movedTarget(SP target, std::optional focalPoint = std::nullopt); + virtual void removeTarget(SP target); - virtual void resizeTarget(const Vector2D& Δ, SP target, eRectCorner corner = CORNER_NONE); - virtual void recalculate(); + virtual void resizeTarget(const Vector2D& Δ, SP target, eRectCorner corner = CORNER_NONE); + virtual void recalculate(); - virtual SP getNextCandidate(SP old); + virtual SP getNextCandidate(SP old); - virtual std::expected layoutMsg(const std::string_view& sv); - virtual std::optional predictSizeForNewTarget(); + virtual Config::ErrorResult layoutMsg(const std::string_view& sv); + virtual std::optional predictSizeForNewTarget(); - virtual void swapTargets(SP a, SP b); - virtual void moveTargetInDirection(SP t, Math::eDirection dir, bool silent); + virtual void swapTargets(SP a, SP b); + virtual void moveTargetInDirection(SP t, Math::eDirection dir, bool silent); + + SP getNodeFromWindow(PHLWINDOW w); private: std::vector> m_dwindleNodesData; @@ -42,7 +62,6 @@ namespace Layout::Tiled { void addTarget(SP target); void calculateWorkspace(); SP getNodeFromTarget(SP); - SP getNodeFromWindow(PHLWINDOW w); int getNodes(); SP getFirstNode(); SP getClosestNode(const Vector2D&, SP skip = nullptr); diff --git a/src/layout/algorithm/tiled/master/MasterAlgorithm.cpp b/src/layout/algorithm/tiled/master/MasterAlgorithm.cpp index 7017deb18..46506391c 100644 --- a/src/layout/algorithm/tiled/master/MasterAlgorithm.cpp +++ b/src/layout/algorithm/tiled/master/MasterAlgorithm.cpp @@ -5,6 +5,7 @@ #include "../../../target/WindowTarget.hpp" #include "../../../../config/ConfigValue.hpp" +#include "../../../../config/shared/actions/ConfigActions.hpp" #include "../../../../config/shared/workspace/WorkspaceRuleManager.hpp" #include "../../../../desktop/state/FocusState.hpp" #include "../../../../helpers/Monitor.hpp" @@ -18,25 +19,6 @@ using namespace Layout; using namespace Layout::Tiled; using namespace Hyprutils::String; -struct Layout::Tiled::SMasterNodeData { - bool isMaster = false; - float percMaster = 0.5f; - - WP pTarget; - - Vector2D position; - Vector2D size; - - float percSize = 1.f; // size multiplier for resizing children - - bool ignoreFullscreenChecks = false; - - // - bool operator==(const SMasterNodeData& rhs) const { - return pTarget.lock() == rhs.pTarget.lock(); - } -}; - void CMasterAlgorithm::newTarget(SP target) { addTarget(target, true); } @@ -47,7 +29,7 @@ void CMasterAlgorithm::movedTarget(SP target, std::optional f void CMasterAlgorithm::addTarget(SP target, bool firstMap) { static auto PNEWONACTIVE = CConfigValue("master:new_on_active"); - static auto PNEWONTOP = CConfigValue("master:new_on_top"); + static auto PNEWONTOP = CConfigValue("master:new_on_top"); static auto PNEWSTATUS = CConfigValue("master:new_status"); const auto PWORKSPACE = m_parent->space()->workspace(); @@ -79,7 +61,7 @@ void CMasterAlgorithm::addTarget(SP target, bool firstMap) { PNODE->pTarget = target; const auto WINDOWSONWORKSPACE = getNodesNo(); - static auto PMFACT = CConfigValue("master:mfact"); + static auto PMFACT = CConfigValue("master:mfact"); float lastSplitPercent = *PMFACT; auto OPENINGON = isWindowTiled(Desktop::focusState()->window()) && Desktop::focusState()->window()->m_workspace == PWORKSPACE ? @@ -87,7 +69,7 @@ void CMasterAlgorithm::addTarget(SP target, bool firstMap) { getMasterNode(); const auto MOUSECOORDS = g_pInputManager->getMouseCoordsInternal(); - static auto PDROPATCURSOR = CConfigValue("master:drop_at_cursor"); + static auto PDROPATCURSOR = CConfigValue("master:drop_at_cursor"); eOrientation orientation = getDynamicOrientation(); const auto NODEIT = std::ranges::find(m_masterNodesData, PNODE); @@ -97,7 +79,7 @@ void CMasterAlgorithm::addTarget(SP target, bool firstMap) { if (WINDOWSONWORKSPACE > 2) { auto& v = m_masterNodesData; - const std::size_t srcIndex = static_cast(std::distance(v.begin(), NODEIT)); + const std::size_t srcIndex = sc(std::distance(v.begin(), NODEIT)); for (std::size_t i = 0; i < v.size(); ++i) { const CBox box = v[i]->pTarget->position(); @@ -131,8 +113,8 @@ void CMasterAlgorithm::addTarget(SP target, bool firstMap) { break; auto node = std::move(v[srcIndex]); - v.erase(v.begin() + static_cast(srcIndex)); - v.insert(v.begin() + static_cast(insertIndex), std::move(node)); + v.erase(v.begin() + sc(srcIndex)); + v.insert(v.begin() + sc(insertIndex), std::move(node)); break; } @@ -222,7 +204,7 @@ void CMasterAlgorithm::addTarget(SP target, bool firstMap) { void CMasterAlgorithm::removeTarget(SP target) { const auto MASTERSLEFT = getMastersNo(); - static auto SMALLSPLIT = CConfigValue("master:allow_small_split"); + static auto SMALLSPLIT = CConfigValue("master:allow_small_split"); const auto PNODE = getNodeFromTarget(target); @@ -269,8 +251,8 @@ void CMasterAlgorithm::resizeTarget(const Vector2D& Δ, SP target, eRec return; const auto PMONITOR = m_parent->space()->workspace()->m_monitor; - static auto SLAVECOUNTFORCENTER = CConfigValue("master:slave_count_for_center_master"); - static auto PSMARTRESIZING = CConfigValue("master:smart_resizing"); + static auto SLAVECOUNTFORCENTER = CConfigValue("master:slave_count_for_center_master"); + static auto PSMARTRESIZING = CConfigValue("master:smart_resizing"); const auto WORKAREA = PMONITOR->logicalBoxMinusReserved(); const bool DISPLAYBOTTOM = STICKS(PNODE->position.y + PNODE->size.y, WORKAREA.y + WORKAREA.h); @@ -405,7 +387,7 @@ void CMasterAlgorithm::swapTargets(SP a, SP b) { } void CMasterAlgorithm::moveTargetInDirection(SP t, Math::eDirection dir, bool silent) { - static auto PMONITORFALLBACK = CConfigValue("binds:window_direction_monitor_fallback"); + static auto PMONITORFALLBACK = CConfigValue("binds:window_direction_monitor_fallback"); const auto PWINDOW2 = g_pCompositor->getWindowInDirection(t->window(), dir); @@ -446,7 +428,7 @@ void CMasterAlgorithm::recalculate() { calculateWorkspace(); } -std::expected CMasterAlgorithm::layoutMsg(const std::string_view& sv) { +Config::ErrorResult CMasterAlgorithm::layoutMsg(const std::string_view& sv) { auto switchToWindow = [&](SP target) { if (!target || !validMapped(target->window())) return; @@ -459,11 +441,15 @@ std::expected CMasterAlgorithm::layoutMsg(const std::string_v g_pInputManager->m_forcedFocus.reset(); }; - CVarList2 vars(std::string{sv}, 0, 's'); + CVarList2 vars(std::string{sv}, 0, 's'); + + const auto invalidArg = [](std::string msg) { return Config::configError(std::move(msg), Config::eConfigErrorLevel::ERROR, Config::eConfigErrorCode::INVALID_ARGUMENT); }; + const auto noTarget = [](std::string msg) { return Config::configError(std::move(msg), Config::eConfigErrorLevel::WARNING, Config::eConfigErrorCode::NO_TARGET); }; + const auto stateErr = [](std::string msg) { return Config::configError(std::move(msg), Config::eConfigErrorLevel::WARNING, Config::eConfigErrorCode::INVALID_STATE); }; if (vars.size() < 1 || vars[0].empty()) { Log::logger->log(Log::ERR, "layoutmsg called without params"); - return std::unexpected("layoutmsg without params"); + return invalidArg("layoutmsg without params"); } auto command = vars[0]; @@ -479,15 +465,15 @@ std::expected CMasterAlgorithm::layoutMsg(const std::string_v if (command == "swapwithmaster") { if (!PWINDOW) - return std::unexpected("No focused window"); + return noTarget("No focused window"); if (!isWindowTiled(PWINDOW)) - return std::unexpected("focused window isn't tiled"); + return stateErr("focused window isn't tiled"); const auto PMASTER = getMasterNode(); if (!PMASTER) - return std::unexpected("no master node"); + return stateErr("no master node"); const auto NEWCHILD = PMASTER->pTarget.lock(); @@ -521,12 +507,12 @@ std::expected CMasterAlgorithm::layoutMsg(const std::string_v // * auto (default) - swap the focus with the first child, if the current focus was master, otherwise focus master else if (command == "focusmaster") { if (!PWINDOW) - return std::unexpected("no focused window"); + return noTarget("no focused window"); const auto PMASTER = getMasterNode(); if (!PMASTER) - return std::unexpected("no master"); + return stateErr("no master"); const auto& ARG = vars[1]; // returns empty string if out of bounds @@ -559,24 +545,24 @@ std::expected CMasterAlgorithm::layoutMsg(const std::string_v focusAuto(); } else if (command == "cyclenext") { if (!PWINDOW) - return std::unexpected("no window"); + return noTarget("no window"); const bool NOLOOP = vars.size() >= 2 && vars[1] == "noloop"; const auto PNEXTWINDOW = getNextTarget(PWINDOW->layoutTarget(), true, !NOLOOP); switchToWindow(PNEXTWINDOW); } else if (command == "cycleprev") { if (!PWINDOW) - return std::unexpected("no window"); + return noTarget("no window"); const bool NOLOOP = vars.size() >= 2 && vars[1] == "noloop"; const auto PPREVWINDOW = getNextTarget(PWINDOW->layoutTarget(), false, !NOLOOP); switchToWindow(PPREVWINDOW); } else if (command == "swapnext") { if (!validMapped(PWINDOW)) - return std::unexpected("no window"); + return noTarget("no window"); if (PWINDOW->layoutTarget()->floating()) { - g_pKeybindManager->m_dispatchers["swapnext"](""); + Config::Actions::swapNext(true); return {}; } @@ -590,10 +576,10 @@ std::expected CMasterAlgorithm::layoutMsg(const std::string_v } } else if (command == "swapprev") { if (!validMapped(PWINDOW)) - return std::unexpected("no window"); + return noTarget("no window"); if (PWINDOW->layoutTarget()->floating()) { - g_pKeybindManager->m_dispatchers["swapnext"]("prev"); + Config::Actions::swapNext(false); return {}; } @@ -607,19 +593,19 @@ std::expected CMasterAlgorithm::layoutMsg(const std::string_v } } else if (command == "addmaster") { if (!validMapped(PWINDOW)) - return std::unexpected("no window"); + return noTarget("no window"); if (PWINDOW->layoutTarget()->floating()) - return std::unexpected("window is floating"); + return stateErr("window is floating"); const auto PNODE = getNodeFromTarget(PWINDOW->layoutTarget()); const auto WINDOWS = getNodesNo(); const auto MASTERS = getMastersNo(); - static auto SMALLSPLIT = CConfigValue("master:allow_small_split"); + static auto SMALLSPLIT = CConfigValue("master:allow_small_split"); if (MASTERS + 2 > WINDOWS && *SMALLSPLIT == 0) - return std::unexpected("nothing to do"); + return stateErr("nothing to do"); g_pCompositor->setWindowFullscreenInternal(PWINDOW, FSMODE_NONE); @@ -640,10 +626,10 @@ std::expected CMasterAlgorithm::layoutMsg(const std::string_v } else if (command == "removemaster") { if (!validMapped(PWINDOW)) - return std::unexpected("no window"); + return noTarget("no window"); if (PWINDOW->layoutTarget()->floating()) - return std::unexpected("window isnt tiled"); + return stateErr("window isnt tiled"); const auto PNODE = getNodeFromTarget(PWINDOW->layoutTarget()); @@ -651,7 +637,7 @@ std::expected CMasterAlgorithm::layoutMsg(const std::string_v const auto MASTERS = getMastersNo(); if (WINDOWS < 2 || MASTERS < 2) - return std::unexpected("nothing to do"); + return stateErr("nothing to do"); g_pCompositor->setWindowFullscreenInternal(PWINDOW, FSMODE_NONE); @@ -670,7 +656,7 @@ std::expected CMasterAlgorithm::layoutMsg(const std::string_v calculateWorkspace(); } else if (command == "orientationleft" || command == "orientationright" || command == "orientationtop" || command == "orientationbottom" || command == "orientationcenter") { if (!PWINDOW) - return std::unexpected("no window"); + return noTarget("no window"); g_pCompositor->setWindowFullscreenInternal(PWINDOW, FSMODE_NONE); @@ -695,7 +681,7 @@ std::expected CMasterAlgorithm::layoutMsg(const std::string_v } else if (command == "mfact") { if (!PWINDOW) - return std::unexpected("no window"); + return noTarget("no window"); const bool exact = vars[1] == "exact"; @@ -703,7 +689,7 @@ std::expected CMasterAlgorithm::layoutMsg(const std::string_v try { ratio = std::stof(std::string{exact ? vars[2] : vars[1]}); - } catch (...) { return std::unexpected("bad ratio"); } + } catch (...) { return invalidArg("bad ratio"); } const auto PNODE = getNodeFromWindow(PWINDOW); @@ -717,11 +703,11 @@ std::expected CMasterAlgorithm::layoutMsg(const std::string_v const auto PNODE = getNodeFromWindow(PWINDOW); if (!PNODE) - return std::unexpected("window couldnt be found"); + return noTarget("window couldnt be found"); const auto OLDMASTER = PNODE->isMaster ? PNODE : getMasterNode(); if (!OLDMASTER) - return std::unexpected("no old master"); + return stateErr("no old master"); auto oldMasterIt = std::ranges::find(m_masterNodesData, OLDMASTER); @@ -753,11 +739,11 @@ std::expected CMasterAlgorithm::layoutMsg(const std::string_v const auto PNODE = getNodeFromWindow(PWINDOW); if (!PNODE) - return std::unexpected("window couldnt be found"); + return noTarget("window couldnt be found"); const auto OLDMASTER = PNODE->isMaster ? PNODE : getMasterNode(); if (!OLDMASTER) - return std::unexpected("no old master"); + return stateErr("no old master"); auto oldMasterIt = std::ranges::find(m_masterNodesData, OLDMASTER); @@ -916,7 +902,7 @@ SP CMasterAlgorithm::getNodeFromWindow(PHLWINDOW x) { return x ? getNodeFromTarget(x->layoutTarget()) : nullptr; } -SP CMasterAlgorithm::getNodeFromTarget(SP x) { +SP CMasterAlgorithm::getNodeFromTarget(SP x) const { for (const auto& n : m_masterNodesData) { if (n->pTarget == x) return n; @@ -953,10 +939,10 @@ void CMasterAlgorithm::calculateWorkspace() { eOrientation orientation = getDynamicOrientation(); bool centerMasterWindow = false; - static auto SLAVECOUNTFORCENTER = CConfigValue("master:slave_count_for_center_master"); + static auto SLAVECOUNTFORCENTER = CConfigValue("master:slave_count_for_center_master"); static auto CMFALLBACK = CConfigValue("master:center_master_fallback"); - static auto PIGNORERESERVED = CConfigValue("master:center_ignores_reserved"); - static auto PSMARTRESIZING = CConfigValue("master:smart_resizing"); + static auto PIGNORERESERVED = CConfigValue("master:center_ignores_reserved"); + static auto PSMARTRESIZING = CConfigValue("master:smart_resizing"); const auto MASTERS = getMastersNo(); const auto WINDOWS = getNodesNo(); @@ -1003,7 +989,7 @@ void CMasterAlgorithm::calculateWorkspace() { // compute placement of master window(s) if (WINDOWS == 1 && !centerMasterWindow) { - static auto PALWAYSKEEPPOSITION = CConfigValue("master:always_keep_position"); + static auto PALWAYSKEEPPOSITION = CConfigValue("master:always_keep_position"); if (*PALWAYSKEEPPOSITION) { const float WIDTH = WORKAREA.w * PMASTERNODE->percMaster; float nextX = 0; diff --git a/src/layout/algorithm/tiled/master/MasterAlgorithm.hpp b/src/layout/algorithm/tiled/master/MasterAlgorithm.hpp index 5cfa6b368..00e827841 100644 --- a/src/layout/algorithm/tiled/master/MasterAlgorithm.hpp +++ b/src/layout/algorithm/tiled/master/MasterAlgorithm.hpp @@ -7,7 +7,20 @@ namespace Layout { } namespace Layout::Tiled { - struct SMasterNodeData; + + struct SMasterNodeData { + bool isMaster = false; + float percMaster = 0.5f; + WP pTarget; + Vector2D position; + Vector2D size; + float percSize = 1.f; + bool ignoreFullscreenChecks = false; + + bool operator==(const SMasterNodeData& rhs) const { + return pTarget.lock() == rhs.pTarget.lock(); + } + }; //orientation determines which side of the screen the master area resides enum eOrientation : uint8_t { @@ -35,20 +48,22 @@ namespace Layout::Tiled { CMasterAlgorithm() = default; virtual ~CMasterAlgorithm() = default; - virtual void newTarget(SP target); - virtual void movedTarget(SP target, std::optional focalPoint = std::nullopt); - virtual void removeTarget(SP target); + virtual void newTarget(SP target); + virtual void movedTarget(SP target, std::optional focalPoint = std::nullopt); + virtual void removeTarget(SP target); - virtual void resizeTarget(const Vector2D& Δ, SP target, eRectCorner corner = CORNER_NONE); - virtual void recalculate(); + virtual void resizeTarget(const Vector2D& Δ, SP target, eRectCorner corner = CORNER_NONE); + virtual void recalculate(); - virtual SP getNextCandidate(SP old); + virtual SP getNextCandidate(SP old); - virtual std::expected layoutMsg(const std::string_view& sv); - virtual std::optional predictSizeForNewTarget(); + virtual Config::ErrorResult layoutMsg(const std::string_view& sv); + virtual std::optional predictSizeForNewTarget(); - virtual void swapTargets(SP a, SP b); - virtual void moveTargetInDirection(SP t, Math::eDirection dir, bool silent); + virtual void swapTargets(SP a, SP b); + virtual void moveTargetInDirection(SP t, Math::eDirection dir, bool silent); + + SP getNodeFromTarget(SP) const; private: std::vector> m_masterNodesData; @@ -64,7 +79,6 @@ namespace Layout::Tiled { eOrientation getDynamicOrientation(); int getNodesNo(); SP getNodeFromWindow(PHLWINDOW); - SP getNodeFromTarget(SP); SP getMasterNode(); SP getClosestNode(const Vector2D&); void calculateWorkspace(); @@ -73,4 +87,4 @@ namespace Layout::Tiled { bool isWindowTiled(PHLWINDOW); eOrientation defaultOrientation(); }; -}; \ No newline at end of file +}; diff --git a/src/layout/algorithm/tiled/monocle/MonocleAlgorithm.cpp b/src/layout/algorithm/tiled/monocle/MonocleAlgorithm.cpp index e1b4c6088..84b4f9e2c 100644 --- a/src/layout/algorithm/tiled/monocle/MonocleAlgorithm.cpp +++ b/src/layout/algorithm/tiled/monocle/MonocleAlgorithm.cpp @@ -46,8 +46,10 @@ CMonocleAlgorithm::~CMonocleAlgorithm() { continue; const auto WINDOW = TARGET->window(); - if (WINDOW) - WINDOW->setHidden(false); + if (WINDOW) { + WINDOW->setInputBlocked(Desktop::View::INPUT_BLOCK_MONOCLE_INACTIVE, false); + *WINDOW->alpha(Desktop::View::WINDOW_ALPHA_LAYOUT) = 1.F; + } } m_focusCallback.reset(); @@ -81,8 +83,10 @@ void CMonocleAlgorithm::removeTarget(SP target) { // unhide window when removing from monocle layout const auto WINDOW = target->window(); - if (WINDOW) - WINDOW->setHidden(false); + if (WINDOW) { + WINDOW->setInputBlocked(Desktop::View::INPUT_BLOCK_MONOCLE_INACTIVE, false); + *WINDOW->alpha(Desktop::View::WINDOW_ALPHA_LAYOUT) = 1.F; + } const auto INDEX = std::distance(m_targetDatas.begin(), it); m_targetDatas.erase(it); @@ -142,7 +146,8 @@ void CMonocleAlgorithm::recalculate() { TARGET->setPositionGlobal(WORK_AREA); const bool SHOULD_BE_VISIBLE = ((int)i == m_currentVisibleIndex); - WINDOW->setHidden(!SHOULD_BE_VISIBLE); + WINDOW->setInputBlocked(Desktop::View::INPUT_BLOCK_MONOCLE_INACTIVE, !SHOULD_BE_VISIBLE); + *WINDOW->alpha(Desktop::View::WINDOW_ALPHA_LAYOUT) = SHOULD_BE_VISIBLE ? 1.F : 0.F; } } @@ -165,11 +170,11 @@ SP CMonocleAlgorithm::getNextCandidate(SP old) { return next->get()->target.lock(); } -std::expected CMonocleAlgorithm::layoutMsg(const std::string_view& sv) { +Config::ErrorResult CMonocleAlgorithm::layoutMsg(const std::string_view& sv) { CVarList2 vars(std::string{sv}, 0, 's'); if (vars.size() < 1) - return std::unexpected("layoutmsg requires at least 1 argument"); + return Config::configError("layoutmsg requires at least 1 argument", Config::eConfigErrorLevel::ERROR, Config::eConfigErrorCode::INVALID_ARGUMENT); const auto COMMAND = vars[0]; @@ -181,7 +186,7 @@ std::expected CMonocleAlgorithm::layoutMsg(const std::string_ return {}; } - return std::unexpected(std::format("Unknown monocle layoutmsg: {}", COMMAND)); + return Config::configError(std::format("Unknown monocle layoutmsg: {}", COMMAND), Config::eConfigErrorLevel::ERROR, Config::eConfigErrorCode::INVALID_ARGUMENT); } std::optional CMonocleAlgorithm::predictSizeForNewTarget() { @@ -202,7 +207,7 @@ void CMonocleAlgorithm::swapTargets(SP a, SP b) { } void CMonocleAlgorithm::moveTargetInDirection(SP t, Math::eDirection dir, bool silent) { - static auto PMONITORFALLBACK = CConfigValue("binds:window_direction_monitor_fallback"); + static auto PMONITORFALLBACK = CConfigValue("binds:window_direction_monitor_fallback"); if (!*PMONITORFALLBACK) return; // noop diff --git a/src/layout/algorithm/tiled/monocle/MonocleAlgorithm.hpp b/src/layout/algorithm/tiled/monocle/MonocleAlgorithm.hpp index b23f85be7..3f07748b0 100644 --- a/src/layout/algorithm/tiled/monocle/MonocleAlgorithm.hpp +++ b/src/layout/algorithm/tiled/monocle/MonocleAlgorithm.hpp @@ -21,20 +21,20 @@ namespace Layout::Tiled { CMonocleAlgorithm(); virtual ~CMonocleAlgorithm(); - virtual void newTarget(SP target); - virtual void movedTarget(SP target, std::optional focalPoint = std::nullopt); - virtual void removeTarget(SP target); + virtual void newTarget(SP target); + virtual void movedTarget(SP target, std::optional focalPoint = std::nullopt); + virtual void removeTarget(SP target); - virtual void resizeTarget(const Vector2D& Δ, SP target, eRectCorner corner = CORNER_NONE); - virtual void recalculate(); + virtual void resizeTarget(const Vector2D& Δ, SP target, eRectCorner corner = CORNER_NONE); + virtual void recalculate(); - virtual SP getNextCandidate(SP old); + virtual SP getNextCandidate(SP old); - virtual std::expected layoutMsg(const std::string_view& sv); - virtual std::optional predictSizeForNewTarget(); + virtual Config::ErrorResult layoutMsg(const std::string_view& sv); + virtual std::optional predictSizeForNewTarget(); - virtual void swapTargets(SP a, SP b); - virtual void moveTargetInDirection(SP t, Math::eDirection dir, bool silent); + virtual void swapTargets(SP a, SP b); + virtual void moveTargetInDirection(SP t, Math::eDirection dir, bool silent); private: std::vector> m_targetDatas; diff --git a/src/layout/algorithm/tiled/scrolling/ScrollingAlgorithm.cpp b/src/layout/algorithm/tiled/scrolling/ScrollingAlgorithm.cpp index 83ab0974f..28941109d 100644 --- a/src/layout/algorithm/tiled/scrolling/ScrollingAlgorithm.cpp +++ b/src/layout/algorithm/tiled/scrolling/ScrollingAlgorithm.cpp @@ -378,7 +378,7 @@ void SScrollingData::centerCol(SP c) { if (!c) return; - static const auto PFSONONE = CConfigValue("scrolling:fullscreen_on_one_column"); + static const auto PFSONONE = CConfigValue("scrolling:fullscreen_on_one_column"); const auto USABLE = algorithm->usableArea(); int64_t colIdx = idx(c); @@ -390,7 +390,7 @@ void SScrollingData::fitCol(SP c) { if (!c) return; - static const auto PFSONONE = CConfigValue("scrolling:fullscreen_on_one_column"); + static const auto PFSONONE = CConfigValue("scrolling:fullscreen_on_one_column"); const auto USABLE = algorithm->usableArea(); int64_t colIdx = idx(c); @@ -402,7 +402,7 @@ void SScrollingData::centerOrFitCol(SP c) { if (!c) return; - static const auto PFITMETHOD = CConfigValue("scrolling:focus_fit_method"); + static const auto PFITMETHOD = CConfigValue("scrolling:focus_fit_method"); if (*PFITMETHOD == 1) fitCol(c); @@ -411,7 +411,7 @@ void SScrollingData::centerOrFitCol(SP c) { } SP SScrollingData::atCenter() { - static const auto PFSONONE = CConfigValue("scrolling:fullscreen_on_one_column"); + static const auto PFSONONE = CConfigValue("scrolling:fullscreen_on_one_column"); const auto USABLE = algorithm->usableArea(); size_t centerIdx = controller->getStripAtCenter(USABLE, *PFSONONE); @@ -427,7 +427,7 @@ void SScrollingData::recalculate(bool forceInstant) { algorithm->m_parent->space()->workspace()->m_hasFullscreenWindow) return; - static const auto PFSONONE = CConfigValue("scrolling:fullscreen_on_one_column"); + static const auto PFSONONE = CConfigValue("scrolling:fullscreen_on_one_column"); const CBox USABLE = algorithm->usableArea(); const auto WORKAREA = algorithm->m_parent->space()->workArea(); @@ -451,14 +451,14 @@ void SScrollingData::recalculate(bool forceInstant) { } double SScrollingData::maxWidth() { - static const auto PFSONONE = CConfigValue("scrolling:fullscreen_on_one_column"); + static const auto PFSONONE = CConfigValue("scrolling:fullscreen_on_one_column"); const auto USABLE = algorithm->usableArea(); return controller->calculateMaxExtent(USABLE, *PFSONONE); } bool SScrollingData::visible(SP c, bool full) { - static const auto PFSONONE = CConfigValue("scrolling:fullscreen_on_one_column"); + static const auto PFSONONE = CConfigValue("scrolling:fullscreen_on_one_column"); const auto USABLE = algorithm->usableArea(); int64_t colIdx = idx(c); @@ -469,8 +469,8 @@ bool SScrollingData::visible(SP c, bool full) { } CScrollingAlgorithm::CScrollingAlgorithm() { - static const auto PCONFWIDTHS = CConfigValue("scrolling:explicit_column_widths"); - static const auto PCONFDIRECTION = CConfigValue("scrolling:direction"); + static const auto PCONFWIDTHS = CConfigValue("scrolling:explicit_column_widths"); + static const auto PCONFDIRECTION = CConfigValue("scrolling:direction"); m_scrollingData = makeShared(this); m_scrollingData->self = m_scrollingData; @@ -503,7 +503,7 @@ CScrollingAlgorithm::CScrollingAlgorithm() { }; m_configCallback = Event::bus()->m_events.config.reloaded.listen([this, parseColumnWidths, parseDirection] { - static const auto PCONFDIRECTION = CConfigValue("scrolling:direction"); + static const auto PCONFDIRECTION = CConfigValue("scrolling:direction"); m_config.configuredWidths.clear(); m_config.configuredWidths = parseColumnWidths(*PCONFWIDTHS); @@ -513,7 +513,7 @@ CScrollingAlgorithm::CScrollingAlgorithm() { }); m_mouseButtonCallback = Event::bus()->m_events.input.mouse.button.listen([this](IPointer::SButtonEvent e, Event::SCallbackInfo&) { - static const auto PFOLLOW_FOCUS = CConfigValue("scrolling:follow_focus"); + static const auto PFOLLOW_FOCUS = CConfigValue("scrolling:follow_focus"); if (*PFOLLOW_FOCUS && e.state == WL_POINTER_BUTTON_STATE_RELEASED && Desktop::focusState()->window()) focusOnInput(Desktop::focusState()->window()->layoutTarget(), INPUT_MODE_CLICK); @@ -523,7 +523,7 @@ CScrollingAlgorithm::CScrollingAlgorithm() { if (!pWindow) return; - static const auto PFOLLOW_FOCUS = CConfigValue("scrolling:follow_focus"); + static const auto PFOLLOW_FOCUS = CConfigValue("scrolling:follow_focus"); if (!*PFOLLOW_FOCUS && !Desktop::isHardInputFocusReason(reason)) return; @@ -549,7 +549,7 @@ CScrollingAlgorithm::~CScrollingAlgorithm() { } void CScrollingAlgorithm::focusOnInput(SP target, eInputMode input) { - static const auto PFOLLOW_FOCUS_MIN_PERC = CConfigValue("scrolling:follow_min_visible"); + static const auto PFOLLOW_FOCUS_MIN_PERC = CConfigValue("scrolling:follow_min_visible"); if (!target || target->space() != m_parent->space()) return; @@ -579,7 +579,7 @@ void CScrollingAlgorithm::focusOnInput(SP target, eInputMode input) { if (m_scrollingData->visible(TARGETDATA->column.lock(), true) && input != INPUT_MODE_KB) return; - static const auto PFITMETHOD = CConfigValue("scrolling:focus_fit_method"); + static const auto PFITMETHOD = CConfigValue("scrolling:focus_fit_method"); if (*PFITMETHOD == 1) m_scrollingData->fitCol(TARGETDATA->column.lock()); else @@ -671,7 +671,7 @@ void CScrollingAlgorithm::resizeTarget(const Vector2D& delta, SP target if (!DATA->column || !DATA->column->scrollingData) return; - static const auto PFSONONE = CConfigValue("scrolling:fullscreen_on_one_column"); + static const auto PFSONONE = CConfigValue("scrolling:fullscreen_on_one_column"); const auto ADJUSTED_DELTA = m_scrollingData->controller->isPrimaryHorizontal() ? delta : Vector2D{delta.y, delta.x}; const auto USABLE = usableArea(); @@ -859,7 +859,7 @@ void CScrollingAlgorithm::moveTargetInDirection(SP t, Math::eDirection } void CScrollingAlgorithm::moveTargetTo(SP t, Math::eDirection dir, bool silent) { - static auto PMONITORFALLBACK = CConfigValue("binds:window_direction_monitor_fallback"); + static auto PMONITORFALLBACK = CConfigValue("binds:window_direction_monitor_fallback"); const auto DATA = dataFor(t); @@ -984,9 +984,14 @@ void CScrollingAlgorithm::moveTargetTo(SP t, Math::eDirection dir, bool focusTargetUpdate(t); } -std::expected CScrollingAlgorithm::layoutMsg(const std::string_view& sv) { - auto centerOrFit = [this](const SP COL) -> void { - static const auto PFITMETHOD = CConfigValue("scrolling:focus_fit_method"); +Config::ErrorResult CScrollingAlgorithm::layoutMsg(const std::string_view& sv) { + const auto invalidArg = [](std::string msg) { return Config::configError(std::move(msg), Config::eConfigErrorLevel::ERROR, Config::eConfigErrorCode::INVALID_ARGUMENT); }; + const auto noTarget = [](std::string msg) { return Config::configError(std::move(msg), Config::eConfigErrorLevel::WARNING, Config::eConfigErrorCode::NO_TARGET); }; + const auto notFound = [](std::string msg) { return Config::configError(std::move(msg), Config::eConfigErrorLevel::WARNING, Config::eConfigErrorCode::NOT_FOUND); }; + const auto stateErr = [](std::string msg) { return Config::configError(std::move(msg), Config::eConfigErrorLevel::WARNING, Config::eConfigErrorCode::INVALID_STATE); }; + + auto centerOrFit = [this](const SP COL) -> void { + static const auto PFITMETHOD = CConfigValue("scrolling:focus_fit_method"); if (*PFITMETHOD == 1) m_scrollingData->fitCol(COL); else @@ -998,7 +1003,7 @@ std::expected CScrollingAlgorithm::layoutMsg(const std::strin if (ARGS[1] == "+col" || ARGS[1] == "col") { const auto TDATA = dataFor(Desktop::focusState()->window() ? Desktop::focusState()->window()->layoutTarget() : nullptr); if (!TDATA) - return std::unexpected("no window"); + return noTarget("no window"); const auto COL = m_scrollingData->next(TDATA->column.lock()); if (!COL) { @@ -1049,7 +1054,7 @@ std::expected CScrollingAlgorithm::layoutMsg(const std::strin const auto PLUSMINUS = getPlusMinusKeywordResult(ARGS[1], 0); if (!PLUSMINUS.has_value()) - return std::unexpected("failed to parse offset"); + return invalidArg("failed to parse offset"); m_scrollingData->controller->adjustOffset(-(*PLUSMINUS)); m_scrollingData->recalculate(); @@ -1143,12 +1148,12 @@ std::expected CScrollingAlgorithm::layoutMsg(const std::strin const auto PWINDOW = Desktop::focusState()->window(); if (!PWINDOW) - return std::unexpected("no focused window"); + return noTarget("no focused window"); const auto WDATA = dataFor(PWINDOW->layoutTarget()); if (!WDATA || m_scrollingData->columns.size() == 0) - return std::unexpected("can't fit: no window or columns"); + return stateErr("can't fit: no window or columns"); if (ARGS[1] == "active") { // fit the current column to 1.F @@ -1192,7 +1197,7 @@ std::expected CScrollingAlgorithm::layoutMsg(const std::strin } if (!begun) - return std::unexpected("couldn't find beginning"); + return notFound("couldn't find beginning"); const auto USABLE = usableArea(); @@ -1267,11 +1272,11 @@ std::expected CScrollingAlgorithm::layoutMsg(const std::strin } } else if (ARGS[0] == "focus") { const auto TDATA = dataFor(Desktop::focusState()->window() ? Desktop::focusState()->window()->layoutTarget() : nullptr); - static const auto PNOFALLBACK = CConfigValue("general:no_focus_fallback"); - static const auto PCONFWRAPFOCUS = CConfigValue("scrolling:wrap_focus"); + static const auto PNOFALLBACK = CConfigValue("general:no_focus_fallback"); + static const auto PCONFWRAPFOCUS = CConfigValue("scrolling:wrap_focus"); if (!TDATA || ARGS[1].empty()) - return std::unexpected("no window to focus"); + return noTarget("no window to focus"); // Determine if we're in vertical scroll mode (strips are horizontal) const bool isVerticalScroll = (getDynamicDirection() == SCROLL_DIR_DOWN || getDynamicDirection() == SCROLL_DIR_UP); @@ -1294,7 +1299,7 @@ std::expected CScrollingAlgorithm::layoutMsg(const std::strin if (!*PNOFALLBACK) PREV = TDATA->column->targetDatas.back(); else - return std::unexpected("fallback disabled (no target)"); + return notFound("fallback disabled (no target)"); } focusTargetUpdate(PREV->target.lock()); @@ -1307,7 +1312,7 @@ std::expected CScrollingAlgorithm::layoutMsg(const std::strin if (!*PNOFALLBACK) NEXT = TDATA->column->targetDatas.front(); else - return std::unexpected("fallback disabled (no target)"); + return notFound("fallback disabled (no target)"); } focusTargetUpdate(NEXT->target.lock()); @@ -1361,11 +1366,11 @@ std::expected CScrollingAlgorithm::layoutMsg(const std::strin } else if (ARGS[0] == "promote" || ARGS[0] == "consume" || ARGS[0] == "expel" || ARGS[0] == "consume_or_expel") { const auto TDATA = dataFor(Desktop::focusState()->window() ? Desktop::focusState()->window()->layoutTarget() : nullptr); if (!TDATA) - return std::unexpected("no window focused"); + return noTarget("no window focused"); const auto CURRENT_COL = TDATA->column.lock(); if (!CURRENT_COL) - return std::unexpected("no current col"); + return stateErr("no current col"); // expel a target from srcCol into its own new column at insertIdx auto expelTarget = [&](SP tdata, SP srcCol, std::optional insertIdx) { @@ -1388,7 +1393,7 @@ std::expected CScrollingAlgorithm::layoutMsg(const std::strin expelTarget(TDATA, CURRENT_COL, idx == -1 ? std::nullopt : std::optional{idx}); } else if (ARGS[0] == "expel") { if (CURRENT_COL->targetDatas.size() < 2) - return std::unexpected("column has only one window"); + return stateErr("column has only one window"); const auto lastTarget = CURRENT_COL->targetDatas.back(); const auto currentIdx = m_scrollingData->idx(CURRENT_COL); @@ -1399,19 +1404,19 @@ std::expected CScrollingAlgorithm::layoutMsg(const std::strin } else if (ARGS[0] == "consume") { const auto NEXT_COL = m_scrollingData->next(CURRENT_COL); if (!NEXT_COL) - return std::unexpected("no next column"); + return notFound("no next column"); consumeTarget(CURRENT_COL, NEXT_COL); } else if (ARGS[0] == "consume_or_expel") { if (ARGS.size() < 2) - return std::unexpected("not enough args"); + return invalidArg("not enough args"); const std::string& direction = ARGS[1]; const bool prev = direction == "prev"; const bool next = direction == "next"; if (!prev && !next) - return std::unexpected("invalid direction, expected prev or next"); + return invalidArg("invalid direction, expected prev or next"); if (CURRENT_COL->targetDatas.size() > 1) { const auto currentIdx = m_scrollingData->idx(CURRENT_COL); @@ -1419,7 +1424,7 @@ std::expected CScrollingAlgorithm::layoutMsg(const std::strin } else { const auto ADJ_COL = prev ? m_scrollingData->prev(CURRENT_COL) : m_scrollingData->next(CURRENT_COL); if (!ADJ_COL) - return std::unexpected("no adjacent column"); + return notFound("no adjacent column"); CURRENT_COL->remove(TDATA->target.lock()); ADJ_COL->add(TDATA); @@ -1429,27 +1434,27 @@ std::expected CScrollingAlgorithm::layoutMsg(const std::strin m_scrollingData->recalculate(); } else if (ARGS[0] == "swapcol") { - static const auto PCONFWRAPSWAPCOL = CConfigValue("scrolling:wrap_swapcol"); + static const auto PCONFWRAPSWAPCOL = CConfigValue("scrolling:wrap_swapcol"); if (ARGS.size() < 2) - return std::unexpected("not enough args"); + return invalidArg("not enough args"); const auto TDATA = dataFor(Desktop::focusState()->window() ? Desktop::focusState()->window()->layoutTarget() : nullptr); if (!TDATA) - return std::unexpected("no window"); + return noTarget("no window"); const auto CURRENT_COL = TDATA->column.lock(); if (!CURRENT_COL) - return std::unexpected("no current col"); + return stateErr("no current col"); if (m_scrollingData->columns.size() < 2) - return std::unexpected("not enough columns to swap"); + return stateErr("not enough columns to swap"); const int64_t currentIdx = m_scrollingData->idx(CURRENT_COL); const size_t colCount = m_scrollingData->columns.size(); if (currentIdx == -1) - return std::unexpected("no current column"); + return stateErr("no current column"); const std::string& direction = ARGS[1]; int64_t targetIdx = -1; @@ -1466,7 +1471,7 @@ std::expected CScrollingAlgorithm::layoutMsg(const std::strin else targetIdx = (currentIdx == (int64_t)colCount - 1) ? (colCount - 1) : (currentIdx + 1); else - return std::unexpected("no target (invalid direction?)"); + return invalidArg("no target (invalid direction?)"); ; std::swap(m_scrollingData->columns.at(currentIdx), m_scrollingData->columns.at(targetIdx)); @@ -1478,16 +1483,16 @@ std::expected CScrollingAlgorithm::layoutMsg(const std::strin } else if (ARGS[0] == "center") { const auto TDATA = dataFor(Desktop::focusState()->window() ? Desktop::focusState()->window()->layoutTarget() : nullptr); if (!TDATA) - return std::unexpected("no window"); + return noTarget("no window"); const auto CURRENT_COL = TDATA->column.lock(); if (!CURRENT_COL) - return std::unexpected("no current col"); + return stateErr("no current col"); m_scrollingData->centerCol(CURRENT_COL); m_scrollingData->recalculate(); } else - return std::unexpected("no such layoutmsg for scrolling"); + return invalidArg("no such layoutmsg for scrolling"); return {}; } @@ -1561,7 +1566,7 @@ eScrollDirection CScrollingAlgorithm::getDynamicDirection() { if (WORKSPACERULE && WORKSPACERULE->m_layoutopts.contains("direction")) directionString = WORKSPACERULE->m_layoutopts.at("direction"); - static const auto PCONFDIRECTION = CConfigValue("scrolling:direction"); + static const auto PCONFDIRECTION = CConfigValue("scrolling:direction"); std::string configDirection = *PCONFDIRECTION; // Workspace rule overrides global config @@ -1600,6 +1605,6 @@ CBox CScrollingAlgorithm::usableArea() { } float CScrollingAlgorithm::defaultColumnWidth() { - static const auto PCOLWIDTH = CConfigValue("scrolling:column_width"); + static const auto PCOLWIDTH = CConfigValue("scrolling:column_width"); return std::clamp(*PCOLWIDTH, MIN_COLUMN_WIDTH, MAX_COLUMN_WIDTH); } diff --git a/src/layout/algorithm/tiled/scrolling/ScrollingAlgorithm.hpp b/src/layout/algorithm/tiled/scrolling/ScrollingAlgorithm.hpp index 1b2b1d1ea..288d15a62 100644 --- a/src/layout/algorithm/tiled/scrolling/ScrollingAlgorithm.hpp +++ b/src/layout/algorithm/tiled/scrolling/ScrollingAlgorithm.hpp @@ -94,22 +94,23 @@ namespace Layout::Tiled { CScrollingAlgorithm(); virtual ~CScrollingAlgorithm(); - virtual void newTarget(SP target); - virtual void movedTarget(SP target, std::optional focalPoint = std::nullopt); - virtual void removeTarget(SP target); + virtual void newTarget(SP target); + virtual void movedTarget(SP target, std::optional focalPoint = std::nullopt); + virtual void removeTarget(SP target); - virtual void resizeTarget(const Vector2D& Δ, SP target, eRectCorner corner = CORNER_NONE); - virtual void recalculate(); + virtual void resizeTarget(const Vector2D& Δ, SP target, eRectCorner corner = CORNER_NONE); + virtual void recalculate(); - virtual SP getNextCandidate(SP old); + virtual SP getNextCandidate(SP old); - virtual std::expected layoutMsg(const std::string_view& sv); - virtual std::optional predictSizeForNewTarget(); + virtual Config::ErrorResult layoutMsg(const std::string_view& sv); + virtual std::optional predictSizeForNewTarget(); - virtual void swapTargets(SP a, SP b); - virtual void moveTargetInDirection(SP t, Math::eDirection dir, bool silent); + virtual void swapTargets(SP a, SP b); + virtual void moveTargetInDirection(SP t, Math::eDirection dir, bool silent); - CBox usableArea(); + CBox usableArea(); + SP dataFor(SP t); enum eInputMode : uint8_t { INPUT_MODE_SOFT = 0, @@ -131,7 +132,6 @@ namespace Layout::Tiled { eScrollDirection getDynamicDirection(); SP findBestNeighbor(SP pCurrent, SP pTargetCol); - SP dataFor(SP t); SP closestNode(const Vector2D& posGlobglobgabgalab); void focusTargetUpdate(SP target); diff --git a/src/layout/space/Space.cpp b/src/layout/space/Space.cpp index db0144729..26c02aaec 100644 --- a/src/layout/space/Space.cpp +++ b/src/layout/space/Space.cpp @@ -84,10 +84,10 @@ void CSpace::recheckWorkArea() { auto workArea = m_parent->m_monitor->logicalBoxMinusReserved(); - static auto PGAPSOUTDATA = CConfigValue("general:gaps_out"); - static auto PFLOATGAPSDATA = CConfigValue("general:float_gaps"); - auto* const PGAPSOUT = sc((PGAPSOUTDATA.ptr())->getData()); - auto* PFLOATGAPS = sc(PFLOATGAPSDATA.ptr()->getData()); + static auto PGAPSOUTDATA = CConfigValue("general:gaps_out"); + static auto PFLOATGAPSDATA = CConfigValue("general:float_gaps"); + auto* const PGAPSOUT = sc(PGAPSOUTDATA.ptr()); + auto* PFLOATGAPS = sc(PFLOATGAPSDATA.ptr()); if (PFLOATGAPS->m_bottom < 0 || PFLOATGAPS->m_left < 0 || PFLOATGAPS->m_right < 0 || PFLOATGAPS->m_top < 0) PFLOATGAPS = PGAPSOUT; @@ -162,7 +162,7 @@ void CSpace::setFullscreen(SP t, eFullscreenMode mode) { recalculate(); } -std::expected CSpace::layoutMsg(const std::string_view& sv) { +Config::ErrorResult CSpace::layoutMsg(const std::string_view& sv) { if (m_algorithm) return m_algorithm->layoutMsg(sv); diff --git a/src/layout/space/Space.hpp b/src/layout/space/Space.hpp index e29a6d8fb..5d7f9d014 100644 --- a/src/layout/space/Space.hpp +++ b/src/layout/space/Space.hpp @@ -20,38 +20,38 @@ namespace Layout { static SP create(PHLWORKSPACE w); ~CSpace() = default; - void add(SP t); - void remove(SP t); - void move(SP t, std::optional focalPoint = std::nullopt); + void add(SP t); + void remove(SP t); + void move(SP t, std::optional focalPoint = std::nullopt); - void swap(SP a, SP b); + void swap(SP a, SP b); - SP getNextCandidate(SP old); + SP getNextCandidate(SP old); - void setAlgorithmProvider(SP algo); - void recheckWorkArea(); - void setFullscreen(SP t, eFullscreenMode mode); + void setAlgorithmProvider(SP algo); + void recheckWorkArea(); + void setFullscreen(SP t, eFullscreenMode mode); - void moveTargetInDirection(SP t, Math::eDirection dir, bool silent); + void moveTargetInDirection(SP t, Math::eDirection dir, bool silent); - void recalculate(); + void recalculate(); - void toggleTargetFloating(SP t); + void toggleTargetFloating(SP t); - std::expected layoutMsg(const std::string_view& sv); - std::optional predictSizeForNewTiledTarget(); + Config::ErrorResult layoutMsg(const std::string_view& sv); + std::optional predictSizeForNewTiledTarget(); - const CBox& workArea(bool floating = false) const; - PHLWORKSPACE workspace() const; - CBox targetPositionLocal(SP t) const; + const CBox& workArea(bool floating = false) const; + PHLWORKSPACE workspace() const; + CBox targetPositionLocal(SP t) const; - void resizeTarget(const Vector2D& Δ, SP target, eRectCorner corner = CORNER_NONE); - void moveTarget(const Vector2D& Δ, SP target); - void setTargetGeom(const CBox& box, SP target); // only for float + void resizeTarget(const Vector2D& Δ, SP target, eRectCorner corner = CORNER_NONE); + void moveTarget(const Vector2D& Δ, SP target); + void setTargetGeom(const CBox& box, SP target); // only for float - SP algorithm() const; + SP algorithm() const; - const std::vector>& targets() const; + const std::vector>& targets() const; private: CSpace(PHLWORKSPACE parent); @@ -68,4 +68,4 @@ namespace Layout { // for recalc CHyprSignalListener m_geomUpdateCallback; }; -}; \ No newline at end of file +}; diff --git a/src/layout/supplementary/DragController.cpp b/src/layout/supplementary/DragController.cpp index 790361868..3def85966 100644 --- a/src/layout/supplementary/DragController.cpp +++ b/src/layout/supplementary/DragController.cpp @@ -48,7 +48,7 @@ bool CDragStateController::updateDragWindow() { const auto PWORKSPACE = DRAGGINGTARGET->workspace(); const auto DRAGGINGWINDOW = DRAGGINGTARGET->window(); - if (PWORKSPACE->m_hasFullscreenWindow && (!DRAGGINGTARGET->floating() || (!DRAGGINGWINDOW->m_createdOverFullscreen && !DRAGGINGWINDOW->m_pinned))) { + if (PWORKSPACE->m_hasFullscreenWindow && (!DRAGGINGTARGET->floating() || !DRAGGINGWINDOW->isAllowedOverFullscreen())) { Log::logger->log(Log::DEBUG, "Rejecting drag on a fullscreen workspace. (window under fullscreen)"); CKeybindManager::changeMouseBindMode(MBIND_INVALID); return true; @@ -87,7 +87,7 @@ void CDragStateController::dragBegin(SP target, eMouseBindMode mode) { m_dragMode = mode; const auto DRAGGINGTARGET = m_target.lock(); - static auto PDRAGTHRESHOLD = CConfigValue("binds:drag_threshold"); + static auto PDRAGTHRESHOLD = CConfigValue("binds:drag_threshold"); m_mouseMoveEventCount = 1; m_beginDragSizeXY = Vector2D(); @@ -112,7 +112,7 @@ void CDragStateController::dragBegin(SP target, eMouseBindMode mode) { return; // get the grab corner - static auto RESIZECORNER = CConfigValue("general:resize_corner"); + static auto RESIZECORNER = CConfigValue("general:resize_corner"); if (*RESIZECORNER != 0 && *RESIZECORNER <= 4 && DRAGGINGTARGET->floating()) { switch (*RESIZECORNER) { case 1: @@ -193,7 +193,7 @@ void CDragStateController::dragEnd() { return; const bool FLOATEDINTOTILED = !pWindow->m_isFloating && !m_draggingTiled; - static auto PDRAGINTOGROUP = CConfigValue("group:drag_into_group"); + static auto PDRAGINTOGROUP = CConfigValue("group:drag_into_group"); if (pWindow->m_group && DRAGGING_WINDOW->canBeGroupedInto(pWindow->m_group) && *PDRAGINTOGROUP == 1 && !FLOATEDINTOTILED) { pWindow->m_group->add(DRAGGING_WINDOW); @@ -226,7 +226,7 @@ void CDragStateController::mouseMove(const Vector2D& mousePos) { return; const auto DRAGGINGTARGET = m_target.lock(); - static auto PDRAGTHRESHOLD = CConfigValue("binds:drag_threshold"); + static auto PDRAGTHRESHOLD = CConfigValue("binds:drag_threshold"); // Window invalid or drag begin size 0,0 meaning we rejected it. if ((!validMapped(DRAGGINGTARGET->window()) || m_beginDragSizeXY == Vector2D())) { @@ -248,7 +248,7 @@ void CDragStateController::mouseMove(const Vector2D& mousePos) { const auto DELTA = Vector2D(mousePos.x - m_beginDragXY.x, mousePos.y - m_beginDragXY.y); const auto TICKDELTA = Vector2D(mousePos.x - m_lastDragXY.x, mousePos.y - m_lastDragXY.y); - static auto SNAPENABLED = CConfigValue("general:snap:enabled"); + static auto SNAPENABLED = CConfigValue("general:snap:enabled"); const auto TIMERDELTA = std::chrono::duration_cast(std::chrono::high_resolution_clock::now() - TIMER).count(); const auto MSDELTA = std::chrono::duration_cast(std::chrono::high_resolution_clock::now() - MSTIMER).count(); diff --git a/src/layout/supplementary/WorkspaceAlgoMatcher.cpp b/src/layout/supplementary/WorkspaceAlgoMatcher.cpp index 98cdb773c..fe8680380 100644 --- a/src/layout/supplementary/WorkspaceAlgoMatcher.cpp +++ b/src/layout/supplementary/WorkspaceAlgoMatcher.cpp @@ -101,7 +101,7 @@ UP CWorkspaceAlgoMatcher::algoForNameFloat(const std::string } std::string CWorkspaceAlgoMatcher::tiledAlgoForWorkspace(const PHLWORKSPACE& w) { - static auto PLAYOUT = CConfigValue("general:layout"); + static auto PLAYOUT = CConfigValue("general:layout"); auto rule = Config::workspaceRuleMgr()->getWorkspaceRuleFor(w); return rule && rule->m_layout.has_value() ? rule->m_layout.value() : *PLAYOUT; diff --git a/src/layout/target/WindowTarget.cpp b/src/layout/target/WindowTarget.cpp index cc6bfab47..13984104b 100644 --- a/src/layout/target/WindowTarget.cpp +++ b/src/layout/target/WindowTarget.cpp @@ -110,12 +110,12 @@ void CWindowTarget::updatePos() { const bool DISPLAYINVERSELEFT = STICKS(m_box.logicalBox.x, MONITOR_WORKAREA.x + MONITOR_WORKAREA.w); const bool DISPLAYINVERSERIGHT = STICKS(m_box.logicalBox.x + m_box.logicalBox.w, MONITOR_WORKAREA.x); - static auto PGAPSINDATA = CConfigValue("general:gaps_in"); - auto* const PGAPSIN = sc((PGAPSINDATA.ptr())->getData()); + static auto PGAPSINDATA = CConfigValue("general:gaps_in"); + auto* const PGAPSIN = sc((PGAPSINDATA.ptr())); auto gapsIn = (WORKSPACERULE && WORKSPACERULE->m_gapsIn.has_value()) ? WORKSPACERULE->m_gapsIn.value() : *PGAPSIN; - const static auto REQUESTEDRATIO = CConfigValue("layout:single_window_aspect_ratio"); - const static auto REQUESTEDRATIOTOLERANCE = CConfigValue("layout:single_window_aspect_ratio_tolerance"); + const static auto REQUESTEDRATIO = CConfigValue("layout:single_window_aspect_ratio"); + const static auto REQUESTEDRATIOTOLERANCE = CConfigValue("layout:single_window_aspect_ratio_tolerance"); Vector2D ratioPadding; @@ -175,7 +175,7 @@ void CWindowTarget::updatePos() { Vector2D availableSpace = calcSize; - static auto PCLAMP_TILED = CConfigValue("misc:size_limits_tiled"); + static auto PCLAMP_TILED = CConfigValue("misc:size_limits_tiled"); if (*PCLAMP_TILED) { Vector2D minSize = m_window->m_ruleApplicator->minSize().valueOr(Vector2D{MIN_WINDOW_SIZE, MIN_WINDOW_SIZE}); @@ -190,7 +190,7 @@ void CWindowTarget::updatePos() { if (m_window->onSpecialWorkspace() && !m_window->isFullscreen()) { // if special, we adjust the coords a bit - static auto PSCALEFACTOR = CConfigValue("dwindle:special_scale_factor"); + static auto PSCALEFACTOR = CConfigValue("dwindle:special_scale_factor"); CBox wb = {calcPos + (calcSize - calcSize * *PSCALEFACTOR) / 2.f, calcSize * *PSCALEFACTOR}; wb.round(); // avoid rounding mess @@ -278,7 +278,7 @@ std::expected CWindowTarget::desiredGeomet return std::unexpected(GEOMETRY_NO_DESIRED); } - static auto PXWLFORCESCALEZERO = CConfigValue("xwayland:force_zero_scaling"); + static auto PXWLFORCESCALEZERO = CConfigValue("xwayland:force_zero_scaling"); const auto toLogical = [&](SGeometryRequested& req) { if (m_window->m_isX11 && *PXWLFORCESCALEZERO && PMONITOR) req.size /= PMONITOR->m_scale; diff --git a/src/macros.hpp b/src/macros.hpp index fc109296b..9518ba826 100644 --- a/src/macros.hpp +++ b/src/macros.hpp @@ -96,7 +96,7 @@ #define GLCALL(__CALL__) \ { \ __CALL__; \ - static const auto GLDEBUG = CConfigValue("debug:gl_debugging"); \ + static const auto GLDEBUG = CConfigValue("debug:gl_debugging"); \ if (*GLDEBUG) { \ auto err = glGetError(); \ if (err != GL_NO_ERROR) { \ diff --git a/src/main.cpp b/src/main.cpp index 566a2544b..faf780832 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -100,14 +100,15 @@ int main(int argc, char** argv) { return 1; } + const auto FD_STR = *std::next(it); try { - socketFd = std::stoi(*std::next(it)); + socketFd = std::stoi(FD_STR); // check if socketFd is a valid file descriptor if (fcntl(socketFd, F_GETFD) == -1) - throw std::exception(); - } catch (...) { - std::println(stderr, "[ ERROR ] Invalid Wayland FD!"); + throw std::runtime_error("invalid or closed file descriptor"); + } catch (const std::exception& e) { + std::println(stderr, "[ ERROR ] (main.cpp:{}) | Invalid Wayland FD '{}': {}!", __LINE__, FD_STR, e.what()); help(); return 1; @@ -123,13 +124,14 @@ int main(int argc, char** argv) { configPath = *std::next(it); try { - configPath = std::filesystem::canonical(configPath); + const auto ABS_PATH = std::filesystem::canonical(configPath); - if (!std::filesystem::is_regular_file(configPath)) { - throw std::exception(); + if (!std::filesystem::is_regular_file(ABS_PATH)) { + throw std::runtime_error("not a regular file"); } - } catch (...) { - std::println(stderr, "[ ERROR ] Config file '{}' doesn't exist!", configPath); + configPath = ABS_PATH; + } catch (const std::exception& e) { + std::println(stderr, "[ ERROR ] (main.cpp:{}) | Config file '{}' is invalid: {}!", __LINE__, configPath, e.what()); help(); return 1; @@ -165,11 +167,12 @@ int main(int argc, char** argv) { return 1; } + const auto WATCHDOG_STR = *std::next(it); try { - watchdogFd = std::stoi(*std::next(it)); + watchdogFd = std::stoi(WATCHDOG_STR); it++; - } catch (...) { - std::println(stderr, "[ ERROR ] Invalid fd for watchdog fd"); + } catch (const std::exception& e) { + std::println(stderr, "[ ERROR ] (main.cpp:{}) | Invalid watchdog FD '{}': {}!", __LINE__, WATCHDOG_STR, e.what()); help(); return 1; } diff --git a/src/managers/ANRManager.cpp b/src/managers/ANRManager.cpp index 9f613df8a..7fea3c45a 100644 --- a/src/managers/ANRManager.cpp +++ b/src/managers/ANRManager.cpp @@ -58,8 +58,8 @@ CANRManager::CANRManager() { } void CANRManager::onTick() { - static auto PENABLEANR = CConfigValue("misc:enable_anr_dialog"); - static auto PANRTHRESHOLD = CConfigValue("misc:anr_missed_pings"); + static auto PENABLEANR = CConfigValue("misc:enable_anr_dialog"); + static auto PANRTHRESHOLD = CConfigValue("misc:anr_missed_pings"); if (!*PENABLEANR) { m_timer->updateTimeout(TIMER_TIMEOUT * 10); @@ -146,7 +146,7 @@ bool CANRManager::isNotResponding(PHLWINDOW pWindow) { } bool CANRManager::isNotResponding(SP data) { - static auto PANRTHRESHOLD = CConfigValue("misc:anr_missed_pings"); + static auto PANRTHRESHOLD = CConfigValue("misc:anr_missed_pings"); return data->missedResponses > *PANRTHRESHOLD; } diff --git a/src/managers/CursorManager.cpp b/src/managers/CursorManager.cpp index 7564ca753..2bf72f848 100644 --- a/src/managers/CursorManager.cpp +++ b/src/managers/CursorManager.cpp @@ -71,7 +71,7 @@ void CCursorBuffer::endDataPtr() { CCursorManager::CCursorManager() { m_hyprcursor = makeUnique(m_theme.empty() ? nullptr : m_theme.c_str(), hcLogger); m_xcursor = makeUnique(); - static auto PUSEHYPRCURSOR = CConfigValue("cursor:enable_hyprcursor"); + static auto PUSEHYPRCURSOR = CConfigValue("cursor:enable_hyprcursor"); if (m_hyprcursor->valid() && *PUSEHYPRCURSOR) { // find default size. First, HYPRCURSOR_SIZE then default to 24 @@ -160,7 +160,7 @@ void CCursorManager::setAnimationTimer(const int& frame, const int& delay) { void CCursorManager::setCursorFromName(const std::string& name) { - static auto PUSEHYPRCURSOR = CConfigValue("cursor:enable_hyprcursor"); + static auto PUSEHYPRCURSOR = CConfigValue("cursor:enable_hyprcursor"); auto setXCursor = [this](auto const& name) { float scale = std::ceil(m_cursorScale); @@ -272,7 +272,7 @@ SCursorImageData CCursorManager::dataFor(const std::string& name) { } void CCursorManager::setXWaylandCursor() { - static auto PUSEHYPRCURSOR = CConfigValue("cursor:enable_hyprcursor"); + static auto PUSEHYPRCURSOR = CConfigValue("cursor:enable_hyprcursor"); const auto CURSOR = dataFor("left_ptr"); if (CURSOR.surface && *PUSEHYPRCURSOR) g_pXWayland->setCursor(cairo_image_surface_get_data(CURSOR.surface), cairo_image_surface_get_stride(CURSOR.surface), {CURSOR.size, CURSOR.size}, @@ -286,7 +286,7 @@ void CCursorManager::setXWaylandCursor() { } void CCursorManager::updateTheme() { - static auto PUSEHYPRCURSOR = CConfigValue("cursor:enable_hyprcursor"); + static auto PUSEHYPRCURSOR = CConfigValue("cursor:enable_hyprcursor"); float highestScale = 1.0; for (auto const& m : g_pCompositor->m_monitors) { @@ -314,7 +314,7 @@ void CCursorManager::updateTheme() { } bool CCursorManager::changeTheme(const std::string& name, const int size) { - static auto PUSEHYPRCURSOR = CConfigValue("cursor:enable_hyprcursor"); + static auto PUSEHYPRCURSOR = CConfigValue("cursor:enable_hyprcursor"); m_theme = name.empty() ? "" : name; m_size = size <= 0 ? 24 : size; auto xcursor_theme = getenv("XCURSOR_THEME") ? getenv("XCURSOR_THEME") : "default"; diff --git a/src/managers/DonationNagManager.cpp b/src/managers/DonationNagManager.cpp index 62dd15b70..a88b35c56 100644 --- a/src/managers/DonationNagManager.cpp +++ b/src/managers/DonationNagManager.cpp @@ -36,7 +36,7 @@ const std::vector NAG_DATE_POINTS = { // clang-format on CDonationNagManager::CDonationNagManager() { - static auto PNONAG = CConfigValue("ecosystem:no_donation_nag"); + static auto PNONAG = CConfigValue("ecosystem:no_donation_nag"); if (g_pVersionKeeperMgr->fired() || *PNONAG) return; diff --git a/src/managers/KeybindManager.cpp b/src/managers/KeybindManager.cpp index 9bd7ec971..4a5df4be7 100644 --- a/src/managers/KeybindManager.cpp +++ b/src/managers/KeybindManager.cpp @@ -1,54 +1,26 @@ #include "../config/ConfigValue.hpp" -#include "../config/legacy/ConfigManager.hpp" -#include "../config/shared/monitor/MonitorRuleManager.hpp" +#include "../config/ConfigManager.hpp" +#include "../config/legacy/DispatcherTranslator.hpp" +#include "../config/shared/actions/ConfigActions.hpp" #include "../devices/IKeyboard.hpp" -#include "../desktop/state/FocusState.hpp" -#include "../desktop/history/WindowHistoryTracker.hpp" -#include "../desktop/history/WorkspaceHistoryTracker.hpp" #include "../managers/SeatManager.hpp" -#include "../protocols/LayerShell.hpp" #include "../protocols/ShortcutsInhibit.hpp" -#include "../protocols/GlobalShortcuts.hpp" -#include "../protocols/IdleNotify.hpp" #include "../protocols/core/DataDevice.hpp" -#include "../render/decorations/CHyprGroupBarDecoration.hpp" +#include "../errorOverlay/Overlay.hpp" #include "KeybindManager.hpp" #include "PointerManager.hpp" #include "Compositor.hpp" -#include "TokenManager.hpp" #include "eventLoop/EventLoopManager.hpp" #include "debug/log/Logger.hpp" #include "../managers/input/InputManager.hpp" -#include "../managers/animation/DesktopAnimationManager.hpp" -#include "../managers/EventManager.hpp" -#include "../render/Renderer.hpp" -#include "../errorOverlay/Overlay.hpp" -#include "../config/ConfigManager.hpp" -#include "../desktop/rule/windowRule/WindowRule.hpp" -#include "../desktop/rule/Engine.hpp" -#include "../desktop/view/Group.hpp" #include "../layout/LayoutManager.hpp" -#include "../layout/target/WindowTarget.hpp" -#include "../layout/space/Space.hpp" -#include "../layout/algorithm/Algorithm.hpp" -#include "../layout/algorithm/tiled/master/MasterAlgorithm.hpp" -#include "../layout/algorithm/tiled/monocle/MonocleAlgorithm.hpp" #include "../event/EventBus.hpp" -#include "../config/supplementary/executor/Executor.hpp" -#include -#include #include -#include #include #include -#include -#include -#include -#include using namespace Hyprutils::String; -using namespace Hyprutils::OS; #include #include @@ -63,84 +35,86 @@ using namespace Hyprutils::OS; CKeybindManager::CKeybindManager() { // initialize all dispatchers - - m_dispatchers["exec"] = spawn; - m_dispatchers["execr"] = spawnRaw; - m_dispatchers["killactive"] = closeActive; - m_dispatchers["forcekillactive"] = killActive; - m_dispatchers["closewindow"] = closeWindow; - m_dispatchers["killwindow"] = killWindow; - m_dispatchers["signal"] = signalActive; - m_dispatchers["signalwindow"] = signalWindow; - m_dispatchers["togglefloating"] = toggleActiveFloating; - m_dispatchers["setfloating"] = setActiveFloating; - m_dispatchers["settiled"] = setActiveTiled; - m_dispatchers["workspace"] = changeworkspace; - m_dispatchers["renameworkspace"] = renameWorkspace; - m_dispatchers["fullscreen"] = fullscreenActive; - m_dispatchers["fullscreenstate"] = fullscreenStateActive; - m_dispatchers["movetoworkspace"] = moveActiveToWorkspace; - m_dispatchers["movetoworkspacesilent"] = moveActiveToWorkspaceSilent; - m_dispatchers["pseudo"] = toggleActivePseudo; - m_dispatchers["movefocus"] = moveFocusTo; - m_dispatchers["movewindow"] = moveActiveTo; - m_dispatchers["swapwindow"] = swapActive; - m_dispatchers["centerwindow"] = centerWindow; - m_dispatchers["togglegroup"] = toggleGroup; - m_dispatchers["changegroupactive"] = changeGroupActive; - m_dispatchers["movegroupwindow"] = moveGroupWindow; - m_dispatchers["focusmonitor"] = focusMonitor; - m_dispatchers["movecursortocorner"] = moveCursorToCorner; - m_dispatchers["movecursor"] = moveCursor; - m_dispatchers["workspaceopt"] = workspaceOpt; - m_dispatchers["exit"] = exitHyprland; - m_dispatchers["movecurrentworkspacetomonitor"] = moveCurrentWorkspaceToMonitor; - m_dispatchers["focusworkspaceoncurrentmonitor"] = focusWorkspaceOnCurrentMonitor; - m_dispatchers["moveworkspacetomonitor"] = moveWorkspaceToMonitor; - m_dispatchers["togglespecialworkspace"] = toggleSpecialWorkspace; - m_dispatchers["forcerendererreload"] = forceRendererReload; - m_dispatchers["resizeactive"] = resizeActive; - m_dispatchers["moveactive"] = moveActive; - m_dispatchers["cyclenext"] = circleNext; - m_dispatchers["focuswindowbyclass"] = focusWindow; - m_dispatchers["focuswindow"] = focusWindow; - m_dispatchers["tagwindow"] = tagWindow; - m_dispatchers["toggleswallow"] = toggleSwallow; - m_dispatchers["submap"] = setSubmap; - m_dispatchers["pass"] = pass; - m_dispatchers["sendshortcut"] = sendshortcut; - m_dispatchers["sendkeystate"] = sendkeystate; - m_dispatchers["layoutmsg"] = layoutmsg; - m_dispatchers["dpms"] = dpms; - m_dispatchers["movewindowpixel"] = moveWindow; - m_dispatchers["resizewindowpixel"] = resizeWindow; - m_dispatchers["swapnext"] = swapnext; - m_dispatchers["swapactiveworkspaces"] = swapActiveWorkspaces; - m_dispatchers["pin"] = pinActive; - m_dispatchers["mouse"] = mouse; - m_dispatchers["bringactivetotop"] = bringActiveToTop; - m_dispatchers["alterzorder"] = alterZOrder; - m_dispatchers["focusurgentorlast"] = focusUrgentOrLast; - m_dispatchers["focuscurrentorlast"] = focusCurrentOrLast; - m_dispatchers["lockgroups"] = lockGroups; - m_dispatchers["lockactivegroup"] = lockActiveGroup; - m_dispatchers["moveintogroup"] = moveIntoGroup; - m_dispatchers["moveintoorcreategroup"] = moveIntoOrCreateGroup; - m_dispatchers["moveoutofgroup"] = moveOutOfGroup; - m_dispatchers["movewindoworgroup"] = moveWindowOrGroup; - m_dispatchers["setignoregrouplock"] = setIgnoreGroupLock; - m_dispatchers["denywindowfromgroup"] = denyWindowFromGroup; - m_dispatchers["event"] = event; - m_dispatchers["global"] = global; - m_dispatchers["setprop"] = setProp; - m_dispatchers["forceidle"] = forceIdle; + // populate m_dispatchers from the legacy translator + for (const auto& name : {"exec", + "execr", + "killactive", + "forcekillactive", + "closewindow", + "killwindow", + "signal", + "signalwindow", + "togglefloating", + "setfloating", + "settiled", + "workspace", + "renameworkspace", + "fullscreen", + "fullscreenstate", + "movetoworkspace", + "movetoworkspacesilent", + "pseudo", + "movefocus", + "movewindow", + "swapwindow", + "centerwindow", + "togglegroup", + "changegroupactive", + "movegroupwindow", + "focusmonitor", + "movecursortocorner", + "movecursor", + "workspaceopt", + "exit", + "movecurrentworkspacetomonitor", + "focusworkspaceoncurrentmonitor", + "moveworkspacetomonitor", + "togglespecialworkspace", + "forcerendererreload", + "resizeactive", + "moveactive", + "cyclenext", + "focuswindowbyclass", + "focuswindow", + "tagwindow", + "toggleswallow", + "submap", + "pass", + "sendshortcut", + "sendkeystate", + "layoutmsg", + "dpms", + "movewindowpixel", + "resizewindowpixel", + "swapnext", + "swapactiveworkspaces", + "pin", + "mouse", + "bringactivetotop", + "alterzorder", + "focusurgentorlast", + "focuscurrentorlast", + "lockgroups", + "lockactivegroup", + "moveintogroup", + "moveoutofgroup", + "movewindoworgroup", + "moveintoorcreategroup", + "setignoregrouplock", + "denywindowfromgroup", + "event", + "global", + "setprop", + "forceidle"}) { + m_dispatchers[name] = [n = std::string(name)](std::string args) -> SDispatchResult { return Config::Legacy::translator()->run(n, args); }; + } m_scrollTimer.reset(); m_longPressTimer = makeShared( std::nullopt, [this](SP self, void* data) { - if (!m_lastLongPressKeybind || g_pSeatManager->m_keyboard.expired()) + if (!m_lastLongPressKeybind || !m_lastLongPressKeybind->enabled || g_pSeatManager->m_keyboard.expired()) return; const auto PACTIVEKEEB = g_pSeatManager->m_keyboard.lock(); @@ -157,6 +131,8 @@ CKeybindManager::CKeybindManager() { m_repeatKeyTimer = makeShared( std::nullopt, [this](SP self, void* data) { + std::erase_if(m_activeKeybinds, [](const auto& k) { return !k || !k->enabled; }); + if (m_activeKeybinds.empty() || g_pSeatManager->m_keyboard.expired()) return; @@ -165,6 +141,9 @@ CKeybindManager::CKeybindManager() { return; for (const auto& k : m_activeKeybinds) { + if (!k || !k->enabled) + continue; + const auto DISPATCHER = g_pKeybindManager->m_dispatchers.find(k->handler); Log::logger->log(Log::DEBUG, "Keybind repeat triggered, calling dispatcher."); @@ -201,15 +180,35 @@ CKeybindManager::~CKeybindManager() { } } -void CKeybindManager::addKeybind(SKeybind kb) { - m_keybinds.emplace_back(makeShared(kb)); +SP CKeybindManager::addKeybind(SKeybind kb) { + const auto KEYBIND = makeShared(kb); + m_keybinds.emplace_back(KEYBIND); + + m_activeKeybinds.clear(); + m_lastLongPressKeybind.reset(); + + return KEYBIND; +} + +void CKeybindManager::removeKeybind(uint32_t mod, const SParsedKey& key) { + std::erase_if(m_keybinds, [&mod, &key](const auto& el) { return el->modmask == mod && el->key == key.key && el->keycode == key.keycode && el->catchAll == key.catchAll; }); m_activeKeybinds.clear(); m_lastLongPressKeybind.reset(); } -void CKeybindManager::removeKeybind(uint32_t mod, const SParsedKey& key) { - std::erase_if(m_keybinds, [&mod, &key](const auto& el) { return el->modmask == mod && el->key == key.key && el->keycode == key.keycode && el->catchAll == key.catchAll; }); +void CKeybindManager::removeKeybind(const std::string& displayKeys) { + static auto normalize = [](std::string x) -> std::string { + std::string n = x; + replaceInString(n, " ", ""); + std::ranges::transform(n, n.begin(), ::tolower); + return n; + }; + + const auto DISPLAY_KEYS_NORMALIZED = normalize(displayKeys); + + std::erase_if(m_keybinds, + [&displayKeys, &DISPLAY_KEYS_NORMALIZED](const auto& el) { return el->displayKey == displayKeys || DISPLAY_KEYS_NORMALIZED == normalize(el->displayKey); }); m_activeKeybinds.clear(); m_lastLongPressKeybind.reset(); @@ -316,93 +315,6 @@ bool CKeybindManager::ensureMouseBindState() { return false; } -static void updateRelativeCursorCoords() { - static auto PNOWARPS = CConfigValue("cursor:no_warps"); - - if (*PNOWARPS) - return; - - if (Desktop::focusState()->window()) - Desktop::focusState()->window()->m_relativeCursorCoordsOnLastWarp = g_pInputManager->getMouseCoordsInternal() - Desktop::focusState()->window()->m_position; -} - -bool CKeybindManager::tryMoveFocusToMonitor(PHLMONITOR monitor) { - if (!monitor) - return false; - - const auto LASTMONITOR = Desktop::focusState()->monitor(); - if (!LASTMONITOR) - return false; - if (LASTMONITOR == monitor) { - Log::logger->log(Log::DEBUG, "Tried to move to active monitor"); - return false; - } - - static auto PFOLLOWMOUSE = CConfigValue("input:follow_mouse"); - static auto PNOWARPS = CConfigValue("cursor:no_warps"); - - const auto PWORKSPACE = Desktop::focusState()->monitor()->m_activeWorkspace; - const auto PNEWMAINWORKSPACE = monitor->m_activeWorkspace; - - g_pInputManager->unconstrainMouse(); - - const auto PNEWWORKSPACE = monitor->m_activeSpecialWorkspace ? monitor->m_activeSpecialWorkspace : PNEWMAINWORKSPACE; - - const auto PNEWWINDOW = PNEWWORKSPACE->getLastFocusedWindow(); - if (PNEWWINDOW) { - updateRelativeCursorCoords(); - Desktop::focusState()->fullWindowFocus(PNEWWINDOW, Desktop::FOCUS_REASON_KEYBIND); - PNEWWINDOW->warpCursor(); - - if (*PNOWARPS == 0 || *PFOLLOWMOUSE < 2) { - g_pInputManager->m_forcedFocus = PNEWWINDOW; - g_pInputManager->simulateMouseMovement(); - g_pInputManager->m_forcedFocus.reset(); - } - } else { - Desktop::focusState()->rawWindowFocus(nullptr, Desktop::FOCUS_REASON_KEYBIND); - g_pCompositor->warpCursorTo(monitor->middle()); - } - Desktop::focusState()->rawMonitorFocus(monitor); - - return true; -} - -void CKeybindManager::switchToWindow(PHLWINDOW PWINDOWTOCHANGETO, bool forceFSCycle) { - static auto PFOLLOWMOUSE = CConfigValue("input:follow_mouse"); - static auto PNOWARPS = CConfigValue("cursor:no_warps"); - - const auto PLASTWINDOW = Desktop::focusState()->window(); - - if (PWINDOWTOCHANGETO == PLASTWINDOW || !PWINDOWTOCHANGETO) - return; - - // remove constraints - g_pInputManager->unconstrainMouse(); - - if (PLASTWINDOW && PLASTWINDOW->m_workspace == PWINDOWTOCHANGETO->m_workspace && PLASTWINDOW->isFullscreen()) - Desktop::focusState()->fullWindowFocus(PWINDOWTOCHANGETO, Desktop::FOCUS_REASON_KEYBIND, nullptr, forceFSCycle); - else { - updateRelativeCursorCoords(); - Desktop::focusState()->fullWindowFocus(PWINDOWTOCHANGETO, Desktop::FOCUS_REASON_KEYBIND, nullptr, forceFSCycle); - PWINDOWTOCHANGETO->warpCursor(); - - // Move mouse focus to the new window if required by current follow_mouse and warp modes - if (*PNOWARPS == 0 || *PFOLLOWMOUSE < 2) { - g_pInputManager->m_forcedFocus = PWINDOWTOCHANGETO; - g_pInputManager->simulateMouseMovement(); - g_pInputManager->m_forcedFocus.reset(); - } - - if (PLASTWINDOW && PLASTWINDOW->m_monitor != PWINDOWTOCHANGETO->m_monitor) { - // event - const auto PNEWMON = PWINDOWTOCHANGETO->m_monitor.lock(); - - Desktop::focusState()->rawMonitorFocus(PNEWMON); - } - } -}; - bool CKeybindManager::onKeyEvent(std::any event, SP pKeyboard) { if (!g_pCompositor->m_sessionActive || g_pCompositor->m_unsafeState) { m_pressedKeys.clear(); @@ -437,9 +349,9 @@ bool CKeybindManager::onKeyEvent(std::any event, SP pKeyboard) { const auto MODS = g_pInputManager->getModsFromAllKBs(); - m_timeLastMs = e.timeMs; - m_lastCode = KEYCODE; - m_lastMouseCode = 0; + Config::Actions::state()->m_timeLastMs = e.timeMs; + Config::Actions::state()->m_lastCode = KEYCODE; + Config::Actions::state()->m_lastMouseCode = 0; bool mouseBindWasActive = ensureMouseBindState(); @@ -448,7 +360,7 @@ bool CKeybindManager::onKeyEvent(std::any event, SP pKeyboard) { .keycode = KEYCODE, .modmaskAtPressTime = MODS, .sent = true, - .submapAtPress = m_currentSelectedSubmap, + .submapAtPress = SSubmap{.name = Config::Actions::state()->m_currentSubmap}, .mousePosAtPress = g_pInputManager->getMouseCoordsInternal(), }; @@ -495,7 +407,7 @@ bool CKeybindManager::onKeyEvent(std::any event, SP pKeyboard) { bool CKeybindManager::onAxisEvent(const IPointer::SAxisEvent& e, SP pointer) { const auto MODS = g_pInputManager->getModsFromAllKBs(); - static auto PDELAY = CConfigValue("binds:scroll_event_delay"); + static auto PDELAY = CConfigValue("binds:scroll_event_delay"); if (m_scrollTimer.getMillis() < *PDELAY) return true; // timer hasn't passed yet! @@ -528,9 +440,9 @@ bool CKeybindManager::onMouseEvent(const IPointer::SButtonEvent& e, SP bool suppressEvent = false; - m_lastMouseCode = e.button; - m_lastCode = 0; - m_timeLastMs = e.timeMs; + Config::Actions::state()->m_lastMouseCode = e.button; + Config::Actions::state()->m_lastCode = 0; + Config::Actions::state()->m_timeLastMs = e.timeMs; bool mouseBindWasActive = ensureMouseBindState(); @@ -593,15 +505,20 @@ void CKeybindManager::onSwitchOffEvent(const std::string& switchName) { handleKeybinds(0, SPressedKeyWithMods{.keyName = "switch:off:" + switchName}, true, nullptr, nullptr); } -eMultiKeyCase CKeybindManager::mkKeysymSetMatches(const std::set keybindKeysyms, const std::set pressedKeysyms) { +eMultiKeyCase CKeybindManager::mkKeysymSetMatches(const std::vector keybindKeysyms, const std::set pressedKeysyms) { // Returns whether two sets of keysyms are equal, partially equal, or not // matching. (Partially matching means that pressed is a subset of bound) std::set boundKeysNotPressed; std::set pressedKeysNotBound; - std::ranges::set_difference(keybindKeysyms, pressedKeysyms, std::inserter(boundKeysNotPressed, boundKeysNotPressed.begin())); - std::ranges::set_difference(pressedKeysyms, keybindKeysyms, std::inserter(pressedKeysNotBound, pressedKeysNotBound.begin())); + std::set symsKb; + for (const auto& k : keybindKeysyms) { + symsKb.emplace(k); + } + + std::ranges::set_difference(symsKb, pressedKeysyms, std::inserter(boundKeysNotPressed, boundKeysNotPressed.begin())); + std::ranges::set_difference(pressedKeysyms, symsKb, std::inserter(pressedKeysNotBound, pressedKeysNotBound.begin())); if (boundKeysNotPressed.empty() && pressedKeysNotBound.empty()) return MK_FULL_MATCH; @@ -620,16 +537,18 @@ eMultiKeyCase CKeybindManager::mkBindMatches(const SP keybind) { } SSubmap CKeybindManager::getCurrentSubmap() { - return m_currentSelectedSubmap; + return SSubmap{.name = Config::Actions::state()->m_currentSubmap}; } SDispatchResult CKeybindManager::handleKeybinds(const uint32_t modmask, const SPressedKeyWithMods& key, bool pressed, SP keyboard, SP device) { - static auto PDISABLEINHIBIT = CConfigValue("binds:disable_keybind_grabbing"); - static auto PDRAGTHRESHOLD = CConfigValue("binds:drag_threshold"); + static auto PDISABLEINHIBIT = CConfigValue("binds:disable_keybind_grabbing"); + static auto PDRAGTHRESHOLD = CConfigValue("binds:drag_threshold"); bool found = false; SDispatchResult res; + std::erase_if(m_pressedSpecialBinds, [](const auto& k) { return !k || !k->enabled; }); + // Skip keysym tracking for events with no keysym (e.g., scroll wheel events). // Scroll events have keysym=0 and are always "pressed" (never released), // so without this check, 0 gets inserted into m_mkKeys and never removed, @@ -657,10 +576,13 @@ SDispatchResult CKeybindManager::handleKeybinds(const uint32_t modmask, const SP if (!k->dontInhibit && !*PDISABLEINHIBIT && PROTO::shortcutsInhibit->isInhibited()) continue; + if (!k->enabled) + continue; + if (!k->locked && g_pSessionLockManager->isSessionLocked()) continue; - if (!IGNORECONDITIONS && ((modmask != k->modmask && !k->ignoreMods) || (k->submap != m_currentSelectedSubmap && !k->submapUniversal) || k->shadowed)) + if (!IGNORECONDITIONS && ((modmask != k->modmask && !k->ignoreMods) || (k->submap.name != Config::Actions::state()->m_currentSubmap && !k->submapUniversal) || k->shadowed)) continue; if (device) { @@ -674,6 +596,27 @@ SDispatchResult CKeybindManager::handleKeybinds(const uint32_t modmask, const SP case MK_PARTIAL_MATCH: found = true; continue; case MK_FULL_MATCH: found = true; } + } else if (!k->sMkKeys.empty() && key.keyName.empty()) { + // we have a mkKeys array, and no key name. + // for binds with one key, use the legacy matching. + // for multi-key, use proper matching + const bool HAS_MULTIPLE_KEYS = k->sMkKeys.size() > 1; + + if (HAS_MULTIPLE_KEYS) { + // check if we match fully and aren't releasing + // for multi-key binds, this is a requirement, + // but for single-key ones, this would fuck up user's expectations + // where SUPER + X could be blocked because you did SUPER + A and haven't released A yet. + // the downside? SUPER + K and SUPER + K + A will trigger two binds on SUPER + K + A + if (!k->release && mkKeysymSetMatches(k->sMkKeys, m_mkKeys) != MK_FULL_MATCH) + continue; + } + + // check for just the one match + // this is also needed for multi-key binds so that SUPER + A + K can't + // be actuated by SUPER + K + A + if (key.keysym != k->sMkKeys.back()) + continue; } else if (!key.keyName.empty()) { if (key.keyName != k->key) continue; @@ -681,7 +624,7 @@ SDispatchResult CKeybindManager::handleKeybinds(const uint32_t modmask, const SP if (key.keycode != k->keycode) continue; } else if (k->catchAll) { - if (found || key.submapAtPress != m_currentSelectedSubmap) + if (found || key.submapAtPress.name != Config::Actions::state()->m_currentSubmap) continue; } else { // in this case, we only have the keysym to go off of for this keybind, and it's invalid @@ -710,7 +653,7 @@ SDispatchResult CKeybindManager::handleKeybinds(const uint32_t modmask, const SP } if (pressed && k->release && !SPECIALDISPATCHER) { - if (k->nonConsuming) + if (k->nonConsuming || k->autoConsuming) continue; found = true; // suppress the event @@ -729,7 +672,7 @@ SDispatchResult CKeybindManager::handleKeybinds(const uint32_t modmask, const SP continue; } else if (!k->release && !SPECIALDISPATCHER) { - if (k->nonConsuming) + if (k->nonConsuming || k->autoConsuming) continue; found = true; // suppress the event @@ -768,7 +711,7 @@ SDispatchResult CKeybindManager::handleKeybinds(const uint32_t modmask, const SP // call the dispatcher Log::logger->log(Log::DEBUG, "Keybind triggered, calling dispatcher ({}, {}, {}, {})", modmask, key.keyName, key.keysym, DISPATCHER->first); - m_passPressed = sc(pressed); + Config::Actions::state()->m_passPressed = sc(pressed); // if the dispatchers says to pass event then we will if (k->handler == "mouse") @@ -776,14 +719,14 @@ SDispatchResult CKeybindManager::handleKeybinds(const uint32_t modmask, const SP else res = DISPATCHER->second(k->arg); - m_passPressed = -1; + Config::Actions::state()->m_passPressed = -1; if (k->handler == "submap") { found = true; // don't process keybinds on submap change. break; } - if (k->handler != "submap" && !k->submap.reset.empty()) - setSubmap(k->submap.reset); + if (k->handler != "submap" && !k->submap.reset.empty()) // NOLINTNEXTLINE + Config::Actions::setSubmap(k->submap.reset); } if (pressed && k->repeat) { @@ -794,7 +737,7 @@ SDispatchResult CKeybindManager::handleKeybinds(const uint32_t modmask, const SP m_repeatKeyTimer->updateTimeout(std::chrono::milliseconds(KEEB->m_repeatDelay)); } - if (!k->nonConsuming) + if (!k->nonConsuming && !(k->autoConsuming && !res.success)) found = true; } @@ -819,6 +762,11 @@ void CKeybindManager::shadowKeybinds(const xkb_keysym_t& doesntHave, const uint3 for (auto& k : m_keybinds) { + if (!k->enabled) { + k->shadowed = false; + continue; + } + bool shadow = false; if (k->handler == "global" || k->transparent) @@ -896,1629 +844,10 @@ bool CKeybindManager::handleInternalKeybinds(xkb_keysym_t keysym) { return false; } -// Dispatchers -SDispatchResult CKeybindManager::spawn(std::string args) { - const auto PROC = Config::Supplementary::executor()->spawn(args); - if (!PROC.has_value()) - return {.success = false, .error = std::format("Failed to start process. No closing bracket in exec rule. {}", args)}; - return {.success = PROC.value() > 0, .error = std::format("Failed to start process {}", args)}; -} - -SDispatchResult CKeybindManager::spawnRaw(std::string args) { - const auto PROC = Config::Supplementary::executor()->spawnRaw(args); - return {.success = PROC && *PROC > 0, .error = std::format("Failed to start process {}", args)}; -} - -SDispatchResult CKeybindManager::killActive(std::string args) { - const auto PWINDOW = Desktop::focusState()->window(); - - if (!PWINDOW) { - Log::logger->log(Log::ERR, "killActive: no window found"); - return {.success = false, .error = "killActive: no window found"}; - } - - kill(PWINDOW->getPID(), SIGKILL); - - return {}; -} - -SDispatchResult CKeybindManager::closeActive(std::string args) { - if (Desktop::focusState()->window() && Desktop::focusState()->window()->m_closeableSince > Time::steadyNow()) - return {.success = false, .error = "can't close window, it's not closeable yet (noclosefor)"}; - - g_pCompositor->closeWindow(Desktop::focusState()->window()); - - return {}; -} - -SDispatchResult CKeybindManager::closeWindow(std::string args) { - const auto PWINDOW = g_pCompositor->getWindowByRegex(args); - - if (!PWINDOW) { - Log::logger->log(Log::ERR, "closeWindow: no window found"); - return {.success = false, .error = "closeWindow: no window found"}; - } - - if (PWINDOW->m_closeableSince > Time::steadyNow()) - return {.success = false, .error = "can't close window, it's not closeable yet (noclosefor)"}; - - g_pCompositor->closeWindow(PWINDOW); - - return {}; -} - -SDispatchResult CKeybindManager::killWindow(std::string args) { - const auto PWINDOW = g_pCompositor->getWindowByRegex(args); - - if (!PWINDOW) { - Log::logger->log(Log::ERR, "killWindow: no window found"); - return {.success = false, .error = "killWindow: no window found"}; - } - - kill(PWINDOW->getPID(), SIGKILL); - - return {}; -} - -SDispatchResult CKeybindManager::signalActive(std::string args) { - if (!isNumber(args)) - return {.success = false, .error = "signalActive: signal has to be int"}; - - try { - const auto SIGNALNUM = std::stoi(args); - if (SIGNALNUM < 1 || SIGNALNUM > 31) { - Log::logger->log(Log::ERR, "signalActive: invalid signal number {}", SIGNALNUM); - return {.success = false, .error = std::format("signalActive: invalid signal number {}", SIGNALNUM)}; - } - kill(Desktop::focusState()->window()->getPID(), SIGNALNUM); - } catch (const std::exception& e) { - Log::logger->log(Log::ERR, "signalActive: invalid signal format \"{}\"", args); - return {.success = false, .error = std::format("signalActive: invalid signal format \"{}\"", args)}; - } - - kill(Desktop::focusState()->window()->getPID(), std::stoi(args)); - - return {}; -} - -SDispatchResult CKeybindManager::signalWindow(std::string args) { - const auto WINDOWREGEX = args.substr(0, args.find_first_of(',')); - const auto SIGNAL = args.substr(args.find_first_of(',') + 1); - - const auto PWINDOW = g_pCompositor->getWindowByRegex(WINDOWREGEX); - - if (!PWINDOW) { - Log::logger->log(Log::ERR, "signalWindow: no window"); - return {.success = false, .error = "signalWindow: no window"}; - } - - if (!std::ranges::all_of(SIGNAL, ::isdigit)) - return {.success = false, .error = "signalWindow: signal has to be int"}; - - try { - const auto SIGNALNUM = std::stoi(SIGNAL); - if (SIGNALNUM < 1 || SIGNALNUM > 31) { - Log::logger->log(Log::ERR, "signalWindow: invalid signal number {}", SIGNALNUM); - return {.success = false, .error = std::format("signalWindow: invalid signal number {}", SIGNALNUM)}; - } - kill(PWINDOW->getPID(), SIGNALNUM); - } catch (const std::exception& e) { - Log::logger->log(Log::ERR, "signalWindow: invalid signal format \"{}\"", SIGNAL); - return {.success = false, .error = std::format("signalWindow: invalid signal format \"{}\"", SIGNAL)}; - } - - return {}; -} - void CKeybindManager::clearKeybinds() { m_keybinds.clear(); } -static SDispatchResult toggleActiveFloatingCore(std::string args, std::optional floatState) { - PHLWINDOW PWINDOW = nullptr; - - if (args != "active" && args.length() > 1) - PWINDOW = g_pCompositor->getWindowByRegex(args); - else - PWINDOW = Desktop::focusState()->window(); - - if (!PWINDOW) - return {.success = false, .error = "Window not found"}; - - if (floatState.has_value() && floatState == PWINDOW->m_isFloating) - return {}; - - // remove drag status - if (g_layoutManager->dragController()->target()) - CKeybindManager::changeMouseBindMode(MBIND_INVALID); - - g_layoutManager->changeFloatingMode(PWINDOW->layoutTarget()); - - if (PWINDOW->m_isFloating) - g_pCompositor->changeWindowZOrder(PWINDOW, true); - - if (PWINDOW->m_workspace) { - PWINDOW->m_workspace->updateWindows(); - PWINDOW->m_workspace->updateWindowData(); - } - - g_pCompositor->updateAllWindowsAnimatedDecorationValues(); - - return {}; -} - -SDispatchResult CKeybindManager::toggleActiveFloating(std::string args) { - return toggleActiveFloatingCore(args, std::nullopt); -} - -SDispatchResult CKeybindManager::setActiveFloating(std::string args) { - return toggleActiveFloatingCore(args, true); -} - -SDispatchResult CKeybindManager::setActiveTiled(std::string args) { - return toggleActiveFloatingCore(args, false); -} - -SDispatchResult CKeybindManager::centerWindow(std::string args) { - const auto PWINDOW = Desktop::focusState()->window(); - - if (!PWINDOW || !PWINDOW->m_isFloating || PWINDOW->isFullscreen()) - return {.success = false, .error = "No floating window found"}; - - const auto PMONITOR = PWINDOW->m_monitor.lock(); - - PWINDOW->layoutTarget()->setPositionGlobal(CBox{PMONITOR->logicalBoxMinusReserved().middle() - PWINDOW->m_realSize->goal() / 2.F, PWINDOW->layoutTarget()->position().size()}); - - return {}; -} - -SDispatchResult CKeybindManager::toggleActivePseudo(std::string args) { - PHLWINDOW PWINDOW = nullptr; - - if (args != "active" && args.length() > 1) - PWINDOW = g_pCompositor->getWindowByRegex(args); - else - PWINDOW = Desktop::focusState()->window(); - - if (!PWINDOW) - return {.success = false, .error = "Window not found"}; - - PWINDOW->layoutTarget()->setPseudo(!PWINDOW->layoutTarget()->isPseudo()); - - return {}; -} - -static SWorkspaceIDName getWorkspaceToChangeFromArgs(std::string args, PHLWORKSPACE PCURRENTWORKSPACE, PHLMONITORREF PMONITOR) { - if (!args.starts_with("previous")) { - return getWorkspaceIDNameFromString(args); - } - - const bool PER_MON = args.contains("_per_monitor"); - const SWorkspaceIDName PPREVWS = PER_MON ? Desktop::History::workspaceTracker()->previousWorkspaceIDName(PCURRENTWORKSPACE, PMONITOR.lock()) : - Desktop::History::workspaceTracker()->previousWorkspaceIDName(PCURRENTWORKSPACE); - // Do nothing if there's no previous workspace, otherwise switch to it. - if (PPREVWS.id == -1 || PPREVWS.id == PCURRENTWORKSPACE->m_id) { - Log::logger->log(Log::DEBUG, "No previous workspace to change to"); - return {.id = WORKSPACE_NOT_CHANGED}; - } - - if (const auto PWORKSPACETOCHANGETO = g_pCompositor->getWorkspaceByID(PPREVWS.id); PWORKSPACETOCHANGETO) { - return {.id = PWORKSPACETOCHANGETO->m_id, .name = PWORKSPACETOCHANGETO->m_name}; - } - - return {.id = PPREVWS.id, .name = PPREVWS.name.empty() ? std::to_string(PPREVWS.id) : PPREVWS.name}; -} - -SDispatchResult CKeybindManager::changeworkspace(std::string args) { - // Workspace_back_and_forth being enabled means that an attempt to switch to - // the current workspace will instead switch to the previous. - static auto PBACKANDFORTH = CConfigValue("binds:workspace_back_and_forth"); - static auto PWORKSPACECENTERON = CConfigValue("binds:workspace_center_on"); - static auto PHIDESPECIALONWORKSPACECHANGE = CConfigValue("binds:hide_special_on_workspace_change"); - - const auto PMONITOR = Desktop::focusState()->monitor(); - - if (!PMONITOR) - return {.success = false, .error = "Last monitor not found"}; - - const auto PCURRENTWORKSPACE = PMONITOR->m_activeWorkspace; - const bool EXPLICITPREVIOUS = args.contains("previous"); - - const auto& [workspaceToChangeTo, workspaceName, isAutoID] = getWorkspaceToChangeFromArgs(args, PCURRENTWORKSPACE, PMONITOR); - if (workspaceToChangeTo == WORKSPACE_INVALID) { - Log::logger->log(Log::ERR, "Error in changeworkspace, invalid value"); - return {.success = false, .error = "Error in changeworkspace, invalid value"}; - } - - if (workspaceToChangeTo == WORKSPACE_NOT_CHANGED) - return {}; - - const SWorkspaceIDName PPREVWS = args.contains("_per_monitor") ? Desktop::History::workspaceTracker()->previousWorkspaceIDName(PCURRENTWORKSPACE, PMONITOR) : - Desktop::History::workspaceTracker()->previousWorkspaceIDName(PCURRENTWORKSPACE); - - const bool BISWORKSPACECURRENT = workspaceToChangeTo == PCURRENTWORKSPACE->m_id; - if (BISWORKSPACECURRENT && (!(*PBACKANDFORTH || EXPLICITPREVIOUS) || PPREVWS.id == -1)) { - if (*PHIDESPECIALONWORKSPACECHANGE) - PMONITOR->setSpecialWorkspace(nullptr); - - return {.success = false, .error = "Previous workspace doesn't exist"}; - } - - g_pInputManager->unconstrainMouse(); - g_pInputManager->m_emptyFocusCursorSet = false; - - auto pWorkspaceToChangeTo = g_pCompositor->getWorkspaceByID(BISWORKSPACECURRENT ? PPREVWS.id : workspaceToChangeTo); - if (!pWorkspaceToChangeTo) - pWorkspaceToChangeTo = - g_pCompositor->createNewWorkspace(BISWORKSPACECURRENT ? PPREVWS.id : workspaceToChangeTo, PMONITOR->m_id, BISWORKSPACECURRENT ? PPREVWS.name : workspaceName); - - if (!BISWORKSPACECURRENT && pWorkspaceToChangeTo->m_isSpecialWorkspace) { - PMONITOR->setSpecialWorkspace(pWorkspaceToChangeTo); - g_pInputManager->simulateMouseMovement(); - return {}; - } - - g_pInputManager->releaseAllMouseButtons(); - - const auto PMONITORWORKSPACEOWNER = PMONITOR == pWorkspaceToChangeTo->m_monitor ? PMONITOR : pWorkspaceToChangeTo->m_monitor.lock(); - - if (!PMONITORWORKSPACEOWNER) - return {.success = false, .error = "Workspace to switch to has no monitor"}; - - updateRelativeCursorCoords(); - - Desktop::focusState()->rawMonitorFocus(PMONITORWORKSPACEOWNER); - - if (*PHIDESPECIALONWORKSPACECHANGE) - PMONITORWORKSPACEOWNER->setSpecialWorkspace(nullptr); - PMONITORWORKSPACEOWNER->changeWorkspace(pWorkspaceToChangeTo, false, true); - - if (PMONITOR != PMONITORWORKSPACEOWNER) { - Vector2D middle = PMONITORWORKSPACEOWNER->middle(); - if (const auto PLAST = pWorkspaceToChangeTo->getLastFocusedWindow(); PLAST) { - Desktop::focusState()->fullWindowFocus(PLAST, Desktop::FOCUS_REASON_KEYBIND); - if (*PWORKSPACECENTERON == 1) - middle = PLAST->middle(); - } - g_pCompositor->warpCursorTo(middle); - } - - if (!g_pInputManager->m_lastFocusOnLS) { - if (Desktop::focusState()->surface()) - g_pInputManager->sendMotionEventsToFocused(); - else - g_pInputManager->simulateMouseMovement(); - } - - const static auto PWARPONWORKSPACECHANGE = CConfigValue("cursor:warp_on_change_workspace"); - - if (*PWARPONWORKSPACECHANGE > 0) { - auto PLAST = pWorkspaceToChangeTo->getLastFocusedWindow(); - auto HLSurface = Desktop::View::CWLSurface::fromResource(g_pSeatManager->m_state.pointerFocus.lock()); - - if (PLAST && (!HLSurface || HLSurface->view()->type() == Desktop::View::VIEW_TYPE_WINDOW)) - PLAST->warpCursor(*PWARPONWORKSPACECHANGE == 2); - } - - return {}; -} - -SDispatchResult CKeybindManager::fullscreenActive(std::string args) { - const auto PWINDOW = Desktop::focusState()->window(); - const auto ARGS = CConstVarList(args, 2, ' '); - - if (!PWINDOW) - return {.success = false, .error = "Window not found"}; - - const eFullscreenMode MODE = ARGS.size() > 0 && ARGS[0] == "1" ? FSMODE_MAXIMIZED : FSMODE_FULLSCREEN; - - if (ARGS.size() <= 1 || ARGS[1] == "toggle") { - if (PWINDOW->isEffectiveInternalFSMode(MODE)) - g_pCompositor->setWindowFullscreenInternal(PWINDOW, FSMODE_NONE); - else - g_pCompositor->setWindowFullscreenInternal(PWINDOW, MODE); - } else { - if (ARGS[1] == "set") - g_pCompositor->setWindowFullscreenInternal(PWINDOW, MODE); - else if (ARGS[1] == "unset") - g_pCompositor->setWindowFullscreenInternal(PWINDOW, FSMODE_NONE); - } - - return {}; -} - -SDispatchResult CKeybindManager::fullscreenStateActive(std::string args) { - const auto PWINDOW = Desktop::focusState()->window(); - const auto ARGS = CVarList(args, 3, ' '); - - if (!PWINDOW) - return {.success = false, .error = "Window not found"}; - - PWINDOW->m_ruleApplicator->syncFullscreenOverride(Desktop::Types::COverridableVar(false, Desktop::Types::PRIORITY_SET_PROP)); - - int internalMode, clientMode; - try { - internalMode = std::stoi(ARGS[0]); - } catch (std::exception& e) { internalMode = -1; } - try { - clientMode = std::stoi(ARGS[1]); - } catch (std::exception& e) { clientMode = -1; } - - const Desktop::View::SFullscreenState STATE = - Desktop::View::SFullscreenState{.internal = (internalMode != -1 ? sc(internalMode) : PWINDOW->m_fullscreenState.internal), - .client = (clientMode != -1 ? sc(clientMode) : PWINDOW->m_fullscreenState.client)}; - - if (ARGS.size() <= 2 || ARGS[2] == "toggle") { - if (internalMode != -1 && clientMode != -1 && PWINDOW->m_fullscreenState.internal == STATE.internal && PWINDOW->m_fullscreenState.client == STATE.client) - g_pCompositor->setWindowFullscreenState(PWINDOW, Desktop::View::SFullscreenState{.internal = FSMODE_NONE, .client = FSMODE_NONE}); - else if (internalMode != -1 && clientMode == -1 && PWINDOW->m_fullscreenState.internal == STATE.internal) - g_pCompositor->setWindowFullscreenState(PWINDOW, Desktop::View::SFullscreenState{.internal = FSMODE_NONE, .client = PWINDOW->m_fullscreenState.client}); - else if (internalMode == -1 && clientMode != -1 && PWINDOW->m_fullscreenState.client == STATE.client) - g_pCompositor->setWindowFullscreenState(PWINDOW, Desktop::View::SFullscreenState{.internal = PWINDOW->m_fullscreenState.internal, .client = FSMODE_NONE}); - else - g_pCompositor->setWindowFullscreenState(PWINDOW, STATE); - } else if (ARGS[2] == "set") { - g_pCompositor->setWindowFullscreenState(PWINDOW, STATE); - } - - PWINDOW->m_ruleApplicator->syncFullscreenOverride( - Desktop::Types::COverridableVar(PWINDOW->m_fullscreenState.internal == PWINDOW->m_fullscreenState.client, Desktop::Types::PRIORITY_SET_PROP)); - - return {}; -} - -SDispatchResult CKeybindManager::moveActiveToWorkspace(std::string args) { - - PHLWINDOW PWINDOW = nullptr; - - if (args.contains(',')) { - PWINDOW = g_pCompositor->getWindowByRegex(args.substr(args.find_last_of(',') + 1)); - args = args.substr(0, args.find_last_of(',')); - } else { - PWINDOW = Desktop::focusState()->window(); - } - - if (!PWINDOW) - return {.success = false, .error = "Window not found"}; - - const auto& [WORKSPACEID, workspaceName, isAutoID] = getWorkspaceIDNameFromString(args); - if (WORKSPACEID == WORKSPACE_INVALID) { - Log::logger->log(Log::DEBUG, "Invalid workspace in moveActiveToWorkspace"); - return {.success = false, .error = "Invalid workspace in moveActiveToWorkspace"}; - } - - if (WORKSPACEID == PWINDOW->workspaceID()) { - Log::logger->log(Log::DEBUG, "Not moving to workspace because it didn't change."); - return {.success = false, .error = "Not moving to workspace because it didn't change."}; - } - - auto pWorkspace = g_pCompositor->getWorkspaceByID(WORKSPACEID); - PHLMONITOR pMonitor = nullptr; - const auto POLDWS = PWINDOW->m_workspace; - - updateRelativeCursorCoords(); - - g_pHyprRenderer->damageWindow(PWINDOW); - - if (pWorkspace) { - const auto FULLSCREENMODE = PWINDOW->m_fullscreenState.internal; - g_pCompositor->moveWindowToWorkspaceSafe(PWINDOW, pWorkspace); - pMonitor = pWorkspace->m_monitor.lock(); - Desktop::focusState()->rawMonitorFocus(pMonitor); - g_pCompositor->setWindowFullscreenInternal(PWINDOW, FULLSCREENMODE); - } else { - pWorkspace = g_pCompositor->createNewWorkspace(WORKSPACEID, PWINDOW->monitorID(), workspaceName, false); - pMonitor = pWorkspace->m_monitor.lock(); - g_pCompositor->moveWindowToWorkspaceSafe(PWINDOW, pWorkspace); - } - - POLDWS->m_lastFocusedWindow = POLDWS->getFirstWindow(); - - if (pWorkspace->m_isSpecialWorkspace) - pMonitor->setSpecialWorkspace(pWorkspace); - else if (POLDWS->m_isSpecialWorkspace) - POLDWS->m_monitor.lock()->setSpecialWorkspace(nullptr); - - pMonitor->changeWorkspace(pWorkspace); - - Desktop::focusState()->fullWindowFocus(PWINDOW, Desktop::FOCUS_REASON_KEYBIND); - PWINDOW->warpCursor(); - - return {}; -} - -SDispatchResult CKeybindManager::moveActiveToWorkspaceSilent(std::string args) { - PHLWINDOW PWINDOW = nullptr; - - if (args.contains(',')) { - PWINDOW = g_pCompositor->getWindowByRegex(args.substr(args.find_last_of(',') + 1)); - args = args.substr(0, args.find_last_of(',')); - } else { - PWINDOW = Desktop::focusState()->window(); - } - - if (!PWINDOW) - return {.success = false, .error = "Window not found"}; - - const auto& [WORKSPACEID, workspaceName, isAutoID] = getWorkspaceIDNameFromString(args); - if (WORKSPACEID == WORKSPACE_INVALID) { - Log::logger->log(Log::ERR, "Error in moveActiveToWorkspaceSilent, invalid value"); - return {.success = false, .error = "Error in moveActiveToWorkspaceSilent, invalid value"}; - } - - if (WORKSPACEID == PWINDOW->workspaceID()) - return {}; - - g_pHyprRenderer->damageWindow(PWINDOW); - - auto pWorkspace = g_pCompositor->getWorkspaceByID(WORKSPACEID); - const auto OLDMIDDLE = PWINDOW->middle(); - - if (pWorkspace) { - g_pCompositor->moveWindowToWorkspaceSafe(PWINDOW, pWorkspace); - } else { - pWorkspace = g_pCompositor->createNewWorkspace(WORKSPACEID, PWINDOW->monitorID(), workspaceName, false); - g_pCompositor->moveWindowToWorkspaceSafe(PWINDOW, pWorkspace); - } - - if (PWINDOW == Desktop::focusState()->window()) { - if (const auto PATCOORDS = - g_pCompositor->vectorToWindowUnified(OLDMIDDLE, Desktop::View::RESERVED_EXTENTS | Desktop::View::INPUT_EXTENTS | Desktop::View::ALLOW_FLOATING, PWINDOW); - PATCOORDS) - Desktop::focusState()->fullWindowFocus(PATCOORDS, Desktop::FOCUS_REASON_KEYBIND); - else - g_pInputManager->refocus(); - } - - return {}; -} - -SDispatchResult CKeybindManager::moveFocusTo(std::string args) { - static auto PFULLCYCLE = CConfigValue("binds:movefocus_cycles_fullscreen"); - static auto PGROUPCYCLE = CConfigValue("binds:movefocus_cycles_groupfirst"); - static auto PMONITORFALLBACK = CConfigValue("binds:window_direction_monitor_fallback"); - Math::eDirection dir = Math::fromChar(args[0]); - - if (dir == Math::DIRECTION_DEFAULT) { - Log::logger->log(Log::ERR, "Cannot move focus in direction {}, unsupported direction. Supported: l,r,u/t,d/b", args[0]); - return {.success = false, .error = std::format("Cannot move focus in direction {}, unsupported direction. Supported: l,r,u/t,d/b", args[0])}; - } - - const auto PLASTWINDOW = Desktop::focusState()->window(); - if (!PLASTWINDOW || !PLASTWINDOW->aliveAndVisible()) { - if (*PMONITORFALLBACK) - tryMoveFocusToMonitor(g_pCompositor->getMonitorInDirection(dir)); - return {}; - } - - const auto PWINDOWTOCHANGETO = *PFULLCYCLE && PLASTWINDOW->isFullscreen() ? - g_pCompositor->getWindowCycle(PLASTWINDOW, true, {}, false, dir != Math::DIRECTION_DOWN && dir != Math::DIRECTION_RIGHT) : - g_pCompositor->getWindowInDirection(PLASTWINDOW, dir); - - // Prioritize focus change within groups if the window is a part of it. - if (*PGROUPCYCLE && PLASTWINDOW->m_group) { - auto isTheOnlyGroupOnWs = !PWINDOWTOCHANGETO && g_pCompositor->m_monitors.size() == 1; - if (dir == Math::DIRECTION_LEFT && (PLASTWINDOW != PLASTWINDOW->m_group->head() || isTheOnlyGroupOnWs)) { - PLASTWINDOW->m_group->moveCurrent(false); - return {}; - } - - else if (dir == Math::DIRECTION_RIGHT && (PLASTWINDOW != PLASTWINDOW->m_group->tail() || isTheOnlyGroupOnWs)) { - PLASTWINDOW->m_group->moveCurrent(true); - return {}; - } - } - - // Found window in direction, switch to it - if (PWINDOWTOCHANGETO) { - switchToWindow(PWINDOWTOCHANGETO, *PFULLCYCLE && PLASTWINDOW->isFullscreen()); - return {}; - } - - Log::logger->log(Log::DEBUG, "No window found in direction {}, looking for a monitor", Math::toString(dir)); - - if (*PMONITORFALLBACK && tryMoveFocusToMonitor(g_pCompositor->getMonitorInDirection(dir))) - return {}; - - static auto PNOFALLBACK = CConfigValue("general:no_focus_fallback"); - if (*PNOFALLBACK) - return {.success = false, .error = std::format("Nothing to focus to in direction {}", Math::toString(dir))}; - - Log::logger->log(Log::DEBUG, "No monitor found in direction {}, getting the inverse edge", Math::toString(dir)); - - const auto PMONITOR = PLASTWINDOW->m_monitor.lock(); - - if (!PMONITOR) - return {.success = false, .error = "last window has no monitor?"}; - - if (dir == Math::DIRECTION_LEFT || dir == Math::DIRECTION_RIGHT) { - if (STICKS(PLASTWINDOW->m_position.x, PMONITOR->m_position.x) && STICKS(PLASTWINDOW->m_size.x, PMONITOR->m_size.x)) - return {.success = false, .error = "move does not make sense, would return back"}; - } else if (STICKS(PLASTWINDOW->m_position.y, PMONITOR->m_position.y) && STICKS(PLASTWINDOW->m_size.y, PMONITOR->m_size.y)) - return {.success = false, .error = "move does not make sense, would return back"}; - - CBox box = PMONITOR->logicalBox(); - switch (dir) { - case Math::DIRECTION_LEFT: - box.x += box.w; - box.w = 1; - break; - case Math::DIRECTION_RIGHT: - box.x -= 1; - box.w = 1; - break; - case Math::DIRECTION_UP: - box.y += box.h; - box.h = 1; - break; - case Math::DIRECTION_DOWN: - box.y -= 1; - box.h = 1; - break; - default: break; - } - - const auto PWINDOWCANDIDATE = g_pCompositor->getWindowInDirection(box, PMONITOR->m_activeSpecialWorkspace ? PMONITOR->m_activeSpecialWorkspace : PMONITOR->m_activeWorkspace, - dir, PLASTWINDOW, PLASTWINDOW->m_isFloating); - if (PWINDOWCANDIDATE) - switchToWindow(PWINDOWCANDIDATE); - - return {}; -} - -SDispatchResult CKeybindManager::focusUrgentOrLast(std::string args) { - const auto& HISTORY = Desktop::History::windowTracker()->fullHistory(); - const auto PWINDOWURGENT = g_pCompositor->getUrgentWindow(); - const auto PWINDOWPREV = Desktop::focusState()->window() ? (HISTORY.size() < 2 ? nullptr : HISTORY[1].lock()) : (HISTORY.empty() ? nullptr : HISTORY[0].lock()); - - if (!PWINDOWURGENT && !PWINDOWPREV) - return {.success = false, .error = "Window not found"}; - - switchToWindow(PWINDOWURGENT ? PWINDOWURGENT : PWINDOWPREV); - - return {}; -} - -SDispatchResult CKeybindManager::focusCurrentOrLast(std::string args) { - const auto& HISTORY = Desktop::History::windowTracker()->fullHistory(); - - if (HISTORY.size() <= 1) - return {.success = false, .error = "History too short"}; - - const auto PWINDOWPREV = HISTORY[HISTORY.size() - 2].lock(); - - if (!PWINDOWPREV) - return {.success = false, .error = "Window not found"}; - - switchToWindow(PWINDOWPREV); - - return {}; -} - -SDispatchResult CKeybindManager::swapActive(std::string args) { - const auto PLASTWINDOW = Desktop::focusState()->window(); - PHLWINDOW PWINDOWTOCHANGETO = nullptr; - - if (!PLASTWINDOW) - return {.success = false, .error = "Window to swap with not found"}; - - if (PLASTWINDOW->isFullscreen()) - return {.success = false, .error = "Can't swap fullscreen window"}; - - if (isDirection(args)) { - Math::eDirection dir = Math::fromChar(args[0]); - PWINDOWTOCHANGETO = g_pCompositor->getWindowInDirection(PLASTWINDOW, dir); - } else - PWINDOWTOCHANGETO = g_pCompositor->getWindowByRegex(args); - - if (!PWINDOWTOCHANGETO || PWINDOWTOCHANGETO == PLASTWINDOW) { - Log::logger->log(Log::ERR, "Can't swap with {}, invalid window", args); - return {.success = false, .error = std::format("Can't swap with {}, invalid window", args)}; - } - - Log::logger->log(Log::DEBUG, "Swapping active window with {}", args); - - updateRelativeCursorCoords(); - g_layoutManager->switchTargets(PLASTWINDOW->layoutTarget(), PWINDOWTOCHANGETO->layoutTarget(), true); - PLASTWINDOW->warpCursor(); - return {}; -} - -SDispatchResult CKeybindManager::moveActiveTo(std::string args) { - bool silent = args.ends_with(" silent"); - if (silent) - args = args.substr(0, args.length() - 7); - - if (args.starts_with("mon:")) { - const auto PNEWMONITOR = g_pCompositor->getMonitorFromString(args.substr(4)); - if (!PNEWMONITOR) - return {.success = false, .error = std::format("Monitor {} not found", args.substr(4))}; - - if (silent) - moveActiveToWorkspaceSilent(PNEWMONITOR->m_activeWorkspace->getConfigName()); - else - moveActiveToWorkspace(PNEWMONITOR->m_activeWorkspace->getConfigName()); - - return {}; - } - - Math::eDirection dir = Math::fromChar(args[0]); - if (dir == Math::DIRECTION_DEFAULT) { - Log::logger->log(Log::ERR, "Cannot move window in direction {}, unsupported direction. Supported: l,r,u/t,d/b", args[0]); - return {.success = false, .error = std::format("Cannot move window in direction {}, unsupported direction. Supported: l,r,u/t,d/b", args[0])}; - } - - const auto PLASTWINDOW = Desktop::focusState()->window(); - - if (!PLASTWINDOW) - return {.success = false, .error = "Window to move not found"}; - - if (PLASTWINDOW->isFullscreen()) - return {.success = false, .error = "Can't move fullscreen window"}; - - updateRelativeCursorCoords(); - - g_layoutManager->moveInDirection(PLASTWINDOW->layoutTarget(), args, silent); - if (!silent) - PLASTWINDOW->warpCursor(); - - return {}; -} - -SDispatchResult CKeybindManager::toggleGroup(std::string args) { - const auto PWINDOW = Desktop::focusState()->window(); - - if (!PWINDOW) - return {.success = false, .error = "Window not found"}; - - if (PWINDOW->isFullscreen()) - g_pCompositor->setWindowFullscreenInternal(PWINDOW, FSMODE_NONE); - - if (!PWINDOW->m_group) - PWINDOW->m_group = Desktop::View::CGroup::create({PWINDOW}); - else - PWINDOW->m_group->destroy(); - - return {}; -} - -SDispatchResult CKeybindManager::changeGroupActive(std::string args) { - const auto PWINDOW = Desktop::focusState()->window(); - - if (!PWINDOW) - return {.success = false, .error = "Window not found"}; - - if (!PWINDOW->m_group) - return {.success = false, .error = "No group"}; - - if (PWINDOW->m_group->size() == 1) - return {.success = false, .error = "Only one window in group"}; - - if (isNumber(args, false)) { - // index starts from '1'; '0' means last window - try { - const int INDEX = std::stoi(args); - if (INDEX <= 0) - PWINDOW->m_group->setCurrent(PWINDOW->m_group->size() - 1); - else - PWINDOW->m_group->setCurrent(INDEX - 1); - } catch (...) { return {.success = false, .error = "invalid idx"}; } - - return {}; - } - - if (args != "b" && args != "prev") - PWINDOW->m_group->moveCurrent(true); - else - PWINDOW->m_group->moveCurrent(false); - - return {}; -} - -SDispatchResult CKeybindManager::focusMonitor(std::string arg) { - const auto PMONITOR = g_pCompositor->getMonitorFromString(arg); - tryMoveFocusToMonitor(PMONITOR); - - return {}; -} - -SDispatchResult CKeybindManager::moveCursorToCorner(std::string arg) { - if (!isNumber(arg)) { - Log::logger->log(Log::ERR, "moveCursorToCorner, arg has to be a number."); - return {.success = false, .error = "moveCursorToCorner, arg has to be a number."}; - } - - const auto CORNER = std::stoi(arg); - - if (CORNER < 0 || CORNER > 3) { - Log::logger->log(Log::ERR, "moveCursorToCorner, corner not 0 - 3."); - return {.success = false, .error = "moveCursorToCorner, corner not 0 - 3."}; - } - - const auto PWINDOW = Desktop::focusState()->window(); - - if (!PWINDOW) - return {.success = false, .error = "Window not found"}; - - switch (CORNER) { - case 0: - // bottom left - g_pCompositor->warpCursorTo({PWINDOW->m_realPosition->value().x, PWINDOW->m_realPosition->value().y + PWINDOW->m_realSize->value().y}, true); - break; - case 1: - // bottom right - g_pCompositor->warpCursorTo({PWINDOW->m_realPosition->value().x + PWINDOW->m_realSize->value().x, PWINDOW->m_realPosition->value().y + PWINDOW->m_realSize->value().y}, - true); - break; - case 2: - // top right - g_pCompositor->warpCursorTo({PWINDOW->m_realPosition->value().x + PWINDOW->m_realSize->value().x, PWINDOW->m_realPosition->value().y}, true); - break; - case 3: - // top left - g_pCompositor->warpCursorTo({PWINDOW->m_realPosition->value().x, PWINDOW->m_realPosition->value().y}, true); - break; - } - - return {}; -} - -SDispatchResult CKeybindManager::moveCursor(std::string args) { - std::string x_str, y_str; - int x, y; - - size_t i = args.find_first_of(' '); - if (i == std::string::npos) { - Log::logger->log(Log::ERR, "moveCursor, takes 2 arguments."); - return {.success = false, .error = "moveCursor, takes 2 arguments"}; - } - - x_str = args.substr(0, i); - y_str = args.substr(i + 1); - - if (!isNumber(x_str)) { - Log::logger->log(Log::ERR, "moveCursor, x argument has to be a number."); - return {.success = false, .error = "moveCursor, x argument has to be a number."}; - } - if (!isNumber(y_str)) { - Log::logger->log(Log::ERR, "moveCursor, y argument has to be a number."); - return {.success = false, .error = "moveCursor, y argument has to be a number."}; - } - - x = std::stoi(x_str); - y = std::stoi(y_str); - - g_pCompositor->warpCursorTo({x, y}, true); - g_pInputManager->simulateMouseMovement(); - - return {}; -} - -SDispatchResult CKeybindManager::workspaceOpt(std::string args) { - return {.success = false, .error = "workspaceopt is deprecated"}; -} - -SDispatchResult CKeybindManager::renameWorkspace(std::string args) { - try { - const auto FIRSTSPACEPOS = args.find_first_of(' '); - if (FIRSTSPACEPOS != std::string::npos) { - int workspace = std::stoi(args.substr(0, FIRSTSPACEPOS)); - std::string name = args.substr(FIRSTSPACEPOS + 1); - if (const auto& PWS = g_pCompositor->getWorkspaceByID(workspace); PWS) - PWS->rename(name); - else - return {.success = false, .error = "No such workspace"}; - } else if (const auto& PWS = g_pCompositor->getWorkspaceByID(std::stoi(args)); PWS) - PWS->rename(""); - else - return {.success = false, .error = "No such workspace"}; - } catch (std::exception& e) { - Log::logger->log(Log::ERR, R"(Invalid arg in renameWorkspace, expected numeric id only or a numeric id and string name. "{}": "{}")", args, e.what()); - return {.success = false, .error = std::format(R"(Invalid arg in renameWorkspace, expected numeric id only or a numeric id and string name. "{}": "{}")", args, e.what())}; - } - - return {}; -} - -SDispatchResult CKeybindManager::exitHyprland(std::string argz) { - Event::bus()->m_events.exit.emit(); - - if (g_pCompositor->m_finalRequests) - return {}; // Exiting deferred until requests complete - - g_pCompositor->stopCompositor(); - return {}; -} - -SDispatchResult CKeybindManager::moveCurrentWorkspaceToMonitor(std::string args) { - PHLMONITOR PMONITOR = g_pCompositor->getMonitorFromString(args); - - if (!PMONITOR) { - Log::logger->log(Log::ERR, "Ignoring moveCurrentWorkspaceToMonitor: monitor doesn't exist"); - return {.success = false, .error = "Ignoring moveCurrentWorkspaceToMonitor: monitor doesn't exist"}; - } - - // get the current workspace - const auto PCURRENTWORKSPACE = Desktop::focusState()->monitor()->m_activeWorkspace; - if (!PCURRENTWORKSPACE) { - Log::logger->log(Log::ERR, "moveCurrentWorkspaceToMonitor invalid workspace!"); - return {.success = false, .error = "moveCurrentWorkspaceToMonitor invalid workspace!"}; - } - - g_pCompositor->moveWorkspaceToMonitor(PCURRENTWORKSPACE, PMONITOR); - - return {}; -} - -SDispatchResult CKeybindManager::moveWorkspaceToMonitor(std::string args) { - if (!args.contains(' ')) - return {.success = false, .error = "Invalid arguments, expected: workspace monitor"}; - - std::string workspace = args.substr(0, args.find_first_of(' ')); - std::string monitor = args.substr(args.find_first_of(' ') + 1); - - const auto PMONITOR = g_pCompositor->getMonitorFromString(monitor); - - if (!PMONITOR) { - Log::logger->log(Log::ERR, "Ignoring moveWorkspaceToMonitor: monitor doesn't exist"); - return {.success = false, .error = "Ignoring moveWorkspaceToMonitor: monitor doesn't exist"}; - } - - const auto WORKSPACEID = getWorkspaceIDNameFromString(workspace).id; - - if (WORKSPACEID == WORKSPACE_INVALID) { - Log::logger->log(Log::ERR, "moveWorkspaceToMonitor invalid workspace!"); - return {.success = false, .error = "moveWorkspaceToMonitor invalid workspace!"}; - } - - const auto PWORKSPACE = g_pCompositor->getWorkspaceByID(WORKSPACEID); - - if (!PWORKSPACE) { - Log::logger->log(Log::ERR, "moveWorkspaceToMonitor workspace doesn't exist!"); - return {.success = false, .error = "moveWorkspaceToMonitor workspace doesn't exist!"}; - } - - g_pCompositor->moveWorkspaceToMonitor(PWORKSPACE, PMONITOR); - - return {}; -} - -SDispatchResult CKeybindManager::focusWorkspaceOnCurrentMonitor(std::string args) { - auto [workspaceID, workspaceName, isAutoID] = getWorkspaceIDNameFromString(args); - if (workspaceID == WORKSPACE_INVALID) { - Log::logger->log(Log::ERR, "focusWorkspaceOnCurrentMonitor invalid workspace!"); - return {.success = false, .error = "focusWorkspaceOnCurrentMonitor invalid workspace!"}; - } - - const auto PCURRMONITOR = Desktop::focusState()->monitor(); - - if (!PCURRMONITOR) { - Log::logger->log(Log::ERR, "focusWorkspaceOnCurrentMonitor monitor doesn't exist!"); - return {.success = false, .error = "focusWorkspaceOnCurrentMonitor monitor doesn't exist!"}; - } - - auto pWorkspace = g_pCompositor->getWorkspaceByID(workspaceID); - - if (!pWorkspace) { - pWorkspace = g_pCompositor->createNewWorkspace(workspaceID, PCURRMONITOR->m_id, workspaceName); - // we can skip the moving, since it's already on the current monitor - changeworkspace(pWorkspace->getConfigName()); - return {}; - } - - static auto PBACKANDFORTH = CConfigValue("binds:workspace_back_and_forth"); - const auto PREVWS = Desktop::History::workspaceTracker()->previousWorkspaceIDName(pWorkspace); - - if (*PBACKANDFORTH && PCURRMONITOR->activeWorkspaceID() == workspaceID && PREVWS.id != -1) { - // Workspace to focus is previous workspace - pWorkspace = g_pCompositor->getWorkspaceByID(PREVWS.id); - if (!pWorkspace) - pWorkspace = g_pCompositor->createNewWorkspace(PREVWS.id, PCURRMONITOR->m_id, PREVWS.name); - - workspaceID = pWorkspace->m_id; - } - - if (pWorkspace->m_monitor != PCURRMONITOR) { - const auto POLDMONITOR = pWorkspace->m_monitor.lock(); - if (!POLDMONITOR) { // wat - Log::logger->log(Log::ERR, "focusWorkspaceOnCurrentMonitor old monitor doesn't exist!"); - return {.success = false, .error = "focusWorkspaceOnCurrentMonitor old monitor doesn't exist!"}; - } - if (POLDMONITOR->activeWorkspaceID() == workspaceID) { - g_pCompositor->swapActiveWorkspaces(POLDMONITOR, PCURRMONITOR); - return {}; - } else { - g_pCompositor->moveWorkspaceToMonitor(pWorkspace, PCURRMONITOR, true); - } - } - - changeworkspace(pWorkspace->getConfigName()); - - return {}; -} - -SDispatchResult CKeybindManager::toggleSpecialWorkspace(std::string args) { - const auto& [workspaceID, workspaceName, isAutoID] = getWorkspaceIDNameFromString("special:" + args); - if (workspaceID == WORKSPACE_INVALID || !g_pCompositor->isWorkspaceSpecial(workspaceID)) { - Log::logger->log(Log::ERR, "Invalid workspace passed to special"); - return {.success = false, .error = "Invalid workspace passed to special"}; - } - - bool requestedWorkspaceIsAlreadyOpen = false; - const auto PMONITOR = Desktop::focusState()->monitor(); - auto specialOpenOnMonitor = PMONITOR->activeSpecialWorkspaceID(); - - for (auto const& m : g_pCompositor->m_monitors) { - if (m->activeSpecialWorkspaceID() == workspaceID) { - requestedWorkspaceIsAlreadyOpen = true; - break; - } - } - - updateRelativeCursorCoords(); - - PHLWORKSPACEREF focusedWorkspace; - - if (requestedWorkspaceIsAlreadyOpen && specialOpenOnMonitor == workspaceID) { - // already open on this monitor - Log::logger->log(Log::DEBUG, "Toggling special workspace {} to closed", workspaceID); - PMONITOR->setSpecialWorkspace(nullptr); - - focusedWorkspace = PMONITOR->m_activeWorkspace; - } else { - Log::logger->log(Log::DEBUG, "Toggling special workspace {} to open", workspaceID); - auto PSPECIALWORKSPACE = g_pCompositor->getWorkspaceByID(workspaceID); - - if (!PSPECIALWORKSPACE) - PSPECIALWORKSPACE = g_pCompositor->createNewWorkspace(workspaceID, PMONITOR->m_id, workspaceName); - - PMONITOR->setSpecialWorkspace(PSPECIALWORKSPACE); - - focusedWorkspace = PSPECIALWORKSPACE; - } - - const static auto PWARPONTOGGLESPECIAL = CConfigValue("cursor:warp_on_toggle_special"); - - if (*PWARPONTOGGLESPECIAL > 0) { - auto PLAST = focusedWorkspace->getLastFocusedWindow(); - auto HLSurface = Desktop::View::CWLSurface::fromResource(g_pSeatManager->m_state.pointerFocus.lock()); - - if (PLAST && (!HLSurface || HLSurface->view()->type() == Desktop::View::VIEW_TYPE_WINDOW)) - PLAST->warpCursor(*PWARPONTOGGLESPECIAL == 2); - } - - return {}; -} - -SDispatchResult CKeybindManager::forceRendererReload(std::string args) { - bool overAgain = false; - - for (auto const& m : g_pCompositor->m_monitors) { - if (!m->m_output) - continue; - - auto rule = Config::monitorRuleMgr()->get(m); - if (!m->applyMonitorRule(std::move(rule), true)) { - overAgain = true; - break; - } - } - - if (overAgain) - forceRendererReload(args); - - return {}; -} - -SDispatchResult CKeybindManager::resizeActive(std::string args) { - const auto PLASTWINDOW = Desktop::focusState()->window(); - - if (!PLASTWINDOW) - return {.success = false, .error = "No window found"}; - - if (PLASTWINDOW->isFullscreen()) - return {.success = false, .error = "Window is fullscreen"}; - - const auto SIZ = g_pCompositor->parseWindowVectorArgsRelative(args, PLASTWINDOW->m_realSize->goal()); - - if (SIZ.x < 1 || SIZ.y < 1) - return {.success = false, .error = "Invalid size provided"}; - - g_layoutManager->resizeTarget(SIZ - PLASTWINDOW->m_realSize->goal(), PLASTWINDOW->layoutTarget()); - - if (PLASTWINDOW->m_realSize->goal().x > 1 && PLASTWINDOW->m_realSize->goal().y > 1) - PLASTWINDOW->setHidden(false); - - return {}; -} - -SDispatchResult CKeybindManager::moveActive(std::string args) { - const auto PLASTWINDOW = Desktop::focusState()->window(); - - if (!PLASTWINDOW) - return {.success = false, .error = "No window found"}; - - if (PLASTWINDOW->isFullscreen()) - return {.success = false, .error = "Window is fullscreen"}; - - const auto POS = g_pCompositor->parseWindowVectorArgsRelative(args, PLASTWINDOW->m_realPosition->goal()); - - g_layoutManager->moveTarget(POS - PLASTWINDOW->m_realPosition->goal(), PLASTWINDOW->layoutTarget()); - - return {}; -} - -SDispatchResult CKeybindManager::moveWindow(std::string args) { - - const auto WINDOWREGEX = args.substr(args.find_first_of(',') + 1); - const auto MOVECMD = args.substr(0, args.find_first_of(',')); - - const auto PWINDOW = g_pCompositor->getWindowByRegex(WINDOWREGEX); - - if (!PWINDOW) { - Log::logger->log(Log::ERR, "moveWindow: no window"); - return {.success = false, .error = "moveWindow: no window"}; - } - - if (PWINDOW->isFullscreen()) - return {.success = false, .error = "Window is fullscreen"}; - - const auto POS = g_pCompositor->parseWindowVectorArgsRelative(MOVECMD, PWINDOW->m_realPosition->goal()); - - g_layoutManager->moveTarget(POS - PWINDOW->m_realPosition->goal(), PWINDOW->layoutTarget()); - - return {}; -} - -SDispatchResult CKeybindManager::resizeWindow(std::string args) { - - const auto WINDOWREGEX = args.substr(args.find_first_of(',') + 1); - const auto MOVECMD = args.substr(0, args.find_first_of(',')); - - const auto PWINDOW = g_pCompositor->getWindowByRegex(WINDOWREGEX); - - if (!PWINDOW) { - Log::logger->log(Log::ERR, "resizeWindow: no window"); - return {.success = false, .error = "resizeWindow: no window"}; - } - - if (PWINDOW->isFullscreen()) - return {.success = false, .error = "Window is fullscreen"}; - - const auto SIZ = g_pCompositor->parseWindowVectorArgsRelative(MOVECMD, PWINDOW->m_realSize->goal()); - - if (SIZ.x < 1 || SIZ.y < 1) - return {.success = false, .error = "Invalid size provided"}; - - g_layoutManager->resizeTarget(SIZ - PWINDOW->m_realSize->goal(), PWINDOW->layoutTarget(), Layout::CORNER_NONE); - - if (PWINDOW->m_realSize->goal().x > 1 && PWINDOW->m_realSize->goal().y > 1) - PWINDOW->setHidden(false); - - return {}; -} - -SDispatchResult CKeybindManager::circleNext(std::string arg) { - if (!Desktop::focusState()->window()) { - // if we have a clear focus, find the first window and get the next focusable. - const auto PWS = Desktop::focusState()->monitor()->m_activeWorkspace; - if (PWS && PWS->getWindows() > 0) { - const auto PWINDOW = PWS->getFirstWindow(); - switchToWindow(PWINDOW); - } - - return {}; - } - - CVarList args{arg, 0, 's', true}; - - const auto PREV = args.contains("prev") || args.contains("p") || args.contains("last") || args.contains("l"); - - std::optional floatStatus = {}; - if (args.contains("tile") || args.contains("tiled")) { - // if we want just tiled, and we are on a tiled window, use layoutmsg for layouts that support it - - if (!Desktop::focusState()->window()->m_isFloating) { - if (const auto SPACE = Desktop::focusState()->window()->layoutTarget()->space(); SPACE) { - - constexpr const std::array LAYOUTS_WITH_CYCLE_NEXT = { - &typeid(Layout::Tiled::CMonocleAlgorithm), - &typeid(Layout::Tiled::CMasterAlgorithm), - }; - - if (std::ranges::contains(LAYOUTS_WITH_CYCLE_NEXT, &typeid(*SPACE->algorithm()->tiledAlgo().get()))) { - CKeybindManager::layoutmsg(PREV ? "cyclenext, b" : "cyclenext"); - return {}; - } - } - } - } - - if (args.contains("float") || args.contains("floating")) - floatStatus = true; - - const auto VISIBLE = args.contains("visible") || args.contains("v"); - const auto NEXT = args.contains("next") || args.contains("n"); // prev is default in classic alt+tab - const auto HIST = args.contains("hist") || args.contains("h"); - const auto& w = HIST ? g_pCompositor->getWindowCycleHist(Desktop::focusState()->window(), true, floatStatus, VISIBLE, NEXT) : - g_pCompositor->getWindowCycle(Desktop::focusState()->window(), true, floatStatus, VISIBLE, PREV); - - switchToWindow(w, HIST); - - return {}; -} - -SDispatchResult CKeybindManager::focusWindow(std::string regexp) { - const auto PWINDOW = g_pCompositor->getWindowByRegex(regexp); - - if (!PWINDOW) - return {.success = false, .error = "No such window found"}; - - Log::logger->log(Log::DEBUG, "Focusing to window name: {}", PWINDOW->m_title); - - const auto PWORKSPACE = PWINDOW->m_workspace; - if (!PWORKSPACE) { - Log::logger->log(Log::ERR, "BUG THIS: null workspace in focusWindow"); - return {.success = false, .error = "BUG THIS: null workspace in focusWindow"}; - } - - updateRelativeCursorCoords(); - - if (Desktop::focusState()->monitor() && Desktop::focusState()->monitor()->m_activeWorkspace != PWINDOW->m_workspace && - Desktop::focusState()->monitor()->m_activeSpecialWorkspace != PWINDOW->m_workspace) { - Log::logger->log(Log::DEBUG, "Fake executing workspace to move focus"); - changeworkspace(PWORKSPACE->getConfigName()); - } - - Desktop::focusState()->fullWindowFocus(PWINDOW, Desktop::FOCUS_REASON_KEYBIND, nullptr, false); - - PWINDOW->warpCursor(); - - return {}; -} - -SDispatchResult CKeybindManager::tagWindow(std::string args) { - PHLWINDOW PWINDOW = nullptr; - CVarList vars{args, 0, 's', true}; - - if (vars.size() == 1) - PWINDOW = Desktop::focusState()->window(); - else if (vars.size() == 2) - PWINDOW = g_pCompositor->getWindowByRegex(vars[1]); - else - return {.success = false, .error = "Invalid number of arguments, expected 1 or 2 arguments"}; - - if (PWINDOW && PWINDOW->m_ruleApplicator->m_tagKeeper.applyTag(vars[0])) { - PWINDOW->m_ruleApplicator->propertiesChanged(Desktop::Rule::RULE_PROP_TAG); - PWINDOW->updateDecorationValues(); - } - - return {}; -} - -SDispatchResult CKeybindManager::toggleSwallow(std::string args) { - PHLWINDOWREF pWindow = Desktop::focusState()->window(); - - if (!valid(pWindow) || !valid(pWindow->m_swallowed)) - return {}; - - if (pWindow->m_swallowed->m_currentlySwallowed) { - // Unswallow - pWindow->m_swallowed->m_currentlySwallowed = false; - pWindow->m_swallowed->setHidden(false); - g_layoutManager->newTarget(pWindow->m_swallowed->layoutTarget(), pWindow->m_workspace->m_space); - } else { - // Reswallow - pWindow->m_swallowed->m_currentlySwallowed = true; - pWindow->m_swallowed->setHidden(true); - g_layoutManager->removeTarget(pWindow->m_swallowed->layoutTarget()); - } - - return {}; -} - -SDispatchResult CKeybindManager::setSubmap(std::string submap) { - if (submap == "reset" || submap.empty()) { - m_currentSelectedSubmap.name = ""; - Log::logger->log(Log::DEBUG, "Reset active submap to the default one."); - g_pEventManager->postEvent(SHyprIPCEvent{"submap", ""}); - Event::bus()->m_events.keybinds.submap.emit(m_currentSelectedSubmap.name); - return {}; - } - - for (const auto& k : g_pKeybindManager->m_keybinds) { - if (k->submap.name == submap) { - m_currentSelectedSubmap.name = submap; - Log::logger->log(Log::DEBUG, "Changed keybind submap to {}", submap); - g_pEventManager->postEvent(SHyprIPCEvent{"submap", submap}); - Event::bus()->m_events.keybinds.submap.emit(m_currentSelectedSubmap.name); - return {}; - } - } - - Log::logger->log(Log::ERR, "Cannot set submap {}, submap doesn't exist (wasn't registered!)", submap); - return {.success = false, .error = std::format("Cannot set submap {}, submap doesn't exist (wasn't registered!)", submap)}; -} - -SDispatchResult CKeybindManager::pass(std::string regexp) { - - // find the first window passing the regex - const auto PWINDOW = g_pCompositor->getWindowByRegex(regexp); - - if (!PWINDOW) { - Log::logger->log(Log::ERR, "pass: window not found"); - return {.success = false, .error = "pass: window not found"}; - } - - if (!g_pSeatManager->m_keyboard) { - Log::logger->log(Log::ERR, "No kb in pass?"); - return {.success = false, .error = "No kb in pass?"}; - } - - const auto XWTOXW = PWINDOW->m_isX11 && Desktop::focusState()->window() && Desktop::focusState()->window()->m_isX11; - const auto LASTMOUSESURF = g_pSeatManager->m_state.pointerFocus.lock(); - const auto LASTKBSURF = g_pSeatManager->m_state.keyboardFocus.lock(); - - // pass all mf shit - if (!XWTOXW) { - if (g_pKeybindManager->m_lastCode != 0) - g_pSeatManager->setKeyboardFocus(PWINDOW->wlSurface()->resource()); - else - g_pSeatManager->setPointerFocus(PWINDOW->wlSurface()->resource(), {1, 1}); - } - - g_pSeatManager->sendKeyboardMods(g_pInputManager->getModsFromAllKBs(), 0, 0, 0); - - if (g_pKeybindManager->m_passPressed == 1) { - if (g_pKeybindManager->m_lastCode != 0) - g_pSeatManager->sendKeyboardKey(g_pKeybindManager->m_timeLastMs, g_pKeybindManager->m_lastCode - 8, WL_KEYBOARD_KEY_STATE_PRESSED); - else - g_pSeatManager->sendPointerButton(g_pKeybindManager->m_timeLastMs, g_pKeybindManager->m_lastMouseCode, WL_POINTER_BUTTON_STATE_PRESSED); - } else if (g_pKeybindManager->m_passPressed == 0) - if (g_pKeybindManager->m_lastCode != 0) - g_pSeatManager->sendKeyboardKey(g_pKeybindManager->m_timeLastMs, g_pKeybindManager->m_lastCode - 8, WL_KEYBOARD_KEY_STATE_RELEASED); - else - g_pSeatManager->sendPointerButton(g_pKeybindManager->m_timeLastMs, g_pKeybindManager->m_lastMouseCode, WL_POINTER_BUTTON_STATE_RELEASED); - else { - // dynamic call of the dispatcher - if (g_pKeybindManager->m_lastCode != 0) { - g_pSeatManager->sendKeyboardKey(g_pKeybindManager->m_timeLastMs, g_pKeybindManager->m_lastCode - 8, WL_KEYBOARD_KEY_STATE_PRESSED); - g_pSeatManager->sendKeyboardKey(g_pKeybindManager->m_timeLastMs, g_pKeybindManager->m_lastCode - 8, WL_KEYBOARD_KEY_STATE_RELEASED); - } else { - g_pSeatManager->sendPointerButton(g_pKeybindManager->m_timeLastMs, g_pKeybindManager->m_lastMouseCode, WL_POINTER_BUTTON_STATE_PRESSED); - g_pSeatManager->sendPointerButton(g_pKeybindManager->m_timeLastMs, g_pKeybindManager->m_lastMouseCode, WL_POINTER_BUTTON_STATE_RELEASED); - } - } - - if (XWTOXW) - return {}; - - // Massive hack: - // this will make g_pSeatManager NOT send the leave event to XWayland apps, provided we are not on an XWayland window already. - // please kill me - if (PWINDOW->m_isX11) { - if (g_pKeybindManager->m_lastCode != 0) { - g_pSeatManager->m_state.keyboardFocus.reset(); - g_pSeatManager->m_state.keyboardFocusResource.reset(); - } else { - g_pSeatManager->m_state.pointerFocus.reset(); - g_pSeatManager->m_state.pointerFocusResource.reset(); - } - } - - const auto SL = PWINDOW->m_realPosition->goal() - g_pInputManager->getMouseCoordsInternal(); - - if (g_pKeybindManager->m_lastCode != 0) - g_pSeatManager->setKeyboardFocus(LASTKBSURF); - else - g_pSeatManager->setPointerFocus(LASTMOUSESURF, SL); - - return {}; -} - -SDispatchResult CKeybindManager::sendshortcut(std::string args) { - // args=[,WINDOW_RULES] - const auto ARGS = CVarList(args, 3); - if (ARGS.size() != 3) { - Log::logger->log(Log::ERR, "sendshortcut: invalid args"); - return {.success = false, .error = "sendshortcut: invalid args"}; - } - - const auto MOD = g_pKeybindManager->stringToModMask(ARGS[0]); - const auto KEY = ARGS[1]; - uint32_t keycode = 0; - bool isMouse = false; - - // similar to parseKey in ConfigManager - if (isNumber(KEY) && std::stoi(KEY) > 9) - keycode = std::stoi(KEY); - else if (KEY.compare(0, 5, "code:") == 0 && isNumber(KEY.substr(5))) - keycode = std::stoi(KEY.substr(5)); - else if (KEY.compare(0, 6, "mouse:") == 0 && isNumber(KEY.substr(6))) { - keycode = std::stoi(KEY.substr(6)); - isMouse = true; - if (keycode < 272) { - Log::logger->log(Log::ERR, "sendshortcut: invalid mouse button"); - return {.success = false, .error = "sendshortcut: invalid mouse button"}; - } - } else { - - // here, we need to find the keycode from the key name - // this is not possible through xkb's lib, so we need to iterate through all keycodes - // once found, we save it to the cache - - const auto KEYSYM = xkb_keysym_from_name(KEY.c_str(), XKB_KEYSYM_CASE_INSENSITIVE); - keycode = 0; - - const auto KB = g_pSeatManager->m_keyboard; - - if (!KB) { - Log::logger->log(Log::ERR, "sendshortcut: no kb"); - return {.success = false, .error = "sendshortcut: no kb"}; - } - - const auto KEYPAIRSTRING = std::format("{}{}", rc(KB.get()), KEY); - - if (!g_pKeybindManager->m_keyToCodeCache.contains(KEYPAIRSTRING)) { - xkb_keymap* km = KB->m_xkbKeymap; - xkb_state* ks = KB->m_xkbState; - - xkb_keycode_t keycode_min, keycode_max; - keycode_min = xkb_keymap_min_keycode(km); - keycode_max = xkb_keymap_max_keycode(km); - - for (xkb_keycode_t kc = keycode_min; kc <= keycode_max; ++kc) { - xkb_keysym_t sym = xkb_state_key_get_one_sym(ks, kc); - - if (sym == KEYSYM) { - keycode = kc; - g_pKeybindManager->m_keyToCodeCache[KEYPAIRSTRING] = keycode; - } - } - - if (!keycode) { - Log::logger->log(Log::ERR, "sendshortcut: key not found"); - return {.success = false, .error = "sendshortcut: key not found"}; - } - - } else - keycode = g_pKeybindManager->m_keyToCodeCache[KEYPAIRSTRING]; - } - - if (!keycode) { - Log::logger->log(Log::ERR, "sendshortcut: invalid key"); - return {.success = false, .error = "sendshortcut: invalid key"}; - } - - const std::string regexp = ARGS[2]; - PHLWINDOW PWINDOW = nullptr; - const auto LASTSURFACE = Desktop::focusState()->surface(); - - //if regexp is not empty, send shortcut to current window - //else, don't change focus - if (!regexp.empty()) { - PWINDOW = g_pCompositor->getWindowByRegex(regexp); - - if (!PWINDOW) { - Log::logger->log(Log::ERR, "sendshortcut: window not found"); - return {.success = false, .error = "sendshortcut: window not found"}; - } - - if (!g_pSeatManager->m_keyboard) { - Log::logger->log(Log::ERR, "No kb in sendshortcut?"); - return {.success = false, .error = "No kb in sendshortcut?"}; - } - - if (!isMouse) - g_pSeatManager->setKeyboardFocus(PWINDOW->wlSurface()->resource()); - else - g_pSeatManager->setPointerFocus(PWINDOW->wlSurface()->resource(), {1, 1}); - } - - //copied the rest from pass and modified it - // if wl -> xwl, activate destination - if (PWINDOW && PWINDOW->m_isX11 && Desktop::focusState()->window() && !Desktop::focusState()->window()->m_isX11) - g_pXWaylandManager->activateSurface(PWINDOW->wlSurface()->resource(), true); - // if xwl -> xwl, send to current. Timing issues make this not work. - if (PWINDOW && PWINDOW->m_isX11 && Desktop::focusState()->window() && Desktop::focusState()->window()->m_isX11) - PWINDOW = nullptr; - - g_pSeatManager->sendKeyboardMods(MOD, 0, 0, 0); - - if (g_pKeybindManager->m_passPressed == 1) { - if (!isMouse) - g_pSeatManager->sendKeyboardKey(g_pKeybindManager->m_timeLastMs, keycode - 8, WL_KEYBOARD_KEY_STATE_PRESSED); - else - g_pSeatManager->sendPointerButton(g_pKeybindManager->m_timeLastMs, keycode, WL_POINTER_BUTTON_STATE_PRESSED); - } else if (g_pKeybindManager->m_passPressed == 0) { - if (!isMouse) - g_pSeatManager->sendKeyboardKey(g_pKeybindManager->m_timeLastMs, keycode - 8, WL_KEYBOARD_KEY_STATE_RELEASED); - else - g_pSeatManager->sendPointerButton(g_pKeybindManager->m_timeLastMs, keycode, WL_POINTER_BUTTON_STATE_RELEASED); - } else { - // dynamic call of the dispatcher - if (!isMouse) { - g_pSeatManager->sendKeyboardKey(g_pKeybindManager->m_timeLastMs, keycode - 8, WL_KEYBOARD_KEY_STATE_PRESSED); - g_pSeatManager->sendKeyboardKey(g_pKeybindManager->m_timeLastMs, keycode - 8, WL_KEYBOARD_KEY_STATE_RELEASED); - } else { - g_pSeatManager->sendPointerButton(g_pKeybindManager->m_timeLastMs, keycode, WL_POINTER_BUTTON_STATE_PRESSED); - g_pSeatManager->sendPointerButton(g_pKeybindManager->m_timeLastMs, keycode, WL_POINTER_BUTTON_STATE_RELEASED); - } - } - - g_pSeatManager->sendKeyboardMods(0, 0, 0, 0); - - if (!PWINDOW) - return {}; - - if (PWINDOW->m_isX11) { //xwayland hack, see pass - if (!isMouse) { - g_pSeatManager->m_state.keyboardFocus.reset(); - g_pSeatManager->m_state.keyboardFocusResource.reset(); - } else { - g_pSeatManager->m_state.pointerFocus.reset(); - g_pSeatManager->m_state.pointerFocusResource.reset(); - } - } - - const auto SL = PWINDOW->m_realPosition->goal() - g_pInputManager->getMouseCoordsInternal(); - - if (!isMouse) - g_pSeatManager->setKeyboardFocus(LASTSURFACE); - else - g_pSeatManager->setPointerFocus(LASTSURFACE, SL); - - return {}; -} - -SDispatchResult CKeybindManager::layoutmsg(std::string msg) { - auto ret = g_layoutManager->layoutMsg(msg); - if (!ret) - return {.success = false, .error = ret.error()}; - return {}; -} - -SDispatchResult CKeybindManager::dpms(std::string arg) { - SDispatchResult res; - bool enable = arg.starts_with("on"); - std::string port = ""; - - bool isToggle = arg.starts_with("toggle"); - if (arg.find_first_of(' ') != std::string::npos) - port = arg.substr(arg.find_first_of(' ') + 1); - - for (auto const& m : g_pCompositor->m_realMonitors) { - if (!m->m_enabled) - continue; - - if (!port.empty() && m->m_name != port) - continue; - - if (isToggle) - enable = !m->m_dpmsStatus; - - m->setDPMS(enable); - } - - g_pCompositor->m_dpmsStateOn = enable; - - g_pPointerManager->recheckEnteredOutputs(); - - return res; -} - -SDispatchResult CKeybindManager::swapnext(std::string arg) { - - PHLWINDOW toSwap = nullptr; - - if (!Desktop::focusState()->window()) - return {}; - - const auto PLASTWINDOW = Desktop::focusState()->window(); - - const auto PLASTCYCLED = - validMapped(Desktop::focusState()->window()->m_lastCycledWindow) && Desktop::focusState()->window()->m_lastCycledWindow->m_workspace == PLASTWINDOW->m_workspace ? - Desktop::focusState()->window()->m_lastCycledWindow.lock() : - nullptr; - - const bool NEED_PREV = arg == "last" || arg == "l" || arg == "prev" || arg == "p"; - toSwap = g_pCompositor->getWindowCycle(PLASTCYCLED ? PLASTCYCLED : PLASTWINDOW, true, std::nullopt, false, NEED_PREV); - - // sometimes we may come back to ourselves. - if (toSwap == PLASTWINDOW) - toSwap = g_pCompositor->getWindowCycle(PLASTWINDOW, true, std::nullopt, false, NEED_PREV); - - g_layoutManager->switchTargets(PLASTWINDOW->layoutTarget(), toSwap->layoutTarget(), false); - - PLASTWINDOW->m_lastCycledWindow = toSwap; - - Desktop::focusState()->fullWindowFocus(PLASTWINDOW, Desktop::FOCUS_REASON_KEYBIND); - - return {}; -} - -SDispatchResult CKeybindManager::swapActiveWorkspaces(std::string args) { - const auto MON1 = args.substr(0, args.find_first_of(' ')); - const auto MON2 = args.substr(args.find_first_of(' ') + 1); - - const auto PMON1 = g_pCompositor->getMonitorFromString(MON1); - const auto PMON2 = g_pCompositor->getMonitorFromString(MON2); - - if (!PMON1 || !PMON2) - return {.success = false, .error = "No such monitor found"}; - - if (PMON1 == PMON2) - return {}; - - g_pCompositor->swapActiveWorkspaces(PMON1, PMON2); - - return {}; -} - -SDispatchResult CKeybindManager::pinActive(std::string args) { - - PHLWINDOW PWINDOW = nullptr; - - if (args != "active" && args.length() > 1) - PWINDOW = g_pCompositor->getWindowByRegex(args); - else - PWINDOW = Desktop::focusState()->window(); - - if (!PWINDOW) { - Log::logger->log(Log::ERR, "pin: window not found"); - return {.success = false, .error = "pin: window not found"}; - } - - if (!PWINDOW->m_isFloating || PWINDOW->isFullscreen()) - return {.success = false, .error = "Window does not qualify to be pinned"}; - - PWINDOW->m_pinned = !PWINDOW->m_pinned; - - const auto PMONITOR = PWINDOW->m_monitor.lock(); - - if (!PMONITOR) { - Log::logger->log(Log::ERR, "pin: monitor not found"); - return {.success = false, .error = "pin: window not found"}; - } - - PWINDOW->layoutTarget()->assignToSpace(PMONITOR->m_activeWorkspace->m_space); - - PWINDOW->m_ruleApplicator->propertiesChanged(Desktop::Rule::RULE_PROP_PINNED); - - const auto PWORKSPACE = PWINDOW->m_workspace; - - PWORKSPACE->m_lastFocusedWindow = - g_pCompositor->vectorToWindowUnified(g_pInputManager->getMouseCoordsInternal(), Desktop::View::RESERVED_EXTENTS | Desktop::View::INPUT_EXTENTS); - - g_pEventManager->postEvent(SHyprIPCEvent{"pin", std::format("{:x},{}", rc(PWINDOW.get()), sc(PWINDOW->m_pinned))}); - Event::bus()->m_events.window.pin.emit(PWINDOW); - - g_pHyprRenderer->damageWindow(PWINDOW, true); - - return {}; -} - -SDispatchResult CKeybindManager::mouse(std::string args) { - const auto ARGS = CVarList(args.substr(1), 2, ' '); - const auto PRESSED = args[0] == '1'; - - if (!PRESSED) { - return changeMouseBindMode(MBIND_INVALID); - } - - if (ARGS[0] == "movewindow") { - return changeMouseBindMode(MBIND_MOVE); - } else { - try { - switch (std::stoi(ARGS[1])) { - case 1: return changeMouseBindMode(MBIND_RESIZE_FORCE_RATIO); break; - case 2: return changeMouseBindMode(MBIND_RESIZE_BLOCK_RATIO); break; - default: return changeMouseBindMode(MBIND_RESIZE); - } - } catch (std::exception& e) { return changeMouseBindMode(MBIND_RESIZE); } - } -} - SDispatchResult CKeybindManager::changeMouseBindMode(const eMouseBindMode MODE) { if (MODE != MBIND_INVALID) { if (g_layoutManager->dragController()->target()) @@ -2545,601 +874,3 @@ SDispatchResult CKeybindManager::changeMouseBindMode(const eMouseBindMode MODE) return {}; } - -SDispatchResult CKeybindManager::bringActiveToTop(std::string args) { - if (Desktop::focusState()->window() && Desktop::focusState()->window()->m_isFloating) - g_pCompositor->changeWindowZOrder(Desktop::focusState()->window(), true); - - g_pInputManager->simulateMouseMovement(); - - return {}; -} - -SDispatchResult CKeybindManager::alterZOrder(std::string args) { - const auto WINDOWREGEX = args.substr(args.find_first_of(',') + 1); - const auto POSITION = args.substr(0, args.find_first_of(',')); - auto PWINDOW = g_pCompositor->getWindowByRegex(WINDOWREGEX); - - if (!PWINDOW && Desktop::focusState()->window() && Desktop::focusState()->window()->m_isFloating) - PWINDOW = Desktop::focusState()->window(); - - if (!PWINDOW) { - Log::logger->log(Log::ERR, "alterZOrder: no window"); - return {.success = false, .error = "alterZOrder: no window"}; - } - - if (POSITION == "top") - g_pCompositor->changeWindowZOrder(PWINDOW, true); - else if (POSITION == "bottom") - g_pCompositor->changeWindowZOrder(PWINDOW, false); - else { - Log::logger->log(Log::ERR, "alterZOrder: bad position: {}", POSITION); - return {.success = false, .error = "alterZOrder: bad position: {}"}; - } - - g_pInputManager->simulateMouseMovement(); - - return {}; -} - -SDispatchResult CKeybindManager::lockGroups(std::string args) { - if (args == "lock" || args.empty() || args == "lockgroups") - g_pKeybindManager->m_groupsLocked = true; - else if (args == "toggle") - g_pKeybindManager->m_groupsLocked = !g_pKeybindManager->m_groupsLocked; - else - g_pKeybindManager->m_groupsLocked = false; - - g_pEventManager->postEvent(SHyprIPCEvent{"lockgroups", g_pKeybindManager->m_groupsLocked ? "1" : "0"}); - g_pCompositor->updateAllWindowsAnimatedDecorationValues(); - - return {}; -} - -SDispatchResult CKeybindManager::lockActiveGroup(std::string args) { - const auto PWINDOW = Desktop::focusState()->window(); - - if (!PWINDOW) - return {.success = false, .error = "No window found"}; - - if (!PWINDOW->m_group) - return {.success = false, .error = "Not a group"}; - - if (args == "lock") - PWINDOW->m_group->setLocked(true); - else if (args == "toggle") - PWINDOW->m_group->setLocked(!PWINDOW->m_group->locked()); - else - PWINDOW->m_group->setLocked(false); - - PWINDOW->updateDecorationValues(); - - return {}; -} - -void CKeybindManager::moveWindowIntoGroup(PHLWINDOW pWindow, PHLWINDOW pWindowInDirection) { - if (!pWindowInDirection->m_group || pWindowInDirection->m_group->denied()) - return; - - updateRelativeCursorCoords(); - - if (pWindow->m_monitor != pWindowInDirection->m_monitor) { - pWindow->moveToWorkspace(pWindowInDirection->m_workspace); - pWindow->m_monitor = pWindowInDirection->m_monitor; - } - - if (pWindow->m_group) - pWindow->m_group->remove(pWindow); - pWindowInDirection->m_group->add(pWindow); - - pWindowInDirection->m_group->setCurrent(pWindow); - pWindow->updateWindowDecos(); - Desktop::focusState()->fullWindowFocus(pWindow, Desktop::FOCUS_REASON_KEYBIND); - pWindow->warpCursor(); - - g_pEventManager->postEvent(SHyprIPCEvent{"moveintogroup", std::format("{:x}", rc(pWindow.get()))}); -} - -void CKeybindManager::moveWindowOutOfGroup(PHLWINDOW pWindow, const std::string& dir) { - static auto BFOCUSREMOVEDWINDOW = CConfigValue("group:focus_removed_window"); - - if (!pWindow->m_group) - return; - - WP group = pWindow->m_group; - - const auto direction = !dir.empty() ? Math::fromChar(dir[0]) : Math::DIRECTION_DEFAULT; - - pWindow->m_group->remove(pWindow, direction); - - if (*BFOCUSREMOVEDWINDOW || !group) { - Desktop::focusState()->fullWindowFocus(pWindow, Desktop::FOCUS_REASON_KEYBIND); - pWindow->warpCursor(); - } else { - Desktop::focusState()->fullWindowFocus(group->current(), Desktop::FOCUS_REASON_KEYBIND); - group->current()->warpCursor(); - } - - g_pEventManager->postEvent(SHyprIPCEvent{"moveoutofgroup", std::format("{:x}", rc(pWindow.get()))}); -} - -SDispatchResult CKeybindManager::moveIntoGroup(std::string args) { - static auto PIGNOREGROUPLOCK = CConfigValue("binds:ignore_group_lock"); - - if (!*PIGNOREGROUPLOCK && g_pKeybindManager->m_groupsLocked) - return {}; - - Math::eDirection dir = Math::fromChar(args[0]); - if (dir == Math::DIRECTION_DEFAULT) { - Log::logger->log(Log::ERR, "Cannot move into group in direction {}, unsupported direction. Supported: l,r,u/t,d/b", args[0]); - return {.success = false, .error = std::format("Cannot move into group in direction {}, unsupported direction. Supported: l,r,u/t,d/b", args[0])}; - } - - const auto PWINDOW = Desktop::focusState()->window(); - - if (!PWINDOW) - return {}; - - auto PWINDOWINDIR = g_pCompositor->getWindowInDirection(PWINDOW, dir); - - if (!PWINDOWINDIR || !PWINDOWINDIR->m_group) - return {}; - - const auto GROUP = PWINDOWINDIR->m_group; - - // Do not move window into locked group if binds:ignore_group_lock is false - if (!*PIGNOREGROUPLOCK && (GROUP->locked() || (PWINDOW->m_group && PWINDOW->m_group->locked()))) - return {}; - - moveWindowIntoGroup(PWINDOW, PWINDOWINDIR); - - return {}; -} - -SDispatchResult CKeybindManager::moveIntoOrCreateGroup(std::string args) { - static auto PIGNOREGROUPLOCK = CConfigValue("binds:ignore_group_lock"); - - if (!*PIGNOREGROUPLOCK && g_pKeybindManager->m_groupsLocked) - return {}; - - Math::eDirection dir = Math::fromChar(args[0]); - if (dir == Math::DIRECTION_DEFAULT) { - Log::logger->log(Log::ERR, "Cannot move into or create group in direction {}, unsupported direction. Supported: l,r,u/t,d/b", args[0]); - return {.success = false, .error = std::format("Cannot move into or create group in direction {}, unsupported direction. Supported: l,r,u/t,d/b", args[0])}; - } - - const auto PWINDOW = Desktop::focusState()->window(); - - if (!PWINDOW) - return {}; - - auto PWINDOWINDIR = g_pCompositor->getWindowInDirection(PWINDOW, dir); - - if (!PWINDOWINDIR) - return {}; - - if (!PWINDOWINDIR->m_group) { - if (PWINDOWINDIR->isFullscreen()) - return {}; - - PWINDOWINDIR->m_group = Desktop::View::CGroup::create({PWINDOWINDIR}); - } - - const auto GROUP = PWINDOWINDIR->m_group; - - if (!*PIGNOREGROUPLOCK && (GROUP->locked() || (PWINDOW->m_group && PWINDOW->m_group->locked()))) - return {}; - - moveWindowIntoGroup(PWINDOW, PWINDOWINDIR); - - return {}; -} - -SDispatchResult CKeybindManager::moveOutOfGroup(std::string args) { - static auto PIGNOREGROUPLOCK = CConfigValue("binds:ignore_group_lock"); - - if (!*PIGNOREGROUPLOCK && g_pKeybindManager->m_groupsLocked) - return {.success = false, .error = "Groups locked"}; - - PHLWINDOW PWINDOW = nullptr; - - if (args != "active" && args.length() > 1) - PWINDOW = g_pCompositor->getWindowByRegex(args); - else - PWINDOW = Desktop::focusState()->window(); - - if (!PWINDOW) - return {.success = false, .error = "No window found"}; - - if (!PWINDOW->m_group) - return {.success = false, .error = "Window not in a group"}; - - moveWindowOutOfGroup(PWINDOW); - - return {}; -} - -SDispatchResult CKeybindManager::moveWindowOrGroup(std::string args) { - static auto PIGNOREGROUPLOCK = CConfigValue("binds:ignore_group_lock"); - - Math::eDirection dir = Math::fromChar(args[0]); - if (dir == Math::DIRECTION_DEFAULT) { - Log::logger->log(Log::ERR, "Cannot move into group in direction {}, unsupported direction. Supported: l,r,u/t,d/b", args[0]); - return {.success = false, .error = std::format("Cannot move into group in direction {}, unsupported direction. Supported: l,r,u/t,d/b", args[0])}; - } - - const auto PWINDOW = Desktop::focusState()->window(); - if (!PWINDOW) - return {.success = false, .error = "No window found"}; - - if (PWINDOW->isFullscreen()) - return {}; - - if (!*PIGNOREGROUPLOCK && g_pKeybindManager->m_groupsLocked) { - g_layoutManager->moveInDirection(PWINDOW->layoutTarget(), args); - return {}; - } - - const auto PWINDOWINDIR = g_pCompositor->getWindowInDirection(PWINDOW, dir); - - const bool ISWINDOWGROUP = PWINDOW->m_group; - const bool ISWINDOWGROUPLOCKED = ISWINDOWGROUP && PWINDOW->m_group->locked(); - const bool ISWINDOWGROUPSINGLE = ISWINDOWGROUP && PWINDOW->m_group->size() == 1; - const bool ISWINDOWGROUPDENIED = ISWINDOWGROUP && PWINDOW->m_group->denied(); - - updateRelativeCursorCoords(); - - // note: PWINDOWINDIR is not null implies !PWINDOW->m_isFloating - if (PWINDOWINDIR && PWINDOWINDIR->m_group) { // target is group - if (!*PIGNOREGROUPLOCK && (PWINDOWINDIR->m_group->locked() || ISWINDOWGROUPLOCKED || ISWINDOWGROUPDENIED)) { - g_layoutManager->moveInDirection(PWINDOW->layoutTarget(), args); - PWINDOW->warpCursor(); - } else - moveWindowIntoGroup(PWINDOW, PWINDOWINDIR); - } else if (PWINDOWINDIR) { // target is regular window - if ((!*PIGNOREGROUPLOCK && ISWINDOWGROUPLOCKED) || !ISWINDOWGROUP || (ISWINDOWGROUPSINGLE && PWINDOW->m_groupRules & Desktop::View::GROUP_SET_ALWAYS)) { - g_layoutManager->moveInDirection(PWINDOW->layoutTarget(), args); - PWINDOW->warpCursor(); - } else - moveWindowOutOfGroup(PWINDOW, args); - } else if ((*PIGNOREGROUPLOCK || !ISWINDOWGROUPLOCKED) && ISWINDOWGROUP) { // no target window - moveWindowOutOfGroup(PWINDOW, args); - } else if (!PWINDOWINDIR && !ISWINDOWGROUP) { // no target in dir and not in group - g_layoutManager->moveInDirection(PWINDOW->layoutTarget(), args); - PWINDOW->warpCursor(); - } - - PWINDOW->updateDecorationValues(); - - return {}; -} - -SDispatchResult CKeybindManager::setIgnoreGroupLock(std::string args) { - // FIXME: this is no longer possible like this. It's redundant anyways. Can be easily scripted / lua'd - // static auto PIGNOREGROUPLOCK = CConfigValue("binds:ignore_group_lock"); - - // if (args == "toggle") - // *PIGNOREGROUPLOCK = !*PIGNOREGROUPLOCK; - // else - // *PIGNOREGROUPLOCK = args == "on"; - - // g_pEventManager->postEvent(SHyprIPCEvent{"ignoregrouplock", std::to_string(**PIGNOREGROUPLOCK)}); - - return {}; -} - -SDispatchResult CKeybindManager::denyWindowFromGroup(std::string args) { - const auto PWINDOW = Desktop::focusState()->window(); - if (!PWINDOW || (PWINDOW && PWINDOW->m_group)) - return {}; - - if (args == "toggle") - PWINDOW->m_group->setDenied(!PWINDOW->m_group->denied()); - else - PWINDOW->m_group->setDenied(args == "on"); - - PWINDOW->updateDecorationValues(); - - return {}; -} - -SDispatchResult CKeybindManager::global(std::string args) { - const auto APPID = args.substr(0, args.find_first_of(':')); - const auto NAME = args.substr(args.find_first_of(':') + 1); - - if (NAME.empty()) - return {}; - - if (!PROTO::globalShortcuts->isTaken(APPID, NAME)) - return {}; - - PROTO::globalShortcuts->sendGlobalShortcutEvent(APPID, NAME, g_pKeybindManager->m_passPressed); - - return {}; -} - -SDispatchResult CKeybindManager::moveGroupWindow(std::string args) { - const auto BACK = args == "b" || args == "prev"; - - const auto PLASTWINDOW = Desktop::focusState()->window(); - - if (!PLASTWINDOW) - return {.success = false, .error = "No window found"}; - - if (!PLASTWINDOW->m_group) - return {.success = false, .error = "Window not in a group"}; - - const auto GROUP = PLASTWINDOW->m_group; - - if (BACK) - GROUP->swapWithLast(); - else - GROUP->swapWithNext(); - - return {}; -} - -SDispatchResult CKeybindManager::event(std::string args) { - g_pEventManager->postEvent(SHyprIPCEvent{"custom", args}); - return {}; -} - -#include -#include - -template -static void parsePropTrivial(Desktop::Types::COverridableVar& prop, const std::string& s) { - static_assert(std::is_same_v || std::is_same_v || std::is_same_v || std::is_same_v || std::is_same_v, - "Invalid type passed to parsePropTrivial"); - - if (s == "unset") { - prop.unset(Desktop::Types::PRIORITY_SET_PROP); - return; - } - - try { - if constexpr (std::is_same_v) { - if (s == "toggle") - prop.increment(true, Desktop::Types::PRIORITY_SET_PROP); - else - prop = Desktop::Types::COverridableVar(truthy(s), Desktop::Types::PRIORITY_SET_PROP); - } else if constexpr (std::is_same_v || std::is_same_v) { - if (s.starts_with("relative")) { - const auto VAL = std::stoi(s.substr(s.find(' ') + 1)); - prop.increment(VAL, Desktop::Types::PRIORITY_SET_PROP); - } else - prop = Desktop::Types::COverridableVar(std::stoull(s), Desktop::Types::PRIORITY_SET_PROP); - } else if constexpr (std::is_same_v) { - if (s.starts_with("relative")) { - const auto VAL = std::stof(s.substr(s.find(' ') + 1)); - prop.increment(VAL, Desktop::Types::PRIORITY_SET_PROP); - } else - prop = Desktop::Types::COverridableVar(std::stof(s), Desktop::Types::PRIORITY_SET_PROP); - } else if constexpr (std::is_same_v) - prop = Desktop::Types::COverridableVar(s, Desktop::Types::PRIORITY_SET_PROP); - } catch (...) { Log::logger->log(Log::ERR, "Hyprctl: parsePropTrivial: failed to parse setprop for {}", s); } -} - -SDispatchResult CKeybindManager::setProp(std::string args) { - CVarList vars(args, 3, ' '); - - if (vars.size() < 3) - return {.success = false, .error = "Not enough args"}; - - const auto PLASTWINDOW = Desktop::focusState()->window(); - const auto PWINDOW = g_pCompositor->getWindowByRegex(vars[0]); - - if (!PWINDOW) - return {.success = false, .error = "Window not found"}; - - const auto PROP = vars[1]; - const auto VAL = vars[2]; - - bool noFocus = PWINDOW->m_ruleApplicator->noFocus().valueOrDefault(); - - try { - if (PROP == "max_size") { - const auto SIZE = PWINDOW->calculateExpression(VAL); - if (!SIZE) { - Log::logger->log(Log::ERR, "failed to parse {} as an expression", VAL); - throw "failed to parse expression"; - } - PWINDOW->m_ruleApplicator->maxSizeOverride(Desktop::Types::COverridableVar(*SIZE, Desktop::Types::PRIORITY_SET_PROP)); - PWINDOW->clampWindowSize(std::nullopt, PWINDOW->m_ruleApplicator->maxSize().value()); - PWINDOW->setHidden(false); - } else if (PROP == "min_size") { - const auto SIZE = PWINDOW->calculateExpression(VAL); - if (!SIZE) { - Log::logger->log(Log::ERR, "failed to parse {} as an expression", VAL); - throw "failed to parse expression"; - } - PWINDOW->m_ruleApplicator->minSizeOverride(Desktop::Types::COverridableVar(*SIZE, Desktop::Types::PRIORITY_SET_PROP)); - PWINDOW->clampWindowSize(PWINDOW->m_ruleApplicator->minSize().value(), std::nullopt); - PWINDOW->setHidden(false); - } else if (PROP == "active_border_color" || PROP == "inactive_border_color") { - Config::CGradientValueData colorData = {}; - if (vars.size() > 4) { - for (int i = 3; i < sc(vars.size()); ++i) { - const auto TOKEN = vars[i]; - if (TOKEN.ends_with("deg")) - colorData.m_angle = std::stoi(TOKEN.substr(0, TOKEN.size() - 3)) * (PI / 180.0); - else - configStringToInt(TOKEN).and_then([&colorData](const auto& e) { - colorData.m_colors.push_back(e); - return std::invoke_result_t(1); - }); - } - } else if (VAL != "-1") - configStringToInt(VAL).and_then([&colorData](const auto& e) { - colorData.m_colors.push_back(e); - return std::invoke_result_t(1); - }); - - colorData.updateColorsOk(); - - if (PROP == "active_border_color") - PWINDOW->m_ruleApplicator->activeBorderColorOverride(Desktop::Types::COverridableVar(colorData, Desktop::Types::PRIORITY_SET_PROP)); - else - PWINDOW->m_ruleApplicator->inactiveBorderColorOverride(Desktop::Types::COverridableVar(colorData, Desktop::Types::PRIORITY_SET_PROP)); - } else if (PROP == "opacity") { - PWINDOW->m_ruleApplicator->alphaOverride(Desktop::Types::COverridableVar( - Desktop::Types::SAlphaValue{std::stof(VAL), PWINDOW->m_ruleApplicator->alpha().valueOrDefault().overridden}, Desktop::Types::PRIORITY_SET_PROP)); - } else if (PROP == "opacity_inactive") { - PWINDOW->m_ruleApplicator->alphaInactiveOverride(Desktop::Types::COverridableVar( - Desktop::Types::SAlphaValue{std::stof(VAL), PWINDOW->m_ruleApplicator->alphaInactive().valueOrDefault().overridden}, Desktop::Types::PRIORITY_SET_PROP)); - } else if (PROP == "opacity_fullscreen") { - PWINDOW->m_ruleApplicator->alphaFullscreenOverride(Desktop::Types::COverridableVar( - Desktop::Types::SAlphaValue{std::stof(VAL), PWINDOW->m_ruleApplicator->alphaFullscreen().valueOrDefault().overridden}, Desktop::Types::PRIORITY_SET_PROP)); - } else if (PROP == "opacity_override") { - PWINDOW->m_ruleApplicator->alphaOverride(Desktop::Types::COverridableVar( - Desktop::Types::SAlphaValue{PWINDOW->m_ruleApplicator->alpha().valueOrDefault().alpha, sc(configStringToInt(VAL).value_or(0))}, - Desktop::Types::PRIORITY_SET_PROP)); - } else if (PROP == "opacity_inactive_override") { - PWINDOW->m_ruleApplicator->alphaInactiveOverride(Desktop::Types::COverridableVar( - Desktop::Types::SAlphaValue{PWINDOW->m_ruleApplicator->alphaInactive().valueOrDefault().alpha, sc(configStringToInt(VAL).value_or(0))}, - Desktop::Types::PRIORITY_SET_PROP)); - } else if (PROP == "opacity_fullscreen_override") { - PWINDOW->m_ruleApplicator->alphaFullscreenOverride(Desktop::Types::COverridableVar( - Desktop::Types::SAlphaValue{PWINDOW->m_ruleApplicator->alphaFullscreen().valueOrDefault().alpha, sc(configStringToInt(VAL).value_or(0))}, - Desktop::Types::PRIORITY_SET_PROP)); - } else if (PROP == "allows_input") - parsePropTrivial(PWINDOW->m_ruleApplicator->allowsInput(), VAL); - else if (PROP == "decorate") - parsePropTrivial(PWINDOW->m_ruleApplicator->decorate(), VAL); - else if (PROP == "focus_on_activate") - parsePropTrivial(PWINDOW->m_ruleApplicator->focusOnActivate(), VAL); - else if (PROP == "keep_aspect_ratio") - parsePropTrivial(PWINDOW->m_ruleApplicator->keepAspectRatio(), VAL); - else if (PROP == "nearest_neighbor") - parsePropTrivial(PWINDOW->m_ruleApplicator->nearestNeighbor(), VAL); - else if (PROP == "no_anim") - parsePropTrivial(PWINDOW->m_ruleApplicator->noAnim(), VAL); - else if (PROP == "no_blur") - parsePropTrivial(PWINDOW->m_ruleApplicator->noBlur(), VAL); - else if (PROP == "no_dim") - parsePropTrivial(PWINDOW->m_ruleApplicator->noDim(), VAL); - else if (PROP == "no_focus") - parsePropTrivial(PWINDOW->m_ruleApplicator->noFocus(), VAL); - else if (PROP == "no_max_size") - parsePropTrivial(PWINDOW->m_ruleApplicator->noMaxSize(), VAL); - else if (PROP == "no_shadow") - parsePropTrivial(PWINDOW->m_ruleApplicator->noShadow(), VAL); - else if (PROP == "no_shortcuts_inhibit") - parsePropTrivial(PWINDOW->m_ruleApplicator->noShortcutsInhibit(), VAL); - else if (PROP == "dim_around") - parsePropTrivial(PWINDOW->m_ruleApplicator->dimAround(), VAL); - else if (PROP == "opaque") - parsePropTrivial(PWINDOW->m_ruleApplicator->opaque(), VAL); - else if (PROP == "force_rgbx") - parsePropTrivial(PWINDOW->m_ruleApplicator->RGBX(), VAL); - else if (PROP == "sync_fullscreen") - parsePropTrivial(PWINDOW->m_ruleApplicator->syncFullscreen(), VAL); - else if (PROP == "immediate") - parsePropTrivial(PWINDOW->m_ruleApplicator->tearing(), VAL); - else if (PROP == "xray") - parsePropTrivial(PWINDOW->m_ruleApplicator->xray(), VAL); - else if (PROP == "render_unfocused") - parsePropTrivial(PWINDOW->m_ruleApplicator->renderUnfocused(), VAL); - else if (PROP == "no_follow_mouse") - parsePropTrivial(PWINDOW->m_ruleApplicator->noFollowMouse(), VAL); - else if (PROP == "no_screen_share") - parsePropTrivial(PWINDOW->m_ruleApplicator->noScreenShare(), VAL); - else if (PROP == "no_vrr") - parsePropTrivial(PWINDOW->m_ruleApplicator->noVRR(), VAL); - else if (PROP == "persistent_size") - parsePropTrivial(PWINDOW->m_ruleApplicator->persistentSize(), VAL); - else if (PROP == "stay_focused") - parsePropTrivial(PWINDOW->m_ruleApplicator->stayFocused(), VAL); - else if (PROP == "idle_inhibit") - parsePropTrivial(PWINDOW->m_ruleApplicator->idleInhibitMode(), VAL); - else if (PROP == "border_size") - parsePropTrivial(PWINDOW->m_ruleApplicator->borderSize(), VAL); - else if (PROP == "rounding") - parsePropTrivial(PWINDOW->m_ruleApplicator->rounding(), VAL); - else if (PROP == "rounding_power") - parsePropTrivial(PWINDOW->m_ruleApplicator->roundingPower(), VAL); - else if (PROP == "scroll_mouse") - parsePropTrivial(PWINDOW->m_ruleApplicator->scrollMouse(), VAL); - else if (PROP == "scroll_touchpad") - parsePropTrivial(PWINDOW->m_ruleApplicator->scrollTouchpad(), VAL); - else if (PROP == "animation") - parsePropTrivial(PWINDOW->m_ruleApplicator->animationStyle(), VAL); - else - return {.success = false, .error = "prop not found"}; - - } catch (std::exception& e) { return {.success = false, .error = std::format("Error parsing prop value: {}", std::string(e.what()))}; } - - g_pCompositor->updateAllWindowsAnimatedDecorationValues(); - - if (PWINDOW->m_ruleApplicator->noFocus().valueOrDefault() != noFocus) { - // FIXME: what the fuck is going on here? -vax - Desktop::focusState()->rawWindowFocus(nullptr, Desktop::FOCUS_REASON_KEYBIND); - Desktop::focusState()->fullWindowFocus(PWINDOW, Desktop::FOCUS_REASON_KEYBIND); - Desktop::focusState()->fullWindowFocus(PLASTWINDOW, Desktop::FOCUS_REASON_KEYBIND); - } - - if (PROP == "no_vrr") - Config::monitorRuleMgr()->ensureVRR(); - - for (auto const& m : g_pCompositor->m_monitors) { - if (m->m_activeWorkspace) - m->m_activeWorkspace->m_space->recalculate(); - } - - return {}; -} - -SDispatchResult CKeybindManager::forceIdle(std::string args) { - std::optional duration = getPlusMinusKeywordResult(args, 0); - - if (!duration.has_value()) { - Log::logger->log(Log::ERR, "Duration invalid in forceIdle!"); - return {.success = false, .error = "Duration invalid in forceIdle!"}; - } - - PROTO::idle->setTimers(duration.value() * 1000.0); - - return {}; -} - -SDispatchResult CKeybindManager::sendkeystate(std::string args) { - // args=[,WINDOW_RULES] - const auto ARGS = CVarList(args, 4); - if (ARGS.size() != 4) { - Log::logger->log(Log::ERR, "sendkeystate: invalid args"); - return {.success = false, .error = "sendkeystate: invalid args"}; - } - - const auto STATE = ARGS[2]; - - if (STATE != "down" && STATE != "repeat" && STATE != "up") { - Log::logger->log(Log::ERR, "sendkeystate: invalid state, must be 'down', 'repeat', or 'up'"); - return {.success = false, .error = "sendkeystate: invalid state, must be 'down', 'repeat', or 'up'"}; - } - - std::string modifiedArgs = ARGS[0] + "," + ARGS[1] + "," + ARGS[3]; - - const int oldPassPressed = g_pKeybindManager->m_passPressed; - - if (STATE == "down") - g_pKeybindManager->m_passPressed = 1; - else if (STATE == "up") - g_pKeybindManager->m_passPressed = 0; - else if (STATE == "repeat") - g_pKeybindManager->m_passPressed = 1; - - auto result = sendshortcut(modifiedArgs); - - if (STATE == "repeat" && result.success) - result = sendshortcut(modifiedArgs); - - g_pKeybindManager->m_passPressed = oldPassPressed; - - if (!result.success && !result.error.empty()) { - size_t pos = result.error.find("sendshortcut:"); - if (pos != std::string::npos) - result.error = "sendkeystate:" + result.error.substr(pos + 13); - } - - return result; -} diff --git a/src/managers/KeybindManager.hpp b/src/managers/KeybindManager.hpp index a51f9add6..87cae14af 100644 --- a/src/managers/KeybindManager.hpp +++ b/src/managers/KeybindManager.hpp @@ -27,11 +27,11 @@ struct SSubmap { struct SKeybind { std::string key = ""; - std::set sMkKeys = {}; + std::vector sMkKeys = {}; uint32_t keycode = 0; bool catchAll = false; uint32_t modmask = 0; - std::set sMkMods = {}; + std::vector sMkMods = {}; std::string handler = ""; std::string arg = ""; bool locked = false; @@ -42,6 +42,7 @@ struct SKeybind { bool longPress = false; bool mouse = false; bool nonConsuming = false; + bool autoConsuming = false; bool transparent = false; bool ignoreMods = false; bool multiKey = false; @@ -52,6 +53,9 @@ struct SKeybind { bool submapUniversal = false; bool deviceInclusive = false; std::unordered_set devices = {}; + bool enabled = true; + + std::string displayKey = ""; // DO NOT INITIALIZE bool shadowed = false; @@ -94,6 +98,10 @@ namespace Config::Legacy { class CConfigManager; } +namespace Config::Lua { + class CConfigManager; +} + class CKeybindManager { public: CKeybindManager(); @@ -107,8 +115,9 @@ class CKeybindManager { void onSwitchOnEvent(const std::string&); void onSwitchOffEvent(const std::string&); - void addKeybind(SKeybind); + SP addKeybind(SKeybind); void removeKeybind(uint32_t, const SParsedKey&); + void removeKeybind(const std::string& displayKeys); uint32_t stringToModMask(std::string); uint32_t keycodeToModifier(xkb_keycode_t); void clearKeybinds(); @@ -121,6 +130,8 @@ class CKeybindManager { std::vector> m_keybinds; + SP m_currentKeybind; + //since we can't find keycode through keyname in xkb: //on sendshortcut call, we once search for keyname (e.g. "g") the correct keycode (e.g. 42) //and cache it in this map to make sendshortcut calls faster @@ -132,8 +143,6 @@ class CKeybindManager { private: std::vector m_pressedKeys; - inline static SSubmap m_currentSelectedSubmap = {}; - std::vector> m_activeKeybinds; WP m_lastLongPressKeybind; @@ -141,14 +150,8 @@ class CKeybindManager { SP m_repeatKeyTimer; uint32_t m_repeatKeyRate = 50; - uint32_t m_timeLastMs = 0; - uint32_t m_lastCode = 0; - uint32_t m_lastMouseCode = 0; - std::vector> m_pressedSpecialBinds; - int m_passPressed = -1; // used for pass - CTimer m_scrollTimer; SDispatchResult handleKeybinds(const uint32_t, const SPressedKeyWithMods&, bool, SP, SP); @@ -156,7 +159,7 @@ class CKeybindManager { std::set m_mkKeys = {}; std::set m_mkMods = {}; eMultiKeyCase mkBindMatches(const SP); - eMultiKeyCase mkKeysymSetMatches(const std::set, const std::set); + eMultiKeyCase mkKeysymSetMatches(const std::vector, const std::set); bool handleInternalKeybinds(xkb_keysym_t); bool handleVT(xkb_keysym_t); @@ -166,86 +169,10 @@ class CKeybindManager { void updateXKBTranslationState(); bool ensureMouseBindState(); - static bool tryMoveFocusToMonitor(PHLMONITOR monitor); - static void moveWindowOutOfGroup(PHLWINDOW pWindow, const std::string& dir = ""); - static void moveWindowIntoGroup(PHLWINDOW pWindow, PHLWINDOW pWindowInDirection); - - static void switchToWindow(PHLWINDOW PWINDOWTOCHANGETO, bool forceFSCycle = false); - - // -------------- Dispatchers -------------- // - static SDispatchResult closeActive(std::string); - static SDispatchResult killActive(std::string); - static SDispatchResult closeWindow(std::string); - static SDispatchResult killWindow(std::string); - static SDispatchResult signalActive(std::string); - static SDispatchResult signalWindow(std::string); - static SDispatchResult spawn(std::string); - static SDispatchResult spawnRaw(std::string); - static SDispatchResult toggleActiveFloating(std::string); - static SDispatchResult toggleActivePseudo(std::string); - static SDispatchResult setActiveFloating(std::string); - static SDispatchResult setActiveTiled(std::string); - static SDispatchResult changeworkspace(std::string); - static SDispatchResult fullscreenActive(std::string); - static SDispatchResult fullscreenStateActive(std::string args); - static SDispatchResult moveActiveToWorkspace(std::string); - static SDispatchResult moveActiveToWorkspaceSilent(std::string); - static SDispatchResult moveFocusTo(std::string); - static SDispatchResult focusUrgentOrLast(std::string); - static SDispatchResult focusCurrentOrLast(std::string); - static SDispatchResult centerWindow(std::string); - static SDispatchResult moveActiveTo(std::string); - static SDispatchResult swapActive(std::string); - static SDispatchResult toggleGroup(std::string); - static SDispatchResult changeGroupActive(std::string); - static SDispatchResult focusMonitor(std::string); - static SDispatchResult moveCursorToCorner(std::string); - static SDispatchResult moveCursor(std::string); - static SDispatchResult workspaceOpt(std::string); - static SDispatchResult renameWorkspace(std::string); - static SDispatchResult exitHyprland(std::string); - static SDispatchResult moveCurrentWorkspaceToMonitor(std::string); - static SDispatchResult moveWorkspaceToMonitor(std::string); - static SDispatchResult focusWorkspaceOnCurrentMonitor(std::string); - static SDispatchResult toggleSpecialWorkspace(std::string); - static SDispatchResult forceRendererReload(std::string); - static SDispatchResult resizeActive(std::string); - static SDispatchResult moveActive(std::string); - static SDispatchResult moveWindow(std::string); - static SDispatchResult resizeWindow(std::string); - static SDispatchResult circleNext(std::string); - static SDispatchResult focusWindow(std::string); - static SDispatchResult tagWindow(std::string); - static SDispatchResult toggleSwallow(std::string); - static SDispatchResult setSubmap(std::string); - static SDispatchResult pass(std::string); - static SDispatchResult sendshortcut(std::string); - static SDispatchResult sendkeystate(std::string); - static SDispatchResult layoutmsg(std::string); - static SDispatchResult dpms(std::string); - static SDispatchResult swapnext(std::string); - static SDispatchResult swapActiveWorkspaces(std::string); - static SDispatchResult pinActive(std::string); - static SDispatchResult mouse(std::string); - static SDispatchResult bringActiveToTop(std::string); - static SDispatchResult alterZOrder(std::string); - static SDispatchResult lockGroups(std::string); - static SDispatchResult lockActiveGroup(std::string); - static SDispatchResult moveIntoGroup(std::string); - static SDispatchResult moveIntoOrCreateGroup(std::string); - static SDispatchResult moveOutOfGroup(std::string); - static SDispatchResult moveGroupWindow(std::string); - static SDispatchResult moveWindowOrGroup(std::string); - static SDispatchResult setIgnoreGroupLock(std::string); - static SDispatchResult denyWindowFromGroup(std::string); - static SDispatchResult global(std::string); - static SDispatchResult event(std::string); - static SDispatchResult setProp(std::string); - static SDispatchResult forceIdle(std::string); - friend class CCompositor; friend class CInputManager; friend class Config::Legacy::CConfigManager; + friend class Config::Lua::CConfigManager; friend class CWorkspace; friend class CPointerManager; }; diff --git a/src/managers/PointerManager.cpp b/src/managers/PointerManager.cpp index 04deca9a7..b6419e713 100644 --- a/src/managers/PointerManager.cpp +++ b/src/managers/PointerManager.cpp @@ -1,6 +1,7 @@ #include "PointerManager.hpp" #include "../Compositor.hpp" #include "../config/ConfigValue.hpp" +#include "../config/shared/actions/ConfigActions.hpp" #include "../config/legacy/ConfigManager.hpp" #include "../protocols/PointerGestures.hpp" #include "../protocols/RelativePointer.hpp" @@ -415,7 +416,7 @@ SP CPointerManager::renderHWCursorBuffer(SPmonitor->m_output->cursorPlaneSize(); auto const& cursorSize = m_currentCursorImage.size; - static auto PCPUBUFFER = CConfigValue("cursor:use_cpu_buffer"); + static auto PCPUBUFFER = CConfigValue("cursor:use_cpu_buffer"); const bool shouldUseCpuBuffer = *PCPUBUFFER == 1 || (*PCPUBUFFER != 0 && g_pHyprRenderer->isNvidia()); @@ -686,7 +687,7 @@ CBox CPointerManager::getCursorBoxGlobal() { } Vector2D CPointerManager::closestValid(const Vector2D& pos) { - static auto PADDING = CConfigValue("cursor:hotspot_padding"); + static auto PADDING = CConfigValue("cursor:hotspot_padding"); auto CURSOR_PADDING = std::clamp(sc(*PADDING), 0, 100); CBox hotBox = {{pos.x - CURSOR_PADDING, pos.y - CURSOR_PADDING}, {2 * CURSOR_PADDING, 2 * CURSOR_PADDING}}; @@ -926,7 +927,7 @@ void CPointerManager::attachPointer(SP pointer) { if (!pointer) return; - static auto PMOUSEDPMS = CConfigValue("misc:mouse_move_enables_dpms"); + static auto PMOUSEDPMS = CConfigValue("misc:mouse_move_enables_dpms"); // auto listener = m_pointerListeners.emplace_back(makeShared()); @@ -939,8 +940,8 @@ void CPointerManager::attachPointer(SP pointer) { PROTO::idle->onActivity(); - if (!g_pCompositor->m_dpmsStateOn && *PMOUSEDPMS) - CKeybindManager::dpms("on"); + if (!g_pCompositor->m_dpmsStateOn && *PMOUSEDPMS) // NOLINTNEXTLINE + Config::Actions::dpms(Config::Actions::TOGGLE_ACTION_ENABLE, std::nullopt); }); listener->motionAbsolute = pointer->m_pointerEvents.motionAbsolute.listen([](const IPointer::SMotionAbsoluteEvent& event) { @@ -948,8 +949,8 @@ void CPointerManager::attachPointer(SP pointer) { PROTO::idle->onActivity(); - if (!g_pCompositor->m_dpmsStateOn && *PMOUSEDPMS) - CKeybindManager::dpms("on"); + if (!g_pCompositor->m_dpmsStateOn && *PMOUSEDPMS) // NOLINTNEXTLINE + Config::Actions::dpms(Config::Actions::TOGGLE_ACTION_ENABLE, std::nullopt); }); listener->button = pointer->m_pointerEvents.button.listen([weak = WP(pointer)](const IPointer::SButtonEvent& event) { @@ -968,8 +969,8 @@ void CPointerManager::attachPointer(SP pointer) { PROTO::idle->onActivity(); - if (!g_pCompositor->m_dpmsStateOn && *PMOUSEDPMS) - CKeybindManager::dpms("on"); + if (!g_pCompositor->m_dpmsStateOn && *PMOUSEDPMS) // NOLINTNEXTLINE + Config::Actions::dpms(Config::Actions::TOGGLE_ACTION_ENABLE, std::nullopt); }); listener->swipeEnd = pointer->m_pointerEvents.swipeEnd.listen([](const IPointer::SSwipeEndEvent& event) { @@ -987,8 +988,8 @@ void CPointerManager::attachPointer(SP pointer) { PROTO::idle->onActivity(); - if (!g_pCompositor->m_dpmsStateOn && *PMOUSEDPMS) - CKeybindManager::dpms("on"); + if (!g_pCompositor->m_dpmsStateOn && *PMOUSEDPMS) // NOLINTNEXTLINE + Config::Actions::dpms(Config::Actions::TOGGLE_ACTION_ENABLE, std::nullopt); }); listener->pinchEnd = pointer->m_pointerEvents.pinchEnd.listen([](const IPointer::SPinchEndEvent& event) { @@ -1020,7 +1021,7 @@ void CPointerManager::attachTouch(SP touch) { if (!touch) return; - static auto PMOUSEDPMS = CConfigValue("misc:mouse_move_enables_dpms"); + static auto PMOUSEDPMS = CConfigValue("misc:mouse_move_enables_dpms"); // auto listener = m_touchListeners.emplace_back(makeShared()); @@ -1034,8 +1035,8 @@ void CPointerManager::attachTouch(SP touch) { PROTO::idle->onActivity(); - if (!g_pCompositor->m_dpmsStateOn && *PMOUSEDPMS) - CKeybindManager::dpms("on"); + if (!g_pCompositor->m_dpmsStateOn && *PMOUSEDPMS) // NOLINTNEXTLINE + Config::Actions::dpms(Config::Actions::TOGGLE_ACTION_ENABLE, std::nullopt); }); listener->up = touch->m_touchEvents.up.listen([](const ITouch::SUpEvent& event) { @@ -1061,7 +1062,7 @@ void CPointerManager::attachTablet(SP tablet) { if (!tablet) return; - static auto PMOUSEDPMS = CConfigValue("misc:mouse_move_enables_dpms"); + static auto PMOUSEDPMS = CConfigValue("misc:mouse_move_enables_dpms"); // auto listener = m_tabletListeners.emplace_back(makeShared()); @@ -1075,8 +1076,8 @@ void CPointerManager::attachTablet(SP tablet) { PROTO::idle->onActivity(); - if (!g_pCompositor->m_dpmsStateOn && *PMOUSEDPMS) - CKeybindManager::dpms("on"); + if (!g_pCompositor->m_dpmsStateOn && *PMOUSEDPMS) // NOLINTNEXTLINE + Config::Actions::dpms(Config::Actions::TOGGLE_ACTION_ENABLE, std::nullopt); }); listener->proximity = tablet->m_tabletEvents.proximity.listen([](const CTablet::SProximityEvent& event) { @@ -1089,8 +1090,8 @@ void CPointerManager::attachTablet(SP tablet) { PROTO::idle->onActivity(); - if (!g_pCompositor->m_dpmsStateOn && *PMOUSEDPMS) - CKeybindManager::dpms("on"); + if (!g_pCompositor->m_dpmsStateOn && *PMOUSEDPMS) // NOLINTNEXTLINE + Config::Actions::dpms(Config::Actions::TOGGLE_ACTION_ENABLE, std::nullopt); }); listener->button = tablet->m_tabletEvents.button.listen([](const CTablet::SButtonEvent& event) { diff --git a/src/managers/ProtocolManager.cpp b/src/managers/ProtocolManager.cpp index a2b34ced3..3be4523d9 100644 --- a/src/managers/ProtocolManager.cpp +++ b/src/managers/ProtocolManager.cpp @@ -108,11 +108,10 @@ void CProtocolManager::onMonitorModeChange(PHLMONITOR pMonitor) { CProtocolManager::CProtocolManager() { - static const auto PENABLECM = CConfigValue("render:cm_enabled"); - static const auto PDEBUGCM = CConfigValue("debug:full_cm_proto"); - static const auto PCMV1_2 = CConfigValue("experimental:wp_cm_1_2"); - - static const auto PENABLECT = CConfigValue("render:commit_timing_enabled"); + static const auto PENABLECM = CConfigValue("render:cm_enabled"); + static const auto PDEBUGCM = CConfigValue("debug:full_cm_proto"); + static const auto PCMV1_2 = CConfigValue("experimental:wp_cm_1_2"); + static const auto PENABLECT = CConfigValue("render:commit_timing_enabled"); // Outputs are a bit dumb, we have to agree. static auto P = Event::bus()->m_events.monitor.added.listen([this](PHLMONITOR M) { diff --git a/src/managers/SeatManager.cpp b/src/managers/SeatManager.cpp index 5c48eed5a..f3c485f8d 100644 --- a/src/managers/SeatManager.cpp +++ b/src/managers/SeatManager.cpp @@ -120,20 +120,14 @@ void CSeatManager::setKeyboardFocus(SP surf) { m_listeners.keyboardSurfaceDestroy.reset(); - if (m_state.keyboardFocusResource) { - auto client = m_state.keyboardFocusResource->client(); - for (auto const& s : m_seatResources) { - if (s->resource->client() != client) - continue; + // Don't gate leave on m_state.keyboardFocusResource — the WP can + // be stale. sendLeave no-ops on keyboards without m_currentSurface. + for (auto const& k : PROTO::seat->m_keyboards) { + if (!k) + continue; - for (auto const& k : s->resource->m_keyboards) { - if (!k) - continue; - - k->sendMods(0, m_keyboard->m_modifiersState.latched, m_keyboard->m_modifiersState.locked, m_keyboard->m_modifiersState.group); - k->sendLeave(); - } - } + k->sendMods(0, m_keyboard->m_modifiersState.latched, m_keyboard->m_modifiersState.locked, m_keyboard->m_modifiersState.group); + k->sendLeave(); } m_state.keyboardFocusResource.reset(); @@ -232,19 +226,11 @@ void CSeatManager::setPointerFocus(SP surf, const Vector2D& m_listeners.pointerSurfaceDestroy.reset(); - if (m_state.pointerFocusResource) { - auto client = m_state.pointerFocusResource->client(); - for (auto const& s : m_seatResources) { - if (s->resource->client() != client) - continue; + for (auto const& p : PROTO::seat->m_pointers) { + if (!p) + continue; - for (auto const& p : s->resource->m_pointers) { - if (!p) - continue; - - p->sendLeave(); - } - } + p->sendLeave(); } auto lastPointerFocusResource = m_state.pointerFocusResource; @@ -663,7 +649,7 @@ void CSeatManager::setGrab(SP grab) { if (parentLayer && parentLayer->m_layerSurface->m_current.interactivity != ZWLR_LAYER_SURFACE_V1_KEYBOARD_INTERACTIVITY_NONE) { Desktop::focusState()->rawSurfaceFocus(parentLayer->wlSurface()->resource()); } else { - static auto PFOLLOWMOUSE = CConfigValue("input:follow_mouse"); + static auto PFOLLOWMOUSE = CConfigValue("input:follow_mouse"); if (*PFOLLOWMOUSE == 0 || *PFOLLOWMOUSE == 2 || *PFOLLOWMOUSE == 3) { const auto PMONITOR = g_pCompositor->getMonitorFromCursor(); diff --git a/src/managers/SessionLockManager.cpp b/src/managers/SessionLockManager.cpp index e729ae954..8890e2e07 100644 --- a/src/managers/SessionLockManager.cpp +++ b/src/managers/SessionLockManager.cpp @@ -49,7 +49,7 @@ CSessionLockManager::CSessionLockManager() { } void CSessionLockManager::onNewSessionLock(SP pLock) { - static auto PALLOWRELOCK = CConfigValue("misc:allow_session_lock_restore"); + static auto PALLOWRELOCK = CConfigValue("misc:allow_session_lock_restore"); if (PROTO::sessionLock->isLocked() && !*PALLOWRELOCK) { LOGM(Log::DEBUG, "Cannot re-lock, misc:allow_session_lock_restore is disabled"); @@ -224,7 +224,7 @@ bool CSessionLockManager::shallConsiderLockMissing() { if (!m_sessionLock) return true; - static auto LOCKDEAD_SCREEN_DELAY = CConfigValue("misc:lockdead_screen_delay"); + static auto LOCKDEAD_SCREEN_DELAY = CConfigValue("misc:lockdead_screen_delay"); return m_sessionLock->lockTimer.getMillis() > *LOCKDEAD_SCREEN_DELAY; } diff --git a/src/managers/VersionKeeperManager.cpp b/src/managers/VersionKeeperManager.cpp index b1d167fac..9a12adacf 100644 --- a/src/managers/VersionKeeperManager.cpp +++ b/src/managers/VersionKeeperManager.cpp @@ -19,7 +19,7 @@ using namespace Hyprutils::OS; constexpr const char* VERSION_FILE_NAME = "lastVersion"; CVersionKeeperManager::CVersionKeeperManager() { - static auto PNONOTIFY = CConfigValue("ecosystem:no_update_news"); + static auto PNONOTIFY = CConfigValue("ecosystem:no_update_news"); const auto DATAROOT = NFsUtils::getDataHome(); diff --git a/src/managers/WelcomeManager.cpp b/src/managers/WelcomeManager.cpp index 6faf58c38..b4be21caa 100644 --- a/src/managers/WelcomeManager.cpp +++ b/src/managers/WelcomeManager.cpp @@ -9,7 +9,7 @@ using namespace Hyprutils::OS; CWelcomeManager::CWelcomeManager() { - static auto PAUTOGEN = CConfigValue("autogenerated"); + static auto PAUTOGEN = CConfigValue("autogenerated"); if (!*PAUTOGEN) { Log::logger->log(Log::DEBUG, "[welcome] skipping, not autogen"); diff --git a/src/managers/XCursorManager.cpp b/src/managers/XCursorManager.cpp index 90cd2a322..c06e376f5 100644 --- a/src/managers/XCursorManager.cpp +++ b/src/managers/XCursorManager.cpp @@ -571,7 +571,7 @@ std::vector> CXCursorManager::loadAllFromDir(std::string const& pa } void CXCursorManager::syncGsettings() { - static auto SYNCGSETTINGS = CConfigValue("cursor:sync_gsettings_theme"); + static auto SYNCGSETTINGS = CConfigValue("cursor:sync_gsettings_theme"); if (!*SYNCGSETTINGS) return; diff --git a/src/managers/XWaylandManager.cpp b/src/managers/XWaylandManager.cpp index 95fa085b5..d299bf469 100644 --- a/src/managers/XWaylandManager.cpp +++ b/src/managers/XWaylandManager.cpp @@ -178,7 +178,7 @@ Vector2D CHyprXWaylandManager::waylandToXWaylandCoords(const Vector2D& coord) { } Vector2D CHyprXWaylandManager::waylandToXWaylandCoords(const Vector2D& coord, PHLMONITOR preferredMonitor) { - static auto PXWLFORCESCALEZERO = CConfigValue("xwayland:force_zero_scaling"); + static auto PXWLFORCESCALEZERO = CConfigValue("xwayland:force_zero_scaling"); PHLMONITOR pMonitor = preferredMonitor; if (!pMonitor) { @@ -215,7 +215,7 @@ Vector2D CHyprXWaylandManager::xwaylandToWaylandCoords(const Vector2D& coord) { Vector2D CHyprXWaylandManager::xwaylandToWaylandCoords(const Vector2D& coord, PHLMONITOR preferredMonitor) { - static auto PXWLFORCESCALEZERO = CConfigValue("xwayland:force_zero_scaling"); + static auto PXWLFORCESCALEZERO = CConfigValue("xwayland:force_zero_scaling"); PHLMONITOR pMonitor = preferredMonitor; if (!pMonitor) { diff --git a/src/managers/animation/AnimationManager.cpp b/src/managers/animation/AnimationManager.cpp index 85d6197e8..76a124656 100644 --- a/src/managers/animation/AnimationManager.cpp +++ b/src/managers/animation/AnimationManager.cpp @@ -42,7 +42,7 @@ static void updateVariable(CAnimatedVariable& av, const float POINTY, b av.value() = av.begun() + DELTA * POINTY; } -static void updateColorVariable(CAnimatedVariable& av, const float POINTY, bool warp) { +static void updateColorVariable(CAnimatedVariable& av, const float POINTY, bool warp = false) { if (warp || av.value() == av.goal()) { av.warp(true, false); return; @@ -139,15 +139,12 @@ static void handleUpdate(CAnimatedVariable& av, bool warp) { animationsDisabled = animationsDisabled || ls->m_ruleApplicator->noanim().valueOrDefault(); } - const auto SPENT = av.getPercent(); - const auto PBEZIER = g_pAnimationManager->getBezier(av.getBezierName()); - const auto POINTY = PBEZIER->getYForPoint(SPENT); - const bool WARP = animationsDisabled || SPENT >= 1.f; + const auto STEP = av.getCurveStep(); if constexpr (std::same_as) - updateColorVariable(av, POINTY, WARP); + updateColorVariable(av, STEP.value, STEP.finished || animationsDisabled); else - updateVariable(av, POINTY, WARP); + updateVariable(av, STEP.value, STEP.finished || animationsDisabled); av.onUpdate(); } @@ -157,7 +154,7 @@ void CHyprAnimationManager::tick() { m_lastTickTimeMs = std::chrono::duration_cast(std::chrono::high_resolution_clock::now() - lastTick).count() / 1000.0; lastTick = std::chrono::high_resolution_clock::now(); - static auto PANIMENABLED = CConfigValue("animations:enabled"); + static auto PANIMENABLED = CConfigValue("animations:enabled"); if (m_vActiveAnimatedVariables.empty()) { tickDone(); diff --git a/src/managers/animation/DesktopAnimationManager.cpp b/src/managers/animation/DesktopAnimationManager.cpp index 309614090..2b7cb4590 100644 --- a/src/managers/animation/DesktopAnimationManager.cpp +++ b/src/managers/animation/DesktopAnimationManager.cpp @@ -18,25 +18,26 @@ #include using namespace Hyprutils::String; +using namespace Desktop::View; void CDesktopAnimationManager::startAnimation(PHLWINDOW pWindow, eAnimationType type, bool force) { const bool CLOSE = type == ANIMATION_TYPE_OUT; if (CLOSE) - *pWindow->m_alpha = 0.F; + *pWindow->alpha(WINDOW_ALPHA_FADE) = 0.F; else { - pWindow->m_alpha->setValueAndWarp(0.F); - *pWindow->m_alpha = 1.F; + pWindow->alpha(WINDOW_ALPHA_FADE)->setValueAndWarp(0.F); + *pWindow->alpha(WINDOW_ALPHA_FADE) = 1.F; } if (!CLOSE) { pWindow->m_realPosition->setConfig(Config::animationTree()->getAnimationPropertyConfig("windowsIn")); pWindow->m_realSize->setConfig(Config::animationTree()->getAnimationPropertyConfig("windowsIn")); - pWindow->m_alpha->setConfig(Config::animationTree()->getAnimationPropertyConfig("fadeIn")); + pWindow->alpha(WINDOW_ALPHA_FADE)->setConfig(Config::animationTree()->getAnimationPropertyConfig("fadeIn")); } else { pWindow->m_realPosition->setConfig(Config::animationTree()->getAnimationPropertyConfig("windowsOut")); pWindow->m_realSize->setConfig(Config::animationTree()->getAnimationPropertyConfig("windowsOut")); - pWindow->m_alpha->setConfig(Config::animationTree()->getAnimationPropertyConfig("fadeOut")); + pWindow->alpha(WINDOW_ALPHA_FADE)->setConfig(Config::animationTree()->getAnimationPropertyConfig("fadeOut")); } std::string ANIMSTYLE = pWindow->m_realPosition->getStyle(); @@ -248,7 +249,7 @@ void CDesktopAnimationManager::startAnimation(PHLWORKSPACE ws, eAnimationType ty ws->m_alpha->setConfig(Config::animationTree()->getAnimationPropertyConfig(ANIMNAME)); ws->m_renderOffset->setConfig(Config::animationTree()->getAnimationPropertyConfig(ANIMNAME)); } - static auto PWORKSPACEGAP = CConfigValue("general:gaps_workspaces"); + static auto PWORKSPACEGAP = CConfigValue("general:gaps_workspaces"); const auto PMONITOR = ws->m_monitor.lock(); const auto ANIMSTYLE = style.value_or(ws->m_alpha->getStyle()); @@ -466,20 +467,17 @@ void CDesktopAnimationManager::setFullscreenFadeAnimation(PHLWORKSPACE ws, eAnim const auto FULLSCREEN = type == ANIMATION_TYPE_IN; - const auto FSWINDOW = ws->getFullscreenWindow(); - for (auto const& w : g_pCompositor->m_windows) { if (w->m_workspace == ws) { + w->updateFullscreenInputState(); if (w->m_fadingOut || w->m_pinned || w->isFullscreen()) continue; if (!FULLSCREEN) - *w->m_alpha = 1.F; + *w->alpha(WINDOW_ALPHA_FULLSCREEN) = 1.F; else if (!w->isFullscreen()) { - const bool CREATED_OVER_FS = w->m_createdOverFullscreen; - const bool IS_IN_GROUP_OF_FS = FSWINDOW && FSWINDOW->m_group && FSWINDOW->m_group->has(w); - *w->m_alpha = !CREATED_OVER_FS && !IS_IN_GROUP_OF_FS ? 0.f : 1.f; + *w->alpha(WINDOW_ALPHA_FULLSCREEN) = w->isAllowedOverFullscreen() ? 1.f : 0.f; } } } @@ -498,7 +496,8 @@ void CDesktopAnimationManager::setFullscreenFloatingFade(PHLWINDOW pWindow, floa if (pWindow->m_fadingOut || !pWindow->m_isFloating) return; - *pWindow->m_alpha = fade; + *pWindow->alpha(WINDOW_ALPHA_FULLSCREEN) = fade; + pWindow->updateFullscreenInputState(); } void CDesktopAnimationManager::overrideFullscreenFadeAmount(PHLWORKSPACE ws, float fade, PHLWINDOW exclude) { @@ -510,7 +509,8 @@ void CDesktopAnimationManager::overrideFullscreenFadeAmount(PHLWORKSPACE ws, flo if (w->m_fadingOut || w->m_pinned || w->isFullscreen()) continue; - *w->m_alpha = fade; + *w->alpha(WINDOW_ALPHA_FULLSCREEN) = fade; + w->updateFullscreenInputState(); } } diff --git a/src/managers/input/InputManager.cpp b/src/managers/input/InputManager.cpp index c4df7c53b..7ed7b4f6b 100644 --- a/src/managers/input/InputManager.cpp +++ b/src/managers/input/InputManager.cpp @@ -6,6 +6,7 @@ #include #include #include "../../config/ConfigValue.hpp" +#include "../../config/shared/actions/ConfigActions.hpp" #include "../../config/legacy/ConfigManager.hpp" #include "../../desktop/view/WLSurface.hpp" #include "../../desktop/state/FocusState.hpp" @@ -116,7 +117,7 @@ CInputManager::~CInputManager() { } void CInputManager::onMouseMoved(IPointer::SMotionEvent e) { - static auto PNOACCEL = CConfigValue("input:force_no_accel"); + static auto PNOACCEL = CConfigValue("input:force_no_accel"); Vector2D delta = e.delta; Vector2D unaccel = e.unaccel; @@ -212,14 +213,14 @@ void CInputManager::mouseMoveUnified(uint32_t time, bool refocus, bool mouse, st if (MOUSECOORDSFLOORED == m_lastCursorPosFloored && !refocus) return; - static auto PFOLLOWMOUSE = CConfigValue("input:follow_mouse"); - static auto PFOLLOWMOUSETHRESHOLD = CConfigValue("input:follow_mouse_threshold"); - static auto PMOUSEREFOCUS = CConfigValue("input:mouse_refocus"); - static auto PFOLLOWONDND = CConfigValue("misc:always_follow_on_dnd"); - static auto PFLOATBEHAVIOR = CConfigValue("input:float_switch_override_focus"); - static auto PMOUSEFOCUSMON = CConfigValue("misc:mouse_move_focuses_monitor"); - static auto PRESIZEONBORDER = CConfigValue("general:resize_on_border"); - static auto PRESIZECURSORICON = CConfigValue("general:hover_icon_on_border"); + static auto PFOLLOWMOUSE = CConfigValue("input:follow_mouse"); + static auto PFOLLOWMOUSETHRESHOLD = CConfigValue("input:follow_mouse_threshold"); + static auto PMOUSEREFOCUS = CConfigValue("input:mouse_refocus"); + static auto PFOLLOWONDND = CConfigValue("misc:always_follow_on_dnd"); + static auto PFLOATBEHAVIOR = CConfigValue("input:float_switch_override_focus"); + static auto PMOUSEFOCUSMON = CConfigValue("misc:mouse_move_focuses_monitor"); + static auto PRESIZEONBORDER = CConfigValue("general:resize_on_border"); + static auto PRESIZECURSORICON = CConfigValue("general:hover_icon_on_border"); const auto FOLLOWMOUSE = *PFOLLOWONDND && PROTO::data->dndActive() ? 1 : *PFOLLOWMOUSE; @@ -446,7 +447,7 @@ void CInputManager::mouseMoveUnified(uint32_t time, bool refocus, bool mouse, st const auto& PWINDOWIDEAL = getWindowIdeal(); if (PWINDOWIDEAL && - ((PWINDOWIDEAL->m_isFloating && (PWINDOWIDEAL->m_createdOverFullscreen || PWINDOWIDEAL->m_pinned)) /* floating over fullscreen or pinned */ + ((PWINDOWIDEAL->m_isFloating && PWINDOWIDEAL->isAllowedOverFullscreen()) /* floating over fullscreen or pinned */ || (PMONITOR->m_activeSpecialWorkspace == PWINDOWIDEAL->m_workspace) /* on an open special workspace */)) pFoundWindow = PWINDOWIDEAL; @@ -484,7 +485,7 @@ void CInputManager::mouseMoveUnified(uint32_t time, bool refocus, bool mouse, st if (pFoundWindow != PWINDOWIDEAL) pFoundWindow = PWINDOWIDEAL; - if (!(pFoundWindow && (pFoundWindow->m_isFloating && (pFoundWindow->m_createdOverFullscreen || pFoundWindow->m_pinned)))) + if (!(pFoundWindow && (pFoundWindow->m_isFloating && pFoundWindow->isAllowedOverFullscreen()))) pFoundWindow = PWORKSPACE->getFullscreenWindow(); } } @@ -785,12 +786,12 @@ void CInputManager::setClickMode(eClickBehaviorMode mode) { void CInputManager::processMouseDownNormal(const IPointer::SButtonEvent& e, SP mouse) { // notify the keybind manager - static auto PPASSMOUSE = CConfigValue("binds:pass_mouse_when_bound"); + static auto PPASSMOUSE = CConfigValue("binds:pass_mouse_when_bound"); const auto PASS = g_pKeybindManager->onMouseEvent(e, mouse); - static auto PFOLLOWMOUSE = CConfigValue("input:follow_mouse"); - static auto PRESIZEONBORDER = CConfigValue("general:resize_on_border"); - static auto PBORDERSIZE = CConfigValue("general:border_size"); - static auto PBORDERGRABEXTEND = CConfigValue("general:extend_border_grab_area"); + static auto PFOLLOWMOUSE = CConfigValue("input:follow_mouse"); + static auto PRESIZEONBORDER = CConfigValue("general:resize_on_border"); + static auto PBORDERSIZE = CConfigValue("general:border_size"); + static auto PBORDERGRABEXTEND = CConfigValue("general:extend_border_grab_area"); const auto BORDER_GRAB_AREA = *PRESIZEONBORDER ? *PBORDERSIZE + *PBORDERGRABEXTEND : 0; if (!PASS && !*PPASSMOUSE) @@ -891,11 +892,11 @@ void CInputManager::processMouseDownKill(const IPointer::SButtonEvent& e) { } void CInputManager::onMouseWheel(IPointer::SAxisEvent e, SP pointer) { - static auto POFFWINDOWAXIS = CConfigValue("input:off_window_axis_events"); - static auto PINPUTSCROLLFACTOR = CConfigValue("input:scroll_factor"); - static auto PTOUCHPADSCROLLFACTOR = CConfigValue("input:touchpad:scroll_factor"); - static auto PEMULATEDISCRETE = CConfigValue("input:emulate_discrete_scroll"); - static auto PFOLLOWMOUSE = CConfigValue("input:follow_mouse"); + static auto POFFWINDOWAXIS = CConfigValue("input:off_window_axis_events"); + static auto PINPUTSCROLLFACTOR = CConfigValue("input:scroll_factor"); + static auto PTOUCHPADSCROLLFACTOR = CConfigValue("input:touchpad:scroll_factor"); + static auto PEMULATEDISCRETE = CConfigValue("input:emulate_discrete_scroll"); + static auto PFOLLOWMOUSE = CConfigValue("input:follow_mouse"); const bool ISTOUCHPADSCROLL = *PTOUCHPADSCROLLFACTOR <= 0.f || e.source == WL_POINTER_AXIS_SOURCE_FINGER; auto factor = ISTOUCHPADSCROLL ? *PTOUCHPADSCROLLFACTOR : *PINPUTSCROLLFACTOR; @@ -1041,7 +1042,7 @@ void CInputManager::newVirtualKeyboard(SP keyboard) } void CInputManager::setupKeyboard(SP keeb) { - static auto PDPMS = CConfigValue("misc:key_press_enables_dpms"); + static auto PDPMS = CConfigValue("misc:key_press_enables_dpms"); m_hids.emplace_back(keeb); @@ -1069,8 +1070,8 @@ void CInputManager::setupKeyboard(SP keeb) { if (PKEEB->m_enabled) PROTO::idle->onActivity(); - if (PKEEB->m_enabled && *PDPMS && !g_pCompositor->m_dpmsStateOn) - g_pKeybindManager->dpms("on"); + if (PKEEB->m_enabled && *PDPMS && !g_pCompositor->m_dpmsStateOn) // NOLINTNEXTLINE + Config::Actions::dpms(Config::Actions::TOGGLE_ACTION_ENABLE, std::nullopt); }); keeb->m_keyboardEvents.modifiers.listenStatic([this, keeb = keeb.get()] { @@ -1081,8 +1082,8 @@ void CInputManager::setupKeyboard(SP keeb) { if (PKEEB->m_enabled) PROTO::idle->onActivity(); - if (PKEEB->m_enabled && *PDPMS && !g_pCompositor->m_dpmsStateOn) - g_pKeybindManager->dpms("on"); + if (PKEEB->m_enabled && *PDPMS && !g_pCompositor->m_dpmsStateOn) // NOLINTNEXTLINE + Config::Actions::dpms(Config::Actions::TOGGLE_ACTION_ENABLE, std::nullopt); }); keeb->m_keyboardEvents.keymap.listenStatic([keeb = keeb.get()] { @@ -1138,8 +1139,8 @@ void CInputManager::applyConfigToKeyboard(SP pKeyboard) { const auto VARIANT = Config::mgr()->getDeviceString(devname, "kb_variant", "input:kb_variant"); const auto OPTIONS = Config::mgr()->getDeviceString(devname, "kb_options", "input:kb_options"); - const auto ENABLED = HASCONFIG ? Config::mgr()->getDeviceInt(devname, "enabled") : true; - const auto ALLOWBINDS = HASCONFIG ? Config::mgr()->getDeviceInt(devname, "keybinds") : true; + const auto ENABLED = HASCONFIG && Config::mgr()->deviceConfigExplicitlySet(devname, "enabled") ? Config::mgr()->getDeviceInt(devname, "enabled") : true; + const auto ALLOWBINDS = HASCONFIG && Config::mgr()->deviceConfigExplicitlySet(devname, "keybinds") ? Config::mgr()->getDeviceInt(devname, "keybinds") : true; pKeyboard->m_enabled = ENABLED; pKeyboard->m_resolveBindsBySym = RESOLVEBINDSBYSYM; @@ -1259,7 +1260,7 @@ void CInputManager::setPointerConfigs() { const auto HASCONFIG = Config::mgr()->deviceConfigExists(devname); if (HASCONFIG) { - const auto ENABLED = Config::mgr()->getDeviceInt(devname, "enabled"); + const auto ENABLED = HASCONFIG && Config::mgr()->deviceConfigExplicitlySet(devname, "enabled") ? Config::mgr()->getDeviceInt(devname, "enabled") : true; if (ENABLED && !m->m_connected) { g_pPointerManager->attachPointer(m); m->m_connected = true; @@ -1979,7 +1980,7 @@ void CInputManager::setCursorIconOnBorder(PHLWINDOW w) { return; } - static auto PEXTENDBORDERGRAB = CConfigValue("general:extend_border_grab_area"); + static auto PEXTENDBORDERGRAB = CConfigValue("general:extend_border_grab_area"); const int BORDERSIZE = w->getRealBorderSize(); const int ROUNDING = w->rounding(); @@ -2072,7 +2073,7 @@ void CInputManager::setCursorIconOnBorder(PHLWINDOW w) { } void CInputManager::recheckMouseWarpOnMouseInput() { - static auto PWARPFORNONMOUSE = CConfigValue("cursor:warp_back_after_non_mouse_input"); + static auto PWARPFORNONMOUSE = CConfigValue("cursor:warp_back_after_non_mouse_input"); if (!m_lastInputMouse && *PWARPFORNONMOUSE) g_pPointerManager->warpTo(m_lastMousePos); diff --git a/src/managers/input/Touch.cpp b/src/managers/input/Touch.cpp index 942c168cb..f49db8b78 100644 --- a/src/managers/input/Touch.cpp +++ b/src/managers/input/Touch.cpp @@ -15,13 +15,13 @@ void CInputManager::onTouchDown(ITouch::SDownEvent e) { m_lastInputTouch = true; - static auto PSWIPETOUCH = CConfigValue("gestures:workspace_swipe_touch"); - static auto PGAPSOUTDATA = CConfigValue("general:gaps_out"); - auto* const PGAPSOUT = sc((PGAPSOUTDATA.ptr())->getData()); + static auto PSWIPETOUCH = CConfigValue("gestures:workspace_swipe_touch"); + static auto PGAPSOUTDATA = CConfigValue("general:gaps_out"); + auto* const PGAPSOUT = sc((PGAPSOUTDATA.ptr())); // TODO: WORKSPACERULE.gapsOut.value_or() auto gapsOut = *PGAPSOUT; - static auto PBORDERSIZE = CConfigValue("general:border_size"); - static auto PSWIPEINVR = CConfigValue("gestures:workspace_swipe_touch_invert"); + static auto PBORDERSIZE = CConfigValue("general:border_size"); + static auto PSWIPEINVR = CConfigValue("gestures:workspace_swipe_touch_invert"); Event::SCallbackInfo info; Event::bus()->m_events.input.touch.down.emit(e, info); @@ -149,8 +149,8 @@ void CInputManager::onTouchMove(ITouch::SMotionEvent e) { const auto ANIMSTYLE = g_pUnifiedWorkspaceSwipe->m_workspaceBegin->m_renderOffset->getStyle(); const bool VERTANIMS = ANIMSTYLE == "slidevert" || ANIMSTYLE.starts_with("slidefadevert"); - static auto PSWIPEINVR = CConfigValue("gestures:workspace_swipe_touch_invert"); - static auto PSWIPEDIST = CConfigValue("gestures:workspace_swipe_distance"); + static auto PSWIPEINVR = CConfigValue("gestures:workspace_swipe_touch_invert"); + static auto PSWIPEDIST = CConfigValue("gestures:workspace_swipe_distance"); const auto SWIPEDISTANCE = std::clamp(*PSWIPEDIST, sc(1LL), sc(UINT32_MAX)); // Handle the workspace swipe if there is one if (g_pUnifiedWorkspaceSwipe->m_initialDirection == -1) { diff --git a/src/managers/input/UnifiedWorkspaceSwipeGesture.cpp b/src/managers/input/UnifiedWorkspaceSwipeGesture.cpp index c2c8a72ad..e2eac6938 100644 --- a/src/managers/input/UnifiedWorkspaceSwipeGesture.cpp +++ b/src/managers/input/UnifiedWorkspaceSwipeGesture.cpp @@ -34,13 +34,13 @@ void CUnifiedWorkspaceSwipeGesture::update(double delta) { if (!isGestureInProgress()) return; - static auto PSWIPEDIST = CConfigValue("gestures:workspace_swipe_distance"); - static auto PSWIPENEW = CConfigValue("gestures:workspace_swipe_create_new"); - static auto PSWIPEDIRLOCK = CConfigValue("gestures:workspace_swipe_direction_lock"); - static auto PSWIPEDIRLOCKTHRESHOLD = CConfigValue("gestures:workspace_swipe_direction_lock_threshold"); - static auto PSWIPEFOREVER = CConfigValue("gestures:workspace_swipe_forever"); - static auto PSWIPEUSER = CConfigValue("gestures:workspace_swipe_use_r"); - static auto PWORKSPACEGAP = CConfigValue("general:gaps_workspaces"); + static auto PSWIPEDIST = CConfigValue("gestures:workspace_swipe_distance"); + static auto PSWIPENEW = CConfigValue("gestures:workspace_swipe_create_new"); + static auto PSWIPEDIRLOCK = CConfigValue("gestures:workspace_swipe_direction_lock"); + static auto PSWIPEDIRLOCKTHRESHOLD = CConfigValue("gestures:workspace_swipe_direction_lock_threshold"); + static auto PSWIPEFOREVER = CConfigValue("gestures:workspace_swipe_forever"); + static auto PSWIPEUSER = CConfigValue("gestures:workspace_swipe_use_r"); + static auto PWORKSPACEGAP = CConfigValue("general:gaps_workspaces"); const auto SWIPEDISTANCE = std::clamp(*PSWIPEDIST, sc(1LL), sc(UINT32_MAX)); const auto XDISTANCE = m_monitor->m_size.x + *PWORKSPACEGAP; @@ -179,12 +179,12 @@ void CUnifiedWorkspaceSwipeGesture::end() { if (!isGestureInProgress()) return; - static auto PSWIPEPERC = CConfigValue("gestures:workspace_swipe_cancel_ratio"); - static auto PSWIPEDIST = CConfigValue("gestures:workspace_swipe_distance"); - static auto PSWIPEFORC = CConfigValue("gestures:workspace_swipe_min_speed_to_force"); - static auto PSWIPENEW = CConfigValue("gestures:workspace_swipe_create_new"); - static auto PSWIPEUSER = CConfigValue("gestures:workspace_swipe_use_r"); - static auto PWORKSPACEGAP = CConfigValue("general:gaps_workspaces"); + static auto PSWIPEPERC = CConfigValue("gestures:workspace_swipe_cancel_ratio"); + static auto PSWIPEDIST = CConfigValue("gestures:workspace_swipe_distance"); + static auto PSWIPEFORC = CConfigValue("gestures:workspace_swipe_min_speed_to_force"); + static auto PSWIPENEW = CConfigValue("gestures:workspace_swipe_create_new"); + static auto PSWIPEUSER = CConfigValue("gestures:workspace_swipe_use_r"); + static auto PWORKSPACEGAP = CConfigValue("general:gaps_workspaces"); const auto ANIMSTYLE = m_workspaceBegin->m_renderOffset->getStyle(); const bool VERTANIMS = ANIMSTYLE == "slidevert" || ANIMSTYLE.starts_with("slidefadevert"); diff --git a/src/managers/input/trackpad/TrackpadGestures.cpp b/src/managers/input/trackpad/TrackpadGestures.cpp index e054c2f9f..a1a389773 100644 --- a/src/managers/input/trackpad/TrackpadGestures.cpp +++ b/src/managers/input/trackpad/TrackpadGestures.cpp @@ -118,7 +118,7 @@ void CTrackpadGestures::gestureBegin(const IPointer::SSwipeBeginEvent& e) { } void CTrackpadGestures::gestureUpdate(const IPointer::SSwipeUpdateEvent& e) { - static auto PDISABLEINHIBIT = CConfigValue("binds:disable_keybind_grabbing"); + static auto PDISABLEINHIBIT = CConfigValue("binds:disable_keybind_grabbing"); if (m_gestureFindFailed) return; @@ -193,7 +193,7 @@ void CTrackpadGestures::gestureBegin(const IPointer::SPinchBeginEvent& e) { } void CTrackpadGestures::gestureUpdate(const IPointer::SPinchUpdateEvent& e) { - static auto PDISABLEINHIBIT = CConfigValue("binds:disable_keybind_grabbing"); + static auto PDISABLEINHIBIT = CConfigValue("binds:disable_keybind_grabbing"); if (m_gestureFindFailed) return; diff --git a/src/managers/input/trackpad/gestures/CloseGesture.cpp b/src/managers/input/trackpad/gestures/CloseGesture.cpp index 0c37ee36c..225f3132e 100644 --- a/src/managers/input/trackpad/gestures/CloseGesture.cpp +++ b/src/managers/input/trackpad/gestures/CloseGesture.cpp @@ -9,6 +9,8 @@ #include "../../../../desktop/state/FocusState.hpp" #include "../../../../layout/target/Target.hpp" +using namespace Desktop::View; + constexpr const float MAX_DISTANCE = 200.F; static std::vector> trackpadCloseTimers; @@ -33,18 +35,18 @@ void CCloseTrackpadGesture::begin(const ITrackpadGesture::STrackpadGestureBegin& if (!m_window) return; - m_alphaFrom = m_window->m_alpha->goal(); + m_alphaFrom = m_window->alphaGoal(WINDOW_ALPHA_FADE); m_posFrom = m_window->m_realPosition->goal(); m_sizeFrom = m_window->m_realSize->goal(); g_pDesktopAnimationManager->startAnimation(m_window.lock(), CDesktopAnimationManager::ANIMATION_TYPE_OUT, true); - *m_window->m_alpha = 0.f; + *m_window->alpha(WINDOW_ALPHA_FADE) = 0.f; - m_alphaTo = m_window->m_alpha->goal(); + m_alphaTo = m_window->alphaGoal(WINDOW_ALPHA_FADE); m_posTo = m_window->m_realPosition->goal(); m_sizeTo = m_window->m_realSize->goal(); - m_window->m_alpha->setValueAndWarp(m_alphaFrom); + m_window->alpha(WINDOW_ALPHA_FADE)->setValueAndWarp(m_alphaFrom); m_window->m_realPosition->setValueAndWarp(m_posFrom); m_window->m_realSize->setValueAndWarp(m_sizeFrom); @@ -61,7 +63,7 @@ void CCloseTrackpadGesture::update(const ITrackpadGesture::STrackpadGestureUpdat const auto FADEPERCENT = std::clamp(m_lastDelta / MAX_DISTANCE, 0.F, 1.F); - m_window->m_alpha->setValueAndWarp(lerpVal(m_alphaFrom, m_alphaTo, FADEPERCENT)); + m_window->alpha(WINDOW_ALPHA_FADE)->setValueAndWarp(lerpVal(m_alphaFrom, m_alphaTo, FADEPERCENT)); m_window->m_realPosition->setValueAndWarp(lerpVal(m_posFrom, m_posTo, FADEPERCENT)); m_window->m_realSize->setValueAndWarp(lerpVal(m_sizeFrom, m_sizeTo, FADEPERCENT)); @@ -71,7 +73,7 @@ void CCloseTrackpadGesture::update(const ITrackpadGesture::STrackpadGestureUpdat } void CCloseTrackpadGesture::end(const ITrackpadGesture::STrackpadGestureEnd& e) { - static const auto PTIMEOUT = CConfigValue("gestures:close_max_timeout"); + static const auto PTIMEOUT = CConfigValue("gestures:close_max_timeout"); if (!m_window) return; @@ -81,20 +83,20 @@ void CCloseTrackpadGesture::end(const ITrackpadGesture::STrackpadGestureEnd& e) if (COMPLETION < 0.2F) { // revert the animation g_pHyprRenderer->damageWindow(m_window.lock()); - *m_window->m_alpha = m_alphaFrom; - *m_window->m_realPosition = m_posFrom; - *m_window->m_realSize = m_sizeFrom; + *m_window->alpha(WINDOW_ALPHA_FADE) = m_alphaFrom; + *m_window->m_realPosition = m_posFrom; + *m_window->m_realSize = m_sizeFrom; return; } // commence. Close the window and restore our current state to avoid a harsh anim - const auto CURRENT_ALPHA = m_window->m_alpha->value(); + const auto CURRENT_ALPHA = m_window->alphaValue(WINDOW_ALPHA_FADE); const auto CURRENT_POS = m_window->m_realPosition->value(); const auto CURRENT_SIZE = m_window->m_realSize->value(); - g_pCompositor->closeWindow(m_window.lock()); + Desktop::focusState()->window()->sendClose(); - m_window->m_alpha->setValueAndWarp(CURRENT_ALPHA); + m_window->alpha(WINDOW_ALPHA_FADE)->setValueAndWarp(CURRENT_ALPHA); m_window->m_realPosition->setValueAndWarp(CURRENT_POS); m_window->m_realSize->setValueAndWarp(CURRENT_SIZE); @@ -136,7 +138,7 @@ void CCloseTrackpadGesture::end(const ITrackpadGesture::STrackpadGestureEnd& e) window->layoutTarget()->recalc(); window->updateDecorationValues(); window->sendWindowSize(true); - *window->m_alpha = 1.F; + *window->alpha(WINDOW_ALPHA_FADE) = 1.F; }, nullptr); trackpadCloseTimers.emplace_back(timer); diff --git a/src/managers/input/trackpad/gestures/CursorZoomGesture.cpp b/src/managers/input/trackpad/gestures/CursorZoomGesture.cpp index 97dfe1582..515edbb6b 100644 --- a/src/managers/input/trackpad/gestures/CursorZoomGesture.cpp +++ b/src/managers/input/trackpad/gestures/CursorZoomGesture.cpp @@ -21,7 +21,7 @@ void CCursorZoomTrackpadGesture::begin(const ITrackpadGesture::STrackpadGestureB for (auto const& m : g_pCompositor->m_monitors) { switch (m_mode) { case MODE_TOGGLE: - static auto PZOOMFACTOR = CConfigValue("cursor:zoom_factor"); + static auto PZOOMFACTOR = CConfigValue("cursor:zoom_factor"); *m->m_cursorZoom = m_zoomed ? m_zoomValue : *PZOOMFACTOR; break; case MODE_MULT: *m->m_cursorZoom = std::clamp(m->m_cursorZoom->goal() * m_zoomValue, 1.0F, 100.0F); break; diff --git a/src/managers/input/trackpad/gestures/LuaFunctionGesture.cpp b/src/managers/input/trackpad/gestures/LuaFunctionGesture.cpp new file mode 100644 index 000000000..66f3dd481 --- /dev/null +++ b/src/managers/input/trackpad/gestures/LuaFunctionGesture.cpp @@ -0,0 +1,22 @@ +#include "LuaFunctionGesture.hpp" + +#include "../../../../config/lua/ConfigManager.hpp" + +CLuaFunctionGesture::CLuaFunctionGesture(int ref) : m_luaFnId(ref) { + ; +} + +void CLuaFunctionGesture::begin(const ITrackpadGesture::STrackpadGestureBegin& e) { + ; // intentionally blank +} + +void CLuaFunctionGesture::update(const ITrackpadGesture::STrackpadGestureUpdate& e) { + ; // intentionally blank +} + +void CLuaFunctionGesture::end(const ITrackpadGesture::STrackpadGestureEnd& e) { + if (!Config::Lua::mgr()) + return; + + Config::Lua::mgr()->callLuaFn(m_luaFnId); +} diff --git a/src/managers/input/trackpad/gestures/LuaFunctionGesture.hpp b/src/managers/input/trackpad/gestures/LuaFunctionGesture.hpp new file mode 100644 index 000000000..0fbb870cc --- /dev/null +++ b/src/managers/input/trackpad/gestures/LuaFunctionGesture.hpp @@ -0,0 +1,16 @@ +#pragma once + +#include "ITrackpadGesture.hpp" + +class CLuaFunctionGesture : public ITrackpadGesture { + public: + CLuaFunctionGesture(int ref); + virtual ~CLuaFunctionGesture() = default; + + virtual void begin(const ITrackpadGesture::STrackpadGestureBegin& e); + virtual void update(const ITrackpadGesture::STrackpadGestureUpdate& e); + virtual void end(const ITrackpadGesture::STrackpadGestureEnd& e); + + private: + int m_luaFnId = 0; +}; diff --git a/src/managers/input/trackpad/gestures/WorkspaceSwipeGesture.cpp b/src/managers/input/trackpad/gestures/WorkspaceSwipeGesture.cpp index 0ccd24627..7ede6830b 100644 --- a/src/managers/input/trackpad/gestures/WorkspaceSwipeGesture.cpp +++ b/src/managers/input/trackpad/gestures/WorkspaceSwipeGesture.cpp @@ -9,7 +9,7 @@ void CWorkspaceSwipeGesture::begin(const ITrackpadGesture::STrackpadGestureBegin& e) { ITrackpadGesture::begin(e); - static auto PSWIPENEW = CConfigValue("gestures:workspace_swipe_create_new"); + static auto PSWIPENEW = CConfigValue("gestures:workspace_swipe_create_new"); if (g_pSessionLockManager->isSessionLocked() || g_pUnifiedWorkspaceSwipe->isGestureInProgress()) return; @@ -32,7 +32,7 @@ void CWorkspaceSwipeGesture::update(const ITrackpadGesture::STrackpadGestureUpda const float DELTA = distance(e); - static auto PSWIPEINVR = CConfigValue("gestures:workspace_swipe_invert"); + static auto PSWIPEINVR = CConfigValue("gestures:workspace_swipe_invert"); const double D = g_pUnifiedWorkspaceSwipe->m_delta + (*PSWIPEINVR ? -DELTA : DELTA); g_pUnifiedWorkspaceSwipe->update(D); diff --git a/src/managers/permissions/DynamicPermissionManager.cpp b/src/managers/permissions/DynamicPermissionManager.cpp index d63a72a06..4f299db12 100644 --- a/src/managers/permissions/DynamicPermissionManager.cpp +++ b/src/managers/permissions/DynamicPermissionManager.cpp @@ -76,7 +76,7 @@ void CDynamicPermissionManager::addConfigPermissionRule(const std::string& binar eDynamicPermissionAllowMode CDynamicPermissionManager::clientPermissionMode(wl_client* client, eDynamicPermissionType permission) { - static auto PPERM = CConfigValue("ecosystem:enforce_permissions"); + static auto PPERM = CConfigValue("ecosystem:enforce_permissions"); if (*PPERM == 0) return PERMISSION_RULE_ALLOW_MODE_ALLOW; @@ -148,7 +148,7 @@ eDynamicPermissionAllowMode CDynamicPermissionManager::clientPermissionMode(wl_c } eDynamicPermissionAllowMode CDynamicPermissionManager::clientPermissionModeWithString(pid_t pid, const std::string& str, eDynamicPermissionType permission) { - static auto PPERM = CConfigValue("ecosystem:enforce_permissions"); + static auto PPERM = CConfigValue("ecosystem:enforce_permissions"); if (*PPERM == 0) return PERMISSION_RULE_ALLOW_MODE_ALLOW; diff --git a/src/managers/screenshare/ScreenshareFrame.cpp b/src/managers/screenshare/ScreenshareFrame.cpp index de71fcaac..7a596d0c6 100644 --- a/src/managers/screenshare/ScreenshareFrame.cpp +++ b/src/managers/screenshare/ScreenshareFrame.cpp @@ -18,6 +18,7 @@ using namespace Hyprgraphics::Egl; using namespace Screenshare; +using namespace Desktop::View; CScreenshareFrame::CScreenshareFrame(WP session, bool overlayCursor, bool isFirst) : m_session(session), m_bufferSize(m_session->bufferSize()), m_overlayCursor(overlayCursor), m_isFirst(isFirst) { @@ -259,7 +260,7 @@ void CScreenshareFrame::renderMonitor() { const auto PWORKSPACE = w->m_workspace; - if UNLIKELY (!PWORKSPACE && !w->m_fadingOut && w->m_alpha->value() != 0.f) + if UNLIKELY (!PWORKSPACE && !w->m_fadingOut && w->alphaValue(WINDOW_ALPHA_FADE) * w->alphaValue(WINDOW_ALPHA_FULLSCREEN) != 0.f) continue; const auto renderOffset = PWORKSPACE && !w->m_pinned ? PWORKSPACE->m_renderOffset->value() : Vector2D{}; @@ -441,6 +442,7 @@ bool CScreenshareFrame::copyShm() { if (!m_copied) { LOGM(Log::TRACE, "Copied frame via shm"); m_callback(RESULT_COPIED); + m_copied = true; } return true; diff --git a/src/notification/Notification.cpp b/src/notification/Notification.cpp new file mode 100644 index 000000000..be5b3ca40 --- /dev/null +++ b/src/notification/Notification.cpp @@ -0,0 +1,90 @@ +#include "Notification.hpp" +#include "../debug/log/Logger.hpp" + +using namespace Notification; + +CNotification::CNotification(std::string&& text, float timeout, CHyprColor color, eIcons icon, float fontSize) : + m_createdAt(Time::steadyNow()), m_text(std::move(text)), m_color(color == CHyprColor(0) ? ICONS_COLORS[icon] : color), m_icon(icon), m_fontSize(fontSize), + m_timeMs(std::max(timeout, 1.F)) { + ; +} + +void CNotification::setText(std::string&& text) { + m_cache.textTex.reset(); + m_text = std::move(text); +} + +void CNotification::setColor(CHyprColor color) { + m_color = color; +} + +void CNotification::setIcon(eIcons icon) { + m_cache.iconTex.reset(); + m_icon = icon; +} + +void CNotification::setFontSize(float fontSize) { + m_cache = {}; + m_fontSize = fontSize; +} + +void CNotification::resetTimeout(float timeMs) { + m_timeMs = timeMs; + m_startedAt = Time::steadyNow(); +} + +const std::string& CNotification::text() const { + return m_text; +} + +const CHyprColor& CNotification::color() const { + return m_color; +} + +eIcons CNotification::icon() const { + return m_icon; +} + +float CNotification::fontSize() const { + return m_fontSize; +} + +float CNotification::timeMs() const { + return m_timeMs; +} + +float CNotification::timeElapsedSinceCreationMs() const { + return std::chrono::duration_cast(Time::steadyNow() - m_createdAt).count(); +} + +float CNotification::timeElapsedMs() const { + if (isLocked()) + return m_lastPauseElapsed; + return std::chrono::duration_cast(Time::steadyNow() - m_startedAt).count(); +} + +bool CNotification::isLocked() const { + return m_pauseLocks > 0; +} + +void CNotification::lock() { + m_pauseLocks++; + if (m_pauseLocks == 1) + m_lastPauseElapsed = timeElapsedMs(); +} + +void CNotification::unlock() { + if (m_pauseLocks == 0) { + Log::logger->log(Log::ERR, "CNotification: BUG THIS: unlock on 0 locks??"); + return; + } + + m_pauseLocks--; + + if (m_pauseLocks == 0) + m_startedAt = Time::steadyNow() - std::chrono::milliseconds(sc(m_lastPauseElapsed)); +} + +bool CNotification::gone() const { + return !isLocked() && Time::steadyNow() > m_startedAt + std::chrono::milliseconds(sc(m_timeMs)); +} diff --git a/src/notification/Notification.hpp b/src/notification/Notification.hpp new file mode 100644 index 000000000..a1ae69513 --- /dev/null +++ b/src/notification/Notification.hpp @@ -0,0 +1,62 @@ +#pragma once + +#include "SharedDefines.hpp" +#include "../desktop/DesktopTypes.hpp" +#include "../render/Texture.hpp" +#include "../helpers/time/Time.hpp" + +namespace Notification { + class CNotification { + public: + CNotification(std::string&& text, float timeout, CHyprColor color, eIcons icon, float fontSize); + + struct SRenderCache { + SP textTex; + SP iconTex; + + Vector2D textSize = {}; + Vector2D iconSize = {}; + + PHLMONITORREF monitor; + std::string fontFamily; + int fontSizePx = -1; + eIconBackend iconBackend = ICONS_BACKEND_NONE; + } m_cache; + + void setText(std::string&& text); + void setColor(CHyprColor color); + void setIcon(eIcons icon); + void setFontSize(float fontSize); + void resetTimeout(float timeMs); + + const std::string& text() const; + const CHyprColor& color() const; + eIcons icon() const; + float fontSize() const; + float timeMs() const; + + float timeElapsedSinceCreationMs() const; + float timeElapsedMs() const; + bool gone() const; + + // lock the notification - stops the timer. + bool isLocked() const; + void lock(); + void unlock(); + + private: + const Time::steady_tp m_createdAt = Time::steadyNow(); + + std::string m_text = ""; + CHyprColor m_color; + + eIcons m_icon = ICON_NONE; + float m_fontSize = 13.F; + + Time::steady_tp m_startedAt = Time::steadyNow(); + float m_timeMs = 0.F; + float m_lastPauseElapsed = 0.F; + + size_t m_pauseLocks = 0; + }; +} \ No newline at end of file diff --git a/src/notification/NotificationOverlay.cpp b/src/notification/NotificationOverlay.cpp index 0f38bfa3f..204f230ba 100644 --- a/src/notification/NotificationOverlay.cpp +++ b/src/notification/NotificationOverlay.cpp @@ -5,6 +5,7 @@ #include "NotificationOverlay.hpp" #include "../Compositor.hpp" #include "../config/ConfigValue.hpp" +#include "../desktop/state/FocusState.hpp" #include "../render/pass/RectPassElement.hpp" #include "../render/pass/TexPassElement.hpp" #include "../event/EventBus.hpp" @@ -26,16 +27,20 @@ static inline auto iconBackendFromLayout(PangoLayout* layout) { return ICONS_BACKEND_NONE; } -static constexpr auto ANIM_DURATION_MS = 600.F; -static constexpr auto ANIM_LAG_MS = 100.F; -static constexpr auto NOTIF_LEFTBAR_SIZE = 5.F; -static constexpr auto NOTIF_PAD_X = 20.F; -static constexpr auto NOTIF_PAD_Y = 10.F; -static constexpr auto NOTIF_OFFSET_Y = 10.F; -static constexpr auto NOTIF_GAP_Y = 10.F; -static constexpr auto NOTIF_DAMAGE_PAD_X = 20.F; -static constexpr auto ICON_PAD = 3.F; -static constexpr auto ICON_SCALE = 0.9F; +static constexpr auto ANIM_DURATION_MS = 600.F; +static constexpr auto ANIM_LAG_MS = 100.F; +static constexpr auto NOTIF_LEFTBAR_SIZE = 5.F; +static constexpr auto NOTIF_PAD_X = 20.F; +static constexpr auto NOTIF_PAD_Y = 10.F; +static constexpr auto NOTIF_OFFSET_Y = 10.F; +static constexpr auto NOTIF_GAP_Y = 10.F; +static constexpr auto NOTIF_DAMAGE_PAD_X = 20.F; +static constexpr auto ICON_PAD = 3.F; +static constexpr auto ICON_SCALE = 0.9F; + +static inline CHyprColor resolvedColor(const eIcons icon, const CHyprColor& color) { + return color == CHyprColor(0) ? ICONS_COLORS[icon] : color; +} UP& Notification::overlay() { static UP p = makeUnique(); @@ -79,51 +84,94 @@ eIconBackend CNotificationOverlay::iconBackendForFont(const std::string& fontFam return m_cachedIconBackend; } -void CNotificationOverlay::ensureNotificationCache(SNotification& notif, PHLMONITOR pMonitor, const std::string& fontFamily) { +void CNotificationOverlay::ensureNotificationCache(CNotification& notif, PHLMONITOR pMonitor, const std::string& fontFamily) { const auto iconBackend = iconBackendForFont(fontFamily); - const auto fontSizePx = std::clamp(sc(notif.fontSize * ((pMonitor->m_pixelSize.x * pMonitor->m_scale) / 1920.f)), 8, 40); + const auto fontSizePx = std::clamp(sc(notif.fontSize() * ((pMonitor->m_pixelSize.x * pMonitor->m_scale) / 1920.F)), 8, 40); - const bool cacheValid = notif.cache.monitor == pMonitor && notif.cache.fontFamily == fontFamily && notif.cache.fontSizePx == fontSizePx && - notif.cache.iconBackend == iconBackend && notif.cache.textTex && (notif.icon == ICON_NONE || notif.cache.iconTex); + const bool cacheValid = notif.m_cache.monitor == pMonitor && notif.m_cache.fontFamily == fontFamily && notif.m_cache.fontSizePx == fontSizePx && + notif.m_cache.iconBackend == iconBackend && notif.m_cache.textTex && (notif.icon() == ICON_NONE || notif.m_cache.iconTex); if (cacheValid) return; - notif.cache = {}; - notif.cache.monitor = pMonitor; - notif.cache.fontFamily = fontFamily; - notif.cache.fontSizePx = fontSizePx; - notif.cache.iconBackend = iconBackend; + notif.m_cache = {}; + notif.m_cache.monitor = pMonitor; + notif.m_cache.fontFamily = fontFamily; + notif.m_cache.fontSizePx = fontSizePx; + notif.m_cache.iconBackend = iconBackend; - notif.cache.textTex = g_pHyprRenderer->renderText(notif.text, CHyprColor{1.F, 1.F, 1.F, 1.F}, fontSizePx, false, fontFamily); - if (notif.cache.textTex) - notif.cache.textSize = notif.cache.textTex->m_size; + notif.m_cache.textTex = g_pHyprRenderer->renderText(notif.text(), CHyprColor{1.F, 1.F, 1.F, 1.F}, fontSizePx, false, fontFamily); + if (notif.m_cache.textTex) + notif.m_cache.textSize = notif.m_cache.textTex->m_size; - if (notif.icon != ICON_NONE) { - const auto iconGlyph = ICONS_ARRAY[iconBackend][notif.icon]; + if (notif.icon() != ICON_NONE) { + const auto iconGlyph = ICONS_ARRAY[iconBackend][notif.icon()]; const auto iconSizePx = std::max(8, sc(std::round(fontSizePx * ICON_SCALE))); - notif.cache.iconTex = g_pHyprRenderer->renderText(iconGlyph, CHyprColor{1.F, 1.F, 1.F, 1.F}, iconSizePx, false, fontFamily); - if (notif.cache.iconTex) - notif.cache.iconSize = notif.cache.iconTex->m_size; + notif.m_cache.iconTex = g_pHyprRenderer->renderText(iconGlyph, CHyprColor{1.F, 1.F, 1.F, 1.F}, iconSizePx, false, fontFamily); + if (notif.m_cache.iconTex) + notif.m_cache.iconSize = notif.m_cache.iconTex->m_size; } } -void CNotificationOverlay::addNotification(const std::string& text, const CHyprColor& color, const float timeMs, const eIcons icon, const float fontSize) { - const auto PNOTIF = m_notifications.emplace_back(makeUnique()).get(); - - PNOTIF->text = text; - PNOTIF->color = color == CHyprColor(0) ? ICONS_COLORS[icon] : color; - PNOTIF->started.reset(); - PNOTIF->timeMs = timeMs; - PNOTIF->icon = icon; - PNOTIF->fontSize = fontSize; - +void CNotificationOverlay::scheduleFrames() const { for (auto const& m : g_pCompositor->m_monitors) { g_pCompositor->scheduleFrameForMonitor(m); } } +CBox CNotificationOverlay::notificationDamageForMonitor(PHLMONITOR pMonitor) { + if (!pMonitor || m_notifications.empty()) + return {}; + + const float reservedTop = pMonitor->m_reservedArea.top(); + const float reservedRight = pMonitor->m_reservedArea.right(); + + float offsetY = NOTIF_OFFSET_Y + reservedTop; + float maxWidth = 0.F; + + static auto fontFamily = CConfigValue("misc:font_family"); + + for (auto const& notif : m_notifications) { + ensureNotificationCache(*notif, pMonitor, *fontFamily); + + const auto ICONPADFORNOTIF = notif->icon() == ICON_NONE ? 0.F : ICON_PAD; + const auto ICONW = notif->m_cache.iconSize.x; + const auto ICONH = notif->m_cache.iconSize.y; + const auto TEXTW = notif->m_cache.textSize.x; + const auto TEXTH = notif->m_cache.textSize.y; + const auto NOTIFSIZE = Vector2D{TEXTW + NOTIF_PAD_X + ICONW + (2 * ICONPADFORNOTIF), std::max(TEXTH, ICONH) + NOTIF_PAD_Y}; + + offsetY += NOTIFSIZE.y + NOTIF_GAP_Y; + + if (maxWidth < NOTIFSIZE.x) + maxWidth = NOTIFSIZE.x; + } + + return CBox{sc(pMonitor->m_position.x + pMonitor->m_size.x - reservedRight - maxWidth - NOTIF_DAMAGE_PAD_X), sc(pMonitor->m_position.y + reservedTop), + sc(maxWidth + NOTIF_DAMAGE_PAD_X), sc(offsetY + NOTIF_OFFSET_Y - reservedTop)}; +} + +SP CNotificationOverlay::addNotification(const std::string& text, const CHyprColor& color, const float timeMs, const eIcons icon, const float fontSize) { + auto PNOTIF = makeShared(std::string{text}, timeMs, color, icon, fontSize); + + m_notifications.emplace_back(PNOTIF); + + const auto focusedMonitor = Desktop::focusState()->monitor(); + if (focusedMonitor) { + const auto damage = notificationDamageForMonitor(focusedMonitor); + + if (damage.width > 0 && damage.height > 0) + g_pHyprRenderer->damageBox(damage); + + if (m_lastDamage.width > 0 && m_lastDamage.height > 0) + g_pHyprRenderer->damageBox(m_lastDamage); + } + + scheduleFrames(); + return PNOTIF; +} + void CNotificationOverlay::dismissNotifications(const int amount) { if (amount == -1) m_notifications.clear(); @@ -135,13 +183,26 @@ void CNotificationOverlay::dismissNotifications(const int amount) { } } - for (auto const& m : g_pCompositor->m_monitors) { - g_pCompositor->scheduleFrameForMonitor(m); - } + scheduleFrames(); +} + +void CNotificationOverlay::dismissNotification(const SP& notification) { + if (!notification) + return; + + std::erase_if(m_notifications, [&](const auto& notif) { return notif == notification; }); + scheduleFrames(); +} + +std::vector> CNotificationOverlay::getNotifications() const { + return m_notifications; } CBox CNotificationOverlay::drawNotifications(PHLMONITOR pMonitor) { - float offsetY = NOTIF_OFFSET_Y; + const float reservedTopPx = pMonitor->m_reservedArea.top() * pMonitor->m_scale; + const float reservedRightPx = pMonitor->m_reservedArea.right() * pMonitor->m_scale; + + float offsetY = NOTIF_OFFSET_Y + reservedTopPx; float maxWidth = 0; const auto MONSIZE = pMonitor->m_transformedSize; @@ -152,42 +213,45 @@ CBox CNotificationOverlay::drawNotifications(PHLMONITOR pMonitor) { for (auto const& notif : m_notifications) { ensureNotificationCache(*notif, pMonitor, *fontFamily); - const auto ICONPADFORNOTIF = notif->icon == ICON_NONE ? 0.F : ICON_PAD; - const auto ICONW = notif->cache.iconSize.x; - const auto ICONH = notif->cache.iconSize.y; - const auto TEXTW = notif->cache.textSize.x; - const auto TEXTH = notif->cache.textSize.y; - const auto NOTIFSIZE = Vector2D{TEXTW + NOTIF_PAD_X + ICONW + 2 * ICONPADFORNOTIF, std::max(TEXTH, ICONH) + NOTIF_PAD_Y}; + const auto ICONPADFORNOTIF = notif->icon() == ICON_NONE ? 0.F : ICON_PAD; + const auto ICONW = notif->m_cache.iconSize.x; + const auto ICONH = notif->m_cache.iconSize.y; + const auto TEXTW = notif->m_cache.textSize.x; + const auto TEXTH = notif->m_cache.textSize.y; + const auto NOTIFSIZE = Vector2D{TEXTW + NOTIF_PAD_X + ICONW + (2 * ICONPADFORNOTIF), std::max(TEXTH, ICONH) + NOTIF_PAD_Y}; - const float elapsed = notif->started.getMillis(); - const float lifeMs = std::max(notif->timeMs, 1.F); + const float elapsedSinceCreation = notif->timeElapsedSinceCreationMs(); + const float elapsed = notif->timeElapsedMs(); + const float lifeMs = notif->timeMs(); // first rect (bg, col) - const float FIRSTRECTANIMP = std::clamp( - (elapsed > (ANIM_DURATION_MS - ANIM_LAG_MS) ? (elapsed > lifeMs - (ANIM_DURATION_MS - ANIM_LAG_MS) ? lifeMs - elapsed : (ANIM_DURATION_MS - ANIM_LAG_MS)) : elapsed) / - (ANIM_DURATION_MS - ANIM_LAG_MS), - 0.F, 1.F); + const float FIRSTRECTANIMP = std::clamp((elapsedSinceCreation > (ANIM_DURATION_MS - ANIM_LAG_MS) ? + (elapsed > lifeMs - (ANIM_DURATION_MS - ANIM_LAG_MS) ? lifeMs - elapsed : (ANIM_DURATION_MS - ANIM_LAG_MS)) : + elapsedSinceCreation) / + (ANIM_DURATION_MS - ANIM_LAG_MS), + 0.F, 1.F); const float FIRSTRECTPERC = FIRSTRECTANIMP >= 0.99f ? 1.f : PBEZIER->getYForPoint(FIRSTRECTANIMP); // second rect (fg, black) - const float SECONDRECTANIMP = - std::clamp((elapsed > ANIM_DURATION_MS ? (elapsed > lifeMs - ANIM_DURATION_MS ? lifeMs - elapsed : ANIM_DURATION_MS) : elapsed) / ANIM_DURATION_MS, 0.F, 1.F); + const float SECONDRECTANIMP = std::clamp( + (elapsedSinceCreation > ANIM_DURATION_MS ? (elapsed > lifeMs - ANIM_DURATION_MS ? lifeMs - elapsed : ANIM_DURATION_MS) : elapsedSinceCreation) / ANIM_DURATION_MS, 0.F, + 1.F); const float SECONDRECTPERC = SECONDRECTANIMP >= 0.99f ? 1.f : PBEZIER->getYForPoint(SECONDRECTANIMP); // third rect (horiz, col) const float THIRDRECTPERC = std::clamp(elapsed / lifeMs, 0.F, 1.F); - const float firstRectX = MONSIZE.x - (NOTIFSIZE.x + NOTIF_LEFTBAR_SIZE) * FIRSTRECTPERC; + const float firstRectX = MONSIZE.x - reservedRightPx - (NOTIFSIZE.x + NOTIF_LEFTBAR_SIZE) * FIRSTRECTPERC; const float firstRectW = (NOTIFSIZE.x + NOTIF_LEFTBAR_SIZE) * FIRSTRECTPERC; - const float secondRectX = MONSIZE.x - NOTIFSIZE.x * SECONDRECTPERC; + const float secondRectX = MONSIZE.x - reservedRightPx - NOTIFSIZE.x * SECONDRECTPERC; const float secondRectW = NOTIFSIZE.x * SECONDRECTPERC; CRectPassElement::SRectData bgData; bgData.box = {firstRectX, offsetY, firstRectW, NOTIFSIZE.y}; - bgData.color = notif->color; + bgData.color = notif->color(); g_pHyprRenderer->m_renderPass.add(makeUnique(bgData)); CRectPassElement::SRectData fgData; @@ -197,20 +261,20 @@ CBox CNotificationOverlay::drawNotifications(PHLMONITOR pMonitor) { CRectPassElement::SRectData progressData; progressData.box = {secondRectX + 3, offsetY + NOTIFSIZE.y - 4, THIRDRECTPERC * std::max(0.0, NOTIFSIZE.x - 6.0), 2}; - progressData.color = notif->color; + progressData.color = notif->color(); g_pHyprRenderer->m_renderPass.add(makeUnique(progressData)); - if (notif->icon != ICON_NONE && notif->cache.iconTex) { + if (notif->icon() != ICON_NONE && notif->m_cache.iconTex) { CTexPassElement::SRenderData iconData; - iconData.tex = notif->cache.iconTex; + iconData.tex = notif->m_cache.iconTex; iconData.box = {secondRectX + NOTIF_LEFTBAR_SIZE + ICONPADFORNOTIF - 1, offsetY - 2 + std::round((NOTIFSIZE.y - ICONH) / 2.0), ICONW, ICONH}; iconData.a = 1.F; g_pHyprRenderer->m_renderPass.add(makeUnique(std::move(iconData))); } - if (notif->cache.textTex) { + if (notif->m_cache.textTex) { CTexPassElement::SRenderData textData; - textData.tex = notif->cache.textTex; + textData.tex = notif->m_cache.textTex; textData.box = {secondRectX + NOTIF_LEFTBAR_SIZE + ICONW + 2 * ICONPADFORNOTIF, offsetY - 2 + std::round((NOTIFSIZE.y - TEXTH) / 2.0), TEXTW, TEXTH}; textData.a = 1.F; g_pHyprRenderer->m_renderPass.add(makeUnique(std::move(textData))); @@ -224,7 +288,7 @@ CBox CNotificationOverlay::drawNotifications(PHLMONITOR pMonitor) { } // cleanup notifs - std::erase_if(m_notifications, [](const auto& notif) { return notif->started.getMillis() > notif->timeMs; }); + std::erase_if(m_notifications, [&](const auto& notif) { return notif->gone(); }); return CBox{sc(pMonitor->m_position.x + pMonitor->m_size.x - maxWidth - NOTIF_DAMAGE_PAD_X), sc(pMonitor->m_position.y), sc(maxWidth + NOTIF_DAMAGE_PAD_X), sc(offsetY + NOTIF_OFFSET_Y)}; diff --git a/src/notification/NotificationOverlay.hpp b/src/notification/NotificationOverlay.hpp index b516cf22c..ed802a581 100644 --- a/src/notification/NotificationOverlay.hpp +++ b/src/notification/NotificationOverlay.hpp @@ -1,70 +1,35 @@ #pragma once -#include "../defines.hpp" -#include "../helpers/time/Timer.hpp" -#include "../render/Texture.hpp" -#include "../SharedDefs.hpp" +#include "SharedDefines.hpp" +#include "Notification.hpp" -#include +#include "../desktop/DesktopTypes.hpp" namespace Notification { - enum eIconBackend : uint8_t { - ICONS_BACKEND_NONE = 0, - ICONS_BACKEND_NF, - ICONS_BACKEND_FA - }; - - static const std::array, 3 /* backends */> ICONS_ARRAY = { - std::array{"[!]", "[i]", "[Hint]", "[Err]", "[?]", "[ok]", ""}, - std::array{"", "", "", "", "", "󰸞", ""}, std::array{"", "", "", "", "", ""}}; - static const std::array ICONS_COLORS = {CHyprColor{1.0, 204 / 255.0, 102 / 255.0, 1.0}, - CHyprColor{128 / 255.0, 255 / 255.0, 255 / 255.0, 1.0}, - CHyprColor{179 / 255.0, 255 / 255.0, 204 / 255.0, 1.0}, - CHyprColor{255 / 255.0, 77 / 255.0, 77 / 255.0, 1.0}, - CHyprColor{255 / 255.0, 204 / 255.0, 153 / 255.0, 1.0}, - CHyprColor{128 / 255.0, 255 / 255.0, 128 / 255.0, 1.0}, - CHyprColor{0, 0, 0, 1.0}}; - - struct SNotification { - struct SRenderCache { - SP textTex; - SP iconTex; - - Vector2D textSize = {}; - Vector2D iconSize = {}; - - PHLMONITORREF monitor; - std::string fontFamily; - int fontSizePx = -1; - eIconBackend iconBackend = ICONS_BACKEND_NONE; - } cache; - - std::string text = ""; - CHyprColor color; - CTimer started; - float timeMs = 0; - eIcons icon = ICON_NONE; - float fontSize = 13.f; - }; - class CNotificationOverlay { public: CNotificationOverlay(); ~CNotificationOverlay(); - void draw(PHLMONITOR pMonitor); - void addNotification(const std::string& text, const CHyprColor& color, const float timeMs, const eIcons icon = ICON_NONE, const float fontSize = 13.f); - void dismissNotifications(const int amount); - bool hasAny(); + void draw(PHLMONITOR pMonitor); + SP addNotification(const std::string& text, const CHyprColor& color, const float timeMs, const eIcons icon = ICON_NONE, const float fontSize = 13.f); + void dismissNotifications(const int amount); + void dismissNotification(const SP& notification); + + std::vector> getNotifications() const; + + bool hasAny(); private: - void ensureNotificationCache(SNotification& notif, PHLMONITOR pMonitor, const std::string& fontFamily); + void ensureNotificationCache(CNotification& notif, PHLMONITOR pMonitor, const std::string& fontFamily); eIconBackend iconBackendForFont(const std::string& fontFamily); + CBox notificationDamageForMonitor(PHLMONITOR pMonitor); CBox drawNotifications(PHLMONITOR pMonitor); + void scheduleFrames() const; CBox m_lastDamage; - std::vector> m_notifications; + std::vector> m_notifications; std::string m_cachedIconBackendFontFamily; eIconBackend m_cachedIconBackend = ICONS_BACKEND_NONE; @@ -72,4 +37,4 @@ namespace Notification { }; UP& overlay(); -} \ No newline at end of file +} diff --git a/src/notification/SharedDefines.hpp b/src/notification/SharedDefines.hpp new file mode 100644 index 000000000..61ab7899f --- /dev/null +++ b/src/notification/SharedDefines.hpp @@ -0,0 +1,26 @@ +#pragma once + +#include "../SharedDefs.hpp" +#include "../helpers/Color.hpp" + +#include +#include + +namespace Notification { + enum eIconBackend : uint8_t { + ICONS_BACKEND_NONE = 0, + ICONS_BACKEND_NF, + ICONS_BACKEND_FA + }; + + static const std::array, 3 /* backends */> ICONS_ARRAY = { + std::array{"[!]", "[i]", "[Hint]", "[Err]", "[?]", "[ok]", ""}, + std::array{"", "", "", "", "", "󰸞", ""}, std::array{"", "", "", "", "", ""}}; + static const std::array ICONS_COLORS = {CHyprColor{1.0, 204 / 255.0, 102 / 255.0, 1.0}, + CHyprColor{128 / 255.0, 255 / 255.0, 255 / 255.0, 1.0}, + CHyprColor{179 / 255.0, 255 / 255.0, 204 / 255.0, 1.0}, + CHyprColor{255 / 255.0, 77 / 255.0, 77 / 255.0, 1.0}, + CHyprColor{255 / 255.0, 204 / 255.0, 153 / 255.0, 1.0}, + CHyprColor{128 / 255.0, 255 / 255.0, 128 / 255.0, 1.0}, + CHyprColor{0, 0, 0, 1.0}}; +} diff --git a/src/plugins/PluginAPI.cpp b/src/plugins/PluginAPI.cpp index 4722924d2..c49fa0a02 100644 --- a/src/plugins/PluginAPI.cpp +++ b/src/plugins/PluginAPI.cpp @@ -4,6 +4,7 @@ #include "../plugins/PluginSystem.hpp" #include "../managers/eventLoop/EventLoopManager.hpp" #include "../config/legacy/ConfigManager.hpp" +#include "../config/lua/ConfigManager.hpp" #include "../notification/NotificationOverlay.hpp" #include "../layout/target/Target.hpp" #include "../layout/supplementary/WorkspaceAlgoMatcher.hpp" @@ -175,6 +176,9 @@ APICALL bool HyprlandAPI::removeWindowDecoration(HANDLE handle, IHyprWindowDecor APICALL bool HyprlandAPI::addConfigValue(HANDLE handle, const std::string& name, const Hyprlang::CConfigValue& value) { auto* const PLUGIN = g_pPluginSystem->getPluginByHandle(handle); + if (Config::mgr()->type() != Config::CONFIG_LEGACY) + return false; + if (!g_pPluginSystem->m_allowConfigVars) return false; @@ -191,6 +195,9 @@ APICALL bool HyprlandAPI::addConfigValue(HANDLE handle, const std::string& name, APICALL bool HyprlandAPI::addConfigKeyword(HANDLE handle, const std::string& name, Hyprlang::PCONFIGHANDLERFUNC fn, Hyprlang::SHandlerOptions opts) { auto* const PLUGIN = g_pPluginSystem->getPluginByHandle(handle); + if (Config::mgr()->type() != Config::CONFIG_LEGACY) + return false; + if (!g_pPluginSystem->m_allowConfigVars) return false; @@ -204,6 +211,9 @@ APICALL bool HyprlandAPI::addConfigKeyword(HANDLE handle, const std::string& nam APICALL Hyprlang::CConfigValue* HyprlandAPI::getConfigValue(HANDLE handle, const std::string& name) { auto* const PLUGIN = g_pPluginSystem->getPluginByHandle(handle); + if (Config::mgr()->type() != Config::CONFIG_LEGACY) + return nullptr; + if (!PLUGIN) return nullptr; @@ -431,3 +441,58 @@ APICALL bool HyprlandAPI::unregisterHyprCtlCommand(HANDLE handle, SP value) { + auto* const PLUGIN = g_pPluginSystem->getPluginByHandle(handle); + + if (!PLUGIN) + return false; + + PLUGIN->m_registeredApiValues.emplace_back(value); + + auto ret = Config::mgr()->registerPluginValue(handle, value); + if (!ret) { + Log::logger->log(Log::ERR, "failed to register plugin value \"{}\": {}", value->name(), ret.error()); + return false; + } + + value->commence(); + + return true; +} + +APICALL bool HyprlandAPI::addLuaFunction(HANDLE handle, const std::string& namespace_, const std::string& name, PLUGIN_LUA_FN fn) { + auto* const PLUGIN = g_pPluginSystem->getPluginByHandle(handle); + + if (!PLUGIN) + return false; + + if (!Config::mgr() || Config::mgr()->type() != Config::CONFIG_LUA) + return false; + + auto ret = dynamicPointerCast(WP(Config::mgr()))->registerPluginLuaFunction(handle, namespace_, name, fn); + if (!ret) { + Log::logger->log(Log::ERR, "failed to register lua plugin function {}.{}: {}", namespace_, name, ret.error()); + return false; + } + + return true; +} + +APICALL bool HyprlandAPI::removeLuaFunction(HANDLE handle, const std::string& namespace_, const std::string& name) { + auto* const PLUGIN = g_pPluginSystem->getPluginByHandle(handle); + + if (!PLUGIN) + return false; + + if (!Config::mgr() || Config::mgr()->type() != Config::CONFIG_LUA) + return false; + + auto ret = dynamicPointerCast(WP(Config::mgr()))->unregisterPluginLuaFunction(handle, namespace_, name); + if (!ret) { + Log::logger->log(Log::ERR, "failed to unregister lua plugin function {}.{}: {}", namespace_, name, ret.error()); + return false; + } + + return true; +} diff --git a/src/plugins/PluginAPI.hpp b/src/plugins/PluginAPI.hpp index 77bb9926e..5321c8db1 100644 --- a/src/plugins/PluginAPI.hpp +++ b/src/plugins/PluginAPI.hpp @@ -25,6 +25,7 @@ Feel like the API is missing something you'd like to use in your plugin? Open an #include "../SharedDefs.hpp" #include "../defines.hpp" #include "../version.h" +#include "../config/values/types/IValue.hpp" #include #include @@ -71,6 +72,9 @@ class IHyprLayout; class IHyprWindowDecoration; struct SConfigValue; class Hypr_dummyClass {}; +extern "C" { +struct lua_State; +} namespace Layout { class ITiledAlgorithm; @@ -78,6 +82,7 @@ namespace Layout { }; using HOOK_CALLBACK_FN = Hypr_dummyClass; +using PLUGIN_LUA_FN = int (*)(lua_State* L); /* These methods are for the plugin to implement @@ -129,16 +134,20 @@ namespace HyprlandAPI { After you have registered ALL of your config values, you may call `getConfigValue` returns: true on success, false on fail + + deprecated: please use V2 */ - APICALL bool addConfigValue(HANDLE handle, const std::string& name, const Hyprlang::CConfigValue& value); + APICALL [[deprecated]] bool addConfigValue(HANDLE handle, const std::string& name, const Hyprlang::CConfigValue& value); /* Add a config keyword. This method may only be called in "pluginInit" returns: true on success, false on fail + + deprecated: please use V2 */ - APICALL bool addConfigKeyword(HANDLE handle, const std::string& name, Hyprlang::PCONFIGHANDLERFUNC fn, Hyprlang::SHandlerOptions opts); + APICALL [[deprecated]] bool addConfigKeyword(HANDLE handle, const std::string& name, Hyprlang::PCONFIGHANDLERFUNC fn, Hyprlang::SHandlerOptions opts); /* Get a config value. @@ -147,8 +156,10 @@ namespace HyprlandAPI { returns: a pointer to the config value struct, which is guaranteed to be valid for the life of this plugin, unless another `addConfigValue` is called afterwards. nullptr on error. + + Deprecated: please use V2 */ - APICALL Hyprlang::CConfigValue* getConfigValue(HANDLE handle, const std::string& name); + APICALL [[deprecated]] Hyprlang::CConfigValue* getConfigValue(HANDLE handle, const std::string& name); /* Deprecated: doesn't do anything anymore, use Event::bus() @@ -325,6 +336,32 @@ namespace HyprlandAPI { returns: true on success. False otherwise. */ APICALL bool unregisterHyprCtlCommand(HANDLE handle, SP cmd); + + /* + Add a new config value. Keep the pointer, you can use it for retrieving the value. + + Please note this value name must start with plugin:, e.g. plugin:my_plugin:value + + returns: true on success. False otherwise. + */ + APICALL bool addConfigValueV2(HANDLE handle, SP value); + + /* + Register a plugin-owned Lua C callback under hl.plugin... + + Callbacks are removed automatically on plugin unload. + + returns: true on success. False otherwise. + */ + APICALL bool addLuaFunction(HANDLE handle, const std::string& namespace_, const std::string& name, PLUGIN_LUA_FN fn); + + /* + Unregister a plugin-owned Lua C callback from hl.plugin... + + returns: true on success. False otherwise. + */ + APICALL bool removeLuaFunction(HANDLE handle, const std::string& namespace_, const std::string& name); + }; // NOLINTBEGIN diff --git a/src/plugins/PluginSystem.cpp b/src/plugins/PluginSystem.cpp index 18b6adde4..b39131a5a 100644 --- a/src/plugins/PluginSystem.cpp +++ b/src/plugins/PluginSystem.cpp @@ -2,7 +2,7 @@ #include #include -#include "../config/legacy/ConfigManager.hpp" +#include "../config/ConfigManager.hpp" #include "../debug/HyprCtl.hpp" #include "../managers/eventLoop/EventLoopManager.hpp" #include "../managers/permissions/DynamicPermissionManager.hpp" @@ -175,9 +175,8 @@ void CPluginSystem::unloadPlugin(const CPlugin* plugin, bool eject) { HyprlandAPI::unregisterHyprCtlCommand(plugin->m_handle, sp); } - // FIXME: this is wrong and if I forget to fix this by the time I add another config parser - // this will explode and I will be mad because I am a RETARD - Config::Legacy::mgr()->removePluginConfig(plugin->m_handle); + if (Config::mgr()) + Config::mgr()->onPluginUnload(plugin->m_handle); // save these two for dlclose and a log, // as erase_if will kill the pointer diff --git a/src/plugins/PluginSystem.hpp b/src/plugins/PluginSystem.hpp index 85afaefa4..4921f0d6d 100644 --- a/src/plugins/PluginSystem.hpp +++ b/src/plugins/PluginSystem.hpp @@ -3,6 +3,7 @@ #include "../defines.hpp" #include "../helpers/defer/Promise.hpp" #include "../helpers/time/Timer.hpp" +#include "../config/values/types/IValue.hpp" #include "PluginAPI.hpp" #include "../managers/permissions/DynamicPermissionManager.hpp" #include @@ -25,9 +26,10 @@ class CPlugin { std::vector m_registeredDecorations; //std::vector>> m_registeredCallbacks; - std::vector m_registeredDispatchers; - std::vector> m_registeredHyprctlCommands; - std::vector m_registeredAlgos; + std::vector m_registeredDispatchers; + std::vector> m_registeredHyprctlCommands; + std::vector m_registeredAlgos; + std::vector> m_registeredApiValues; }; class CPluginSystem { diff --git a/src/protocols/CTMControl.cpp b/src/protocols/CTMControl.cpp index 3a590cdea..16ebb50dd 100644 --- a/src/protocols/CTMControl.cpp +++ b/src/protocols/CTMControl.cpp @@ -108,7 +108,7 @@ void CHyprlandCTMControlProtocol::destroyResource(CHyprlandCTMControlResource* r } bool CHyprlandCTMControlProtocol::isCTMAnimationEnabled() { - static auto PENABLEANIM = CConfigValue("render:ctm_animation"); + static auto PENABLEANIM = CConfigValue("render:ctm_animation"); if (*PENABLEANIM == 2 /* auto */) { if (!g_pHyprRenderer->isNvidia()) diff --git a/src/protocols/Fifo.cpp b/src/protocols/Fifo.cpp index 355644d96..7ba1bf20f 100644 --- a/src/protocols/Fifo.cpp +++ b/src/protocols/Fifo.cpp @@ -30,7 +30,7 @@ CFifoResource::CFifoResource(UP&& resource_, SP s if (!m_surface->m_current.barrierSet) { // that might mean an empty commit with a barrier_set alone - static const auto PPEND = CConfigValue("debug:fifo_pending_workaround"); + static const auto PPEND = CConfigValue("debug:fifo_pending_workaround"); if (!m_surface->m_pending.fifoScheduled) m_surface->m_pending.fifoScheduled = checkMonitors(*PPEND); @@ -44,7 +44,7 @@ CFifoResource::CFifoResource(UP&& resource_, SP s if (!state || !state->surfaceLocked) return; - static const auto PPEND = CConfigValue("debug:fifo_pending_workaround"); + static const auto PPEND = CConfigValue("debug:fifo_pending_workaround"); //#TODO: // this feels wrong, but if we have no pending frames, presented might never come because diff --git a/src/protocols/ForeignToplevelWlr.cpp b/src/protocols/ForeignToplevelWlr.cpp index 56591261f..146465c2e 100644 --- a/src/protocols/ForeignToplevelWlr.cpp +++ b/src/protocols/ForeignToplevelWlr.cpp @@ -131,7 +131,7 @@ CForeignToplevelHandleWlr::CForeignToplevelHandleWlr(SPcloseWindow(PWINDOW); + PWINDOW->sendClose(); }); } diff --git a/src/protocols/LinuxDMABUF.cpp b/src/protocols/LinuxDMABUF.cpp index 7886826b4..b4c4ae288 100644 --- a/src/protocols/LinuxDMABUF.cpp +++ b/src/protocols/LinuxDMABUF.cpp @@ -27,7 +27,7 @@ static std::optional devIDFromFD(int fd) { CDMABUFFormatTable::CDMABUFFormatTable(SDMABUFTranche _rendererTranche, std::vector> tranches_) : m_rendererTranche(_rendererTranche), m_monitorTranches(tranches_) { - static const auto PSKIP_NON_KMS = CConfigValue("quirks:skip_non_kms_dmabuf_formats"); + static const auto PSKIP_NON_KMS = CConfigValue("quirks:skip_non_kms_dmabuf_formats"); std::vector formatsVec; std::set> formats; @@ -486,7 +486,7 @@ CLinuxDMABufV1Protocol::CLinuxDMABufV1Protocol(const wl_interface* iface, const }); static auto configReloaded = Event::bus()->m_events.config.reloaded.listen([this] { - static const auto PSKIP_NON_KMS = CConfigValue("quirks:skip_non_kms_dmabuf_formats"); + static const auto PSKIP_NON_KMS = CConfigValue("quirks:skip_non_kms_dmabuf_formats"); static auto prev = *PSKIP_NON_KMS; if (prev != *PSKIP_NON_KMS) { prev = *PSKIP_NON_KMS; diff --git a/src/protocols/PointerConstraints.cpp b/src/protocols/PointerConstraints.cpp index a78f3548b..8bfa8b3a5 100644 --- a/src/protocols/PointerConstraints.cpp +++ b/src/protocols/PointerConstraints.cpp @@ -27,7 +27,7 @@ CPointerConstraint::CPointerConstraint(SP resource_, SPsetSetRegion([this](CZwpLockedPointerV1* p, wl_resource* region) { onSetRegion(region); }); resource_->setSetCursorPositionHint([this](CZwpLockedPointerV1* p, wl_fixed_t x, wl_fixed_t y) { - static auto PXWLFORCESCALEZERO = CConfigValue("xwayland:force_zero_scaling"); + static auto PXWLFORCESCALEZERO = CConfigValue("xwayland:force_zero_scaling"); if (!m_hlSurface) return; diff --git a/src/protocols/PrimarySelection.cpp b/src/protocols/PrimarySelection.cpp index 7da1fa0ab..04cb79d75 100644 --- a/src/protocols/PrimarySelection.cpp +++ b/src/protocols/PrimarySelection.cpp @@ -112,7 +112,7 @@ CPrimarySelectionDevice::CPrimarySelectionDevice(SPsetOnDestroy([this](CZwpPrimarySelectionDeviceV1* r) { PROTO::primarySelection->destroyResource(this); }); m_resource->setSetSelection([](CZwpPrimarySelectionDeviceV1* r, wl_resource* sourceR, uint32_t serial) { - static auto PPRIMARYSEL = CConfigValue("misc:middle_click_paste"); + static auto PPRIMARYSEL = CConfigValue("misc:middle_click_paste"); if (!*PPRIMARYSEL) { LOGM(Log::DEBUG, "Ignoring primary selection: disabled in config"); diff --git a/src/protocols/VirtualKeyboard.cpp b/src/protocols/VirtualKeyboard.cpp index 22f696329..34cc638d6 100644 --- a/src/protocols/VirtualKeyboard.cpp +++ b/src/protocols/VirtualKeyboard.cpp @@ -11,7 +11,7 @@ using namespace Hyprutils::OS; static std::string virtualKeyboardNameForWlClient(wl_client* client) { std::string name = "hl-virtual-keyboard"; - static auto PVKNAMEPROC = CConfigValue("misc:name_vk_after_proc"); + static auto PVKNAMEPROC = CConfigValue("misc:name_vk_after_proc"); if (!*PVKNAMEPROC) return name; diff --git a/src/protocols/XDGForeignV2.cpp b/src/protocols/XDGForeignV2.cpp index f67cb86cf..198fceb32 100644 --- a/src/protocols/XDGForeignV2.cpp +++ b/src/protocols/XDGForeignV2.cpp @@ -46,6 +46,19 @@ std::string_view CXDGExportedResourceV2::handle() const { CXDGForeignExporterProtocolV2::CXDGForeignExporterProtocolV2(const wl_interface* iface, const int& ver, const std::string& name) : IWaylandProtocol(iface, ver, name) {} +static SP xdgToplevelFromWlSurface(wl_resource* surface) { + const auto wlSurf = CWLSurfaceResource::fromResource(surface); + + if (!wlSurf || !wlSurf->m_role || wlSurf->m_role->role() != SURFACE_ROLE_XDG_SHELL) + return nullptr; + + const auto xdgSurfResource = sc(wlSurf->m_role.get())->m_xdgSurface.lock(); + if (!xdgSurfResource || xdgSurfResource->m_toplevel.expired()) + return nullptr; + + return xdgSurfResource->m_toplevel.lock(); +} + void CXDGForeignExporterProtocolV2::bindManager(wl_client* client, void* data, uint32_t ver, uint32_t id) { const auto RESOURCE = m_exporters.emplace_back(makeUnique(client, ver, id)).get(); @@ -56,21 +69,16 @@ void CXDGForeignExporterProtocolV2::bindManager(wl_client* client, void* data, u } RESOURCE->setExportToplevel([this](CZxdgExporterV2* exporter, uint32_t id, wl_resource* surface) { - auto wlSurf = CWLSurfaceResource::fromResource(surface); + auto TOPLEVEL = xdgToplevelFromWlSurface(surface); - if (wlSurf->m_role != SURFACE_ROLE_XDG_SHELL) { + if (!TOPLEVEL) { exporter->error(zxdgExporterV2Error::ZXDG_EXPORTER_V2_ERROR_INVALID_SURFACE, "surface must be an xdg_toplevel"); return; } - auto xdgSurfResource = sc(wlSurf->m_role.get())->m_xdgSurface.lock(); - if (xdgSurfResource->m_toplevel.expired()) - return; - - auto xdgSurf = xdgSurfResource->m_toplevel.lock(); - const std::string HANDLE = g_pTokenManager->getRandomUUID(); + const std::string HANDLE = g_pTokenManager->getRandomUUID(); const auto [ELM, EMPLACED] = - m_exported.emplace(HANDLE, makeShared(makeShared(exporter->client(), exporter->version(), id), xdgSurf, HANDLE)); + m_exported.emplace(HANDLE, makeShared(makeShared(exporter->client(), exporter->version(), id), TOPLEVEL, HANDLE)); // This should only happen if we have our generated handles collide. if UNLIKELY (!EMPLACED) { @@ -102,34 +110,52 @@ void CXDGForeignExporterProtocolV2::destroyExported(CXDGExportedResourceV2* r) { CXDGImportedResourceV2::CXDGImportedResourceV2(SP imported, SP exported, const std::string& handle) : m_resource(imported), m_exported(exported), m_handle(handle) { - if UNLIKELY (!m_resource->resource() || m_exported.expired()) + if UNLIKELY (!good()) return; m_resource->setData(this); m_resource->setSetParentOf([this](CZxdgImportedV2* r, wl_resource* surf) { - const auto CHILDSURF = CWLSurfaceResource::fromResource(surf); + if (m_invalid) + return; - if (CHILDSURF->m_role != SURFACE_ROLE_XDG_SHELL) { + const auto CHILDTOPLEVEL = xdgToplevelFromWlSurface(surf); + + if (!CHILDTOPLEVEL) { m_resource->error(zxdgImportedV2Error::ZXDG_IMPORTED_V2_ERROR_INVALID_SURFACE, "surface must be an xdg_toplevel"); return; } - const auto CHILDXDGSURF = sc(CHILDSURF->m_role.get())->m_xdgSurface.lock(); - if (CHILDXDGSURF->m_toplevel.expired()) - return; - if LIKELY (auto exportedTopLevel = m_exported->xdgSurf(); !exportedTopLevel.expired()) - CHILDXDGSURF->m_toplevel->setNewParent(exportedTopLevel.lock()); + CHILDTOPLEVEL->setNewParent(exportedTopLevel.lock()); }); - m_listeners.exportedDestroyed = m_exported->m_events.destroy.listen([this]() { PROTO::xdgForeignImporter->destroyImported(this); }); + if (exported) + m_listeners.exportedDestroyed = exported->m_events.destroy.listen([this]() { invalidate(); }); + m_resource->setDestroy([this](CZxdgImportedV2*) { PROTO::xdgForeignImporter->destroyImported(this); }); m_resource->setOnDestroy([this](CZxdgImportedV2*) { PROTO::xdgForeignImporter->destroyImported(this); }); + + if (m_exported.expired()) + invalidate(); } -CXDGImportedResourceV2::~CXDGImportedResourceV2() { - m_resource->sendDestroyed(); +CXDGImportedResourceV2::~CXDGImportedResourceV2() {} + +bool CXDGImportedResourceV2::good() const { + return m_resource->resource(); +} + +void CXDGImportedResourceV2::invalidate() { + if (m_invalid) + return; + + m_invalid = true; + + if (!m_destroyedSent && m_resource && m_resource->resource()) { + m_destroyedSent = true; + m_resource->sendDestroyed(); + } } CXDGForeignImporterProtocolV2::CXDGForeignImporterProtocolV2(const wl_interface* iface, const int& ver, const std::string& name) : IWaylandProtocol(iface, ver, name) { @@ -151,15 +177,11 @@ void CXDGForeignImporterProtocolV2::bindManager(wl_client* client, void* data, u auto imported = m_imports.emplace_back(makeUnique(makeShared(importer->client(), importer->version(), id), exported, HANDLE)).get(); - if UNLIKELY (!imported->m_resource->resource()) { + if UNLIKELY (!imported->good()) { wl_client_post_no_memory(importer->client()); m_imports.pop_back(); return; } - - // Couldn't find the handle. - if UNLIKELY (imported->m_exported.expired()) - destroyImported(imported); }); RESOURCE->setDestroy([this](CZxdgImporterV2* r) { onImporterDestroyed(r); }); diff --git a/src/protocols/XDGForeignV2.hpp b/src/protocols/XDGForeignV2.hpp index 78c8dbf71..092945d4d 100644 --- a/src/protocols/XDGForeignV2.hpp +++ b/src/protocols/XDGForeignV2.hpp @@ -42,10 +42,15 @@ class CXDGImportedResourceV2 { CXDGImportedResourceV2(SP resource, SP exported, const std::string& handle); ~CXDGImportedResourceV2(); + bool good() const; + void invalidate(); + private: SP m_resource; WP m_exported; std::string m_handle; + bool m_invalid = false; + bool m_destroyedSent = false; struct { CHyprSignalListener exportedDestroyed; diff --git a/src/protocols/XDGOutput.cpp b/src/protocols/XDGOutput.cpp index 3553a74b8..c29fcdd11 100644 --- a/src/protocols/XDGOutput.cpp +++ b/src/protocols/XDGOutput.cpp @@ -105,7 +105,7 @@ CXDGOutput::CXDGOutput(SP resource_, PHLMONITOR monitor_) : m_mon } void CXDGOutput::sendDetails() { - static auto PXWLFORCESCALEZERO = CConfigValue("xwayland:force_zero_scaling"); + static auto PXWLFORCESCALEZERO = CConfigValue("xwayland:force_zero_scaling"); if UNLIKELY (!m_monitor || !m_outputProto || m_outputProto->isDefunct()) return; diff --git a/src/protocols/core/Compositor.cpp b/src/protocols/core/Compositor.cpp index 293cd2858..0200ebd4a 100644 --- a/src/protocols/core/Compositor.cpp +++ b/src/protocols/core/Compositor.cpp @@ -604,7 +604,7 @@ void CWLSurfaceResource::commitState(SSurfaceState& state) { } PImageDescription CWLSurfaceResource::getPreferredImageDescription() { - static const auto PFORCE_HDR = CConfigValue("quirks:prefer_hdr"); + static const auto PFORCE_HDR = CConfigValue("quirks:prefer_hdr"); const auto WINDOW = m_hlSurface ? Desktop::View::CWindow::fromView(m_hlSurface->view()) : nullptr; if (*PFORCE_HDR == 1 || (*PFORCE_HDR == 2 && m_hlSurface && WINDOW && WINDOW->m_class == "gamescope")) diff --git a/src/render/GLRenderer.cpp b/src/render/GLRenderer.cpp index 129295acd..35aca7db8 100644 --- a/src/render/GLRenderer.cpp +++ b/src/render/GLRenderer.cpp @@ -83,7 +83,7 @@ bool CHyprGLRenderer::beginRenderInternal(PHLMONITOR pMonitor, CRegion& damage, void CHyprGLRenderer::endRender(const std::function& renderingDoneCallback) { const auto PMONITOR = g_pHyprRenderer->m_renderData.pMonitor; - static auto PNVIDIAANTIFLICKER = CConfigValue("opengl:nvidia_anti_flicker"); + static auto PNVIDIAANTIFLICKER = CConfigValue("opengl:nvidia_anti_flicker"); g_pHyprRenderer->m_renderData.damage = m_renderPass.render(g_pHyprRenderer->m_renderData.damage); diff --git a/src/render/OpenGL.cpp b/src/render/OpenGL.cpp index bf914e31f..6bb04cfae 100644 --- a/src/render/OpenGL.cpp +++ b/src/render/OpenGL.cpp @@ -59,6 +59,7 @@ using namespace Hyprutils::OS; using namespace NColorManagement; +using namespace Desktop::View; using namespace Render; using namespace Render::GL; @@ -315,7 +316,7 @@ CHyprOpenGLImpl::CHyprOpenGLImpl() : m_drmFD(g_pCompositor->m_drmRenderNode.fd > loadGLProc(&m_proc.eglQueryDisplayAttribEXT, "eglQueryDisplayAttribEXT"); } - static const auto GLDEBUG = CConfigValue("debug:gl_debugging"); + static const auto GLDEBUG = CConfigValue("debug:gl_debugging"); if (EGLEXTENSIONS.contains("EGL_KHR_debug") && *GLDEBUG) { loadGLProc(&m_proc.eglDebugMessageControlKHR, "eglDebugMessageControlKHR"); static const EGLAttrib debugAttrs[] = { @@ -781,8 +782,8 @@ void CHyprOpenGLImpl::begin(PHLMONITOR pMonitor, const CRegion& damage_, SP("cursor:zoom_disable_aa"); - static auto PFPINVALIDATE = CConfigValue("debug:invalidate_fp16"); + static auto PZOOMDISABLEAA = CConfigValue("cursor:zoom_disable_aa"); + static auto PFPINVALIDATE = CConfigValue("debug:invalidate_fp16"); auto& m_renderData = g_pHyprRenderer->m_renderData; const auto PMONITOR = m_renderData.pMonitor; TRACY_GPU_ZONE("RenderEnd"); @@ -851,7 +852,7 @@ void CHyprOpenGLImpl::end() { m_renderData.pMonitor.reset(); - static const auto GLDEBUG = CConfigValue("debug:gl_debugging"); + static const auto GLDEBUG = CConfigValue("debug:gl_debugging"); if (*GLDEBUG) { // check for gl errors @@ -875,7 +876,7 @@ const std::array FRAG_SHADERS = { bool CHyprOpenGLImpl::initShaders(const std::string& path) { auto shaders = makeShared(); - static const auto PCM = CConfigValue("render:cm_enabled"); + static const auto PCM = CConfigValue("render:cm_enabled"); try { auto shaderLoader = makeUnique(SHADER_INCLUDES, FRAG_SHADERS, path); @@ -904,7 +905,7 @@ bool CHyprOpenGLImpl::initShaders(const std::string& path) { void CHyprOpenGLImpl::applyScreenShader(const std::string& path) { - static auto PDT = CConfigValue("debug:damage_tracking"); + static auto PDT = CConfigValue("debug:damage_tracking"); m_finalScreenShader->destroy(); @@ -1201,8 +1202,8 @@ void CHyprOpenGLImpl::passCMUniforms(WP shader, const PImageDescription } WP CHyprOpenGLImpl::renderToOutputInternal() { - static const auto PDT = CConfigValue("debug:damage_tracking"); - static const auto PCURSORTIMEOUT = CConfigValue("cursor:inactive_timeout"); + static const auto PDT = CConfigValue("debug:damage_tracking"); + static const auto PCURSORTIMEOUT = CConfigValue("cursor:inactive_timeout"); auto& m_renderData = g_pHyprRenderer->m_renderData; @@ -1274,8 +1275,8 @@ WP CHyprOpenGLImpl::renderToOutputInternal() { } WP CHyprOpenGLImpl::renderToFBInternal(SP tex, const STextureRenderData& data, eTextureType texType, const CBox& newBox) { - static const auto PENABLECM = CConfigValue("render:cm_enabled"); - static auto PBLEND = CConfigValue("render:use_shader_blur_blend"); + static const auto PENABLECM = CConfigValue("render:cm_enabled"); + static auto PBLEND = CConfigValue("render:use_shader_blur_blend"); auto& m_renderData = g_pHyprRenderer->m_renderData; @@ -1632,10 +1633,10 @@ SP CHyprOpenGLImpl::blurFramebufferWithDamage(float a, CRegion* or const auto& glMatrix = g_pHyprRenderer->projectBoxToTarget(MONITORBOX, TRANSFORM); // get the config settings - static auto PBLURSIZE = CConfigValue("decoration:blur:size"); - static auto PBLURPASSES = CConfigValue("decoration:blur:passes"); - static auto PBLURVIBRANCY = CConfigValue("decoration:blur:vibrancy"); - static auto PBLURVIBRANCYDARKNESS = CConfigValue("decoration:blur:vibrancy_darkness"); + static auto PBLURSIZE = CConfigValue("decoration:blur:size"); + static auto PBLURPASSES = CConfigValue("decoration:blur:passes"); + static auto PBLURVIBRANCY = CConfigValue("decoration:blur:vibrancy"); + static auto PBLURVIBRANCYDARKNESS = CConfigValue("decoration:blur:vibrancy_darkness"); const auto BLUR_PASSES = std::clamp(*PBLURPASSES, sc(1), sc(8)); @@ -1654,9 +1655,9 @@ SP CHyprOpenGLImpl::blurFramebufferWithDamage(float a, CRegion* or // Begin with base color adjustments - global brightness and contrast // TODO: make this a part of the first pass maybe to save on a drawcall? { - static auto PBLURCONTRAST = CConfigValue("decoration:blur:contrast"); - static auto PBLURBRIGHTNESS = CConfigValue("decoration:blur:brightness"); - static auto PBLEND = CConfigValue("render:use_shader_blur_blend"); + static auto PBLURCONTRAST = CConfigValue("decoration:blur:contrast"); + static auto PBLURBRIGHTNESS = CConfigValue("decoration:blur:brightness"); + static auto PBLEND = CConfigValue("render:use_shader_blur_blend"); PMIRRORSWAPFB->bind(); @@ -1773,8 +1774,8 @@ SP CHyprOpenGLImpl::blurFramebufferWithDamage(float a, CRegion* or // finalize the image { - static auto PBLURNOISE = CConfigValue("decoration:blur:noise"); - static auto PBLURBRIGHTNESS = CConfigValue("decoration:blur:brightness"); + static auto PBLURNOISE = CConfigValue("decoration:blur:noise"); + static auto PBLURBRIGHTNESS = CConfigValue("decoration:blur:brightness"); if (currentRenderToFB == PMIRRORFB) PMIRRORSWAPFB->bind(); @@ -1839,9 +1840,9 @@ SP CHyprOpenGLImpl::blurFramebufferWithDamage(float a, CRegion* or } void CHyprOpenGLImpl::preRender(PHLMONITOR pMonitor) { - static auto PBLURNEWOPTIMIZE = CConfigValue("decoration:blur:new_optimizations"); - static auto PBLURXRAY = CConfigValue("decoration:blur:xray"); - static auto PBLUR = CConfigValue("decoration:blur:enabled"); + static auto PBLURNEWOPTIMIZE = CConfigValue("decoration:blur:new_optimizations"); + static auto PBLURXRAY = CConfigValue("decoration:blur:xray"); + static auto PBLUR = CConfigValue("decoration:blur:enabled"); if (!*PBLURNEWOPTIMIZE || !pMonitor->m_blurFBDirty || !*PBLUR) return; @@ -1867,7 +1868,8 @@ void CHyprOpenGLImpl::preRender(PHLMONITOR pMonitor) { const auto PSURFACE = pWindow->wlSurface()->resource(); const auto PWORKSPACE = pWindow->m_workspace; - const float A = pWindow->m_alpha->value() * pWindow->m_activeInactiveAlpha->value() * PWORKSPACE->m_alpha->value(); + const float A = pWindow->alphaValue(WINDOW_ALPHA_FADE) * pWindow->alphaValue(WINDOW_ALPHA_FULLSCREEN) * pWindow->alphaValue(WINDOW_ALPHA_LAYOUT) * + pWindow->alphaValue(WINDOW_ALPHA_ACTIVE) * PWORKSPACE->m_alpha->value(); if (A >= 1.f) { // if (PSURFACE->opaque) @@ -1888,7 +1890,7 @@ void CHyprOpenGLImpl::preRender(PHLMONITOR pMonitor) { bool hasWindows = false; for (auto const& w : g_pCompositor->m_windows) { - if (w->m_workspace == pMonitor->m_activeWorkspace && !w->isHidden() && w->m_isMapped && (!w->m_isFloating || *PBLURXRAY)) { + if (w->m_workspace == pMonitor->m_activeWorkspace && w->visible() && w->m_isMapped && (!w->m_isFloating || *PBLURXRAY)) { // check if window is valid if (!windowShouldBeBlurred(w)) @@ -1927,7 +1929,7 @@ void CHyprOpenGLImpl::renderTextureWithBlurInternal(SP tex, const CBox TRACY_GPU_ZONE("RenderTextureWithBlur"); - static auto PBLEND = CConfigValue("render:use_shader_blur_blend"); + static auto PBLEND = CConfigValue("render:use_shader_blur_blend"); const auto NEEDS_STENCIL = data.discardMode != 0 && (!data.blockBlurOptimization || (data.discardMode & DISCARD_ALPHA)); if (!*PBLEND) { @@ -1979,7 +1981,7 @@ void CHyprOpenGLImpl::renderTextureWithBlurInternal(SP tex, const CBox transformedBox.width / m_renderData.pMonitor->m_pixelSize.x * m_renderData.pMonitor->m_transformedSize.x, transformedBox.height / m_renderData.pMonitor->m_pixelSize.y * m_renderData.pMonitor->m_transformedSize.y}; - static auto PBLURIGNOREOPACITY = CConfigValue("decoration:blur:ignore_opacity"); + static auto PBLURIGNOREOPACITY = CConfigValue("decoration:blur:ignore_opacity"); g_pHyprRenderer->pushMonitorTransformEnabled(true); bool renderModif = g_pHyprRenderer->m_renderData.renderModif.enabled; @@ -2228,7 +2230,7 @@ void CHyprOpenGLImpl::renderRoundedShadow(const CBox& box, int round, float roun CBox newBox = box; g_pHyprRenderer->m_renderData.renderModif.applyToBox(newBox); - static auto PSHADOWPOWER = CConfigValue("decoration:shadow:render_power"); + static auto PSHADOWPOWER = CConfigValue("decoration:shadow:render_power"); const auto SHADOWPOWER = std::clamp(sc(*PSHADOWPOWER), 1, 4); diff --git a/src/render/Renderer.cpp b/src/render/Renderer.cpp index a77e855d1..b5a32a2b9 100644 --- a/src/render/Renderer.cpp +++ b/src/render/Renderer.cpp @@ -68,6 +68,7 @@ using namespace Hyprutils::Utils; using namespace Hyprutils::OS; using enum NContentType::eContentType; using namespace NColorManagement; +using namespace Desktop::View; using namespace Render; extern "C" { @@ -178,7 +179,7 @@ IHyprRenderer::IHyprRenderer() { m_renderUnfocusedTimer = makeShared( std::nullopt, [this](SP self, void* data) { - static auto PFPS = CConfigValue("misc:render_unfocused_fps"); + static auto PFPS = CConfigValue("misc:render_unfocused_fps"); if (m_renderUnfocused.empty()) return; @@ -234,8 +235,8 @@ bool IHyprRenderer::shouldRenderWindow(PHLWINDOW pWindow, PHLMONITOR pMonitor) { return true; // if the window is being moved to a workspace that is not invisible, and the alpha is > 0.F, render it. - if (pWindow->m_monitorMovedFrom != -1 && pWindow->m_movingToWorkspaceAlpha->isBeingAnimated() && pWindow->m_movingToWorkspaceAlpha->value() > 0.F && pWindow->m_workspace && - !pWindow->m_workspace->isVisible()) + if (pWindow->m_monitorMovedFrom != -1 && pWindow->alpha(WINDOW_ALPHA_MOVE_TO_WORKSPACE)->isBeingAnimated() && pWindow->alphaValue(WINDOW_ALPHA_MOVE_TO_WORKSPACE) > 0.F && + pWindow->m_workspace && !pWindow->m_workspace->isVisible()) return true; const auto PWINDOWWORKSPACE = pWindow->m_workspace; @@ -244,7 +245,8 @@ bool IHyprRenderer::shouldRenderWindow(PHLWINDOW pWindow, PHLMONITOR pMonitor) { return true; // if hidden behind fullscreen - if (PWINDOWWORKSPACE->m_hasFullscreenWindow && !pWindow->isFullscreen() && (!pWindow->m_isFloating || !pWindow->m_createdOverFullscreen) && pWindow->m_alpha->value() == 0) + if (PWINDOWWORKSPACE->m_hasFullscreenWindow && !pWindow->isAllowedOverFullscreen() && + pWindow->alphaValue(WINDOW_ALPHA_FADE) * pWindow->alphaValue(WINDOW_ALPHA_FULLSCREEN) == 0) return false; if (!PWINDOWWORKSPACE->m_renderOffset->isBeingAnimated() && !PWINDOWWORKSPACE->m_alpha->isBeingAnimated() && !PWINDOWWORKSPACE->isVisible()) @@ -325,7 +327,7 @@ void IHyprRenderer::renderWorkspaceWindowsFullscreen(PHLMONITOR pMonitor, PHLWOR if (!shouldRenderWindow(w, pMonitor)) continue; - if (w->m_alpha->value() == 0.f) + if (w->alphaValue(WINDOW_ALPHA_FADE) * w->alphaValue(WINDOW_ALPHA_FULLSCREEN) == 0.f) continue; if (w->isFullscreen()) @@ -356,6 +358,9 @@ void IHyprRenderer::renderWorkspaceWindowsFullscreen(PHLMONITOR pMonitor, PHLWOR if (pWorkspace->m_isSpecialWorkspace && w->m_monitor != pWorkspace->m_monitor) continue; // special on another are rendered as a part of the base pass + if (w->isFadingOutUnderFullscreen()) + continue; // render these over fullscreen so the fade-out is visible + renderWindow(w, pMonitor, time, true, RENDER_PASS_ALL); } @@ -394,8 +399,8 @@ void IHyprRenderer::renderWorkspaceWindowsFullscreen(PHLMONITOR pMonitor, PHLWOR // then render windows over fullscreen. for (auto const& w : g_pCompositor->m_windows) { - const bool shouldSkipWindow = w->workspaceID() != pWorkspaceWindow->workspaceID() || !w->m_isFloating || (!w->m_createdOverFullscreen && !w->m_pinned) || - (!w->m_isMapped && !w->m_fadingOut) || w->isFullscreen(); + const bool shouldSkipWindow = + w->workspaceID() != pWorkspaceWindow->workspaceID() || !w->m_isFloating || !w->shouldRenderOverFullscreen() || (!w->m_isMapped && !w->m_fadingOut) || w->isFullscreen(); if (shouldSkipWindow) continue; @@ -527,6 +532,9 @@ void IHyprRenderer::renderWindow(PHLWINDOW pWindow, PHLMONITOR pMonitor, const T if (pWindow->isHidden() && !standalone) return; + if (!standalone && pWindow->effectiveAlpha() == 0.F && !pWindow->m_alpha.isBeingAnimated()) + return; + if (pWindow->m_fadingOut) { if (pMonitor == pWindow->m_monitor) // TODO: fix this renderSnapshot(pWindow); @@ -540,7 +548,7 @@ void IHyprRenderer::renderWindow(PHLWINDOW pWindow, PHLMONITOR pMonitor, const T const auto PWORKSPACE = pWindow->m_workspace; const auto REALPOS = pWindow->m_realPosition->value() + (pWindow->m_pinned ? Vector2D{} : PWORKSPACE->m_renderOffset->value()); - static auto PDIMAROUND = CConfigValue("decoration:dim_around"); + static auto PDIMAROUND = CConfigValue("decoration:dim_around"); CSurfacePassElement::SRenderData renderdata = {pMonitor, time}; CBox textureBox = {REALPOS.x, REALPOS.y, std::max(pWindow->m_realSize->value().x, 5.0), std::max(pWindow->m_realSize->value().y, 5.0)}; @@ -569,9 +577,10 @@ void IHyprRenderer::renderWindow(PHLWINDOW pWindow, PHLMONITOR pMonitor, const T renderdata.surface = pWindow->wlSurface()->resource(); renderdata.dontRound = pWindow->isEffectiveInternalFSMode(FSMODE_FULLSCREEN); - renderdata.fadeAlpha = pWindow->m_alpha->value() * (pWindow->m_pinned || USE_WORKSPACE_FADE_ALPHA ? 1.f : PWORKSPACE->m_alpha->value()) * - (USE_WORKSPACE_FADE_ALPHA ? pWindow->m_movingToWorkspaceAlpha->value() : 1.F) * pWindow->m_movingFromWorkspaceAlpha->value(); - renderdata.alpha = pWindow->m_activeInactiveAlpha->value(); + renderdata.fadeAlpha = pWindow->alphaValue(WINDOW_ALPHA_FADE) * pWindow->alphaValue(WINDOW_ALPHA_FULLSCREEN) * pWindow->alphaValue(WINDOW_ALPHA_LAYOUT) * + (pWindow->m_pinned || USE_WORKSPACE_FADE_ALPHA ? 1.f : PWORKSPACE->m_alpha->value()) * + (USE_WORKSPACE_FADE_ALPHA ? pWindow->alphaValue(WINDOW_ALPHA_MOVE_TO_WORKSPACE) : 1.F) * pWindow->alphaValue(WINDOW_ALPHA_MOVE_FROM_WORKSPACE); + renderdata.alpha = pWindow->alphaValue(WINDOW_ALPHA_ACTIVE); renderdata.decorate = decorate && !pWindow->m_X11DoesntWantBorders && !pWindow->isEffectiveInternalFSMode(FSMODE_FULLSCREEN); renderdata.rounding = standalone || renderdata.dontRound ? 0 : pWindow->rounding() * pMonitor->m_scale; renderdata.roundingPower = standalone || renderdata.dontRound ? 2.0f : pWindow->roundingPower(); @@ -643,7 +652,7 @@ void IHyprRenderer::renderWindow(PHLWINDOW pWindow, PHLMONITOR pMonitor, const T } } - static auto PXWLUSENN = CConfigValue("xwayland:use_nearest_neighbor"); + static auto PXWLUSENN = CConfigValue("xwayland:use_nearest_neighbor"); if ((pWindow->m_isX11 && *PXWLUSENN) || pWindow->m_ruleApplicator->nearestNeighbor().valueOrDefault()) renderdata.useNearestNeighbor = true; @@ -713,7 +722,7 @@ void IHyprRenderer::renderWindow(PHLWINDOW pWindow, PHLMONITOR pMonitor, const T renderdata.squishOversized = false; // don't squish popups renderdata.popup = true; - static CConfigValue PBLURIGNOREA = CConfigValue("decoration:blur:popups_ignorealpha"); + static CConfigValue PBLURIGNOREA = CConfigValue("decoration:blur:popups_ignorealpha"); renderdata.blur = shouldBlur(pWindow->m_popupHead); @@ -837,8 +846,8 @@ UP IHyprRenderer::bindTempFB(SP fb) { } bool IHyprRenderer::preBlurQueued(PHLMONITORREF pMonitor) { - static auto PBLURNEWOPTIMIZE = CConfigValue("decoration:blur:new_optimizations"); - static auto PBLUR = CConfigValue("decoration:blur:enabled"); + static auto PBLURNEWOPTIMIZE = CConfigValue("decoration:blur:new_optimizations"); + static auto PBLUR = CConfigValue("decoration:blur:enabled"); if (!pMonitor) return false; @@ -898,7 +907,7 @@ void IHyprRenderer::renderLayer(PHLLS pLayer, PHLMONITOR pMonitor, const Time::s (lockscreen && !pLayer->m_ruleApplicator->aboveLock().valueOrDefault())) return; - static auto PDIMAROUND = CConfigValue("decoration:dim_around"); + static auto PDIMAROUND = CConfigValue("decoration:dim_around"); if (*PDIMAROUND && pLayer->m_ruleApplicator->dimAround().valueOrDefault() && !m_bRenderingSnapshot && !popups) { CRectPassElement::SRectData data; @@ -1001,9 +1010,9 @@ void IHyprRenderer::renderIMEPopup(CInputPopup* pPopup, PHLMONITOR pMonitor, con renderdata.w = SURF->m_current.size.x; renderdata.h = SURF->m_current.size.y; - static auto PBLUR = CConfigValue("decoration:blur:enabled"); - static auto PBLURIMES = CConfigValue("decoration:blur:input_methods"); - static auto PBLURIGNOREA = CConfigValue("decoration:blur:input_methods_ignorealpha"); + static auto PBLUR = CConfigValue("decoration:blur:enabled"); + static auto PBLURIMES = CConfigValue("decoration:blur:input_methods"); + static auto PBLURIGNOREA = CConfigValue("decoration:blur:input_methods_ignorealpha"); renderdata.blur = *PBLURIMES && *PBLUR; if (renderdata.blur) { @@ -1057,11 +1066,11 @@ void IHyprRenderer::renderSessionLockSurface(WP pSurface, P } void IHyprRenderer::renderAllClientsForWorkspace(PHLMONITOR pMonitor, PHLWORKSPACE pWorkspace, const Time::steady_tp& time, const Vector2D& translate, const float& scale) { - static auto PDIMSPECIAL = CConfigValue("decoration:dim_special"); - static auto PBLURSPECIAL = CConfigValue("decoration:blur:special"); - static auto PBLUR = CConfigValue("decoration:blur:enabled"); - static auto PXPMODE = CConfigValue("render:xp_mode"); - static auto PSESSIONLOCKXRAY = CConfigValue("misc:session_lock_xray"); + static auto PDIMSPECIAL = CConfigValue("decoration:dim_special"); + static auto PBLURSPECIAL = CConfigValue("decoration:blur:special"); + static auto PBLUR = CConfigValue("decoration:blur:enabled"); + static auto PXPMODE = CConfigValue("render:xp_mode"); + static auto PSESSIONLOCKXRAY = CConfigValue("misc:session_lock_xray"); if UNLIKELY (!pMonitor) return; @@ -1243,15 +1252,15 @@ SP IHyprRenderer::getBackground(PHLMONITOR pMonitor) { } void IHyprRenderer::renderBackground(PHLMONITOR pMonitor) { - static auto PRENDERTEX = CConfigValue("misc:disable_hyprland_logo"); - static auto PBACKGROUNDCOLOR = CConfigValue("misc:background_color"); - static auto PNOSPLASH = CConfigValue("misc:disable_splash_rendering"); + static auto PRENDERTEX = CConfigValue("misc:disable_hyprland_logo"); + static auto PBACKGROUNDCOLOR = CConfigValue("misc:background_color"); + static auto PNOSPLASH = CConfigValue("misc:disable_splash_rendering"); if (*PRENDERTEX /* inverted cfg flag */ || pMonitor->m_backgroundOpacity->isBeingAnimated()) m_renderPass.add(makeUnique(CClearPassElement::SClearData{CHyprColor(*PBACKGROUNDCOLOR)})); if (!*PRENDERTEX) { - static auto PBACKGROUNDCOLOR = CConfigValue("misc:background_color"); + static auto PBACKGROUNDCOLOR = CConfigValue("misc:background_color"); if (!pMonitor->m_background) pMonitor->m_background = getBackground(pMonitor); @@ -1302,8 +1311,8 @@ void IHyprRenderer::requestBackgroundResource() { if (m_backgroundResource) return; - static auto PNOWALLPAPER = CConfigValue("misc:disable_hyprland_logo"); - static auto PFORCEWALLPAPER = CConfigValue("misc:force_default_wallpaper"); + static auto PNOWALLPAPER = CConfigValue("misc:disable_hyprland_logo"); + static auto PFORCEWALLPAPER = CConfigValue("misc:force_default_wallpaper"); const auto FORCEWALLPAPER = std::clamp(*PFORCEWALLPAPER, sc(-1), sc(2)); @@ -1400,8 +1409,8 @@ SP IHyprRenderer::getBlurTexture(PHLMONITORREF pMonitor) { } bool IHyprRenderer::shouldUseNewBlurOptimizations(PHLLS pLayer, PHLWINDOW pWindow) { - static auto PBLURNEWOPTIMIZE = CConfigValue("decoration:blur:new_optimizations"); - static auto PBLURXRAY = CConfigValue("decoration:blur:xray"); + static auto PBLURNEWOPTIMIZE = CConfigValue("decoration:blur:new_optimizations"); + static auto PBLURXRAY = CConfigValue("decoration:blur:xray"); if (!getBlurTexture(m_renderData.pMonitor)) return false; @@ -1483,9 +1492,10 @@ SP IHyprRenderer::renderText(const std::string& text, CHyprColor col, pango_layout_set_ellipsize(layoutText, PANGO_ELLIPSIZE_END); } - pango_layout_get_size(layoutText, &textW, &textH); - textW /= PANGO_SCALE; - textH /= PANGO_SCALE; + PangoRectangle rectInk = {}, rectLog = {}; + pango_layout_get_pixel_extents(layoutText, &rectInk, &rectLog); + textW = std::max(rectLog.width, rectInk.x + rectInk.width); + textH = std::max(rectLog.height, rectInk.y + rectInk.height); pango_font_description_free(pangoFD); g_object_unref(layoutText); @@ -1596,7 +1606,7 @@ void IHyprRenderer::renderLockscreen(PHLMONITOR pMonitor, const Time::steady_tp& } void IHyprRenderer::renderSessionLockPrimer(PHLMONITOR pMonitor) { - static auto PSESSIONLOCKXRAY = CConfigValue("misc:session_lock_xray"); + static auto PSESSIONLOCKXRAY = CConfigValue("misc:session_lock_xray"); if (*PSESSIONLOCKXRAY) return; @@ -1904,11 +1914,11 @@ void IHyprRenderer::renderMonitor(PHLMONITOR pMonitor, bool commit) { static std::chrono::high_resolution_clock::time_point renderStartOverlay = std::chrono::high_resolution_clock::now(); static std::chrono::high_resolution_clock::time_point endRenderOverlay = std::chrono::high_resolution_clock::now(); - static auto PDEBUGOVERLAY = CConfigValue("debug:overlay"); - static auto PDAMAGETRACKINGMODE = CConfigValue("debug:damage_tracking"); - static auto PDAMAGEBLINK = CConfigValue("debug:damage_blink"); - static auto PSOLDAMAGE = CConfigValue("debug:render_solitary_wo_damage"); - static auto PVFR = CConfigValue("debug:vfr"); + static auto PDEBUGOVERLAY = CConfigValue("debug:overlay"); + static auto PDAMAGETRACKINGMODE = CConfigValue("debug:damage_tracking"); + static auto PDAMAGEBLINK = CConfigValue("debug:damage_blink"); + static auto PSOLDAMAGE = CConfigValue("debug:render_solitary_wo_damage"); + static auto PVFR = CConfigValue("debug:vfr"); static int damageBlinkCleanup = 0; // because double-buffered @@ -2210,10 +2220,10 @@ static hdr_output_metadata createHDRMetadata(SImageDescription settings, S } void IHyprRenderer::handleFullscreenSettings(PHLMONITOR pMonitor) { - static auto PCT = CConfigValue("render:send_content_type"); - static auto PAUTOHDR = CConfigValue("render:cm_auto_hdr"); - static auto PNONSHADER = CConfigValue("render:non_shader_cm"); - static auto PNSINTEROP = CConfigValue("render:non_shader_cm_interop"); + static auto PCT = CConfigValue("render:send_content_type"); + static auto PAUTOHDR = CConfigValue("render:cm_auto_hdr"); + static auto PNONSHADER = CConfigValue("render:non_shader_cm"); + static auto PNSINTEROP = CConfigValue("render:non_shader_cm_interop"); const bool configuredHDR = (pMonitor->m_cmType == NCMType::CM_HDR_EDID || pMonitor->m_cmType == NCMType::CM_HDR); bool wantHDR = configuredHDR; @@ -2628,7 +2638,7 @@ void IHyprRenderer::damageSurface(SP pSurface, double x, dou m->addDamage(damageBoxForEach); } - static auto PLOGDAMAGE = CConfigValue("debug:log_damage"); + static auto PLOGDAMAGE = CConfigValue("debug:log_damage"); if (*PLOGDAMAGE) Log::logger->log(Log::DEBUG, "Damage: Surface (extents): xy: {}, {} wh: {}, {}", damageBox.pixman()->extents.x1, damageBox.pixman()->extents.y1, @@ -2653,7 +2663,7 @@ void IHyprRenderer::damageWindow(PHLWINDOW pWindow, bool forceFull) { } } - static auto PLOGDAMAGE = CConfigValue("debug:log_damage"); + static auto PLOGDAMAGE = CConfigValue("debug:log_damage"); if (*PLOGDAMAGE) Log::logger->log(Log::DEBUG, "Damage: Window ({}): xy: {}, {} wh: {}, {}", pWindow->m_title, windowBox.x, windowBox.y, windowBox.width, windowBox.height); @@ -2666,7 +2676,7 @@ void IHyprRenderer::damageMonitor(PHLMONITOR pMonitor) { CBox damageBox = {0, 0, INT16_MAX, INT16_MAX}; pMonitor->addDamage(damageBox); - static auto PLOGDAMAGE = CConfigValue("debug:log_damage"); + static auto PLOGDAMAGE = CConfigValue("debug:log_damage"); if (*PLOGDAMAGE) Log::logger->log(Log::DEBUG, "Damage: Monitor {}", pMonitor->m_name); @@ -2686,7 +2696,7 @@ void IHyprRenderer::damageBox(const CBox& box, bool skipFrameSchedule) { } } - static auto PLOGDAMAGE = CConfigValue("debug:log_damage"); + static auto PLOGDAMAGE = CConfigValue("debug:log_damage"); if (*PLOGDAMAGE) Log::logger->log(Log::DEBUG, "Damage: Box: xy: {}, {} wh: {}, {}", box.x, box.y, box.w, box.h); @@ -2795,11 +2805,11 @@ void IHyprRenderer::setCursorFromName(const std::string& name, bool force) { } void IHyprRenderer::ensureCursorRenderingMode() { - static auto PINVISIBLE = CConfigValue("cursor:invisible"); - static auto PCURSORTIMEOUT = CConfigValue("cursor:inactive_timeout"); - static auto PHIDEONTOUCH = CConfigValue("cursor:hide_on_touch"); - static auto PHIDEONTABLET = CConfigValue("cursor:hide_on_tablet"); - static auto PHIDEONKEY = CConfigValue("cursor:hide_on_key_press"); + static auto PINVISIBLE = CConfigValue("cursor:invisible"); + static auto PCURSORTIMEOUT = CConfigValue("cursor:inactive_timeout"); + static auto PHIDEONTOUCH = CConfigValue("cursor:hide_on_touch"); + static auto PHIDEONTABLET = CConfigValue("cursor:hide_on_tablet"); + static auto PHIDEONKEY = CConfigValue("cursor:hide_on_key_press"); if (*PCURSORTIMEOUT <= 0) m_cursorHiddenConditions.hiddenOnTimeout = false; @@ -2953,7 +2963,7 @@ bool IHyprRenderer::isMgpu() { } void IHyprRenderer::addWindowToRenderUnfocused(PHLWINDOW window) { - static auto PFPS = CConfigValue("misc:render_unfocused_fps"); + static auto PFPS = CConfigValue("misc:render_unfocused_fps"); if (*PFPS <= 0) return; @@ -3114,7 +3124,7 @@ void IHyprRenderer::makeSnapshot(WP popup) { } void IHyprRenderer::renderSnapshot(PHLWINDOW pWindow) { - static auto PDIMAROUND = CConfigValue("decoration:dim_around"); + static auto PDIMAROUND = CConfigValue("decoration:dim_around"); PHLWINDOWREF ref{pWindow}; @@ -3144,8 +3154,9 @@ void IHyprRenderer::renderSnapshot(PHLWINDOW pWindow) { if (*PDIMAROUND && pWindow->m_ruleApplicator->dimAround().valueOrDefault()) { CRectPassElement::SRectData data; - data.box = {0, 0, m_renderData.pMonitor->m_pixelSize.x, m_renderData.pMonitor->m_pixelSize.y}; - data.color = CHyprColor(0, 0, 0, *PDIMAROUND * pWindow->m_alpha->value()); + data.box = {0, 0, m_renderData.pMonitor->m_pixelSize.x, m_renderData.pMonitor->m_pixelSize.y}; + data.color = + CHyprColor(0, 0, 0, *PDIMAROUND * pWindow->alphaValue(WINDOW_ALPHA_FADE) * pWindow->alphaValue(WINDOW_ALPHA_FULLSCREEN) * pWindow->alphaValue(WINDOW_ALPHA_LAYOUT)); m_renderPass.add(makeUnique(data)); } @@ -3155,7 +3166,8 @@ void IHyprRenderer::renderSnapshot(PHLWINDOW pWindow) { data.box = CBox{pWindow->m_realPosition->value(), pWindow->m_realSize->value()}.translate(-PMONITOR->m_position).scale(PMONITOR->m_scale).round(); data.color = CHyprColor{0, 0, 0, 0}; data.blur = true; - data.blurA = sqrt(pWindow->m_alpha->value()); // sqrt makes the blur fadeout more realistic. + data.blurA = sqrt(pWindow->alphaValue(WINDOW_ALPHA_FADE) * pWindow->alphaValue(WINDOW_ALPHA_FULLSCREEN) * + pWindow->alphaValue(WINDOW_ALPHA_LAYOUT)); // sqrt makes the blur fadeout more realistic. data.round = pWindow->rounding(); data.roundingPower = pWindow->roundingPower(); data.xray = pWindow->m_ruleApplicator->xray().valueOr(false); @@ -3167,7 +3179,7 @@ void IHyprRenderer::renderSnapshot(PHLWINDOW pWindow) { data.flipEndFrame = true; data.tex = FBDATA->getTexture(); data.box = windowBox; - data.a = pWindow->m_alpha->value(); + data.a = pWindow->alphaValue(WINDOW_ALPHA_FADE) * pWindow->alphaValue(WINDOW_ALPHA_FULLSCREEN) * pWindow->alphaValue(WINDOW_ALPHA_LAYOUT); data.damage = fakeDamage; m_renderPass.add(makeUnique(std::move(data))); @@ -3219,7 +3231,7 @@ void IHyprRenderer::renderSnapshot(WP popup) { if (!popup->m_snapshotFB) return; - static CConfigValue PBLURIGNOREA = CConfigValue("decoration:blur:popups_ignorealpha"); + static CConfigValue PBLURIGNOREA = CConfigValue("decoration:blur:popups_ignorealpha"); const auto FBDATA = popup->m_snapshotFB; @@ -3269,7 +3281,7 @@ bool IHyprRenderer::shouldBlur(PHLLS ls) { if (m_bRenderingSnapshot) return false; - static auto PBLUR = CConfigValue("decoration:blur:enabled"); + static auto PBLUR = CConfigValue("decoration:blur:enabled"); return *PBLUR && ls->m_ruleApplicator->blur().valueOrDefault(); } @@ -3277,21 +3289,21 @@ bool IHyprRenderer::shouldBlur(PHLWINDOW w) { if (m_bRenderingSnapshot) return false; - static auto PBLUR = CConfigValue("decoration:blur:enabled"); + static auto PBLUR = CConfigValue("decoration:blur:enabled"); const bool DONT_BLUR = w->m_ruleApplicator->noBlur().valueOrDefault() || w->m_ruleApplicator->RGBX().valueOrDefault() || w->opaque(); return *PBLUR && !DONT_BLUR; } bool IHyprRenderer::shouldBlur(WP p) { - static CConfigValue PBLURPOPUPS = CConfigValue("decoration:blur:popups"); - static CConfigValue PBLUR = CConfigValue("decoration:blur:enabled"); + static CConfigValue PBLURPOPUPS = CConfigValue("decoration:blur:popups"); + static CConfigValue PBLUR = CConfigValue("decoration:blur:enabled"); return *PBLURPOPUPS && *PBLUR; } SP IHyprRenderer::renderSplash(const std::function(const int, const int, unsigned char* const)>& handleData, const int fontSize, const int maxWidth, const int maxHeight) { - static auto PSPLASHCOLOR = CConfigValue("misc:col.splash"); + static auto PSPLASHCOLOR = CConfigValue("misc:col.splash"); static auto PSPLASHFONT = CConfigValue("misc:splash_font_family"); static auto FALLBACKFONT = CConfigValue("misc:font_family"); diff --git a/src/render/ShaderLoader.cpp b/src/render/ShaderLoader.cpp index 388c1d432..50ab61d04 100644 --- a/src/render/ShaderLoader.cpp +++ b/src/render/ShaderLoader.cpp @@ -135,7 +135,7 @@ std::string CShaderLoader::process(const std::string& filename, const std::map("render:cm_enabled"); + static const auto PCM = CConfigValue("render:cm_enabled"); if (!*PCM) features &= ~(SH_FEAT_CM | SH_FEAT_TONEMAP | SH_FEAT_SDR_MOD); diff --git a/src/render/decorations/CHyprBorderDecoration.cpp b/src/render/decorations/CHyprBorderDecoration.cpp index 603d86357..93565f166 100644 --- a/src/render/decorations/CHyprBorderDecoration.cpp +++ b/src/render/decorations/CHyprBorderDecoration.cpp @@ -147,7 +147,7 @@ eDecorationLayer CHyprBorderDecoration::getDecorationLayer() { } uint64_t CHyprBorderDecoration::getDecorationFlags() { - static auto PPARTOFWINDOW = CConfigValue("decoration:border_part_of_window"); + static auto PPARTOFWINDOW = CConfigValue("decoration:border_part_of_window"); return *PPARTOFWINDOW && !doesntWantBorders() ? DECORATION_PART_OF_MAIN_WINDOW : 0; } diff --git a/src/render/decorations/CHyprDropShadowDecoration.cpp b/src/render/decorations/CHyprDropShadowDecoration.cpp index 3085facbd..8c681301f 100644 --- a/src/render/decorations/CHyprDropShadowDecoration.cpp +++ b/src/render/decorations/CHyprDropShadowDecoration.cpp @@ -38,7 +38,7 @@ std::string CHyprDropShadowDecoration::getDisplayName() { } void CHyprDropShadowDecoration::damageEntire() { - static auto PSHADOWS = CConfigValue("decoration:shadow:enabled"); + static auto PSHADOWS = CConfigValue("decoration:shadow:enabled"); if (*PSHADOWS != 1) return; // disabled @@ -88,7 +88,7 @@ void CHyprDropShadowDecoration::draw(PHLMONITOR pMonitor, float const& a) { } bool CHyprDropShadowDecoration::canRender(PHLMONITOR pMonitor) { - static auto PSHADOWS = CConfigValue("decoration:shadow:enabled"); + static auto PSHADOWS = CConfigValue("decoration:shadow:enabled"); if (*PSHADOWS != 1) return false; // disabled @@ -115,9 +115,9 @@ SShadowRenderData CHyprDropShadowDecoration::getRenderData(PHLMONITOR pMonitor, const auto PWINDOW = m_window.lock(); - static auto PSHADOWSIZE = CConfigValue("decoration:shadow:range"); - static auto PSHADOWSCALE = CConfigValue("decoration:shadow:scale"); - static auto PSHADOWOFFSET = CConfigValue("decoration:shadow:offset"); + static auto PSHADOWSIZE = CConfigValue("decoration:shadow:range"); + static auto PSHADOWSCALE = CConfigValue("decoration:shadow:scale"); + static auto PSHADOWOFFSET = CConfigValue("decoration:shadow:offset"); const auto BORDERSIZE = PWINDOW->getRealBorderSize(); const auto ROUNDINGBASE = PWINDOW->rounding(); @@ -200,7 +200,7 @@ eDecorationLayer CHyprDropShadowDecoration::getDecorationLayer() { } void CHyprDropShadowDecoration::drawShadowInternal(const CBox& box, int round, float roundingPower, int range, CHyprColor color, float a) { - static auto PSHADOWSHARP = CConfigValue("decoration:shadow:sharp"); + static auto PSHADOWSHARP = CConfigValue("decoration:shadow:sharp"); if (box.w < 1 || box.h < 1) return; diff --git a/src/render/decorations/CHyprGroupBarDecoration.cpp b/src/render/decorations/CHyprGroupBarDecoration.cpp index b8f6d814f..fa4b157e6 100644 --- a/src/render/decorations/CHyprGroupBarDecoration.cpp +++ b/src/render/decorations/CHyprGroupBarDecoration.cpp @@ -23,23 +23,23 @@ static SP m_tGradientLockedInactive; constexpr int BAR_TEXT_PAD = 2; CHyprGroupBarDecoration::CHyprGroupBarDecoration(PHLWINDOW pWindow) : IHyprWindowDecoration(pWindow), m_window(pWindow) { - static auto PGRADIENTS = CConfigValue("group:groupbar:enabled"); - static auto PENABLED = CConfigValue("group:groupbar:gradients"); + static auto PGRADIENTS = CConfigValue("group:groupbar:enabled"); + static auto PENABLED = CConfigValue("group:groupbar:gradients"); if (*PENABLED && *PGRADIENTS) refreshGroupBarGradients(); } SDecorationPositioningInfo CHyprGroupBarDecoration::getPositioningInfo() { - static auto PHEIGHT = CConfigValue("group:groupbar:height"); - static auto PINDICATORGAP = CConfigValue("group:groupbar:indicator_gap"); - static auto PINDICATORHEIGHT = CConfigValue("group:groupbar:indicator_height"); - static auto PRENDERTITLES = CConfigValue("group:groupbar:render_titles"); - static auto PGRADIENTS = CConfigValue("group:groupbar:gradients"); - static auto PPRIORITY = CConfigValue("group:groupbar:priority"); - static auto PSTACKED = CConfigValue("group:groupbar:stacked"); - static auto POUTERGAP = CConfigValue("group:groupbar:gaps_out"); - static auto PKEEPUPPERGAP = CConfigValue("group:groupbar:keep_upper_gap"); + static auto PHEIGHT = CConfigValue("group:groupbar:height"); + static auto PINDICATORGAP = CConfigValue("group:groupbar:indicator_gap"); + static auto PINDICATORHEIGHT = CConfigValue("group:groupbar:indicator_height"); + static auto PRENDERTITLES = CConfigValue("group:groupbar:render_titles"); + static auto PGRADIENTS = CConfigValue("group:groupbar:gradients"); + static auto PPRIORITY = CConfigValue("group:groupbar:priority"); + static auto PSTACKED = CConfigValue("group:groupbar:stacked"); + static auto POUTERGAP = CConfigValue("group:groupbar:gaps_out"); + static auto PKEEPUPPERGAP = CConfigValue("group:groupbar:keep_upper_gap"); SDecorationPositioningInfo info; info.policy = DECORATION_POSITION_STICKY; @@ -105,33 +105,33 @@ void CHyprGroupBarDecoration::draw(PHLMONITOR pMonitor, float const& a) { if (!VISIBLE) return; - static auto PRENDERTITLES = CConfigValue("group:groupbar:render_titles"); - static auto PTITLEFONTSIZE = CConfigValue("group:groupbar:font_size"); - static auto PHEIGHT = CConfigValue("group:groupbar:height"); - static auto PINDICATORGAP = CConfigValue("group:groupbar:indicator_gap"); - static auto PINDICATORHEIGHT = CConfigValue("group:groupbar:indicator_height"); - static auto PGRADIENTS = CConfigValue("group:groupbar:gradients"); - static auto PSTACKED = CConfigValue("group:groupbar:stacked"); - static auto PROUNDING = CConfigValue("group:groupbar:rounding"); - static auto PROUNDINGPOWER = CConfigValue("group:groupbar:rounding_power"); - static auto PGRADIENTROUNDING = CConfigValue("group:groupbar:gradient_rounding"); - static auto PGRADIENTROUNDINGPOWER = CConfigValue("group:groupbar:gradient_rounding_power"); - static auto PGRADIENTROUNDINGONLYEDGES = CConfigValue("group:groupbar:gradient_round_only_edges"); - static auto PROUNDONLYEDGES = CConfigValue("group:groupbar:round_only_edges"); - static auto PGROUPCOLACTIVE = CConfigValue("group:groupbar:col.active"); - static auto PGROUPCOLINACTIVE = CConfigValue("group:groupbar:col.inactive"); - static auto PGROUPCOLACTIVELOCKED = CConfigValue("group:groupbar:col.locked_active"); - static auto PGROUPCOLINACTIVELOCKED = CConfigValue("group:groupbar:col.locked_inactive"); - static auto POUTERGAP = CConfigValue("group:groupbar:gaps_out"); - static auto PINNERGAP = CConfigValue("group:groupbar:gaps_in"); - static auto PKEEPUPPERGAP = CConfigValue("group:groupbar:keep_upper_gap"); - static auto PTEXTOFFSET = CConfigValue("group:groupbar:text_offset"); - static auto PTEXTPADDING = CConfigValue("group:groupbar:text_padding"); - static auto PBLUR = CConfigValue("group:groupbar:blur"); - auto* const GROUPCOLACTIVE = sc((PGROUPCOLACTIVE.ptr())->getData()); - auto* const GROUPCOLINACTIVE = sc((PGROUPCOLINACTIVE.ptr())->getData()); - auto* const GROUPCOLACTIVELOCKED = sc((PGROUPCOLACTIVELOCKED.ptr())->getData()); - auto* const GROUPCOLINACTIVELOCKED = sc((PGROUPCOLINACTIVELOCKED.ptr())->getData()); + static auto PRENDERTITLES = CConfigValue("group:groupbar:render_titles"); + static auto PTITLEFONTSIZE = CConfigValue("group:groupbar:font_size"); + static auto PHEIGHT = CConfigValue("group:groupbar:height"); + static auto PINDICATORGAP = CConfigValue("group:groupbar:indicator_gap"); + static auto PINDICATORHEIGHT = CConfigValue("group:groupbar:indicator_height"); + static auto PGRADIENTS = CConfigValue("group:groupbar:gradients"); + static auto PSTACKED = CConfigValue("group:groupbar:stacked"); + static auto PROUNDING = CConfigValue("group:groupbar:rounding"); + static auto PROUNDINGPOWER = CConfigValue("group:groupbar:rounding_power"); + static auto PGRADIENTROUNDING = CConfigValue("group:groupbar:gradient_rounding"); + static auto PGRADIENTROUNDINGPOWER = CConfigValue("group:groupbar:gradient_rounding_power"); + static auto PGRADIENTROUNDINGONLYEDGES = CConfigValue("group:groupbar:gradient_round_only_edges"); + static auto PROUNDONLYEDGES = CConfigValue("group:groupbar:round_only_edges"); + static auto PGROUPCOLACTIVE = CConfigValue("group:groupbar:col.active"); + static auto PGROUPCOLINACTIVE = CConfigValue("group:groupbar:col.inactive"); + static auto PGROUPCOLACTIVELOCKED = CConfigValue("group:groupbar:col.locked_active"); + static auto PGROUPCOLINACTIVELOCKED = CConfigValue("group:groupbar:col.locked_inactive"); + static auto POUTERGAP = CConfigValue("group:groupbar:gaps_out"); + static auto PINNERGAP = CConfigValue("group:groupbar:gaps_in"); + static auto PKEEPUPPERGAP = CConfigValue("group:groupbar:keep_upper_gap"); + static auto PTEXTOFFSET = CConfigValue("group:groupbar:text_offset"); + static auto PTEXTPADDING = CConfigValue("group:groupbar:text_padding"); + static auto PBLUR = CConfigValue("group:groupbar:blur"); + auto* const GROUPCOLACTIVE = sc((PGROUPCOLACTIVE.ptr())); + auto* const GROUPCOLINACTIVE = sc((PGROUPCOLINACTIVE.ptr())); + auto* const GROUPCOLACTIVELOCKED = sc((PGROUPCOLACTIVELOCKED.ptr())); + auto* const GROUPCOLINACTIVELOCKED = sc((PGROUPCOLINACTIVELOCKED.ptr())); const auto ASSIGNEDBOX = assignedBoxGlobal(); @@ -282,17 +282,17 @@ void CHyprGroupBarDecoration::invalidateTextures() { CTitleTex::CTitleTex(PHLWINDOW pWindow, const Vector2D& bufferSize, const float monitorScale) : m_content(pWindow->m_title), m_windowOwner(pWindow) { static auto FALLBACKFONT = CConfigValue("misc:font_family"); static auto PTITLEFONTFAMILY = CConfigValue("group:groupbar:font_family"); - static auto PTITLEFONTSIZE = CConfigValue("group:groupbar:font_size"); - static auto PTEXTCOLORACTIVE = CConfigValue("group:groupbar:text_color"); - static auto PTEXTCOLORINACTIVE = CConfigValue("group:groupbar:text_color_inactive"); - static auto PTEXTCOLORLOCKEDACTIVE = CConfigValue("group:groupbar:text_color_locked_active"); - static auto PTEXTCOLORLOCKEDINACTIVE = CConfigValue("group:groupbar:text_color_locked_inactive"); + static auto PTITLEFONTSIZE = CConfigValue("group:groupbar:font_size"); + static auto PTEXTCOLORACTIVE = CConfigValue("group:groupbar:text_color"); + static auto PTEXTCOLORINACTIVE = CConfigValue("group:groupbar:text_color_inactive"); + static auto PTEXTCOLORLOCKEDACTIVE = CConfigValue("group:groupbar:text_color_locked_active"); + static auto PTEXTCOLORLOCKEDINACTIVE = CConfigValue("group:groupbar:text_color_locked_inactive"); - static auto PTITLEFONTWEIGHTACTIVE = CConfigValue("group:groupbar:font_weight_active"); - static auto PTITLEFONTWEIGHTINACTIVE = CConfigValue("group:groupbar:font_weight_inactive"); + static auto PTITLEFONTWEIGHTACTIVE = CConfigValue("group:groupbar:font_weight_active"); + static auto PTITLEFONTWEIGHTINACTIVE = CConfigValue("group:groupbar:font_weight_inactive"); - const auto FONTWEIGHTACTIVE = sc((PTITLEFONTWEIGHTACTIVE.ptr())->getData()); - const auto FONTWEIGHTINACTIVE = sc((PTITLEFONTWEIGHTINACTIVE.ptr())->getData()); + const auto FONTWEIGHTACTIVE = sc((PTITLEFONTWEIGHTACTIVE.ptr())); + const auto FONTWEIGHTINACTIVE = sc((PTITLEFONTWEIGHTINACTIVE.ptr())); const CHyprColor COLORACTIVE = CHyprColor(*PTEXTCOLORACTIVE); const CHyprColor COLORINACTIVE = *PTEXTCOLORINACTIVE == -1 ? COLORACTIVE : CHyprColor(*PTEXTCOLORINACTIVE); @@ -351,17 +351,17 @@ static SP renderGradient(Config::CGradientValueData* grad) { } void refreshGroupBarGradients() { - static auto PGRADIENTS = CConfigValue("group:groupbar:enabled"); - static auto PENABLED = CConfigValue("group:groupbar:gradients"); + static auto PGRADIENTS = CConfigValue("group:groupbar:enabled"); + static auto PENABLED = CConfigValue("group:groupbar:gradients"); - static auto PGROUPCOLACTIVE = CConfigValue("group:groupbar:col.active"); - static auto PGROUPCOLINACTIVE = CConfigValue("group:groupbar:col.inactive"); - static auto PGROUPCOLACTIVELOCKED = CConfigValue("group:groupbar:col.locked_active"); - static auto PGROUPCOLINACTIVELOCKED = CConfigValue("group:groupbar:col.locked_inactive"); - auto* const GROUPCOLACTIVE = sc((PGROUPCOLACTIVE.ptr())->getData()); - auto* const GROUPCOLINACTIVE = sc((PGROUPCOLINACTIVE.ptr())->getData()); - auto* const GROUPCOLACTIVELOCKED = sc((PGROUPCOLACTIVELOCKED.ptr())->getData()); - auto* const GROUPCOLINACTIVELOCKED = sc((PGROUPCOLINACTIVELOCKED.ptr())->getData()); + static auto PGROUPCOLACTIVE = CConfigValue("group:groupbar:col.active"); + static auto PGROUPCOLINACTIVE = CConfigValue("group:groupbar:col.inactive"); + static auto PGROUPCOLACTIVELOCKED = CConfigValue("group:groupbar:col.locked_active"); + static auto PGROUPCOLINACTIVELOCKED = CConfigValue("group:groupbar:col.locked_inactive"); + auto* const GROUPCOLACTIVE = sc((PGROUPCOLACTIVE.ptr())); + auto* const GROUPCOLINACTIVE = sc((PGROUPCOLINACTIVE.ptr())); + auto* const GROUPCOLACTIVELOCKED = sc((PGROUPCOLACTIVELOCKED.ptr())); + auto* const GROUPCOLINACTIVELOCKED = sc((PGROUPCOLINACTIVELOCKED.ptr())); if (m_tGradientActive && m_tGradientActive->ok()) { m_tGradientActive.reset(); @@ -380,9 +380,9 @@ void refreshGroupBarGradients() { } bool CHyprGroupBarDecoration::onBeginWindowDragOnDeco(const Vector2D& pos) { - static auto PSTACKED = CConfigValue("group:groupbar:stacked"); - static auto POUTERGAP = CConfigValue("group:groupbar:gaps_out"); - static auto PINNERGAP = CConfigValue("group:groupbar:gaps_in"); + static auto PSTACKED = CConfigValue("group:groupbar:stacked"); + static auto POUTERGAP = CConfigValue("group:groupbar:gaps_out"); + static auto PINNERGAP = CConfigValue("group:groupbar:gaps_in"); if (m_window->m_group->size() == 1) return false; @@ -413,9 +413,9 @@ bool CHyprGroupBarDecoration::onBeginWindowDragOnDeco(const Vector2D& pos) { } bool CHyprGroupBarDecoration::onEndWindowDragOnDeco(const Vector2D& pos, PHLWINDOW pDraggedWindow) { - static auto PDRAGINTOGROUP = CConfigValue("group:drag_into_group"); - static auto PMERGEFLOATEDINTOTILEDONGROUPBAR = CConfigValue("group:merge_floated_into_tiled_on_groupbar"); - static auto PMERGEGROUPSONGROUPBAR = CConfigValue("group:merge_groups_on_groupbar"); + static auto PDRAGINTOGROUP = CConfigValue("group:drag_into_group"); + static auto PMERGEFLOATEDINTOTILEDONGROUPBAR = CConfigValue("group:merge_floated_into_tiled_on_groupbar"); + static auto PMERGEGROUPSONGROUPBAR = CConfigValue("group:merge_groups_on_groupbar"); const bool FLOATEDINTOTILED = !m_window->m_isFloating && !g_layoutManager->dragController()->draggingTiled(); if (!pDraggedWindow->canBeGroupedInto(m_window->m_group) || (*PDRAGINTOGROUP != 1 && *PDRAGINTOGROUP != 2) || (FLOATEDINTOTILED && !*PMERGEFLOATEDINTOTILEDONGROUPBAR) || @@ -431,16 +431,16 @@ bool CHyprGroupBarDecoration::onEndWindowDragOnDeco(const Vector2D& pos, PHLWIND } bool CHyprGroupBarDecoration::onMouseButtonOnDeco(const Vector2D& pos, const IPointer::SButtonEvent& e) { - static auto PSTACKED = CConfigValue("group:groupbar:stacked"); - static auto POUTERGAP = CConfigValue("group:groupbar:gaps_out"); - static auto PINNERGAP = CConfigValue("group:groupbar:gaps_in"); + static auto PSTACKED = CConfigValue("group:groupbar:stacked"); + static auto POUTERGAP = CConfigValue("group:groupbar:gaps_out"); + static auto PINNERGAP = CConfigValue("group:groupbar:gaps_in"); if (m_window->isEffectiveInternalFSMode(FSMODE_FULLSCREEN)) return true; const float BARRELATIVEX = pos.x - assignedBoxGlobal().x; const float BARRELATIVEY = pos.y - assignedBoxGlobal().y; const int WINDOWINDEX = *PSTACKED ? (BARRELATIVEY / (m_barHeight + *POUTERGAP)) : (BARRELATIVEX) / (m_barWidth + *PINNERGAP); - static auto PFOLLOWMOUSE = CConfigValue("input:follow_mouse"); + static auto PFOLLOWMOUSE = CConfigValue("input:follow_mouse"); // close window on middle click if (e.button == 274) { @@ -481,7 +481,7 @@ bool CHyprGroupBarDecoration::onMouseButtonOnDeco(const Vector2D& pos, const IPo } bool CHyprGroupBarDecoration::onScrollOnDeco(const Vector2D& pos, const IPointer::SAxisEvent e) { - static auto PGROUPBARSCROLLING = CConfigValue("group:groupbar:scrolling"); + static auto PGROUPBARSCROLLING = CConfigValue("group:groupbar:scrolling"); if (!*PGROUPBARSCROLLING || !m_window->m_group) return false; @@ -529,6 +529,6 @@ CBox CHyprGroupBarDecoration::assignedBoxGlobal() { } bool CHyprGroupBarDecoration::visible() { - static auto PENABLED = CConfigValue("group:groupbar:enabled"); + static auto PENABLED = CConfigValue("group:groupbar:enabled"); return *PENABLED && m_window->m_ruleApplicator->decorate().valueOrDefault(); } diff --git a/src/render/gl/GLFramebuffer.cpp b/src/render/gl/GLFramebuffer.cpp index 59c0c4e35..d8b198c3e 100644 --- a/src/render/gl/GLFramebuffer.cpp +++ b/src/render/gl/GLFramebuffer.cpp @@ -127,21 +127,17 @@ bool CGLFramebuffer::readPixels(CHLBufferReference buffer, uint32_t offsetX, uin uint32_t packStride = minStride(PFORMAT, m_size.x); int glFormat = PFORMAT->glFormat; - if (glFormat == GL_RGBA) - glFormat = GL_BGRA_EXT; - - if (glFormat != GL_BGRA_EXT && glFormat != GL_RGB) { - if (PFORMAT->swizzle.has_value()) { - if (PFORMAT->swizzle == SWIZZLE_RGBA) - glFormat = GL_RGBA; - else if (PFORMAT->swizzle == SWIZZLE_BGRA) - glFormat = GL_BGRA_EXT; - else { - LOGM(Log::ERR, "Copied frame via shm might be broken or color flipped"); - glFormat = GL_RGBA; - } + if (PFORMAT->swizzle.has_value()) { + if (PFORMAT->swizzle == SWIZZLE_RGBA) + glFormat = GL_RGBA; + else if (PFORMAT->swizzle == SWIZZLE_BGRA) + glFormat = GL_BGRA_EXT; + else { + LOGM(Log::ERR, "Copied frame via shm might be broken or color flipped"); + glFormat = GL_RGBA; } - } + } else if (glFormat == GL_RGBA) + glFormat = GL_BGRA_EXT; // This could be optimized by using a pixel buffer object to make this async, // but really clients should just use a dma buffer anyways. diff --git a/src/render/pass/Pass.cpp b/src/render/pass/Pass.cpp index e4ef6f965..05e83e709 100644 --- a/src/render/pass/Pass.cpp +++ b/src/render/pass/Pass.cpp @@ -29,7 +29,7 @@ void CRenderPass::add(UP&& el) { void CRenderPass::simplify(bool willBlur, const CRegion& liveBlurRegion) { const auto pMonitor = g_pHyprRenderer->m_renderData.pMonitor; - static auto PDEBUGPASS = CConfigValue("debug:pass"); + static auto PDEBUGPASS = CConfigValue("debug:pass"); // TODO: use precompute blur for instances where there is nothing in between @@ -102,7 +102,7 @@ void CRenderPass::clear() { CRegion CRenderPass::render(const CRegion& damage_) { const auto pMonitor = g_pHyprRenderer->m_renderData.pMonitor; - static auto PDEBUGPASS = CConfigValue("debug:pass"); + static auto PDEBUGPASS = CConfigValue("debug:pass"); // single pass: cache blur results and gather aggregate info bool willBlur = false, willDisableSimplification = false, willPrecomputeBlur = false; @@ -309,8 +309,8 @@ void CRenderPass::renderDebugData() { float CRenderPass::oneBlurRadius() { // TODO: is this exact range correct? - static auto PBLURSIZE = CConfigValue("decoration:blur:size"); - static auto PBLURPASSES = CConfigValue("decoration:blur:passes"); + static auto PBLURSIZE = CConfigValue("decoration:blur:size"); + static auto PBLURPASSES = CConfigValue("decoration:blur:passes"); const auto BLUR_PASSES = std::clamp(*PBLURPASSES, sc(1), sc(8)); diff --git a/src/xwayland/Server.cpp b/src/xwayland/Server.cpp index b33dbfb5f..8fe46da93 100644 --- a/src/xwayland/Server.cpp +++ b/src/xwayland/Server.cpp @@ -128,7 +128,7 @@ static std::string getSocketPath(int display, bool isLinux) { } static bool openSockets(std::array& sockets, int display) { - static auto CREATEABSTRACTSOCKET = CConfigValue("xwayland:create_abstract_socket"); + static auto CREATEABSTRACTSOCKET = CConfigValue("xwayland:create_abstract_socket"); if (!ensureSocketDirExists()) return false; diff --git a/src/xwayland/XWM.cpp b/src/xwayland/XWM.cpp index 665aedfb4..2ffe99edb 100644 --- a/src/xwayland/XWM.cpp +++ b/src/xwayland/XWM.cpp @@ -1483,7 +1483,7 @@ int SXSelection::onRead(int fd, uint32_t mask) { if (bytesRead == 0) { if (transfer->data.empty()) { - Log::logger->log(Log::WARN, "[xwm] Transfer ended with zero bytes — rejecting"); + Log::logger->log(Log::WARN, "[xwm] Transfer ended with zero bytes - rejecting"); g_pXWayland->m_wm->selectionSendNotify(&transfer->request, false); transfers.erase(it); return 0; diff --git a/src/xwayland/XWayland.cpp b/src/xwayland/XWayland.cpp index a022217ab..12e48bc0c 100644 --- a/src/xwayland/XWayland.cpp +++ b/src/xwayland/XWayland.cpp @@ -11,7 +11,7 @@ CXWayland::CXWayland(const bool wantsEnabled) { for (auto& w : g_pCompositor->m_windows) { if (!w->m_isX11) continue; - g_pCompositor->closeWindow(w); + w->sendClose(); } unsetenv("DISPLAY"); m_enabled = false; diff --git a/tests/config/MonitorParser.cpp b/tests/config/MonitorParser.cpp index 29bd8db10..1d8fe6648 100644 --- a/tests/config/MonitorParser.cpp +++ b/tests/config/MonitorParser.cpp @@ -128,7 +128,7 @@ TEST(Config, monitorParserTransformValid) { for (int i = 0; i <= 7; i++) { CMonitorRuleParser parser("DP-1"); EXPECT_TRUE(parser.parseTransform(std::to_string(i))); - EXPECT_EQ(parser.rule().m_transform, static_cast(i)); + EXPECT_EQ(parser.rule().m_transform, sc(i)); } } diff --git a/tests/config/lua/ConfigValueTypes.cpp b/tests/config/lua/ConfigValueTypes.cpp new file mode 100644 index 000000000..76de98bf9 --- /dev/null +++ b/tests/config/lua/ConfigValueTypes.cpp @@ -0,0 +1,400 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include + +#include + +extern "C" { +#include +#include +} + +using namespace Config; +using namespace Config::Lua; + +namespace { + class CLuaState { + public: + CLuaState() : m_lua(luaL_newstate()) { + luaL_openlibs(m_lua); + } + + ~CLuaState() { + if (m_lua) + lua_close(m_lua); + } + + lua_State* get() const { + return m_lua; + } + + private: + lua_State* m_lua = nullptr; + }; + + void pushVec2Table(lua_State* L, double x, double y) { + lua_createtable(L, 2, 2); + lua_pushnumber(L, x); + lua_rawseti(L, -2, 1); + lua_pushnumber(L, y); + lua_rawseti(L, -2, 2); + } + + void pushExpressionVec2Table(lua_State* L, const char* x, const char* y) { + lua_createtable(L, 2, 2); + lua_pushstring(L, x); + lua_rawseti(L, -2, 1); + lua_pushstring(L, y); + lua_rawseti(L, -2, 2); + } +} + +TEST(ConfigLuaValueTypes, boolParseAndReset) { + CLuaState S; + const auto L = S.get(); + + CLuaConfigBool value(false); + EXPECT_FALSE(value.parsed()); + EXPECT_FALSE(value.setByUser()); + + lua_pushboolean(L, true); + const auto err = value.parse(L); + lua_pop(L, 1); + + EXPECT_EQ(err.errorCode, PARSE_ERROR_OK); + EXPECT_TRUE(value.parsed()); + EXPECT_TRUE(value.setByUser()); + + value.reset(); + EXPECT_FALSE(value.parsed()); + + value.resetSetByUser(); + EXPECT_FALSE(value.setByUser()); +} + +TEST(ConfigLuaValueTypes, boolBadType) { + CLuaState S; + const auto L = S.get(); + + CLuaConfigBool value(false); + + lua_pushinteger(L, 1); + const auto err = value.parse(L); + lua_pop(L, 1); + + EXPECT_EQ(err.errorCode, PARSE_ERROR_BAD_TYPE); + EXPECT_NE(err.message.find("requires a bool"), std::string::npos); +} + +TEST(ConfigLuaValueTypes, intBooleanCastAndRangeAndMap) { + CLuaState S; + const auto L = S.get(); + + CLuaConfigInt ranged(0, 0, 10); + + lua_pushboolean(L, true); + auto err = ranged.parse(L); + lua_pop(L, 1); + EXPECT_EQ(err.errorCode, PARSE_ERROR_OK); + EXPECT_EQ(ranged.parsed(), 1); + + lua_pushinteger(L, 11); + err = ranged.parse(L); + lua_pop(L, 1); + EXPECT_EQ(err.errorCode, PARSE_ERROR_OUT_OF_RANGE); + + CLuaConfigInt mapped(0, std::nullopt, std::nullopt, std::unordered_map{{"auto", -1}, {"on", 1}}); + + lua_pushstring(L, "auto"); + err = mapped.parse(L); + lua_pop(L, 1); + EXPECT_EQ(err.errorCode, PARSE_ERROR_OK); + EXPECT_EQ(mapped.parsed(), -1); + + lua_pushstring(L, "missing"); + err = mapped.parse(L); + lua_pop(L, 1); + EXPECT_EQ(err.errorCode, PARSE_ERROR_BAD_VALUE); + EXPECT_NE(err.message.find("unknown string value"), std::string::npos); +} + +TEST(ConfigLuaValueTypes, floatParseAndRange) { + CLuaState S; + const auto L = S.get(); + + CLuaConfigFloat value(0.F, -1.F, 1.F); + + lua_pushnumber(L, 0.5); + auto err = value.parse(L); + lua_pop(L, 1); + EXPECT_EQ(err.errorCode, PARSE_ERROR_OK); + EXPECT_FLOAT_EQ(value.parsed(), 0.5F); + + lua_pushinteger(L, 2); + err = value.parse(L); + lua_pop(L, 1); + EXPECT_EQ(err.errorCode, PARSE_ERROR_OUT_OF_RANGE); +} + +TEST(ConfigLuaValueTypes, stringValidatorAndEmptyPush) { + CLuaState S; + const auto L = S.get(); + + CLuaConfigString validated("default", std::optional(const STRING&)>>{[](const STRING& s) -> std::expected { + if (s.starts_with("ok:")) + return {}; + return std::unexpected("must start with ok:"); + }}); + + lua_pushstring(L, "ok:value"); + auto err = validated.parse(L); + lua_pop(L, 1); + EXPECT_EQ(err.errorCode, PARSE_ERROR_OK); + EXPECT_EQ(validated.parsed(), "ok:value"); + + lua_pushstring(L, "bad"); + err = validated.parse(L); + lua_pop(L, 1); + EXPECT_EQ(err.errorCode, PARSE_ERROR_BAD_VALUE); + EXPECT_NE(err.message.find("must start with ok:"), std::string::npos); + + CLuaConfigString emptyDefault(STRVAL_EMPTY); + emptyDefault.push(L); + ASSERT_TRUE(lua_isstring(L, -1)); + EXPECT_STREQ(lua_tostring(L, -1), ""); + lua_pop(L, 1); +} + +TEST(ConfigLuaValueTypes, colorParseAndPush) { + CLuaState S; + const auto L = S.get(); + + CLuaConfigColor value(0); + + lua_pushstring(L, "0xFF00FF00"); + auto err = value.parse(L); + lua_pop(L, 1); + EXPECT_EQ(err.errorCode, PARSE_ERROR_OK); + EXPECT_EQ(value.parsed(), 0xFF00FF00); + + value.push(L); + ASSERT_TRUE(lua_isstring(L, -1)); + EXPECT_STREQ(lua_tostring(L, -1), "0xFF00FF00"); + lua_pop(L, 1); + + lua_pushstring(L, "@@@notacolor@@@"); + err = value.parse(L); + lua_pop(L, 1); + EXPECT_EQ(err.errorCode, PARSE_ERROR_BAD_VALUE); +} + +TEST(ConfigLuaValueTypes, vec2ParseValidateAndPush) { + CLuaState S; + const auto L = S.get(); + + CLuaConfigVec2 value({0, 0}, std::optional(const VEC2&)>>{[](const VEC2& v) -> std::expected { + if (v.x < 0 || v.y < 0) + return std::unexpected("must be non-negative"); + return {}; + }}); + + pushVec2Table(L, 10, 20); + auto err = value.parse(L); + lua_pop(L, 1); + EXPECT_EQ(err.errorCode, PARSE_ERROR_OK); + EXPECT_FLOAT_EQ(value.parsed().x, 10.F); + EXPECT_FLOAT_EQ(value.parsed().y, 20.F); + + value.push(L); + ASSERT_TRUE(lua_istable(L, -1)); + lua_rawgeti(L, -1, 1); + EXPECT_DOUBLE_EQ(lua_tonumber(L, -1), 10); + lua_pop(L, 1); + lua_getfield(L, -1, "y"); + EXPECT_DOUBLE_EQ(lua_tonumber(L, -1), 20); + lua_pop(L, 2); + + pushVec2Table(L, -1, 0); + err = value.parse(L); + lua_pop(L, 1); + EXPECT_EQ(err.errorCode, PARSE_ERROR_BAD_VALUE); + EXPECT_NE(err.message.find("must be non-negative"), std::string::npos); + + lua_createtable(L, 3, 0); + lua_pushnumber(L, 1); + lua_rawseti(L, -2, 1); + lua_pushnumber(L, 2); + lua_rawseti(L, -2, 2); + lua_pushnumber(L, 3); + lua_rawseti(L, -2, 3); + err = value.parse(L); + lua_pop(L, 1); + EXPECT_EQ(err.errorCode, PARSE_ERROR_BAD_VALUE); +} + +TEST(ConfigLuaValueTypes, expressionVec2AcceptsStringNumberAndTableForms) { + CLuaState S; + const auto L = S.get(); + + CLuaConfigExpressionVec2 value; + + lua_pushstring(L, "monitor_w*0.5 monitor_h*0.25"); + auto err = value.parse(L); + lua_pop(L, 1); + EXPECT_EQ(err.errorCode, PARSE_ERROR_OK); + EXPECT_EQ(value.parsed().x, "monitor_w*0.5"); + EXPECT_EQ(value.parsed().y, "monitor_h*0.25"); + + pushVec2Table(L, 150, 200); + err = value.parse(L); + lua_pop(L, 1); + EXPECT_EQ(err.errorCode, PARSE_ERROR_OK); + EXPECT_EQ(value.parsed().x, "150"); + EXPECT_EQ(value.parsed().y, "200"); + + pushExpressionVec2Table(L, "monitor_h * 0.5", "monitor_h * 0.25"); + err = value.parse(L); + lua_pop(L, 1); + EXPECT_EQ(err.errorCode, PARSE_ERROR_OK); + EXPECT_EQ(value.parsed().x, "monitor_h * 0.5"); + EXPECT_EQ(value.parsed().y, "monitor_h * 0.25"); + + value.push(L); + ASSERT_TRUE(lua_istable(L, -1)); + lua_rawgeti(L, -1, 1); + EXPECT_STREQ(lua_tostring(L, -1), "monitor_h * 0.5"); + lua_pop(L, 1); + lua_getfield(L, -1, "y"); + EXPECT_STREQ(lua_tostring(L, -1), "monitor_h * 0.25"); + lua_pop(L, 2); + + lua_createtable(L, 1, 0); + lua_pushnumber(L, 1); + lua_rawseti(L, -2, 1); + err = value.parse(L); + lua_pop(L, 1); + EXPECT_EQ(err.errorCode, PARSE_ERROR_BAD_VALUE); +} + +TEST(ConfigLuaValueTypes, cssGapParseFormsAndRange) { + CLuaState S; + const auto L = S.get(); + + CLuaConfigCssGap value(1, 0, 10); + + lua_pushinteger(L, 7); + auto err = value.parse(L); + lua_pop(L, 1); + EXPECT_EQ(err.errorCode, PARSE_ERROR_OK); + EXPECT_EQ(value.parsed().m_top, 7); + EXPECT_EQ(value.parsed().m_right, 7); + EXPECT_EQ(value.parsed().m_bottom, 7); + EXPECT_EQ(value.parsed().m_left, 7); + + lua_createtable(L, 0, 2); + lua_pushinteger(L, 3); + lua_setfield(L, -2, "top"); + lua_pushinteger(L, 4); + lua_setfield(L, -2, "left"); + err = value.parse(L); + lua_pop(L, 1); + EXPECT_EQ(err.errorCode, PARSE_ERROR_OK); + EXPECT_EQ(value.parsed().m_top, 3); + EXPECT_EQ(value.parsed().m_right, 0); + EXPECT_EQ(value.parsed().m_bottom, 0); + EXPECT_EQ(value.parsed().m_left, 4); + + lua_createtable(L, 0, 1); + lua_pushinteger(L, 100); + lua_setfield(L, -2, "top"); + err = value.parse(L); + lua_pop(L, 1); + EXPECT_EQ(err.errorCode, PARSE_ERROR_OUT_OF_RANGE); + + lua_createtable(L, 0, 1); + lua_pushstring(L, "bad"); + lua_setfield(L, -2, "top"); + err = value.parse(L); + lua_pop(L, 1); + EXPECT_EQ(err.errorCode, PARSE_ERROR_BAD_TYPE); +} + +TEST(ConfigLuaValueTypes, fontWeightParse) { + CLuaState S; + const auto L = S.get(); + + CLuaConfigFontWeight value(400); + + lua_pushstring(L, "bold"); + auto err = value.parse(L); + lua_pop(L, 1); + EXPECT_EQ(err.errorCode, PARSE_ERROR_OK); + EXPECT_EQ(value.parsed().m_value, 700); + + lua_pushinteger(L, -1); + err = value.parse(L); + lua_pop(L, 1); + EXPECT_EQ(err.errorCode, PARSE_ERROR_OUT_OF_RANGE); + + lua_pushstring(L, "definitely-not-a-weight"); + err = value.parse(L); + lua_pop(L, 1); + EXPECT_EQ(err.errorCode, PARSE_ERROR_BAD_VALUE); +} + +TEST(ConfigLuaValueTypes, gradientParseAndPush) { + CLuaState S; + const auto L = S.get(); + + CLuaConfigGradient value(CHyprColor(0xFF000000)); + + lua_pushstring(L, "0xFF112233"); + auto err = value.parse(L); + lua_pop(L, 1); + EXPECT_EQ(err.errorCode, PARSE_ERROR_OK); + ASSERT_EQ(value.parsed().m_colors.size(), 1); + + lua_createtable(L, 0, 2); + lua_createtable(L, 2, 0); + lua_pushstring(L, "0xFF0000FF"); + lua_rawseti(L, -2, 1); + lua_pushstring(L, "0xFFFF0000"); + lua_rawseti(L, -2, 2); + lua_setfield(L, -2, "colors"); + lua_pushnumber(L, 90); + lua_setfield(L, -2, "angle"); + + err = value.parse(L); + lua_pop(L, 1); + EXPECT_EQ(err.errorCode, PARSE_ERROR_OK); + ASSERT_EQ(value.parsed().m_colors.size(), 2); + EXPECT_NEAR(value.parsed().m_angle, std::numbers::pi_v / 2.F, 0.0001F); + + value.push(L); + ASSERT_TRUE(lua_istable(L, -1)); + lua_getfield(L, -1, "colors"); + ASSERT_TRUE(lua_istable(L, -1)); + lua_rawgeti(L, -1, 1); + EXPECT_STREQ(lua_tostring(L, -1), "0xFF0000FF"); + lua_pop(L, 1); + lua_pop(L, 1); + lua_getfield(L, -1, "angle"); + EXPECT_NEAR(lua_tonumber(L, -1), 90.0, 0.001); + lua_pop(L, 2); + + lua_createtable(L, 0, 0); + err = value.parse(L); + lua_pop(L, 1); + EXPECT_EQ(err.errorCode, PARSE_ERROR_BAD_VALUE); +} diff --git a/tests/config/lua/LuaBindingsInternal.cpp b/tests/config/lua/LuaBindingsInternal.cpp new file mode 100644 index 000000000..d94ea2dc8 --- /dev/null +++ b/tests/config/lua/LuaBindingsInternal.cpp @@ -0,0 +1,256 @@ +#include +#include + +#include + +#include + +#include + +extern "C" { +#include +#include +} + +using namespace Config::Lua; +using namespace Config::Lua::Bindings; + +namespace Config::Lua { + class CConfigManagerPluginLuaTestAccessor { + public: + static void initializeLuaState(CConfigManager& mgr, lua_State* L) { + mgr.m_lua = L; + lua_pushlightuserdata(L, &mgr); + lua_setfield(L, LUA_REGISTRYINDEX, "hl_lua_manager"); + } + }; +} + +namespace { + class CLuaState { + public: + CLuaState() : m_lua(luaL_newstate()) { + luaL_openlibs(m_lua); + } + + ~CLuaState() { + if (m_lua) + lua_close(m_lua); + } + + lua_State* get() const { + return m_lua; + } + + private: + lua_State* m_lua = nullptr; + }; + + int testPluginFn(lua_State* L) { + lua_pushstring(L, "pong"); + return 1; + } +} + +TEST(ConfigLuaBindingsInternal, parseDirectionAliases) { + EXPECT_EQ(Internal::parseDirectionStr("left"), Math::DIRECTION_LEFT); + EXPECT_EQ(Internal::parseDirectionStr("l"), Math::DIRECTION_LEFT); + EXPECT_EQ(Internal::parseDirectionStr("right"), Math::DIRECTION_RIGHT); + EXPECT_EQ(Internal::parseDirectionStr("r"), Math::DIRECTION_RIGHT); + EXPECT_EQ(Internal::parseDirectionStr("up"), Math::DIRECTION_UP); + EXPECT_EQ(Internal::parseDirectionStr("t"), Math::DIRECTION_UP); + EXPECT_EQ(Internal::parseDirectionStr("down"), Math::DIRECTION_DOWN); + EXPECT_EQ(Internal::parseDirectionStr("b"), Math::DIRECTION_DOWN); + EXPECT_EQ(Internal::parseDirectionStr("???"), Math::DIRECTION_DEFAULT); +} + +TEST(ConfigLuaBindingsInternal, parseToggleAliases) { + EXPECT_EQ(Internal::parseToggleStr(""), Config::Actions::TOGGLE_ACTION_TOGGLE); + EXPECT_EQ(Internal::parseToggleStr("toggle"), Config::Actions::TOGGLE_ACTION_TOGGLE); + EXPECT_EQ(Internal::parseToggleStr("enable"), Config::Actions::TOGGLE_ACTION_ENABLE); + EXPECT_EQ(Internal::parseToggleStr("on"), Config::Actions::TOGGLE_ACTION_ENABLE); + EXPECT_EQ(Internal::parseToggleStr("disable"), Config::Actions::TOGGLE_ACTION_DISABLE); + EXPECT_EQ(Internal::parseToggleStr("off"), Config::Actions::TOGGLE_ACTION_DISABLE); +} + +TEST(ConfigLuaBindingsInternal, argStrConvertsStringsAndNumbers) { + CLuaState S; + const auto L = S.get(); + + lua_pushstring(L, "abc"); + EXPECT_EQ(Internal::argStr(L, -1), "abc"); + lua_pop(L, 1); + + lua_pushnumber(L, 42); + EXPECT_EQ(Internal::argStr(L, -1), "42"); + lua_pop(L, 1); +} + +TEST(ConfigLuaBindingsInternal, tableOptHelpersReadOptionalFields) { + CLuaState S; + const auto L = S.get(); + + lua_createtable(L, 0, 5); + lua_pushstring(L, "value"); + lua_setfield(L, -2, "s"); + lua_pushnumber(L, 5.5); + lua_setfield(L, -2, "n"); + lua_pushboolean(L, true); + lua_setfield(L, -2, "b"); + lua_pushstring(L, "not-number"); + lua_setfield(L, -2, "n2"); + lua_pushnil(L); + lua_setfield(L, -2, "nilv"); + + EXPECT_EQ(Internal::tableOptStr(L, -1, "s").value_or(""), "value"); + EXPECT_DOUBLE_EQ(Internal::tableOptNum(L, -1, "n").value_or(0), 5.5); + EXPECT_EQ(Internal::tableOptBool(L, -1, "b").value_or(false), true); + EXPECT_FALSE(Internal::tableOptNum(L, -1, "n2").has_value()); + EXPECT_FALSE(Internal::tableOptStr(L, -1, "missing").has_value()); + EXPECT_FALSE(Internal::tableOptBool(L, -1, "nilv").has_value()); + + lua_pop(L, 1); +} + +TEST(ConfigLuaBindingsInternal, selectorHelpersAcceptStringAndNumberSelectors) { + CLuaState S; + const auto L = S.get(); + + lua_createtable(L, 0, 4); + lua_pushstring(L, "DP-1"); + lua_setfield(L, -2, "monitor"); + lua_pushnumber(L, 7); + lua_setfield(L, -2, "workspace"); + lua_pushnumber(L, 1337); + lua_setfield(L, -2, "window"); + + EXPECT_EQ(Internal::tableOptMonitorSelector(L, -1, "monitor", "test.fn").value_or(""), "DP-1"); + EXPECT_EQ(Internal::tableOptWorkspaceSelector(L, -1, "workspace", "test.fn").value_or(""), "7"); + EXPECT_EQ(Internal::tableOptWindowSelector(L, -1, "window", "test.fn").value_or(""), "1337"); + + EXPECT_FALSE(Internal::tableOptMonitorSelector(L, -1, "missing", "test.fn").has_value()); + EXPECT_FALSE(Internal::tableOptWorkspaceSelector(L, -1, "missing", "test.fn").has_value()); + EXPECT_FALSE(Internal::tableOptWindowSelector(L, -1, "missing", "test.fn").has_value()); + + EXPECT_EQ(Internal::requireTableFieldMonitorSelector(L, -1, "monitor", "test.fn"), "DP-1"); + EXPECT_EQ(Internal::requireTableFieldWorkspaceSelector(L, -1, "workspace", "test.fn"), "7"); + EXPECT_EQ(Internal::requireTableFieldWindowSelector(L, -1, "window", "test.fn"), "1337"); + + lua_pop(L, 1); +} + +TEST(ConfigLuaBindingsInternal, pushWindowUpvalAcceptsNumberAndStringSelectors) { + CLuaState S; + const auto L = S.get(); + + lua_createtable(L, 0, 1); + lua_pushnumber(L, 42); + lua_setfield(L, -2, "window"); + + Internal::pushWindowUpval(L, -1); + ASSERT_TRUE(lua_isstring(L, -1)); + EXPECT_STREQ(lua_tostring(L, -1), "42"); + lua_pop(L, 1); + + lua_pushstring(L, "0xabc"); + lua_setfield(L, -2, "window"); + + Internal::pushWindowUpval(L, -1); + ASSERT_TRUE(lua_isstring(L, -1)); + EXPECT_STREQ(lua_tostring(L, -1), "0xabc"); + lua_pop(L, 1); + + lua_pushnil(L); + lua_setfield(L, -2, "window"); + + Internal::pushWindowUpval(L, -1); + EXPECT_TRUE(lua_isnil(L, -1)); + lua_pop(L, 1); + + lua_pop(L, 1); +} + +TEST(ConfigLuaBindingsInternal, parseTableFieldMissingFieldAndPrefixedErrors) { + CLuaState S; + const auto L = S.get(); + + CLuaConfigInt parser(0); + + lua_newtable(L); + auto err = Internal::parseTableField(L, -1, "required", parser); + EXPECT_EQ(err.errorCode, PARSE_ERROR_BAD_VALUE); + EXPECT_NE(err.message.find("missing required field"), std::string::npos); + lua_pop(L, 1); + + lua_createtable(L, 0, 1); + lua_pushstring(L, "bad"); + lua_setfield(L, -2, "count"); + + err = Internal::parseTableField(L, -1, "count", parser); + EXPECT_EQ(err.errorCode, PARSE_ERROR_BAD_TYPE); + EXPECT_NE(err.message.find("field \"count\":"), std::string::npos); + lua_pop(L, 1); +} + +TEST(ConfigLuaBindingsInternal, pluginBindingIsTableWithLoadFunction) { + CLuaState S; + const auto L = S.get(); + + lua_newtable(L); + Internal::registerConfigRuleBindings(L, nullptr); + + lua_getfield(L, -1, "plugin"); + ASSERT_TRUE(lua_istable(L, -1)); + + lua_getfield(L, -1, "load"); + EXPECT_TRUE(lua_isfunction(L, -1)); + lua_pop(L, 1); + + lua_pop(L, 2); +} + +TEST(ConfigLuaBindingsInternal, pluginLuaFnIsUnloadedWithoutDanglingCall) { + CLuaState S; + const auto L = S.get(); + + auto PREVCOMPOSITOR = std::move(g_pCompositor); + g_pCompositor = makeUnique(true); + + CConfigManager mgr; + CConfigManagerPluginLuaTestAccessor::initializeLuaState(mgr, L); + + lua_newtable(L); + Internal::registerConfigRuleBindings(L, &mgr); + lua_setglobal(L, "hl"); + + const auto HANDLE = reinterpret_cast(0x1BADB002); + + const auto regResult = mgr.registerPluginLuaFunction(HANDLE, "demo", "ping", testPluginFn); + ASSERT_TRUE(regResult.has_value()) << regResult.error(); + + ASSERT_EQ(luaL_dostring(L, R"( + local f = hl.plugin.demo.ping + assert(type(f) == "function") + captured = f + local v = f() + assert(v == "pong") + )"), + LUA_OK); + + mgr.onPluginUnload(HANDLE); + + ASSERT_EQ(luaL_dostring(L, R"( + assert(hl.plugin.demo == nil) + )"), + LUA_OK); + + ASSERT_EQ(luaL_dostring(L, R"( + local ok, err = pcall(captured) + assert(ok == false) + assert(type(err) == "string") + assert(string.find(err, "no longer available", 1, true) ~= nil) + )"), + LUA_OK); + + g_pCompositor = std::move(PREVCOMPOSITOR); +} diff --git a/tests/config/lua/LuaConfigUtils.cpp b/tests/config/lua/LuaConfigUtils.cpp new file mode 100644 index 000000000..a165755eb --- /dev/null +++ b/tests/config/lua/LuaConfigUtils.cpp @@ -0,0 +1,105 @@ +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +using namespace Config; +using namespace Config::Lua; + +namespace { + class CUnsupportedValue : public Values::IValue { + public: + CUnsupportedValue() : Values::IValue(0) { + m_name = "unsupported"; + m_description = "unsupported"; + } + + const std::type_info* underlying() const override { + return &typeid(void); + } + + void commence() override { + ; + } + }; +} + +TEST(ConfigLuaUtils, fromGenericValueMapsSupportedTypes) { + { + auto out = fromGenericValue(makeShared("a", "", 1)); + ASSERT_NE(out.get(), nullptr); + EXPECT_NE(dynamic_cast(out.get()), nullptr); + } + { + auto out = fromGenericValue(makeShared("a", "", 1.F)); + ASSERT_NE(out.get(), nullptr); + EXPECT_NE(dynamic_cast(out.get()), nullptr); + } + { + auto out = fromGenericValue(makeShared("a", "", true)); + ASSERT_NE(out.get(), nullptr); + EXPECT_NE(dynamic_cast(out.get()), nullptr); + } + { + auto out = fromGenericValue(makeShared("a", "", "x")); + ASSERT_NE(out.get(), nullptr); + EXPECT_NE(dynamic_cast(out.get()), nullptr); + } + { + auto out = fromGenericValue(makeShared("a", "", 0xFF000000)); + ASSERT_NE(out.get(), nullptr); + EXPECT_NE(dynamic_cast(out.get()), nullptr); + } + { + auto out = fromGenericValue(makeShared("a", "", VEC2{1, 2})); + ASSERT_NE(out.get(), nullptr); + EXPECT_NE(dynamic_cast(out.get()), nullptr); + } + { + auto out = fromGenericValue(makeShared("a", "", 5)); + ASSERT_NE(out.get(), nullptr); + EXPECT_NE(dynamic_cast(out.get()), nullptr); + } + { + auto out = fromGenericValue(makeShared("a", "", 500)); + ASSERT_NE(out.get(), nullptr); + EXPECT_NE(dynamic_cast(out.get()), nullptr); + } + { + auto out = fromGenericValue(makeShared("a", "", CHyprColor(0xFF112233))); + ASSERT_NE(out.get(), nullptr); + EXPECT_NE(dynamic_cast(out.get()), nullptr); + } +} + +TEST(ConfigLuaUtils, fromGenericValueReturnsNullForUnsupportedType) { + auto out = fromGenericValue(makeShared()); + EXPECT_EQ(out.get(), nullptr); +} + +TEST(ConfigLuaUtils, fromGenericValueCopiesRefreshBits) { + const auto REFRESH = Config::Supplementary::REFRESH_LAYOUTS | Config::Supplementary::REFRESH_WINDOW_STATES; + + auto out = fromGenericValue(makeShared("a", "", 1, Values::SIntValueOptions{.refresh = REFRESH})); + ASSERT_NE(out.get(), nullptr); + EXPECT_EQ(out->refreshBits(), REFRESH); +} diff --git a/tests/config/lua/LuaEventHandler.cpp b/tests/config/lua/LuaEventHandler.cpp new file mode 100644 index 000000000..570014d38 --- /dev/null +++ b/tests/config/lua/LuaEventHandler.cpp @@ -0,0 +1,115 @@ +#include + +#include + +#include + +extern "C" { +#include +#include +} + +using namespace Config::Lua; + +namespace { + class CLuaState { + public: + CLuaState() : m_lua(luaL_newstate()) { + luaL_openlibs(m_lua); + } + + ~CLuaState() { + if (m_lua) + lua_close(m_lua); + } + + lua_State* get() const { + return m_lua; + } + + private: + lua_State* m_lua = nullptr; + }; + + int refGlobalFunction(lua_State* L, const char* name) { + lua_getglobal(L, name); + EXPECT_TRUE(lua_isfunction(L, -1)); + return luaL_ref(L, LUA_REGISTRYINDEX); + } + + int getGlobalInt(lua_State* L, const char* name) { + lua_getglobal(L, name); + const int val = sc(lua_tointeger(L, -1)); + lua_pop(L, 1); + return val; + } +} + +TEST(ConfigLuaEventHandler, knownEventsContainsExpectedEntries) { + const auto& known = CLuaEventHandler::knownEvents(); + EXPECT_TRUE(known.contains("config.reloaded")); + EXPECT_TRUE(known.contains("keybinds.submap")); + EXPECT_TRUE(known.contains("window.open")); +} + +TEST(ConfigLuaEventHandler, registerDispatchAndUnregister) { + CLuaState S; + const auto L = S.get(); + CLuaEventHandler handler(L); + + ASSERT_EQ(luaL_dostring(L, R"( + count = 0 + lastSubmap = "" + function onSubmap(name) + count = count + 1 + lastSubmap = name + end + )"), + LUA_OK); + + const int ref = refGlobalFunction(L, "onSubmap"); + const auto handle = handler.registerEvent("keybinds.submap", ref); + ASSERT_TRUE(handle.has_value()); + + Event::bus()->m_events.keybinds.submap.emit("main"); + EXPECT_EQ(getGlobalInt(L, "count"), 1); + + lua_getglobal(L, "lastSubmap"); + ASSERT_TRUE(lua_isstring(L, -1)); + EXPECT_STREQ(lua_tostring(L, -1), "main"); + lua_pop(L, 1); + + EXPECT_TRUE(handler.unregisterEvent(*handle)); + + Event::bus()->m_events.keybinds.submap.emit("other"); + EXPECT_EQ(getGlobalInt(L, "count"), 1); +} + +TEST(ConfigLuaEventHandler, registerUnknownEventReturnsNullopt) { + CLuaState S; + const auto L = S.get(); + CLuaEventHandler handler(L); + + ASSERT_EQ(luaL_dostring(L, "function cb() end"), LUA_OK); + + const int ref = refGlobalFunction(L, "cb"); + const auto handle = handler.registerEvent("totally.unknown", ref); + + EXPECT_FALSE(handle.has_value()); + luaL_unref(L, LUA_REGISTRYINDEX, ref); +} + +TEST(ConfigLuaEventHandler, callbackLuaErrorDoesNotCrashDispatch) { + CLuaState S; + const auto L = S.get(); + CLuaEventHandler handler(L); + + ASSERT_EQ(luaL_dostring(L, "function broken() error('boom') end"), LUA_OK); + + const int ref = refGlobalFunction(L, "broken"); + const auto h = handler.registerEvent("config.reloaded", ref); + ASSERT_TRUE(h.has_value()); + + Event::bus()->m_events.config.reloaded.emit(); + SUCCEED(); +} diff --git a/tests/config/lua/LuaObjectsBasic.cpp b/tests/config/lua/LuaObjectsBasic.cpp new file mode 100644 index 000000000..10758b7d4 --- /dev/null +++ b/tests/config/lua/LuaObjectsBasic.cpp @@ -0,0 +1,266 @@ +#include + +#include +#include + +#include +#include + +#include + +extern "C" { +#include +#include +} + +using namespace Config::Lua; + +namespace { + class CLuaState { + public: + CLuaState() : m_lua(luaL_newstate()) { + luaL_openlibs(m_lua); + } + + ~CLuaState() { + if (m_lua) + lua_close(m_lua); + } + + lua_State* get() const { + return m_lua; + } + + private: + lua_State* m_lua = nullptr; + }; + + int getGlobalInt(lua_State* L, const char* name) { + lua_getglobal(L, name); + const int v = sc(lua_tointeger(L, -1)); + lua_pop(L, 1); + return v; + } + + bool getGlobalBool(lua_State* L, const char* name) { + lua_getglobal(L, name); + const bool v = lua_toboolean(L, -1) != 0; + lua_pop(L, 1); + return v; + } +} + +TEST(ConfigLuaObjects, keybindCanToggleEnabledFromLua) { + CLuaState S; + const auto L = S.get(); + + Objects::CLuaKeybind{}.setup(L); + + auto keybind = makeShared(); + keybind->enabled = true; + + Objects::CLuaKeybind::push(L, keybind); + lua_setglobal(L, "kb"); + + ASSERT_EQ(luaL_dostring(L, R"( + assert(kb:is_enabled() == true) + kb:set_enabled(false) + assert(kb:is_enabled() == false) + )"), + LUA_OK); + + EXPECT_FALSE(keybind->enabled); +} + +TEST(ConfigLuaObjects, keybindExposesMetadataAndRemoveMethods) { + CLuaState S; + const auto L = S.get(); + + Objects::CLuaKeybind{}.setup(L); + + auto keybind = makeShared(); + keybind->enabled = true; + keybind->description = "Close active window"; + keybind->hasDescription = true; + keybind->displayKey = "SUPER + Q"; + keybind->submap.name = "default"; + keybind->handler = "exec"; + keybind->arg = "kitty"; + keybind->modmask = HL_MODIFIER_META; + keybind->key = "Q"; + keybind->keycode = 24; + keybind->repeat = true; + keybind->locked = true; + keybind->release = false; + keybind->nonConsuming = true; + keybind->transparent = false; + keybind->ignoreMods = false; + keybind->longPress = false; + keybind->dontInhibit = true; + keybind->click = false; + keybind->drag = false; + keybind->submapUniversal = false; + keybind->deviceInclusive = true; + keybind->devices = {"kbd-a", "kbd-b"}; + + Objects::CLuaKeybind::push(L, keybind); + lua_setglobal(L, "kb"); + + ASSERT_EQ(luaL_dostring(L, R"( + assert(kb.enabled == true) + assert(kb.description == "Close active window") + assert(kb.display_key == "SUPER + Q") + assert(kb.submap == "default") + assert(kb.handler == "exec") + assert(kb.arg == "kitty") + assert(kb.modmask ~= nil) + assert(kb.key == "Q") + assert(kb.keycode == 24) + assert(kb.repeating == true) + assert(kb.locked == true) + assert(kb.non_consuming == true) + assert(kb.dont_inhibit == true) + assert(type(kb.devices) == "table") + + kb:remove() + kb:unbind() + )"), + LUA_OK); +} + +TEST(ConfigLuaObjects, objectsAreReadOnlyFromLua) { + CLuaState S; + const auto L = S.get(); + + Objects::CLuaKeybind{}.setup(L); + + auto keybind = makeShared(); + Objects::CLuaKeybind::push(L, keybind); + lua_setglobal(L, "kb"); + + EXPECT_NE(luaL_dostring(L, "kb.foo = 1"), LUA_OK); + ASSERT_TRUE(lua_isstring(L, -1)); + EXPECT_NE(std::string(lua_tostring(L, -1)).find("read-only"), std::string::npos); + lua_pop(L, 1); +} + +TEST(ConfigLuaObjects, keybindSupportsEqAndToString) { + CLuaState S; + const auto L = S.get(); + + Objects::CLuaKeybind{}.setup(L); + + auto keybindA = makeShared(); + auto keybindB = makeShared(); + + Objects::CLuaKeybind::push(L, keybindA); + lua_setglobal(L, "kb1"); + + Objects::CLuaKeybind::push(L, keybindA); + lua_setglobal(L, "kb2"); + + Objects::CLuaKeybind::push(L, keybindB); + lua_setglobal(L, "kb3"); + + ASSERT_EQ(luaL_dostring(L, R"( + eq12 = (kb1 == kb2) + neq13 = (kb1 ~= kb3) + + local s = tostring(kb1) + hasPrefix = type(s) == "string" and string.sub(s, 1, 11) == "HL.Keybind(" + )"), + LUA_OK); + + EXPECT_TRUE(getGlobalBool(L, "eq12")); + EXPECT_TRUE(getGlobalBool(L, "neq13")); + EXPECT_TRUE(getGlobalBool(L, "hasPrefix")); +} + +TEST(ConfigLuaObjects, eventSubscriptionRemoveAndIsActive) { + CLuaState S; + const auto L = S.get(); + + Objects::CLuaEventSubscription{}.setup(L); + CLuaEventHandler handler(L); + + ASSERT_EQ(luaL_dostring(L, R"( + count = 0 + function onReload() + count = count + 1 + end + )"), + LUA_OK); + + lua_getglobal(L, "onReload"); + ASSERT_TRUE(lua_isfunction(L, -1)); + const int ref = luaL_ref(L, LUA_REGISTRYINDEX); + + const auto handle = handler.registerEvent("config.reloaded", ref); + ASSERT_TRUE(handle.has_value()); + + Objects::CLuaEventSubscription::push(L, &handler, *handle); + lua_setglobal(L, "sub"); + + Event::bus()->m_events.config.reloaded.emit(); + EXPECT_EQ(getGlobalInt(L, "count"), 1); + + ASSERT_EQ(luaL_dostring(L, R"( + assert(sub:is_active() == true) + sub:remove() + assert(sub:is_active() == false) + sub:remove() + )"), + LUA_OK); + + Event::bus()->m_events.config.reloaded.emit(); + EXPECT_EQ(getGlobalInt(L, "count"), 1); +} + +TEST(ConfigLuaObjects, eventSubscriptionSupportsEqAndToString) { + CLuaState S; + const auto L = S.get(); + + Objects::CLuaEventSubscription{}.setup(L); + CLuaEventHandler handler(L); + + ASSERT_EQ(luaL_dostring(L, R"( + function cbA() end + function cbB() end + )"), + LUA_OK); + + lua_getglobal(L, "cbA"); + ASSERT_TRUE(lua_isfunction(L, -1)); + const int refA = luaL_ref(L, LUA_REGISTRYINDEX); + + lua_getglobal(L, "cbB"); + ASSERT_TRUE(lua_isfunction(L, -1)); + const int refB = luaL_ref(L, LUA_REGISTRYINDEX); + + const auto handleA = handler.registerEvent("config.reloaded", refA); + const auto handleB = handler.registerEvent("config.reloaded", refB); + ASSERT_TRUE(handleA.has_value()); + ASSERT_TRUE(handleB.has_value()); + + Objects::CLuaEventSubscription::push(L, &handler, *handleA); + lua_setglobal(L, "sub1"); + + Objects::CLuaEventSubscription::push(L, &handler, *handleA); + lua_setglobal(L, "sub2"); + + Objects::CLuaEventSubscription::push(L, &handler, *handleB); + lua_setglobal(L, "sub3"); + + ASSERT_EQ(luaL_dostring(L, R"( + eq12 = (sub1 == sub2) + neq13 = (sub1 ~= sub3) + + local s = tostring(sub1) + hasPrefix = type(s) == "string" and string.sub(s, 1, 21) == "HL.EventSubscription(" + )"), + LUA_OK); + + EXPECT_TRUE(getGlobalBool(L, "eq12")); + EXPECT_TRUE(getGlobalBool(L, "neq13")); + EXPECT_TRUE(getGlobalBool(L, "hasPrefix")); +} diff --git a/tests/helpers/Expression.cpp b/tests/helpers/Expression.cpp index 8db677763..6b0d1873a 100644 --- a/tests/helpers/Expression.cpp +++ b/tests/helpers/Expression.cpp @@ -65,3 +65,14 @@ TEST(Helpers, expressionZero) { EXPECT_DOUBLE_EQ(expr.compute("0 * 999").value(), 0.0); EXPECT_DOUBLE_EQ(expr.compute("5 - 5").value(), 0.0); } + +TEST(Helpers, expressionVec2ParsesLegacyString) { + auto parsed = parseExpressionVec2("monitor_w*0.5 monitor_h*0.25"); + ASSERT_TRUE(parsed.has_value()) << parsed.error(); + + EXPECT_EQ(parsed->x, "monitor_w*0.5"); + EXPECT_EQ(parsed->y, "monitor_h*0.25"); + EXPECT_EQ(parsed->toString(), "monitor_w*0.5 monitor_h*0.25"); + + EXPECT_FALSE(parseExpressionVec2("monitor_w*0.5").has_value()); +} diff --git a/tests/helpers/MathTransform.cpp b/tests/helpers/MathTransform.cpp index 686721b95..fe935d16b 100644 --- a/tests/helpers/MathTransform.cpp +++ b/tests/helpers/MathTransform.cpp @@ -1,4 +1,5 @@ #include +#include #include @@ -19,7 +20,7 @@ TEST(Helpers, mathWlTransformToHyprutils) { TEST(Helpers, mathWlTransformToHyprutilsInvalid) { // Invalid value falls back to NORMAL - EXPECT_EQ(wlTransformToHyprutils(static_cast(99)), eTransform::HYPRUTILS_TRANSFORM_NORMAL); + EXPECT_EQ(wlTransformToHyprutils(sc(99)), eTransform::HYPRUTILS_TRANSFORM_NORMAL); } // invertTransform @@ -47,7 +48,7 @@ TEST(Helpers, mathInvertTransformFlippedRotated) { TEST(Helpers, mathInvertTransformDoubleInvert) { // Double invert returns original for all transforms for (int i = 0; i <= 7; i++) { - auto t = static_cast(i); + auto t = sc(i); EXPECT_EQ(invertTransform(invertTransform(t)), t); } } @@ -57,7 +58,7 @@ TEST(Helpers, mathInvertTransformDoubleInvert) { TEST(Helpers, mathComposeTransformIdentity) { // Composing with NORMAL is identity for (int i = 0; i <= 7; i++) { - auto t = static_cast(i); + auto t = sc(i); EXPECT_EQ(composeTransform(t, eTransform::HYPRUTILS_TRANSFORM_NORMAL), t); EXPECT_EQ(composeTransform(eTransform::HYPRUTILS_TRANSFORM_NORMAL, t), t); }