Merge branch 'hyprwm:main' into main

This commit is contained in:
function i_use_lfs_btw() 2026-04-30 16:59:20 +03:00 committed by GitHub
commit 06c5dbd133
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
296 changed files with 22690 additions and 9725 deletions

View file

@ -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

View file

@ -45,6 +45,7 @@ runs:
libxkbcommon \
libxkbfile \
lld \
lua \
meson \
muparser \
ninja \

View file

@ -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()

View file

@ -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)

View file

@ -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

View file

@ -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]

View file

@ -88,6 +88,10 @@ INFO COMMANDS
Returns the current random splash.
**status**
Returns internal status information like config format or backend.
OPTIONS
=======

View file

@ -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
}

356
example/hyprland.lua Normal file
View file

@ -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,
})

30
flake.lock generated
View file

@ -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": {

View file

@ -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)"

View file

@ -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"

View file

@ -93,6 +93,7 @@ hyprctl [<OPTIONS>]... <ARGUMENTS>
| (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"
;
<WINDOW_STATE> ::= (-1) "Current"
@ -157,7 +158,6 @@ hyprctl [<OPTIONS>]... <ARGUMENTS>
| (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"

View file

@ -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"

View file

@ -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

View file

@ -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)

View file

@ -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

View file

@ -4,8 +4,6 @@
#include <any>
#define private public
#include <src/config/legacy/ConfigManager.hpp>
#include <src/config/supplementary/ConfigDescriptions.hpp>
#include <src/managers/input/InputManager.hpp>
#include <src/managers/PointerManager.hpp>
#include <src/managers/input/trackpad/TrackpadGestures.hpp>
@ -23,6 +21,11 @@
using namespace Hyprutils::Utils;
using namespace Hyprutils::String;
extern "C" {
#include <lua.h>
#include <lauxlib.h>
}
#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);

View file

@ -3,6 +3,8 @@
#include <format>
#include <print>
#include "shared.hpp"
namespace NLog {
template <typename... Args>
//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);
}
}
}

View file

@ -43,7 +43,7 @@ std::vector<SInstanceData> 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<SInstanceData> 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;
}
}

View file

@ -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 <algorithm>
#include <filesystem>
#include <hyprutils/os/Process.hpp>
#include <hyprutils/memory/WeakPtr.hpp>
#include <hyprutils/memory/Casts.hpp>
using namespace Hyprutils::Memory;
#include <csignal>
#include <cerrno>
@ -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<CProcess> 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<CProcess> hyprlandProc;
static bool launchHyprland(Path configPath, Path binaryPath) {
NLog::log("{}Launching Hyprland", Colors::YELLOW);
hyprlandProc = makeShared<CProcess>(binaryPath, std::vector<std::string>{"--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<const char*> 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<const char*, CTestCase&>& 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<const char*, CTestCase&>& 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<char*> args{argv + 1, sc<std::size_t>(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<const char*> args{const_cast<const char**>(argv + 1), sc<std::size_t>(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;
}

View file

@ -1,12 +1,12 @@
// Stolen from hyprutils
#pragma once
#include <iostream>
#include <cmath>
#include <string>
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")

View file

@ -1,4 +1,3 @@
#include "../../shared.hpp"
#include "../../hyprctlCompat.hpp"
#include "../shared.hpp"
#include "tests.hpp"
@ -7,6 +6,7 @@
#include <hyprutils/os/FileDescriptor.hpp>
#include <hyprutils/os/Process.hpp>
#include <optional>
#include <sys/poll.h>
#include <unistd.h>
#include <csignal>
@ -17,15 +17,6 @@ using namespace Hyprutils::Memory;
#define SP CSharedPointer
struct SClient {
SP<CProcess> proc;
std::array<char, 1024> readBuf;
CFileDescriptor readFd, writeFd;
struct pollfd fds;
};
static int ret = 0;
static bool waitForWindow(SP<CProcess> proc, int windowsBefore) {
int counter = 0;
while (Tests::processAlive(proc->pid()) && Tests::windowCount() == windowsBefore) {
@ -40,59 +31,72 @@ static bool waitForWindow(SP<CProcess> proc, int windowsBefore) {
return Tests::processAlive(proc->pid());
}
static bool startClient(SClient& client) {
namespace {
class CClient {
SP<CProcess> proc;
std::array<char, 1024> 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<CProcess>(binaryDir + "/child-window", std::vector<std::string>{});
this->proc = makeShared<CProcess>(binaryDir + "/child-window", std::vector<std::string>{});
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<CClient> 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<CClient> 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);

View file

@ -1,4 +1,3 @@
#include "../../shared.hpp"
#include "../../hyprctlCompat.hpp"
#include "../shared.hpp"
#include "tests.hpp"
@ -7,6 +6,7 @@
#include <hyprutils/os/FileDescriptor.hpp>
#include <hyprutils/os/Process.hpp>
#include <optional>
#include <sys/poll.h>
#include <unistd.h>
#include <csignal>
@ -17,100 +17,103 @@ using namespace Hyprutils::Memory;
#define SP CSharedPointer
struct SClient {
SP<CProcess> proc;
std::array<char, 1024> readBuf;
CFileDescriptor readFd, writeFd;
struct pollfd fds;
};
namespace {
class CClient {
SP<CProcess> proc;
std::array<char, 1024> readBuf;
CFileDescriptor readFd, writeFd;
struct pollfd fds;
static int ret = 0;
public:
CClient();
~CClient();
int getLastDelta();
};
}
static bool startClient(SClient& client) {
client.proc = makeShared<CProcess>(binaryDir + "/pointer-scroll", std::vector<std::string>{});
CClient::CClient() {
this->proc = makeShared<CProcess>(binaryDir + "/pointer-scroll", std::vector<std::string>{});
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<CClient> 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);

View file

@ -1,4 +1,3 @@
#include "../../shared.hpp"
#include "../../hyprctlCompat.hpp"
#include "../shared.hpp"
#include "tests.hpp"
@ -7,6 +6,7 @@
#include <hyprutils/os/FileDescriptor.hpp>
#include <hyprutils/os/Process.hpp>
#include <optional>
#include <sys/poll.h>
#include <unistd.h>
#include <csignal>
@ -17,102 +17,105 @@ using namespace Hyprutils::Memory;
#define SP CSharedPointer
struct SClient {
SP<CProcess> proc;
std::array<char, 1024> readBuf;
CFileDescriptor readFd, writeFd;
struct pollfd fds;
};
namespace {
class CClient {
SP<CProcess> proc;
std::array<char, 1024> 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<CProcess>(binaryDir + "/pointer-warp", std::vector<std::string>{});
CClient::CClient() {
this->proc = makeShared<CProcess>(binaryDir + "/pointer-warp", std::vector<std::string>{});
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<CClient> 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);

View file

@ -1,4 +1,3 @@
#include "../../shared.hpp"
#include "../../hyprctlCompat.hpp"
#include "../shared.hpp"
#include "tests.hpp"
@ -7,6 +6,7 @@
#include <hyprutils/os/FileDescriptor.hpp>
#include <hyprutils/os/Process.hpp>
#include <optional>
#include <sys/poll.h>
#include <csignal>
#include <thread>
@ -17,106 +17,109 @@ using namespace Hyprutils::Memory;
#define SP CSharedPointer
struct SClient {
SP<CProcess> proc;
std::array<char, 1024> readBuf;
CFileDescriptor readFd, writeFd;
struct pollfd fds;
};
namespace {
static int ret = 0;
class CClient {
SP<CProcess> proc;
std::array<char, 1024> readBuf;
CFileDescriptor readFd, writeFd;
struct pollfd fds;
static bool startClient(SClient& client) {
public:
CClient();
~CClient();
};
}
CClient::CClient() {
Tests::killAllWindows();
client.proc = makeShared<CProcess>(binaryDir + "/shortcut-inhibitor", std::vector<std::string>{});
this->proc = makeShared<CProcess>(binaryDir + "/shortcut-inhibitor", std::vector<std::string>{});
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<CClient> 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);

View file

@ -1,12 +1,9 @@
#pragma once
#include <map>
#include <vector>
#include <functional>
#include "../../shared.hpp"
inline std::vector<std::function<bool()>> clientTestFns;
inline std::map<const char*, CTestCase&> 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

View file

@ -5,18 +5,11 @@
#include <hyprutils/os/Process.hpp>
#include <hyprutils/memory/WeakPtr.hpp>
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)

View file

@ -6,32 +6,24 @@
#include <chrono>
#include <thread>
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)

View file

@ -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);

View file

@ -8,8 +8,6 @@
#include <hyprutils/memory/WeakPtr.hpp>
#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)

View file

@ -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)

View file

@ -10,14 +10,13 @@
#include <cerrno>
#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)

View file

@ -10,26 +10,22 @@
#include <cerrno>
#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)

View file

@ -12,8 +12,6 @@
#include <cerrno>
#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);

View file

@ -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<CProcess> 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);
}

View file

@ -6,8 +6,6 @@
#include <hyprutils/os/Process.hpp>
#include <hyprutils/memory/WeakPtr.hpp>
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)

View file

@ -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);

View file

@ -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);

View file

@ -10,8 +10,6 @@
#include <cerrno>
#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);
}

View file

@ -10,48 +10,42 @@
#include <cerrno>
#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)

View file

@ -10,27 +10,21 @@
#include <cerrno>
#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)

View file

@ -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);

View file

@ -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)

View file

@ -1,35 +1,42 @@
#include "tests.hpp"
#include "../../shared.hpp"
#include "../../hyprctlCompat.hpp"
#include <thread>
#include <algorithm>
#include <chrono>
#include <ranges>
#include <set>
#include <thread>
#include <hyprutils/os/Process.hpp>
#include <hyprutils/memory/WeakPtr.hpp>
#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<std::string>& expectedBlockedBy) {
const std::set<std::string> blockedBy = blockedByLine | std::ranges::views::split(',') | std::ranges::to<std::set<std::string>>();
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)

View file

@ -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)

View file

@ -1,12 +1,9 @@
#pragma once
#include <map>
#include <vector>
#include <functional>
#include "../shared.hpp"
inline std::vector<std::function<bool()>> testFns;
inline std::map<const char*, CTestCase&> 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

File diff suppressed because it is too large Load diff

View file

@ -11,8 +11,6 @@
#include <cerrno>
#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)

View file

@ -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);

View file

@ -1,4 +1,5 @@
#include "shared.hpp"
#include <cassert>
#include <csignal>
#include <cerrno>
#include <thread>
@ -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<CProcess> Tests::spawnKitty(const std::string& class_, const std::vector<std::string> 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);
}

View file

@ -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);
};

View file

@ -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

294
hyprtester/test.lua Normal file
View file

@ -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 })

746
meta/generateLuaStubs.py Normal file
View file

@ -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<string, string|number|boolean>"
out["HL.WindowRuleSpec"]["name"] = "string"
out["HL.WindowRuleSpec"]["enabled"] = "boolean"
out["HL.WindowRuleSpec"]["match"] = "table<string, string|number|boolean>"
out["HL.LayerRuleSpec"]["name"] = "string"
out["HL.LayerRuleSpec"]["enabled"] = "boolean"
out["HL.LayerRuleSpec"]["match"] = "table<string, string|boolean>"
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<std::string> 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<string, string|number|boolean>): 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())

1202
meta/hl.meta.lua Normal file

File diff suppressed because it is too large Load diff

View file

@ -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

View file

@ -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()

View file

@ -2,6 +2,7 @@
#include <re2/re2.h>
#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<CTokenManager>();
// 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<Hyprlang::INT>("general:resize_on_border");
static auto PBORDERSIZE = CConfigValue<Hyprlang::INT>("general:border_size");
static auto PBORDERGRABEXTEND = CConfigValue<Hyprlang::INT>("general:extend_border_grab_area");
static auto PSPECIALFALLTHRU = CConfigValue<Hyprlang::INT>("input:special_fallthrough");
static auto PMODALPARENTBLOCKING = CConfigValue<Hyprlang::INT>("general:modal_parent_blocking");
static auto PFOLLOWMOUSESHRINK = CConfigValue<Hyprlang::INT>("input:follow_mouse_shrink");
static auto PRESIZEONBORDER = CConfigValue<Config::INTEGER>("general:resize_on_border");
static auto PBORDERSIZE = CConfigValue<Config::INTEGER>("general:border_size");
static auto PBORDERGRABEXTEND = CConfigValue<Config::INTEGER>("general:extend_border_grab_area");
static auto PSPECIALFALLTHRU = CConfigValue<Config::INTEGER>("input:special_fallthrough");
static auto PMODALPARENTBLOCKING = CConfigValue<Config::INTEGER>("general:modal_parent_blocking");
static auto PFOLLOWMOUSESHRINK = CConfigValue<Config::INTEGER>("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<Hyprlang::INT>("binds:focus_preferred_method");
static auto PMONITORFALLBACK = CConfigValue<Hyprlang::INT>("binds:window_direction_monitor_fallback");
static auto PMETHOD = CConfigValue<Config::INTEGER>("binds:focus_preferred_method");
static auto PMONITORFALLBACK = CConfigValue<Config::INTEGER>("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<bool> floating) {
}
template <typename WINDOWPTR>
static bool isWindowAvailableForCycle(WINDOWPTR pWindow, WINDOWPTR w, bool focusableOnly, std::optional<bool> 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 <typename WINDOWPTR>
static bool isWindowAvailableForCycle(WINDOWPTR pWindow, WINDOWPTR w, bool focusableOnly, std::optional<bool> 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 <typename Iterator>
@ -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<bool> 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<bool> 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<bool> 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<bool> 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<CBox> CCompositor::calculateX11WorkArea() {
static auto PXWLFORCESCALEZERO = CConfigValue<Hyprlang::INT>("xwayland:force_zero_scaling");
static auto PXWLFORCESCALEZERO = CConfigValue<Config::INTEGER>("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<Hyprlang::INT>("binds:hide_special_on_workspace_change");
static auto PHIDESPECIALONWORKSPACECHANGE = CConfigValue<Config::INTEGER>("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<Hyprlang::INT>("render:direct_scanout");
static auto PALLOWPINFULLSCREEN = CConfigValue<Hyprlang::INT>("binds:allow_pin_fullscreen");
static auto PDIRECTSCANOUT = CConfigValue<Config::INTEGER>("render:direct_scanout");
static auto PALLOWPINFULLSCREEN = CConfigValue<Config::INTEGER>("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<Hyprlang::INT>("cursor:no_warps");
static auto PNOWARPS = CConfigValue<Config::INTEGER>("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<CWLSurfaceResource> pSurface) {
std::pair<SP<CWLSurfaceResource>, bool> result = {pSurface, false};
@ -2576,9 +2593,9 @@ std::vector<PHLWORKSPACE> CCompositor::getWorkspacesCopy() {
}
void CCompositor::performUserChecks() {
static auto PNOCHECKXDG = CConfigValue<Hyprlang::INT>("misc:disable_xdg_env_checks");
static auto PNOCHECKGUIUTILS = CConfigValue<Hyprlang::INT>("misc:disable_hyprland_guiutils_check");
static auto PNOWATCHDOG = CConfigValue<Hyprlang::INT>("misc:disable_watchdog_warning");
static auto PNOCHECKXDG = CConfigValue<Config::INTEGER>("misc:disable_xdg_env_checks");
static auto PNOCHECKGUIUTILS = CConfigValue<Config::INTEGER>("misc:disable_hyprland_guiutils_check");
static auto PNOWATCHDOG = CConfigValue<Config::INTEGER>("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<Hyprlang::INT>("group:group_on_movetoworkspace");
static auto PGROUPONMOVETOWORKSPACE = CConfigValue<Config::INTEGER>("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<Hyprlang::INT>("xwayland:force_zero_scaling");
static auto PXWLFORCESCALEZERO = CConfigValue<Config::INTEGER>("xwayland:force_zero_scaling");
std::vector<PHLMONITOR> toArrange(m_monitors.begin(), m_monitors.end());
std::vector<PHLMONITOR> arranged;

View file

@ -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<bool> floating = std::nullopt, bool visible = false, bool prev = false);
PHLWINDOW getWindowCycleHist(PHLWINDOWREF cur, bool focusableOnly = false, std::optional<bool> floating = std::nullopt, bool visible = false, bool next = false);
PHLWINDOW getWindowCycle(PHLWINDOW cur, bool focusableOnly = false, std::optional<bool> floating = std::nullopt, bool visible = false, bool prev = false,
bool allowFullscreenBlocked = false);
PHLWINDOW getWindowCycleHist(PHLWINDOWREF cur, bool focusableOnly = false, std::optional<bool> 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<CWLSurfaceResource>);
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!

View file

@ -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 <hyprutils/path/Path.hpp>
@ -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<Legacy::CConfigManager>();
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<Lua::CConfigManager>();
} else {
filePath.replace_extension(".conf");
Log::logger->log(Log::DEBUG, "[cfg] Lua config not found, using legacy config at {}", filePath.string());
g_mgr = makeUnique<Legacy::CConfigManager>();
}
} 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<Lua::CConfigManager>();
} else {
Log::logger->log(Log::DEBUG, "[cfg] Config is NOT lua, loading regular mgr");
g_mgr = makeUnique<Legacy::CConfigManager>();
}
}
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<IConfigManager>& Config::mgr() {
return g_mgr;
}
}
const char* Config::typeToString(eConfigManagerType t) {
switch (t) {
case CONFIG_LUA: return "lua";
case CONFIG_LEGACY: return "hyprlang";
default: return "error";
}
}

View file

@ -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 {
// <type>* 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<void, std::string> generateDefaultConfig(const std::filesystem::path&, bool safeMode = false) = 0;
virtual void handlePluginLoads() = 0;
virtual std::expected<void, std::string> registerPluginValue(void* handle, SP<Config::Values::IValue> value) = 0;
virtual void onPluginUnload(void* handle) = 0;
};
bool initConfigManager();
UP<IConfigManager>& mgr();
};
};

View file

@ -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) {

View file

@ -2,42 +2,59 @@
#include <string>
#include <typeindex>
#include <typeinfo>
#include <hyprlang.hpp>
#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 <typename T>
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<T>, got {} but has {}", typeid(T).name(), TYPE.name());
// RASSERT(typeid(T) == TYPE || STRINGEX || CUSTOMEX, "Mismatched type in CConfigValue<T>, 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<T* const*>(p_);
return *rc<T* const*>(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<std::string>::ptr() const {
template <>
inline std::string CConfigValue<std::string>::operator*() const {
return std::string{*rc<const Hyprlang::STRING*>(p_)};
if (m_typeIndex == typeid(std::string))
return **rc<const std::string* const*>(m_p);
else if (m_typeIndex == typeid(const char*))
return std::string{*rc<const Hyprlang::STRING*>(m_hlangp)};
else
RASSERT(false, "CConfigValue<std::string> on a FUCKED type");
return "FUCK";
}
template <>
inline Hyprlang::STRING* CConfigValue<Hyprlang::STRING>::ptr() const {
return rc<Hyprlang::STRING*>(*p_);
inline Config::INTEGER CConfigValue<Config::INTEGER>::operator*() const {
if (m_typeIndex == typeid(bool))
return **rc<const bool* const*>(m_p);
else if (m_typeIndex == typeid(Config::INTEGER))
return **rc<const Config::INTEGER* const*>(m_p);
else
RASSERT(false, "CConfigValue<Config::INTEGER> on a FUCKED type");
return -1;
}
template <>
inline Hyprlang::STRING CConfigValue<Hyprlang::STRING>::operator*() const {
return *rc<const Hyprlang::STRING*>(p_);
inline Config::IComplexConfigValue* CConfigValue<Config::IComplexConfigValue>::ptr() const {
if (m_hlangp)
return rc<Config::IComplexConfigValue*>((*rc<Hyprlang::CUSTOMTYPE* const*>(m_hlangp))->getData());
else
return *rc<Config::IComplexConfigValue* const*>(m_p);
}
template <>
inline Hyprlang::CUSTOMTYPE* CConfigValue<Hyprlang::CUSTOMTYPE>::ptr() const {
return *rc<Hyprlang::CUSTOMTYPE* const*>(p_);
}
template <>
inline Hyprlang::CUSTOMTYPE CConfigValue<Hyprlang::CUSTOMTYPE>::operator*() const {
RASSERT(false, "Impossible to implement operator* of CConfigValue<Hyprlang::CUSTOMTYPE>, use ptr()");
return *ptr();
}

View file

@ -1,6 +1,8 @@
#include <re2/re2.h>
#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<CConfigManager> 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<Hyprlang::CConfig>(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<Hyprlang::INT>(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<Config::Values::CIntValue*>(v.get()))
registerConfigVar(NAME, Hyprlang::INT{p->defaultVal()});
else if (auto p = dc<Config::Values::CFloatValue*>(v.get()))
registerConfigVar(NAME, Hyprlang::FLOAT{p->defaultVal()});
else if (auto p = dc<Config::Values::CBoolValue*>(v.get()))
registerConfigVar(NAME, Hyprlang::INT{p->defaultVal() ? 1 : 0});
else if (auto p = dc<Config::Values::CStringValue*>(v.get()))
registerConfigVar(NAME, Hyprlang::STRING{p->defaultVal().c_str()});
else if (auto p = dc<Config::Values::CColorValue*>(v.get()))
registerConfigVar(NAME, Hyprlang::INT{p->defaultVal()});
else if (auto p = dc<Config::Values::CVec2Value*>(v.get()))
registerConfigVar(NAME, Hyprlang::VEC2{p->defaultVal().x, p->defaultVal().y});
else if (auto p = dc<Config::Values::CCssGapValue*>(v.get()))
registerConfigVar(NAME, Hyprlang::CConfigCustomValueType{configHandleGapSet, configHandleGapDestroy, std::to_string(p->defaultVal().m_top).c_str()});
else if (auto p = dc<Config::Values::CFontWeightValue*>(v.get()))
registerConfigVar(NAME,
Hyprlang::CConfigCustomValueType{&configHandleFontWeightSet, configHandleFontWeightDestroy, std::format("{}", p->defaultVal().m_value).c_str()});
else if (auto p = dc<Config::Values::CGradientValue*>(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<std::string> 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<std::string> 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<Hyprlang::STRING>(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<Hyprlang::STRING>(VAL->getValue()));
if (!res)
return res.error();
}
}
Desktop::Rule::ruleEngine()->registerRule(std::move(rule));
@ -1248,8 +922,11 @@ std::optional<std::string> 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<Hyprlang::STRING>(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<Hyprlang::STRING>(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<std::string> CConfigManager::handleRawExec(const std::string& comm
return {};
}
g_pKeybindManager->spawnRaw(args);
Config::Supplementary::executor()->spawnRaw(args);
return {};
}
@ -1564,7 +1241,7 @@ std::optional<std::string> CConfigManager::handleExec(const std::string& command
return {};
}
g_pKeybindManager->spawn(args);
Config::Supplementary::executor()->spawn(args);
return {};
}
@ -1584,7 +1261,7 @@ std::optional<std::string> CConfigManager::handleExecRawOnce(const std::string&
std::optional<std::string> 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<std::string> 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<std::string> 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<std::string> CConfigManager::handleBind(const std::string& command
else if ((ARGS.size() > sc<size_t>(4) + DESCR_OFFSET + DEVICE_OFFSET && !mouse) || (ARGS.size() > sc<size_t>(3) + DESCR_OFFSET + DEVICE_OFFSET && mouse))
return "bind: too many args";
std::set<xkb_keysym_t> KEYSYMS;
std::set<xkb_keysym_t> MODS;
std::vector<xkb_keysym_t> KEYSYMS;
std::vector<xkb_keysym_t> 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<std::string> 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<std::string> 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<std::string> 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<void, std::string> CConfigManager::registerPluginValue(void* handle, SP<Config::Values::IValue> value) {
const std::string NAME = value->name();
if (!NAME.starts_with("plugin:"))
return std::unexpected("name must start with plugin:");
if (auto p = dc<Config::Values::CIntValue*>(value.get()))
addPluginConfigVar(handle, NAME, Hyprlang::INT{p->defaultVal()});
else if (auto p = dc<Config::Values::CFloatValue*>(value.get()))
addPluginConfigVar(handle, NAME, Hyprlang::FLOAT{p->defaultVal()});
else if (auto p = dc<Config::Values::CBoolValue*>(value.get()))
addPluginConfigVar(handle, NAME, Hyprlang::INT{p->defaultVal() ? 1 : 0});
else if (auto p = dc<Config::Values::CStringValue*>(value.get()))
addPluginConfigVar(handle, NAME, Hyprlang::STRING{p->defaultVal().c_str()});
else if (auto p = dc<Config::Values::CColorValue*>(value.get()))
addPluginConfigVar(handle, NAME, Hyprlang::INT{p->defaultVal()});
else if (auto p = dc<Config::Values::CVec2Value*>(value.get()))
addPluginConfigVar(handle, NAME, Hyprlang::VEC2{p->defaultVal().x, p->defaultVal().y});
else if (auto p = dc<Config::Values::CCssGapValue*>(value.get()))
addPluginConfigVar(handle, NAME, Hyprlang::CConfigCustomValueType{configHandleGapSet, configHandleGapDestroy, std::to_string(p->defaultVal().m_top).c_str()});
else if (auto p = dc<Config::Values::CFontWeightValue*>(value.get()))
addPluginConfigVar(handle, NAME,
Hyprlang::CConfigCustomValueType{&configHandleFontWeightSet, configHandleFontWeightDestroy, std::format("{}", p->defaultVal().m_value).c_str()});
else if (auto p = dc<Config::Values::CGradientValue*>(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 {};
}

View file

@ -66,6 +66,9 @@ namespace Config::Legacy {
virtual void handlePluginLoads() override;
virtual bool configVerifPassed() override;
virtual std::expected<void, std::string> registerPluginValue(void* handle, SP<Config::Values::IValue> 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);

View file

@ -1,6 +1,6 @@
#pragma once
#include <string>
#include <string_view>
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)};

View file

@ -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 <hyprutils/string/String.hpp>
#include <hyprutils/string/VarList2.hpp>
using namespace Hyprutils::String;
using namespace Config;
using namespace Config::Legacy;
using namespace Config::Actions;
UP<CDispatcherTranslator>& Legacy::translator() {
static UP<CDispatcherTranslator> p = makeUnique<CDispatcherTranslator>();
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<PHLWINDOW> 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<eFullscreenMode>(internalMode) : PWINDOW->m_fullscreenState.internal;
eFullscreenMode cm = clientMode != -1 ? sc<eFullscreenMode>(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:<monitor>" 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<bool> onlyTiled = {};
std::optional<bool> 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<uintptr_t>(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<PHLMONITOR> 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<float> 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;
}

View file

@ -0,0 +1,23 @@
#pragma once
#include "../../SharedDefs.hpp"
#include "../../helpers/memory/Memory.hpp"
#include <functional>
#include <unordered_map>
#include <string>
namespace Config::Legacy {
class CDispatcherTranslator {
public:
CDispatcherTranslator();
~CDispatcherTranslator() = default;
SDispatchResult run(const std::string& dispatcher, const std::string& data);
private:
std::unordered_map<std::string, std::function<SDispatchResult(const std::string&)>> m_dispMap;
};
UP<CDispatcherTranslator>& translator();
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,189 @@
#pragma once
#include <hyprutils/animation/AnimationConfig.hpp>
#include <vector>
#include <string>
#include <optional>
#include <chrono>
#include <string_view>
#include <unordered_map>
#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 <lua.h>
#include <lualib.h>
#include <lauxlib.h>
}
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<std::string>& getConfigPaths() override;
virtual std::expected<void, std::string> generateDefaultConfig(const std::filesystem::path&, bool safeMode) override;
virtual void handlePluginLoads() override;
virtual bool configVerifPassed() override;
virtual std::expected<void, std::string> registerPluginValue(void* handle, SP<Config::Values::IValue> value) override;
virtual void onPluginUnload(void* handle) override;
int invokePluginLuaFunctionByID(uint64_t id, lua_State* L);
std::expected<void, std::string> registerPluginLuaFunction(void* handle, const std::string& namespace_, const std::string& name, PLUGIN_LUA_FN fn);
std::expected<void, std::string> 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<std::string> 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<CLuaEventHandler> m_eventHandler;
struct SLuaTimer {
SP<CEventLoopTimer> timer;
int luaRef = LUA_NOREF; // registry ref to the lua callback
};
std::vector<SLuaTimer> m_luaTimers;
std::vector<std::string> m_registeredPlugins;
std::unordered_map<std::string, UP<ILuaConfigValue>> m_configValues;
struct SDeviceConfig {
SDeviceConfig();
std::unordered_map<std::string, UP<ILuaConfigValue>> values;
};
std::unordered_map<std::string, SDeviceConfig> m_deviceConfigs;
std::vector<std::string> m_errors, m_configPaths;
std::vector<Config::SConfigError> m_evalIssues;
// named window/layer rules for merge-on-redeclaration
std::unordered_map<std::string, SP<Desktop::Rule::CWindowRule>> m_luaWindowRules;
std::unordered_map<std::string, SP<Desktop::Rule::CLayerRule>> 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<void, std::string> registerPluginLuaFunctionInState(uint64_t id, const std::string& namespace_, const std::string& name);
std::expected<void, std::string> 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<int> m_heldLuaRefs;
// this is here for legacy reasons.
std::unordered_map<std::string, const void*> m_configPtrMap;
// this is here for plugin reasons.
std::unordered_map<void* /* HANDLE */, std::vector<std::string>> 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<SPluginLuaFunction> m_pluginLuaFunctions;
ILuaConfigValue* findDeviceValue(const std::string& dev, const std::string& field);
friend class CConfigManagerPluginLuaTestAccessor;
};
WP<CConfigManager> mgr();
}

View file

@ -0,0 +1,18 @@
#pragma once
#include <string_view>
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)};

View file

@ -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,
})
)#";
}

View file

@ -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);
}

View file

@ -0,0 +1,13 @@
#pragma once
extern "C" {
#include <lua.h>
}
namespace Config::Lua {
class CConfigManager;
}
namespace Config::Lua::Bindings {
void registerBindings(lua_State* L, CConfigManager* mgr);
}

View file

@ -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 <lauxlib.h>
}
#include <format>
using namespace Config::Lua;
using namespace Config::Lua::Objects;
void CLuaEventHandler::dispatch(const std::string& name, int nargs, const std::function<void()>& 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<lua_Integer>(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<lua_Integer>(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<uint64_t> 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<std::string>& CLuaEventHandler::knownEvents() {
static const std::unordered_set<std::string> 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;
}

View file

@ -0,0 +1,61 @@
#pragma once
#include <functional>
#include <string>
#include <unordered_map>
#include <unordered_set>
#include <vector>
#include <optional>
#include <cstdint>
#include "../../helpers/memory/Memory.hpp"
#include "../../helpers/signal/Signal.hpp"
#include "../../desktop/DesktopTypes.hpp"
extern "C" {
#include <lua.h>
}
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<uint64_t> registerEvent(const std::string& name, int luaRef);
bool unregisterEvent(uint64_t handle);
void clearEvents();
static const std::unordered_set<std::string>& knownEvents();
private:
struct SSubscription {
std::string eventName;
int luaRef = -1;
};
lua_State* m_lua = nullptr;
std::unordered_map<std::string, std::vector<uint64_t>> m_callbacks;
std::unordered_map<uint64_t, SSubscription> m_subscriptions;
std::unordered_set<uint64_t> m_activeHandles;
std::unordered_set<uint64_t> m_reentrancyWarnedHandles;
uint64_t m_nextHandle = 1;
size_t m_dispatchDepth = 0;
std::vector<CHyprSignalListener> m_listeners;
static constexpr size_t MAX_DISPATCH_DEPTH = 32;
void dispatch(const std::string& name, int nargs, const std::function<void()>& pushArgs);
};
}

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -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<std::string> 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<double> 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<bool> 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<PHLMONITORREF*>(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<PHLWORKSPACEREF*>(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<PHLWINDOWREF*>(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<PHLMONITOR> 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<PHLWORKSPACE> 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<PHLWINDOW> 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<std::string> 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<PHLMONITORREF*>(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<std::string> 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<PHLWORKSPACEREF*>(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<std::string> 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<PHLWINDOWREF*>(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<uintptr_t>(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<std::string> 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<std::string> 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<std::string> 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<PHLWINDOW> 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<std::string, std::string> 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<void, std::string> Internal::addWindowRuleEffectFromLua(lua_State* L, const SWindowRuleEffectDesc& desc, const SP<Desktop::Rule::CWindowRule>& rule) {
auto val = UP<ILuaConfigValue>(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<CLuaConfigExpressionVec2*>(val.get()); expr)
return rule->addEffect(desc.effect, expr->parsed());
return rule->addEffect(desc.effect, val->toString());
}
std::expected<SP<Desktop::Rule::CWindowRule>, int> Internal::buildRuleFromTable(lua_State* L, int idx) {
SP<Desktop::Rule::CWindowRule> 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<Desktop::Rule::CWindowRule>();
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;
}

View file

@ -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 <functional>
#include <optional>
#include <format>
#include <string>
#include <string_view>
#include <utility>
extern "C" {
#include <lua.h>
#include <lauxlib.h>
}
namespace Desktop::Rule {
class CWindowRule;
}
namespace Config::Lua::Bindings::Internal {
struct SWindowRuleEffectDesc {
const char* name;
std::function<ILuaConfigValue*()> 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<std::string> tableOptStr(lua_State* L, int idx, const char* field);
std::optional<double> tableOptNum(lua_State* L, int idx, const char* field);
std::optional<bool> 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<PHLMONITOR> tableOptMonitor(lua_State* L, int idx, const char* field, const char* fnName);
std::optional<PHLWORKSPACE> tableOptWorkspace(lua_State* L, int idx, const char* field, const char* fnName);
std::optional<PHLWINDOW> tableOptWindow(lua_State* L, int idx, const char* field, const char* fnName);
std::optional<std::string> monitorSelectorFromLuaSelectorOrObject(lua_State* L, int idx, const char* fnName);
std::optional<std::string> workspaceSelectorFromLuaSelectorOrObject(lua_State* L, int idx, const char* fnName);
std::optional<std::string> windowSelectorFromLuaSelectorOrObject(lua_State* L, int idx, const char* fnName);
std::optional<std::string> tableOptMonitorSelector(lua_State* L, int idx, const char* field, const char* fnName);
std::optional<std::string> tableOptWorkspaceSelector(lua_State* L, int idx, const char* field, const char* fnName);
std::optional<std::string> 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<PHLWINDOW> 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<std::string, std::string> ruleValueToString(lua_State* L);
std::expected<void, std::string> addWindowRuleEffectFromLua(lua_State* L, const SWindowRuleEffectDesc& desc, const SP<Desktop::Rule::CWindowRule>& rule);
std::expected<SP<Desktop::Rule::CWindowRule>, 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 <typename... Args>
int configError(lua_State* L, std::format_string<Args...> fmt, Args&&... args) {
return configError(L, std::format(fmt, std::forward<Args>(args)...));
}
template <typename... Args>
int dispatcherError(lua_State* L, Config::Actions::eActionErrorLevel level, Config::Actions::eActionErrorCode code, std::format_string<Args...> fmt, Args&&... args) {
return dispatcherError(L, std::format(fmt, std::forward<Args>(args)...), level, code);
}
template <typename... Args>
int configError(lua_State* L, int stackLevel, std::format_string<Args...> fmt, Args&&... args) {
return configError(L, std::format(fmt, std::forward<Args>(args)...), Config::Actions::eActionErrorLevel::ERROR, Config::Actions::eActionErrorCode::UNKNOWN, stackLevel);
}
template <typename T, size_t N>
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 <typename T>
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);
}

View file

@ -0,0 +1,135 @@
#include "LuaBindingsInternal.hpp"
#include "../objects/LuaNotification.hpp"
#include "../../../helpers/MiscFunctions.hpp"
#include "../../../notification/NotificationOverlay.hpp"
#include <algorithm>
#include <array>
#include <cctype>
#include <optional>
#include <string>
using namespace Config;
using namespace Config::Lua;
using namespace Config::Lua::Bindings;
static std::optional<eIcons> iconFromString(std::string iconName) {
std::ranges::transform(iconName, iconName.begin(), [](const unsigned char c) { return std::tolower(c); });
static constexpr std::array<std::pair<const char*, eIcons>, 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<eIcons> parseIconArg(lua_State* L, int idx) {
if (lua_isnumber(L, idx)) {
const auto raw = sc<int>(lua_tonumber(L, idx));
if (raw >= ICON_WARNING && raw <= ICON_NONE)
return sc<eIcons>(raw);
return std::nullopt;
}
if (lua_isstring(L, idx))
return iconFromString(lua_tostring(L, idx));
return std::nullopt;
}
static std::optional<CHyprColor> parseColorArg(lua_State* L, int idx) {
if (lua_isnumber(L, idx))
return CHyprColor(sc<uint64_t>(lua_tonumber(L, idx)));
if (lua_isstring(L, idx)) {
auto parsed = configStringToInt(lua_tostring(L, idx));
if (!parsed)
return std::nullopt;
return CHyprColor(sc<uint64_t>(*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<double> 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<float>(*parsedFontSize);
}
auto notification = Notification::overlay()->addNotification(text, color, sc<float>(*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");
}

View file

@ -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<PHLMONITOR> monitor;
std::optional<PHLWORKSPACE> workspace;
std::optional<bool> floating;
std::optional<bool> mapped;
std::optional<std::string> className;
std::optional<std::string> title;
std::optional<std::string> tag;
};
struct SLayerQuery {
std::optional<PHLMONITOR> monitor;
std::optional<std::string> 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);
}

View file

@ -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");
}

View file

@ -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 <hyprutils/string/String.hpp>
#include <hyprutils/string/VarList.hpp>
using namespace Config;
using namespace Config::Lua;
using namespace Config::Lua::Bindings;
using namespace Hyprutils::String;
static std::optional<eKeyboardModifiers> 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<void, std::string> parseKeyString(SKeybind& kb, std::string_view sv) {
bool modsEnded = false, specialSym = false;
CVarList2 vl(sv, 0, '+', true);
uint32_t modMask = 0;
std::vector<xkb_keysym_t> 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<CConfigManager*>(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<std::string> {
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<CConfigManager*>(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<CConfigManager*>(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<CConfigManager*>(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<CEventLoopTimer>(
std::chrono::milliseconds(timeoutMs),
[L, ref, repeat, timeoutMs, mgr](SP<CEventLoopTimer> 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);
}

View file

@ -0,0 +1,74 @@
#include "LuaEventSubscription.hpp"
#include <format>
#include <string_view>
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<SEventSubscriptionRef*>(luaL_checkudata(L, 1, MT));
const auto* rhs = sc<SEventSubscriptionRef*>(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<SEventSubscriptionRef*>(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<SEventSubscriptionRef*>(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<SEventSubscriptionRef*>(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<SEventSubscriptionRef>, 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);
}

View file

@ -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);
};
}

View file

@ -0,0 +1,83 @@
#include "LuaGroup.hpp"
#include "LuaWindow.hpp"
#include "LuaObjectHelpers.hpp"
#include "../../../desktop/view/Group.hpp"
#include <string_view>
using namespace Config::Lua;
static constexpr const char* MT = "HL.Group";
static int groupEq(lua_State* L) {
const auto* lhs = sc<WP<Desktop::View::CGroup>*>(luaL_checkudata(L, 1, MT));
const auto* rhs = sc<WP<Desktop::View::CGroup>*>(luaL_checkudata(L, 2, MT));
lua_pushboolean(L, lhs->lock() == rhs->lock());
return 1;
}
static int groupToString(lua_State* L) {
const auto* ref = sc<WP<Desktop::View::CGroup>*>(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<WP<Desktop::View::CGroup>*>(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<lua_Integer>(group->size()));
else if (key == "current_index")
lua_pushinteger(L, sc<lua_Integer>(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<WP<Desktop::View::CGroup>>, groupEq, groupToString);
}
void Objects::CLuaGroup::push(lua_State* L, SP<Desktop::View::CGroup> group) {
new (lua_newuserdata(L, sizeof(WP<Desktop::View::CGroup>))) WP<Desktop::View::CGroup>(group);
luaL_getmetatable(L, MT);
lua_setmetatable(L, -2);
}

View file

@ -0,0 +1,14 @@
#pragma once
#include <hyprutils/memory/WeakPtr.hpp>
#include <lua.hpp>
#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<Desktop::View::CGroup> group);
};
};

View file

@ -0,0 +1,182 @@
#include "LuaKeybind.hpp"
#include <optional>
#include <string_view>
using namespace Config::Lua;
static constexpr const char* MT = "HL.Keybind";
namespace {
std::optional<SP<SKeybind>> getKeybindFromUserdata(lua_State* L) {
auto* ref = sc<WP<SKeybind>*>(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<WP<SKeybind>*>(luaL_checkudata(L, 1, MT));
const auto* rhs = sc<WP<SKeybind>*>(luaL_checkudata(L, 2, MT));
lua_pushboolean(L, lhs->lock() == rhs->lock());
return 1;
}
static int keybindToString(lua_State* L) {
const auto* ref = sc<WP<SKeybind>*>(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<lua_Integer>((*keybind)->modmask));
else if (key == "key")
lua_pushstring(L, (*keybind)->key.c_str());
else if (key == "keycode")
lua_pushinteger(L, sc<lua_Integer>((*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<WP<SKeybind>>, keybindEq, keybindToString);
}
void Objects::CLuaKeybind::push(lua_State* L, const SP<SKeybind>& keybind) {
new (lua_newuserdata(L, sizeof(WP<SKeybind>))) WP<SKeybind>(keybind);
luaL_getmetatable(L, MT);
lua_setmetatable(L, -2);
}

View file

@ -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<SKeybind>& keybind);
};
}

View file

@ -0,0 +1,80 @@
#include "LuaLayerRule.hpp"
#include "../../../desktop/rule/Engine.hpp"
#include <string_view>
using namespace Config::Lua;
static constexpr const char* MT = "HL.LayerRule";
//
static int layerRuleEq(lua_State* L) {
const auto* lhs = sc<WP<Desktop::Rule::CLayerRule>*>(luaL_checkudata(L, 1, MT));
const auto* rhs = sc<WP<Desktop::Rule::CLayerRule>*>(luaL_checkudata(L, 2, MT));
lua_pushboolean(L, lhs->lock() == rhs->lock());
return 1;
}
static int layerRuleToString(lua_State* L) {
const auto* ref = sc<WP<Desktop::Rule::CLayerRule>*>(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<WP<Desktop::Rule::CLayerRule>*>(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<WP<Desktop::Rule::CLayerRule>*>(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<WP<Desktop::Rule::CLayerRule>>, layerRuleEq, layerRuleToString);
}
void Objects::CLuaLayerRule::push(lua_State* L, const SP<Desktop::Rule::CLayerRule>& rule) {
new (lua_newuserdata(L, sizeof(WP<Desktop::Rule::CLayerRule>))) WP<Desktop::Rule::CLayerRule>(rule);
luaL_getmetatable(L, MT);
lua_setmetatable(L, -2);
}

View file

@ -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<Desktop::Rule::CLayerRule>& rule);
};
}

View file

@ -0,0 +1,88 @@
#include "LuaLayerSurface.hpp"
#include "LuaMonitor.hpp"
#include "LuaObjectHelpers.hpp"
#include "../../../desktop/view/LayerSurface.hpp"
#include <format>
#include <string_view>
using namespace Config::Lua;
static constexpr const char* MT = "HL.LayerSurface";
//
static int layerSurfaceEq(lua_State* L) {
const auto* lhs = sc<PHLLSREF*>(luaL_checkudata(L, 1, MT));
const auto* rhs = sc<PHLLSREF*>(luaL_checkudata(L, 2, MT));
lua_pushboolean(L, lhs->lock() == rhs->lock());
return 1;
}
static int layerSurfaceToString(lua_State* L) {
const auto* ref = sc<PHLLSREF*>(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<PHLLSREF*>(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<uintptr_t>(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<lua_Integer>(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<lua_Integer>(ls->m_layer));
else if (key == "interactivity")
lua_pushinteger(L, sc<lua_Integer>(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<PHLLSREF>, 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);
}

View file

@ -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);
};
}

View file

@ -0,0 +1,122 @@
#include "LuaMonitor.hpp"
#include "LuaWorkspace.hpp"
#include "LuaObjectHelpers.hpp"
#include "../../../helpers/Monitor.hpp"
#include "../../../desktop/state/FocusState.hpp"
#include <string_view>
using namespace Config::Lua;
static constexpr const char* MT = "HL.Monitor";
//
static int monitorEq(lua_State* L) {
const auto* lhs = sc<PHLMONITORREF*>(luaL_checkudata(L, 1, MT));
const auto* rhs = sc<PHLMONITORREF*>(luaL_checkudata(L, 2, MT));
lua_pushboolean(L, lhs->lock() == rhs->lock());
return 1;
}
static int monitorToString(lua_State* L) {
const auto* ref = sc<PHLMONITORREF*>(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<PHLMONITORREF*>(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<lua_Integer>(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<int>(mon->m_pixelSize.x));
else if (key == "height")
lua_pushinteger(L, sc<int>(mon->m_pixelSize.y));
else if (key == "refresh_rate")
lua_pushnumber(L, mon->m_refreshRate);
else if (key == "x")
lua_pushinteger(L, sc<int>(mon->m_position.x));
else if (key == "y")
lua_pushinteger(L, sc<int>(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<int>(mon->m_position.x));
lua_setfield(L, -2, "x");
lua_pushinteger(L, sc<int>(mon->m_position.y));
lua_setfield(L, -2, "y");
} else if (key == "size") {
lua_newtable(L);
lua_pushinteger(L, sc<int>(mon->m_pixelSize.x));
lua_setfield(L, -2, "width");
lua_pushinteger(L, sc<int>(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<int>(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<PHLMONITORREF>, 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);
}

View file

@ -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);
};
}

View file

@ -0,0 +1,377 @@
#include "LuaNotification.hpp"
#include "../../../helpers/MiscFunctions.hpp"
#include <array>
#include <algorithm>
#include <cctype>
#include <optional>
#include <string_view>
using namespace Config::Lua;
static constexpr const char* MT = "HL.Notification";
namespace {
struct SNotificationRef {
WP<Notification::CNotification> notification;
bool paused = false;
};
std::optional<CHyprColor> parseColor(lua_State* L, int idx) {
if (lua_isnumber(L, idx))
return CHyprColor(sc<uint64_t>(lua_tonumber(L, idx)));
if (lua_isstring(L, idx)) {
auto parsed = configStringToInt(lua_tostring(L, idx));
if (!parsed)
return std::nullopt;
return CHyprColor(sc<uint64_t>(*parsed));
}
return std::nullopt;
}
std::optional<eIcons> iconFromString(std::string iconName) {
std::ranges::transform(iconName, iconName.begin(), [](const unsigned char c) { return std::tolower(c); });
static constexpr std::array<std::pair<const char*, eIcons>, 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<eIcons> parseIcon(lua_State* L, int idx) {
if (lua_isnumber(L, idx)) {
const auto raw = sc<int>(lua_tonumber(L, idx));
if (raw >= ICON_WARNING && raw <= ICON_NONE)
return sc<eIcons>(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<SNotificationRef*>(luaL_checkudata(L, 1, MT));
const auto* rhs = sc<SNotificationRef*>(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<SNotificationRef*>(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<SNotificationRef*>(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<SNotificationRef*>(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<SNotificationRef*>(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<SNotificationRef*>(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<SNotificationRef*>(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<SNotificationRef*>(luaL_checkudata(L, 1, MT));
const auto timeoutMs = sc<float>(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<SNotificationRef*>(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<SNotificationRef*>(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<SNotificationRef*>(luaL_checkudata(L, 1, MT));
const auto fontSize = sc<float>(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<SNotificationRef*>(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<SNotificationRef*>(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<SNotificationRef*>(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<SNotificationRef*>(luaL_checkudata(L, 1, MT));
const auto notification = ref->notification.lock();
if (!notification) {
lua_pushnil(L);
return 1;
}
lua_pushinteger(L, sc<lua_Integer>(notification->color().getAsHex()));
return 1;
}
static int notificationGetIcon(lua_State* L) {
auto* ref = sc<SNotificationRef*>(luaL_checkudata(L, 1, MT));
const auto notification = ref->notification.lock();
if (!notification) {
lua_pushnil(L);
return 1;
}
lua_pushinteger(L, sc<lua_Integer>(notification->icon()));
return 1;
}
static int notificationGetFontSize(lua_State* L) {
auto* ref = sc<SNotificationRef*>(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<SNotificationRef*>(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<SNotificationRef*>(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<SNotificationRef*>(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::CNotification>& notification) {
new (lua_newuserdata(L, sizeof(SNotificationRef))) SNotificationRef{.notification = WP<Notification::CNotification>(notification)};
luaL_getmetatable(L, MT);
lua_setmetatable(L, -2);
}

View file

@ -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::CNotification>& notification);
};
}

View file

@ -0,0 +1,51 @@
#pragma once
#include "../bindings/LuaBindingsInternal.hpp"
#include "../../../helpers/memory/Memory.hpp"
extern "C" {
#include <lauxlib.h>
}
namespace Config::Lua::Objects {
template <typename T>
inline int gcRef(lua_State* L) {
sc<T*>(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;
};
}

Some files were not shown because too many files have changed in this diff Show more