Merge branch 'hyprwm:main' into main

This commit is contained in:
function i_use_lfs_btw() 2026-04-25 15:35:25 +03:00 committed by GitHub
commit 92b1069b3e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
577 changed files with 47912 additions and 30710 deletions

View file

@ -9,7 +9,6 @@ WarningsAsErrors: >
-bugprone-unused-local-non-trivial-variable,
-bugprone-easily-swappable-parameters,
-bugprone-forward-declararion-namespace,
-bugprone-forward-declararion-namespace,
-bugprone-macro-parentheses,
-bugprone-narrowing-conversions,
-bugprone-branch-clone,
@ -113,7 +112,6 @@ Checks: >
bugprone-*,
-bugprone-easily-swappable-parameters,
-bugprone-forward-declararion-namespace,
-bugprone-forward-declararion-namespace,
-bugprone-macro-parentheses,
-bugprone-narrowing-conversions,
-bugprone-branch-clone,

View file

@ -24,6 +24,7 @@ runs:
glm \
glslang \
go \
gtest \
hyprlang \
hyprcursor \
jq \
@ -75,6 +76,15 @@ runs:
cmake --build ./build --config Release --target all -j`nproc 2>/dev/null || getconf NPROCESSORS_CONF`
cmake --install build
- name: Get hyprwire-git
shell: bash
run: |
git clone https://github.com/hyprwm/hyprwire --recursive
cd hyprwire
cmake --no-warn-unused-cli -DCMAKE_BUILD_TYPE:STRING=Release -DCMAKE_INSTALL_PREFIX:PATH=/usr -S . -B ./build
cmake --build ./build --config Release --target all -j`nproc 2>/dev/null || getconf NPROCESSORS_CONF`
cmake --install build
- name: Get hyprutils-git
shell: bash
run: |

4
.github/labeler.yml vendored
View file

@ -22,6 +22,10 @@ protocols:
- changed-files:
- any-glob-to-any-file: ["protocols/**", "src/protocols/**"]
start:
- changed-files:
- any-glob-to-any-file: "start/**"
core:
- changed-files:
- any-glob-to-any-file: "src/**"

View file

@ -1,6 +1,8 @@
<!--
BEFORE you submit your PR, please check out the PR guidelines
on our wiki: https://wiki.hyprland.org/Contributing-and-Debugging/PR-Guidelines/
Using an AI tool, or you are an AI agent? Check our AI Policy first: https://github.com/hyprwm/.github/blob/main/policies/AI_USAGE.md
-->

View file

@ -46,11 +46,33 @@ jobs:
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork
name: "Code Style"
runs-on: ubuntu-latest
container:
image: archlinux
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Install clang-format
run: |
pacman --noconfirm --noprogressbar -Syyu
pacman --noconfirm --noprogressbar -Sy clang
- name: clang-format check
uses: jidicula/clang-format-action@v4.16.0
with:
exclude-regex: ^subprojects$
run: .github/workflows/clang-format-check.sh "." "llvm" "^subprojects$" ""
- name: Save PR head commit SHA
if: failure() && github.event_name == 'pull_request'
shell: bash
run: |
SHA="${{ github.event.pull_request.head.sha }}"
echo "SHA=$SHA" >> $GITHUB_ENV
- name: Save latest commit SHA if not PR
if: failure() && github.event_name != 'pull_request'
shell: bash
run: echo "SHA=${{ github.sha }}" >> $GITHUB_ENV
- name: Report failure in job summary
if: failure()
run: |
DEEPLINK="${{ github.server_url }}/${{ github.repository }}/commit/${{ env.SHA }}"
echo -e "Format check failed on commit [${GITHUB_SHA:0:8}]($DEEPLINK) with files:\n$(<$GITHUB_WORKSPACE/failing-files.txt)" >> $GITHUB_STEP_SUMMARY

92
.github/workflows/clang-format-check.sh vendored Executable file
View file

@ -0,0 +1,92 @@
#!/usr/bin/env bash
#
# Adapted from https://github.com/jidicula/clang-format-action
###############################################################################
# check.sh #
###############################################################################
# USAGE: ./entrypoint.sh [<path>] [<fallback style>]
#
# Checks all C/C++/Protobuf/CUDA files (.h, .H, .hpp, .hh, .h++, .hxx and .c,
# .C, .cpp, .cc, .c++, .cxx, .proto, .cu) in the provided GitHub repository path
# (arg1) for conforming to clang-format. If no path is provided or provided path
# is not a directory, all C/C++/Protobuf/CUDA files are checked. If any files
# are incorrectly formatted, the script lists them and exits with 1.
#
# Define your own formatting rules in a .clang-format file at your repository
# root. Otherwise, the provided style guide (arg2) is used as a fallback.
# format_diff function
# Accepts a filepath argument. The filepath passed to this function must point
# to a C/C++/Protobuf/CUDA file.
format_diff() {
local filepath="$1"
# Invoke clang-format with dry run and formatting error output
local_format="$(clang-format \
--dry-run \
--Werror \
--style=file \
--fallback-style="$FALLBACK_STYLE" \
"${filepath}")"
local format_status="$?"
if [[ ${format_status} -ne 0 ]]; then
# Append Markdown-bulleted monospaced filepath of failing file to
# summary file.
echo "* \`$filepath\`" >>failing-files.txt
echo "Failed on file: $filepath" >&2
echo "$local_format" >&2
exit_code=1 # flip the global exit code
return "${format_status}"
fi
return 0
}
CHECK_PATH="$1"
FALLBACK_STYLE="$2"
EXCLUDE_REGEX="$3"
INCLUDE_REGEX="$4"
# Set the regex to an empty string regex if nothing was provided
if [[ -z $EXCLUDE_REGEX ]]; then
EXCLUDE_REGEX="^$"
fi
# Set the filetype regex if nothing was provided.
# Find all C/C++/Protobuf/CUDA files:
# h, H, hpp, hh, h++, hxx
# c, C, cpp, cc, c++, cxx
# ino, pde
# proto
# cu
if [[ -z $INCLUDE_REGEX ]]; then
INCLUDE_REGEX='^.*\.((((c|C)(c|pp|xx|\+\+)?$)|((h|H)h?(pp|xx|\+\+)?$))|(ino|pde|proto|cu))$'
fi
cd "$GITHUB_WORKSPACE" || exit 2
if [[ ! -d $CHECK_PATH ]]; then
echo "Not a directory in the workspace, fallback to all files." >&2
CHECK_PATH="."
fi
# initialize exit code
exit_code=0
# All files improperly formatted will be printed to the output.
src_files=$(find "$CHECK_PATH" -name .git -prune -o -regextype posix-egrep -regex "$INCLUDE_REGEX" -print)
# check formatting in each source file
IFS=$'\n' # Loop below should separate on new lines, not spaces.
for file in $src_files; do
# Only check formatting if the path doesn't match the regex
if ! [[ ${file} =~ $EXCLUDE_REGEX ]]; then
format_diff "${file}"
fi
done
# global exit code is flipped to nonzero if any invocation of `format_diff` has
# a formatting difference.
exit "$exit_code"

View file

@ -1,28 +0,0 @@
name: clang-format
on: pull_request_target
jobs:
clang-format:
permissions: write-all
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork
name: "Code Style"
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: clang-format check
uses: jidicula/clang-format-action@v4.16.0
with:
exclude-regex: ^subprojects$
- name: Create comment
if: ${{ failure() && github.event_name == 'pull_request' }}
run: |
echo 'Please fix the formatting issues by running [`clang-format`](https://wiki.hyprland.org/Contributing-and-Debugging/PR-Guidelines/#code-style).' > clang-format.patch
- name: Post comment
if: ${{ failure() && github.event_name == 'pull_request' }}
uses: mshick/add-pr-comment@v2
with:
message-path: |
clang-format.patch

45
.github/workflows/new-pr-comment.yml vendored Normal file
View file

@ -0,0 +1,45 @@
name: "New MR welcome comment"
on:
pull_request_target:
types:
- opened
jobs:
comment:
if: >
github.event.pull_request.user.login != 'vaxerski' &&
github.event.pull_request.user.login != 'fufexan' &&
github.event.pull_request.user.login != 'gulafaran' &&
github.event.pull_request.user.login != 'ujint34' &&
github.event.pull_request.user.login != 'paideiadilemma' &&
github.event.pull_request.user.login != 'notashelf'
runs-on: ubuntu-latest
permissions:
pull-requests: write
env:
PR_COMMENT: |
Hello and thank you for making a PR to Hyprland!
Please check the [PR Guidelines](https://wiki.hypr.land/Contributing-and-Debugging/PR-Guidelines/) and make sure your PR follows them.
It will make the entire review process faster. :)
If your code can be tested, please always add tests. See more [here](https://wiki.hypr.land/Contributing-and-Debugging/Tests/).
_beep boop, I'm just a bot. A real human will review your PR soon._
steps:
- name: Add comment to PR
uses: actions/github-script@v7
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
const pr = context.payload.pull_request;
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: pr.number,
body: process.env.PR_COMMENT,
});

View file

@ -10,21 +10,22 @@ jobs:
hyprland:
if: (github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork)
uses: ./.github/workflows/nix.yml
uses: hyprwm/actions/.github/workflows/nix.yml@main
secrets: inherit
with:
checkout: false
command: nix build 'github:${{ github.repository }}?ref=${{ github.ref }}' -L --extra-substituters "https://hyprland.cachix.org"
xdph:
if: (github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork)
needs: hyprland
uses: ./.github/workflows/nix.yml
uses: hyprwm/actions/.github/workflows/nix.yml@main
secrets: inherit
with:
checkout: false
command: nix build 'github:${{ github.repository }}?ref=${{ github.ref }}#xdg-desktop-portal-hyprland' -L --extra-substituters "https://hyprland.cachix.org"
test:
if: (github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork)
needs: hyprland
uses: ./.github/workflows/nix-test.yml
secrets: inherit

View file

@ -10,28 +10,9 @@ jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Install Nix
uses: nixbuild/nix-quick-install-action@v31
- uses: hyprwm/actions/nix-setup@main
with:
nix_conf: |
keep-env-derivations = true
keep-outputs = true
- name: Restore and save Nix store
uses: nix-community/cache-nix-action@v6
with:
# restore and save a cache using this key (per job)
primary-key: nix-${{ runner.os }}-${{ github.job }}
# if there's no cache hit, restore a cache by this prefix
restore-prefixes-first-match: nix-${{ runner.os }}
# collect garbage until the Nix store size (in bytes) is at most this number
# before trying to save a new cache
gc-max-store-size-linux: 5G
- uses: cachix/cachix-action@v15
with:
name: hyprland
authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}"
CACHIX_AUTH_TOKEN: ${{ secrets.CACHIX_AUTH_TOKEN }}
- name: Run test VM
run: nix build 'github:${{ github.repository }}?ref=${{ github.ref }}#checks.x86_64-linux.tests' -L --extra-substituters "https://hyprland.cachix.org"

View file

@ -17,23 +17,7 @@ jobs:
with:
token: ${{ secrets.PAT }}
- name: Install Nix
uses: nixbuild/nix-quick-install-action@v31
with:
nix_conf: |
keep-env-derivations = true
keep-outputs = true
- name: Restore and save Nix store
uses: nix-community/cache-nix-action@v6
with:
# restore and save a cache using this key (per job)
primary-key: nix-${{ runner.os }}-${{ github.job }}
# if there's no cache hit, restore a cache by this prefix
restore-prefixes-first-match: nix-${{ runner.os }}
# collect garbage until the Nix store size (in bytes) is at most this number
# before trying to save a new cache
gc-max-store-size-linux: 5G
- uses: hyprwm/actions/nix-setup@main
- name: Update inputs
run: nix/update-inputs.sh

View file

@ -1,41 +0,0 @@
name: Build
on:
workflow_call:
inputs:
command:
required: true
type: string
description: Command to run
secrets:
CACHIX_AUTH_TOKEN:
required: false
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Install Nix
uses: nixbuild/nix-quick-install-action@v31
with:
nix_conf: |
keep-env-derivations = true
keep-outputs = true
- name: Restore and save Nix store
uses: nix-community/cache-nix-action@v6
with:
# restore and save a cache using this key (per job)
primary-key: nix-${{ runner.os }}-${{ github.job }}
# if there's no cache hit, restore a cache by this prefix
restore-prefixes-first-match: nix-${{ runner.os }}
# collect garbage until the Nix store size (in bytes) is at most this number
# before trying to save a new cache
gc-max-store-size-linux: 5G
- uses: cachix/cachix-action@v15
with:
name: hyprland
authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}"
- run: ${{ inputs.command }}

7
.gitignore vendored
View file

@ -32,16 +32,23 @@ src/render/shaders/*.inc
src/render/shaders/Shaders.hpp
hyprctl/hyprctl
hyprctl/hw-protocols/*.c*
hyprctl/hw-protocols/*.h*
gmon.out
*.out
*.tar.gz
perf.data
flame.svg
PKGBUILD
src/version.h
hyprpm/Makefile
hyprctl/Makefile
example/hyprland.desktop
**/.#*.*
**/#*.*#
debug-tools/flamegraph

View file

@ -17,7 +17,6 @@ set(HYPRLAND_VERSION ${VER})
set(PREFIX ${CMAKE_INSTALL_PREFIX})
set(INCLUDEDIR ${CMAKE_INSTALL_INCLUDEDIR})
set(BINDIR ${CMAKE_INSTALL_BINDIR})
configure_file(hyprland.pc.in hyprland.pc @ONLY)
set(CMAKE_MESSAGE_LOG_LEVEL "STATUS")
@ -25,7 +24,14 @@ message(STATUS "Gathering git info")
# Make shader files includable
execute_process(COMMAND ./scripts/generateShaderIncludes.sh
WORKING_DIRECTORY ${CMAKE_SOURCE_DIR})
WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
RESULT_VARIABLE HYPR_SHADER_GEN_RESULT)
if(NOT HYPR_SHADER_GEN_RESULT EQUAL 0)
message(
FATAL_ERROR
"Failed to generate shader includes (scripts/generateShaderIncludes.sh), exit code: ${HYPR_SHADER_GEN_RESULT}"
)
endif()
find_package(PkgConfig REQUIRED)
@ -80,9 +86,11 @@ message(
if(CMAKE_BUILD_TYPE MATCHES Debug OR CMAKE_BUILD_TYPE MATCHES DEBUG)
message(STATUS "Configuring Hyprland in Debug with CMake")
add_compile_definitions(HYPRLAND_DEBUG)
set(BUILD_TESTING ON)
else()
add_compile_options(-O3)
message(STATUS "Configuring Hyprland in Release with CMake")
set(BUILD_TESTING OFF)
endif()
add_compile_definitions(HYPRLAND_VERSION="${HYPRLAND_VERSION}")
@ -102,6 +110,7 @@ add_compile_options(
-Wno-narrowing
-Wno-pointer-arith
-Wno-clobbered
-frtti
-fmacro-prefix-map=${CMAKE_SOURCE_DIR}/=)
# disable lto as it may break plugins
@ -116,12 +125,19 @@ find_package(Threads REQUIRED)
set(GLES_VERSION "GLES3")
find_package(OpenGL REQUIRED COMPONENTS ${GLES_VERSION})
find_package(glslang CONFIG REQUIRED)
pkg_check_modules(aquamarine_dep REQUIRED IMPORTED_TARGET aquamarine>=0.9.3)
pkg_check_modules(hyprlang_dep REQUIRED IMPORTED_TARGET hyprlang>=0.3.2)
pkg_check_modules(hyprcursor_dep REQUIRED IMPORTED_TARGET hyprcursor>=0.1.7)
pkg_check_modules(hyprutils_dep REQUIRED IMPORTED_TARGET hyprutils>=0.10.2)
pkg_check_modules(hyprgraphics_dep REQUIRED IMPORTED_TARGET hyprgraphics>=0.1.6)
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(HYPRGRAPHICS_MINIMUM_VERSION 0.5.1)
pkg_check_modules(aquamarine_dep REQUIRED IMPORTED_TARGET aquamarine>=${AQUAMARINE_MINIMUM_VERSION})
pkg_check_modules(hyprlang_dep REQUIRED IMPORTED_TARGET hyprlang>=${HYPRLANG_MINIMUM_VERSION})
pkg_check_modules(hyprcursor_dep REQUIRED IMPORTED_TARGET hyprcursor>=${HYPRCURSOR_MINIMUM_VERSION})
pkg_check_modules(hyprutils_dep REQUIRED IMPORTED_TARGET hyprutils>=${HYPRUTILS_MINIMUM_VERSION})
pkg_check_modules(hyprgraphics_dep REQUIRED IMPORTED_TARGET hyprgraphics>=${HYPRGRAPHICS_MINIMUM_VERSION})
string(REPLACE "." ";" AQ_VERSION_LIST ${aquamarine_dep_VERSION})
list(GET AQ_VERSION_LIST 0 AQ_VERSION_MAJOR)
@ -228,29 +244,37 @@ configure_file(
set_source_files_properties(${CMAKE_SOURCE_DIR}/src/version.h PROPERTIES GENERATED TRUE)
set(XKBCOMMON_MINIMUM_VERSION 1.11.0)
set(WAYLAND_SERVER_MINIMUM_VERSION 1.22.91)
set(WAYLAND_SERVER_PROTOCOLS_MINIMUM_VERSION 1.47)
set(LIBINPUT_MINIMUM_VERSION 1.28)
pkg_check_modules(
deps
REQUIRED
IMPORTED_TARGET
xkbcommon>=1.11.0
IMPORTED_TARGET GLOBAL
xkbcommon>=${XKBCOMMON_MINIMUM_VERSION}
uuid
wayland-server>=1.22.90
wayland-protocols>=1.45
wayland-server>=${WAYLAND_SERVER_MINIMUM_VERSION}
wayland-protocols>=${WAYLAND_SERVER_PROTOCOLS_MINIMUM_VERSION}
cairo
pango
pangocairo
pixman-1
xcursor
libdrm
libinput>=1.28
libinput>=${LIBINPUT_MINIMUM_VERSION}
gbm
gio-2.0
re2
muparser)
muparser
lcms2)
find_package(hyprwayland-scanner 0.3.10 REQUIRED)
file(GLOB_RECURSE SRCFILES "src/*.cpp")
get_filename_component(FULL_MAIN_PATH ${CMAKE_CURRENT_SOURCE_DIR}/src/main.cpp ABSOLUTE)
list(REMOVE_ITEM SRCFILES "${FULL_MAIN_PATH}")
set(TRACY_CPP_FILES "")
if(USE_TRACY)
@ -258,7 +282,12 @@ if(USE_TRACY)
message(STATUS "Tracy enabled, TRACY_CPP_FILES: " ${TRACY_CPP_FILES})
endif()
add_executable(Hyprland ${SRCFILES} ${TRACY_CPP_FILES})
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})
set(USE_GPROF OFF)
@ -268,26 +297,11 @@ if(CMAKE_BUILD_TYPE MATCHES Debug OR CMAKE_BUILD_TYPE MATCHES DEBUG)
if(WITH_ASAN)
message(STATUS "Enabling ASan")
target_link_libraries(Hyprland asan)
target_compile_options(Hyprland PUBLIC -fsanitize=address)
target_link_libraries(hyprland_lib PUBLIC asan)
target_compile_options(hyprland_lib PUBLIC -fsanitize=address)
endif()
if(USE_TRACY)
message(STATUS "Tracy is turned on")
option(TRACY_ENABLE "" ON)
option(TRACY_ON_DEMAND "" ON)
add_subdirectory(subprojects/tracy)
target_link_libraries(Hyprland Tracy::TracyClient)
if(USE_TRACY_GPU)
message(STATUS "Tracy GPU Profiling is turned on")
add_compile_definitions(USE_TRACY_GPU)
endif()
endif()
add_compile_options(-fno-pie -fno-builtin)
add_compile_options(-fno-pie -fno-builtin -fno-omit-frame-pointer)
add_link_options(-no-pie -fno-builtin)
if(USE_GPROF)
add_compile_options(-pg)
@ -295,6 +309,27 @@ if(CMAKE_BUILD_TYPE MATCHES Debug OR CMAKE_BUILD_TYPE MATCHES DEBUG)
endif()
endif()
if(USE_TRACY)
message(STATUS "Tracy is turned on")
option(TRACY_ENABLE "" ON)
option(TRACY_ON_DEMAND "" ON)
add_subdirectory(subprojects/tracy)
add_compile_options(-fno-omit-frame-pointer)
target_link_libraries(hyprland_lib PUBLIC Tracy::TracyClient)
if(USE_TRACY_GPU)
message(STATUS "Tracy GPU Profiling is turned on")
add_compile_definitions(USE_TRACY_GPU)
endif()
endif()
if(BUILT_WITH_NIX)
add_compile_definitions(BUILT_WITH_NIX)
endif()
check_include_file("execinfo.h" EXECINFOH)
if(EXECINFOH)
message(STATUS "Configuration supports execinfo")
@ -304,19 +339,19 @@ endif()
include(CheckLibraryExists)
check_library_exists(execinfo backtrace "" HAVE_LIBEXECINFO)
if(HAVE_LIBEXECINFO)
target_link_libraries(Hyprland execinfo)
target_link_libraries(hyprland_lib PUBLIC execinfo)
endif()
check_include_file("sys/timerfd.h" HAS_TIMERFD)
pkg_check_modules(epoll IMPORTED_TARGET epoll-shim)
if(NOT HAS_TIMERFD AND epoll_FOUND)
target_link_libraries(Hyprland PkgConfig::epoll)
target_link_libraries(hyprland_lib PUBLIC PkgConfig::epoll)
endif()
check_include_file("sys/inotify.h" HAS_INOTIFY)
pkg_check_modules(inotify IMPORTED_TARGET libinotify)
if(NOT HAS_INOTIFY AND inotify_FOUND)
target_link_libraries(Hyprland PkgConfig::inotify)
target_link_libraries(hyprland_lib PUBLIC PkgConfig::inotify)
endif()
if(NO_XWAYLAND)
@ -324,10 +359,7 @@ if(NO_XWAYLAND)
add_compile_definitions(NO_XWAYLAND)
else()
message(STATUS "XWAYLAND Enabled (NO_XWAYLAND not defined) checking deps...")
pkg_check_modules(
xdeps
REQUIRED
IMPORTED_TARGET
set(XWAYLAND_DEPENDENCIES
xcb
xcb-render
xcb-xfixes
@ -335,9 +367,21 @@ else()
xcb-composite
xcb-res
xcb-errors)
target_link_libraries(Hyprland PkgConfig::xdeps)
pkg_check_modules(
xdeps
REQUIRED
IMPORTED_TARGET
${XWAYLAND_DEPENDENCIES})
string(JOIN ", " PKGCONFIG_XWAYLAND_DEPENDENCIES ${XWAYLAND_DEPENDENCIES})
string(PREPEND PKGCONFIG_XWAYLAND_DEPENDENCIES ", ")
target_link_libraries(hyprland_lib PUBLIC PkgConfig::xdeps)
endif()
configure_file(hyprland.pc.in hyprland.pc @ONLY)
if(NO_SYSTEMD)
message(STATUS "SYSTEMD support is disabled...")
else()
@ -362,31 +406,38 @@ if(CMAKE_DISABLE_PRECOMPILE_HEADERS)
message(STATUS "Not using precompiled headers")
else()
message(STATUS "Setting precompiled headers")
target_precompile_headers(Hyprland PRIVATE
target_precompile_headers(hyprland_lib PRIVATE
$<$<COMPILE_LANGUAGE:CXX>:src/pch/pch.hpp>)
endif()
message(STATUS "Setting link libraries")
target_link_libraries(
Hyprland
${LIBRT}
hyprland_lib
PUBLIC
PkgConfig::aquamarine_dep
PkgConfig::hyprlang_dep
PkgConfig::hyprutils_dep
PkgConfig::hyprcursor_dep
PkgConfig::hyprgraphics_dep
PkgConfig::deps)
PkgConfig::deps
)
target_link_libraries(
Hyprland
${LIBRT}
hyprland_lib)
if(udis_dep_FOUND)
target_link_libraries(Hyprland PkgConfig::udis_dep)
target_link_libraries(hyprland_lib PUBLIC PkgConfig::udis_dep)
elseif(NOT("${udis_nopc}" MATCHES "udis_nopc-NOTFOUND"))
target_link_libraries(Hyprland ${udis_nopc})
target_link_libraries(hyprland_lib PUBLIC ${udis_nopc})
else()
target_link_libraries(Hyprland libudis86)
target_link_libraries(hyprland_lib PUBLIC libudis86)
endif()
# used by `make installheaders`, to ensure the headers are generated
add_custom_target(generate-protocol-headers)
set(PROTOCOL_SOURCES "")
function(protocolnew protoPath protoName external)
if(external)
@ -400,10 +451,15 @@ function(protocolnew protoPath protoName external)
COMMAND hyprwayland-scanner ${path}/${protoName}.xml
${CMAKE_SOURCE_DIR}/protocols/
WORKING_DIRECTORY ${CMAKE_SOURCE_DIR})
target_sources(Hyprland PRIVATE protocols/${protoName}.cpp
target_sources(hyprland_lib PRIVATE protocols/${protoName}.cpp
protocols/${protoName}.hpp)
target_sources(generate-protocol-headers
PRIVATE ${CMAKE_SOURCE_DIR}/protocols/${protoName}.hpp)
list(APPEND PROTOCOL_SOURCES "${CMAKE_SOURCE_DIR}/protocols/${protoName}.cpp")
set(PROTOCOL_SOURCES "${PROTOCOL_SOURCES}" PARENT_SCOPE)
list(APPEND PROTOCOL_SOURCES "${CMAKE_SOURCE_DIR}/protocols/${protoName}.hpp")
set(PROTOCOL_SOURCES "${PROTOCOL_SOURCES}" PARENT_SCOPE)
endfunction()
function(protocolWayland)
add_custom_command(
@ -413,12 +469,21 @@ function(protocolWayland)
hyprwayland-scanner --wayland-enums
${WAYLAND_SCANNER_PKGDATA_DIR}/wayland.xml ${CMAKE_SOURCE_DIR}/protocols/
WORKING_DIRECTORY ${CMAKE_SOURCE_DIR})
target_sources(Hyprland PRIVATE protocols/wayland.cpp protocols/wayland.hpp)
target_sources(hyprland_lib PRIVATE protocols/wayland.cpp protocols/wayland.hpp)
target_sources(generate-protocol-headers
PRIVATE ${CMAKE_SOURCE_DIR}/protocols/wayland.hpp)
list(APPEND PROTOCOL_SOURCES "${CMAKE_SOURCE_DIR}/protocols/wayland.hpp")
set(PROTOCOL_SOURCES "${PROTOCOL_SOURCES}" PARENT_SCOPE)
list(APPEND PROTOCOL_SOURCES "${CMAKE_SOURCE_DIR}/protocols/wayland.cpp")
set(PROTOCOL_SOURCES "${PROTOCOL_SOURCES}" PARENT_SCOPE)
endfunction()
target_link_libraries(Hyprland OpenGL::EGL OpenGL::GL Threads::Threads)
if(TARGET OpenGL::GL)
target_link_libraries(hyprland_lib PUBLIC OpenGL::EGL OpenGL::GL glslang::glslang glslang::glslang-default-resource-limits glslang::SPIRV Threads::Threads)
else()
target_link_libraries(hyprland_lib PUBLIC OpenGL::EGL OpenGL::GLES3 glslang::glslang glslang::glslang-default-resource-limits glslang::SPIRV Threads::Threads)
endif()
pkg_check_modules(hyprland_protocols_dep hyprland-protocols>=0.6.4)
if(hyprland_protocols_dep_FOUND)
@ -446,8 +511,6 @@ protocolnew("protocols" "kde-server-decoration" true)
protocolnew("protocols" "wlr-data-control-unstable-v1" true)
protocolnew("${HYPRLAND_PROTOCOLS}/protocols" "hyprland-focus-grab-v1" true)
protocolnew("protocols" "wlr-layer-shell-unstable-v1" true)
protocolnew("protocols" "xx-color-management-v4" true)
protocolnew("protocols" "frog-color-management-v1" true)
protocolnew("protocols" "wayland-drm" true)
protocolnew("${HYPRLAND_PROTOCOLS}/protocols" "hyprland-ctm-control-v1" true)
protocolnew("${HYPRLAND_PROTOCOLS}/protocols" "hyprland-surface-v1" true)
@ -471,6 +534,7 @@ protocolnew("unstable/keyboard-shortcuts-inhibit"
protocolnew("unstable/text-input" "text-input-unstable-v3" false)
protocolnew("unstable/pointer-constraints" "pointer-constraints-unstable-v1"
false)
protocolnew("unstable/xdg-foreign" "xdg-foreign-unstable-v2" false)
protocolnew("staging/xdg-activation" "xdg-activation-v1" false)
protocolnew("staging/ext-idle-notify" "ext-idle-notify-v1" false)
protocolnew("staging/ext-session-lock" "ext-session-lock-v1" false)
@ -495,11 +559,14 @@ protocolnew("staging/ext-data-control" "ext-data-control-v1" false)
protocolnew("staging/pointer-warp" "pointer-warp-v1" false)
protocolnew("staging/fifo" "fifo-v1" false)
protocolnew("staging/commit-timing" "commit-timing-v1" false)
protocolnew("staging/ext-image-capture-source" "ext-image-capture-source-v1" false)
protocolnew("staging/ext-image-copy-capture" "ext-image-copy-capture-v1" false)
protocolwayland()
# tools
add_subdirectory(hyprctl)
add_subdirectory(start)
if(NO_HYPRPM)
message(STATUS "hyprpm is disabled")
@ -518,6 +585,11 @@ install(
\"\$ENV{DESTDIR}${CMAKE_INSTALL_FULL_BINDIR}/hyprland\" \
)")
# session file
configure_file(
${CMAKE_SOURCE_DIR}/example/hyprland.desktop.in
${CMAKE_SOURCE_DIR}/example/hyprland.desktop
@ONLY
)
install(FILES ${CMAKE_SOURCE_DIR}/example/hyprland.desktop
DESTINATION ${CMAKE_INSTALL_DATAROOTDIR}/wayland-sessions)
@ -563,10 +635,37 @@ install(
PATTERN "*.hpp"
PATTERN "*.inc")
if(BUILD_TESTING OR BUILD_HYPRTESTER)
message(STATUS "Building hyprtester")
if(BUILD_TESTING OR WITH_TESTS)
message(STATUS "Building tests")
# hyprtester
add_subdirectory(hyprtester)
# GTest
find_package(GTest CONFIG REQUIRED)
include(GoogleTest)
file(GLOB_RECURSE TESTFILES "tests/*.cpp")
add_executable(hyprland_gtests ${TESTFILES})
target_compile_options(hyprland_gtests PRIVATE --coverage)
target_link_options(hyprland_gtests PRIVATE --coverage)
target_include_directories(
hyprland_gtests
PUBLIC "./include"
PRIVATE "./src" "./src/include" "./protocols" "${CMAKE_BINARY_DIR}")
target_link_libraries(hyprland_gtests hyprland_lib GTest::gtest_main)
gtest_discover_tests(hyprland_gtests)
# Enable coverage in main hyprland lib
target_compile_options(hyprland_lib PRIVATE --coverage)
target_link_options(hyprland_lib PRIVATE --coverage)
target_link_libraries(hyprland_lib PUBLIC gcov)
# Enable coverage in hyprland exe
target_compile_options(Hyprland PRIVATE --coverage)
target_link_options(Hyprland PRIVATE --coverage)
target_link_libraries(Hyprland gcov)
endif()
if(BUILD_TESTING)
@ -575,12 +674,8 @@ if(BUILD_TESTING)
enable_testing()
add_custom_target(tests)
add_test(
NAME "Main Test"
WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}/hyprtester
COMMAND hyprtester)
add_dependencies(tests hyprland_gtests)
add_dependencies(tests hyprtester)
else()
message(STATUS "Testing is disabled")
endif()

View file

@ -1,6 +1,6 @@
BSD 3-Clause License
Copyright (c) 2022-2025, vaxerski
Copyright (c) 2022-2026, vaxerski
All rights reserved.
Redistribution and use in source and binary forms, with or without

View file

@ -18,6 +18,7 @@ nopch:
clear:
rm -rf build
rm -f ./protocols/*.h ./protocols/*.c ./protocols/*.cpp ./protocols/*.hpp
rm -f ./hyprctl/hw-protocols/*.cpp ./hyprctl/hw-protocols/*.hpp
all:
$(MAKE) clear
@ -87,12 +88,24 @@ asan:
@echo "Wayland done"
patch -p1 < ./scripts/hyprlandStaticAsan.diff
cmake --no-warn-unused-cli -DCMAKE_BUILD_TYPE:STRING=Debug -DWITH_ASAN:STRING=True -DUSE_TRACY:STRING=False -DUSE_TRACY_GPU:STRING=False -S . -B ./build -G Ninja
cmake --no-warn-unused-cli -DCMAKE_BUILD_TYPE:STRING=Debug -DWITH_ASAN:STRING=True -DUSE_TRACY:STRING=False -DUSE_TRACY_GPU:STRING=False -S . -B ./build
cmake --build ./build --config Debug --target all
@echo "Hyprland done"
ASAN_OPTIONS="detect_odr_violation=0,log_path=asan.log" HYPRLAND_NO_CRASHREPORTER=1 ./build/Hyprland -c ~/.config/hypr/hyprland.conf
format-check:
@find src hyprctl hyprpm start tests -type f \( -name "*.cpp" -o -name "*.hpp" -o -name "*.h" \) \
! -path "src/render/shaders/Shaders.hpp" \
! -path "hyprctl/hw-protocols/*" | \
xargs clang-format --dry-run --Werror
format-fix:
@find src hyprctl hyprpm start tests -type f \( -name "*.cpp" -o -name "*.hpp" -o -name "*.h" \) \
! -path "src/render/shaders/Shaders.hpp" \
! -path "hyprctl/hw-protocols/*" | \
xargs clang-format -i
test:
$(MAKE) debug
./build/hyprtester/hyprtester -c hyprtester/test.conf -b ./build/Hyprland -p hyprtester/plugin/hyprtestplugin.so

View file

@ -1 +1 @@
0.52.0
0.54.0

20
debug-tools/flamegraph.sh Executable file
View file

@ -0,0 +1,20 @@
#!/bin/bash
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
FLAMEGRAPH_DIR="$SCRIPT_DIR/flamegraph"
if [ ! -d "$FLAMEGRAPH_DIR" ]; then
echo "Cloning FlameGraph tools..."
git clone https://github.com/brendangregg/FlameGraph "$FLAMEGRAPH_DIR"
fi
if [ ! -f perf.data ]; then
echo "No perf.data found in current directory."
echo "Run Hyprland under perf first:"
echo " perf record -F 99 -g -- ./build/Hyprland"
exit 1
fi
echo "Generating flame graph..."
perf script | "$FLAMEGRAPH_DIR/stackcollapse-perf.pl" | "$FLAMEGRAPH_DIR/flamegraph.pl" > flame.svg
xdg-open flame.svg

View file

@ -27,7 +27,7 @@ monitor=,preferred,auto,auto
# Set programs that you use
$terminal = kitty
$fileManager = dolphin
$menu = wofi --show drun
$menu = hyprlauncher
#################
@ -237,12 +237,12 @@ $mainMod = SUPER # Sets "Windows" key as main modifier
# Example binds, see https://wiki.hypr.land/Configuring/Binds/ for more
bind = $mainMod, Q, exec, $terminal
bind = $mainMod, C, killactive,
bind = $mainMod, M, exit,
bind = $mainMod, M, exec, command -v hyprshutdown >/dev/null 2>&1 && hyprshutdown || hyprctl dispatch exit
bind = $mainMod, E, exec, $fileManager
bind = $mainMod, V, togglefloating,
bind = $mainMod, R, exec, $menu
bind = $mainMod, P, pseudo, # dwindle
bind = $mainMod, J, togglesplit, # dwindle
bind = $mainMod, J, layoutmsg, togglesplit # dwindle
# Move focus with mainMod + arrow keys
bind = $mainMod, left, movefocus, l
@ -329,3 +329,13 @@ windowrule {
no_focus = true
}
# Hyprland-run windowrule
windowrule {
name = move-hyprland-run
match:class = hyprland-run
move = 20 monitor_h-120
float = yes
}

View file

@ -1,7 +1,7 @@
[Desktop Entry]
Name=Hyprland
Comment=An intelligent dynamic tiling Wayland compositor
Exec=Hyprland
Exec=@PREFIX@/@CMAKE_INSTALL_BINDIR@/start-hyprland
Type=Application
DesktopNames=Hyprland
Keywords=tiling;wayland;compositor;

109
flake.lock generated
View file

@ -16,11 +16,11 @@
]
},
"locked": {
"lastModified": 1763922789,
"narHash": "sha256-XnkWjCpeXfip9tqYdL0b0zzBDjq+dgdISvEdSVGdVyA=",
"lastModified": 1776702787,
"narHash": "sha256-qc5uwEWbuubzYthmZcfCapooZGXhoYZWfTQ24TozbCQ=",
"owner": "hyprwm",
"repo": "aquamarine",
"rev": "a20a0e67a33b6848378a91b871b89588d3a12573",
"rev": "9a1ca6b8cb4d86a599787a55b78f2ddf809bf945",
"type": "github"
},
"original": {
@ -32,15 +32,15 @@
"flake-compat": {
"flake": false,
"locked": {
"lastModified": 1761588595,
"narHash": "sha256-XKUZz9zewJNUj46b4AJdiRZJAvSZ0Dqj2BNfXvFlJC4=",
"owner": "edolstra",
"lastModified": 1767039857,
"narHash": "sha256-vNpUSpF5Nuw8xvDLj2KCwwksIbjua2LZCqhV1LNRDns=",
"owner": "NixOS",
"repo": "flake-compat",
"rev": "f387cd2afec9419c8ee37694406ca490c3f34ee5",
"rev": "5edf11c44bc78a0d334f6334cdaf7d60d732daab",
"type": "github"
},
"original": {
"owner": "edolstra",
"owner": "NixOS",
"repo": "flake-compat",
"type": "github"
}
@ -79,11 +79,11 @@
]
},
"locked": {
"lastModified": 1753964049,
"narHash": "sha256-lIqabfBY7z/OANxHoPeIrDJrFyYy9jAM4GQLzZ2feCM=",
"lastModified": 1776511930,
"narHash": "sha256-fCpwFiTW0rT7oKJqr3cqHMnkwypSwQKpbtUEtxdkgrM=",
"owner": "hyprwm",
"repo": "hyprcursor",
"rev": "44e91d467bdad8dcf8bbd2ac7cf49972540980a5",
"rev": "39435900785d0c560c6ae8777d29f28617d031ef",
"type": "github"
},
"original": {
@ -105,11 +105,11 @@
]
},
"locked": {
"lastModified": 1763733840,
"narHash": "sha256-JnET78yl5RvpGuDQy3rCycOCkiKoLr5DN1fPhRNNMco=",
"lastModified": 1776426399,
"narHash": "sha256-RUESLKNikIeEq9ymGJ6nmcDXiSFQpUW1IhJ245nL3xM=",
"owner": "hyprwm",
"repo": "hyprgraphics",
"rev": "8f1bec691b2d198c60cccabca7a94add2df4ed1a",
"rev": "68d064434787cf1ed4a2fe257c03c5f52f33cf84",
"type": "github"
},
"original": {
@ -144,11 +144,11 @@
]
},
"locked": {
"lastModified": 1763727565,
"narHash": "sha256-vRff/2R1U1jzPBy4OODqh2kfUzmizW/nfV2ROzTDIKo=",
"lastModified": 1776426575,
"narHash": "sha256-KI6nIfVihn/DPaeB5Et46Xg3dkNHrrEtUd5LBBVomB0=",
"owner": "hyprwm",
"repo": "hyprland-guiutils",
"rev": "7724d3a12a0453e7aae05f2ef39474219f05a4b4",
"rev": "a968d211048e3ed538e47b84cb3649299578f19d",
"type": "github"
},
"original": {
@ -167,11 +167,11 @@
]
},
"locked": {
"lastModified": 1759610243,
"narHash": "sha256-+KEVnKBe8wz+a6dTLq8YDcF3UrhQElwsYJaVaHXJtoI=",
"lastModified": 1772460177,
"narHash": "sha256-/6G/MsPvtn7bc4Y32pserBT/Z4SUUdBd4XYJpOEKVR4=",
"owner": "hyprwm",
"repo": "hyprland-protocols",
"rev": "bd153e76f751f150a09328dbdeb5e4fab9d23622",
"rev": "1cb6db5fd6bb8aee419f4457402fa18293ace917",
"type": "github"
},
"original": {
@ -193,11 +193,11 @@
]
},
"locked": {
"lastModified": 1763819661,
"narHash": "sha256-0jLarTR/BLWdGlboM86bPVP2zKJNI2jvo3JietnDkOM=",
"lastModified": 1776426736,
"narHash": "sha256-rl7i4aY+9p8LysJp7o8uRWahCkpFznCgGHXszlTw7b0=",
"owner": "hyprwm",
"repo": "hyprlang",
"rev": "a318deec0c12409ec39c68d2be8096b636dc2a5c",
"rev": "7833ff33b2e82d3406337b5dcf0d1cec595d83e9",
"type": "github"
},
"original": {
@ -238,11 +238,11 @@
]
},
"locked": {
"lastModified": 1763503177,
"narHash": "sha256-VPoiswJBBmTLVuNncvT/8FpFR+sYcAi/LgP/zTZ+5rA=",
"lastModified": 1772462885,
"narHash": "sha256-5pHXrQK9zasMnIo6yME6EOXmWGFMSnCITcfKshhKJ9I=",
"owner": "hyprwm",
"repo": "hyprtoolkit",
"rev": "f4e1e12755567ecf39090203b8f43eace8279630",
"rev": "9af245a69fa6b286b88ddfc340afd288e00a6998",
"type": "github"
},
"original": {
@ -261,11 +261,11 @@
]
},
"locked": {
"lastModified": 1763996058,
"narHash": "sha256-DsqzFZvrEV+aDmavjaD4/bk5qxeZwhGxPWBQdpFyM9Y=",
"lastModified": 1776428866,
"narHash": "sha256-XfRlBolGtjvalTHJp3XvvpYLBjkMhaZLLU0WqZ91Fcg=",
"owner": "hyprwm",
"repo": "hyprutils",
"rev": "0168583075baffa083032ed13a8bea8ea12f281a",
"rev": "eedd60805cd96d4442586f2ba5fe51d549b12674",
"type": "github"
},
"original": {
@ -284,11 +284,11 @@
]
},
"locked": {
"lastModified": 1763640274,
"narHash": "sha256-Uan1Nl9i4TF/kyFoHnTq1bd/rsWh4GAK/9/jDqLbY5A=",
"lastModified": 1776430932,
"narHash": "sha256-Yv3RPiUvl7CAsJgwIVsqcj7akn1gLyJP1F/mocof5hA=",
"owner": "hyprwm",
"repo": "hyprwayland-scanner",
"rev": "f6cf414ca0e16a4d30198fd670ec86df3c89f671",
"rev": "4c2fcc06dc9722c97dbb54ba649c69b18ce83d2e",
"type": "github"
},
"original": {
@ -297,13 +297,39 @@
"type": "github"
}
},
"hyprwire": {
"inputs": {
"hyprutils": [
"hyprutils"
],
"nixpkgs": [
"nixpkgs"
],
"systems": [
"systems"
]
},
"locked": {
"lastModified": 1776728575,
"narHash": "sha256-z9eGphrArEBpl1O/GCH0wlY6z4K9vA6yWh2gAS6qytU=",
"owner": "hyprwm",
"repo": "hyprwire",
"rev": "f3a80888783702a39691b684d099e16b83ed4702",
"type": "github"
},
"original": {
"owner": "hyprwm",
"repo": "hyprwire",
"type": "github"
}
},
"nixpkgs": {
"locked": {
"lastModified": 1763966396,
"narHash": "sha256-6eeL1YPcY1MV3DDStIDIdy/zZCDKgHdkCmsrLJFiZf0=",
"lastModified": 1776548001,
"narHash": "sha256-ZSK0NL4a1BwVbbTBoSnWgbJy9HeZFXLYQizjb2DPF24=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "5ae3b07d8d6527c42f17c876e404993199144b6a",
"rev": "b12141ef619e0a9c1c84dc8c684040326f27cdcc",
"type": "github"
},
"original": {
@ -322,11 +348,11 @@
]
},
"locked": {
"lastModified": 1763988335,
"narHash": "sha256-QlcnByMc8KBjpU37rbq5iP7Cp97HvjRP0ucfdh+M4Qc=",
"lastModified": 1776796298,
"narHash": "sha256-PcRvlWayisPSjd0UcRQbhG8Oqw78AcPE6x872cPRHN8=",
"owner": "cachix",
"repo": "git-hooks.nix",
"rev": "50b9238891e388c9fdc6a5c49e49c42533a1b5ce",
"rev": "3cfd774b0a530725a077e17354fbdb87ea1c4aad",
"type": "github"
},
"original": {
@ -345,6 +371,7 @@
"hyprlang": "hyprlang",
"hyprutils": "hyprutils",
"hyprwayland-scanner": "hyprwayland-scanner",
"hyprwire": "hyprwire",
"nixpkgs": "nixpkgs",
"pre-commit-hooks": "pre-commit-hooks",
"systems": "systems",
@ -388,11 +415,11 @@
]
},
"locked": {
"lastModified": 1761431178,
"narHash": "sha256-xzjC1CV3+wpUQKNF+GnadnkeGUCJX+vgaWIZsnz9tzI=",
"lastModified": 1776608502,
"narHash": "sha256-UH8YoQxx4hFOm6qjMdjRQNRvSejFIR/wBZ8fW1p9sME=",
"owner": "hyprwm",
"repo": "xdg-desktop-portal-hyprland",
"rev": "4b8801228ff958d028f588f0c2b911dbf32297f9",
"rev": "4a293523d36dfa367e67ec304cc718ea66a8fec2",
"type": "github"
},
"original": {

212
flake.nix
View file

@ -65,6 +65,13 @@
inputs.systems.follows = "systems";
};
hyprwire = {
url = "github:hyprwm/hyprwire";
inputs.nixpkgs.follows = "nixpkgs";
inputs.systems.follows = "systems";
inputs.hyprutils.follows = "hyprutils";
};
xdph = {
url = "github:hyprwm/xdg-desktop-portal-hyprland";
inputs.nixpkgs.follows = "nixpkgs";
@ -81,107 +88,122 @@
};
};
outputs = inputs @ {
self,
nixpkgs,
systems,
...
}: let
inherit (nixpkgs) lib;
eachSystem = lib.genAttrs (import systems);
pkgsFor = eachSystem (system:
import nixpkgs {
localSystem = system;
overlays = with self.overlays; [
hyprland-packages
hyprland-extras
];
});
pkgsCrossFor = eachSystem (system: crossSystem:
import nixpkgs {
localSystem = system;
inherit crossSystem;
overlays = with self.overlays; [
hyprland-packages
hyprland-extras
];
});
pkgsDebugFor = eachSystem (system:
import nixpkgs {
localSystem = system;
overlays = with self.overlays; [
hyprland-debug
];
});
pkgsDebugCrossFor = eachSystem (system: crossSystem:
import nixpkgs {
localSystem = system;
inherit crossSystem;
overlays = with self.overlays; [
hyprland-debug
];
});
in {
overlays = import ./nix/overlays.nix {inherit self lib inputs;};
outputs =
inputs@{
self,
nixpkgs,
systems,
...
}:
let
inherit (nixpkgs) lib;
eachSystem = lib.genAttrs (import systems);
pkgsFor = eachSystem (
system:
import nixpkgs {
localSystem = system;
overlays = with self.overlays; [
hyprland-packages
hyprland-extras
];
}
);
pkgsCrossFor = eachSystem (
system: crossSystem:
import nixpkgs {
localSystem = system;
inherit crossSystem;
overlays = with self.overlays; [
hyprland-packages
hyprland-extras
];
}
);
pkgsDebugFor = eachSystem (
system:
import nixpkgs {
localSystem = system;
overlays = with self.overlays; [
hyprland-debug
];
}
);
pkgsDebugCrossFor = eachSystem (
system: crossSystem:
import nixpkgs {
localSystem = system;
inherit crossSystem;
overlays = with self.overlays; [
hyprland-debug
];
}
);
in
{
overlays = import ./nix/overlays.nix { inherit self lib inputs; };
checks = eachSystem (system:
(lib.filterAttrs
(n: _: (lib.hasPrefix "hyprland" n) && !(lib.hasSuffix "debug" n))
self.packages.${system})
// {
inherit (self.packages.${system}) xdg-desktop-portal-hyprland;
pre-commit-check = inputs.pre-commit-hooks.lib.${system}.run {
src = ./.;
hooks = {
hyprland-treewide-formatter = {
enable = true;
entry = "${self.formatter.${system}}/bin/hyprland-treewide-formatter";
pass_filenames = false;
excludes = ["subprojects"];
always_run = true;
checks = eachSystem (
system:
(lib.filterAttrs (
n: _: (lib.hasPrefix "hyprland" n) && !(lib.hasSuffix "debug" n)
) self.packages.${system})
// {
inherit (self.packages.${system}) xdg-desktop-portal-hyprland;
pre-commit-check = inputs.pre-commit-hooks.lib.${system}.run {
src = ./.;
hooks = {
hyprland-treewide-formatter = {
enable = true;
entry = "${self.formatter.${system}}/bin/hyprland-treewide-formatter";
pass_filenames = false;
excludes = [ "subprojects" ];
always_run = true;
};
};
};
};
}
// (import ./nix/tests inputs pkgsFor.${system}));
}
// (import ./nix/tests inputs pkgsFor.${system})
);
packages = eachSystem (system: {
default = self.packages.${system}.hyprland;
inherit
(pkgsFor.${system})
# hyprland-packages
hyprland
hyprland-unwrapped
# hyprland-extras
xdg-desktop-portal-hyprland
;
inherit (pkgsDebugFor.${system}) hyprland-debug;
hyprland-cross = (pkgsCrossFor.${system} "aarch64-linux").hyprland;
hyprland-debug-cross = (pkgsDebugCrossFor.${system} "aarch64-linux").hyprland-debug;
});
packages = eachSystem (system: {
default = self.packages.${system}.hyprland;
inherit (pkgsFor.${system})
# hyprland-packages
hyprland
hyprland-unwrapped
hyprland-with-tests
# hyprland-extras
xdg-desktop-portal-hyprland
;
inherit (pkgsDebugFor.${system}) hyprland-debug;
hyprland-cross = (pkgsCrossFor.${system} "aarch64-linux").hyprland;
hyprland-debug-cross = (pkgsDebugCrossFor.${system} "aarch64-linux").hyprland-debug;
});
devShells = eachSystem (system: {
default =
pkgsFor.${system}.mkShell.override {
inherit (self.packages.${system}.default) stdenv;
} {
name = "hyprland-shell";
hardeningDisable = ["fortify"];
inputsFrom = [pkgsFor.${system}.hyprland];
packages = [pkgsFor.${system}.clang-tools];
inherit (self.checks.${system}.pre-commit-check) shellHook;
};
});
devShells = eachSystem (system: {
default =
pkgsFor.${system}.mkShell.override
{
inherit (self.packages.${system}.default) stdenv;
}
{
name = "hyprland-shell";
hardeningDisable = [ "fortify" ];
inputsFrom = [ pkgsFor.${system}.hyprland ];
packages = [ pkgsFor.${system}.clang-tools ];
inherit (self.checks.${system}.pre-commit-check) shellHook;
};
});
formatter = eachSystem (system: pkgsFor.${system}.callPackage ./nix/formatter.nix {});
formatter = eachSystem (system: pkgsFor.${system}.callPackage ./nix/formatter.nix { });
nixosModules.default = import ./nix/module.nix inputs;
homeManagerModules.default = import ./nix/hm-module.nix self;
nixosModules.default = import ./nix/module.nix inputs;
homeManagerModules.default = import ./nix/hm-module.nix self;
# Hydra build jobs
# Recent versions of Hydra can aggregate jobsets from 'hydraJobs' instead of a release.nix
# or similar. Remember to filter large or incompatible attributes here. More eval jobs can
# be added by merging, e.g., self.packages // self.devShells.
hydraJobs = self.packages;
};
# Hydra build jobs
# Recent versions of Hydra can aggregate jobsets from 'hydraJobs' instead of a release.nix
# or similar. Remember to filter large or incompatible attributes here. More eval jobs can
# be added by merging, e.g., self.packages // self.devShells.
hydraJobs = self.packages;
};
}

View file

@ -5,11 +5,32 @@ project(
DESCRIPTION "Control utility for Hyprland"
)
pkg_check_modules(hyprctl_deps REQUIRED IMPORTED_TARGET hyprutils>=0.2.4 re2)
pkg_check_modules(hyprctl_deps REQUIRED IMPORTED_TARGET hyprutils>=0.2.4 hyprwire re2)
add_executable(hyprctl "main.cpp")
file(GLOB_RECURSE HYPRCTL_SRCFILES CONFIGURE_DEPENDS "src/*.cpp" "hw-protocols/*.cpp" "include/*.hpp")
add_executable(hyprctl ${HYPRCTL_SRCFILES})
target_link_libraries(hyprctl PUBLIC PkgConfig::hyprctl_deps)
target_include_directories(hyprctl PRIVATE "hw-protocols")
# Hyprwire
function(hyprprotocol protoPath protoName)
set(path ${CMAKE_CURRENT_SOURCE_DIR}/${protoPath})
add_custom_command(
OUTPUT ${CMAKE_CURRENT_SOURCE_DIR}/hw-protocols/${protoName}-client.cpp
${CMAKE_CURRENT_SOURCE_DIR}/hw-protocols/${protoName}-client.hpp
${CMAKE_CURRENT_SOURCE_DIR}/hw-protocols/${protoName}-spec.hpp
COMMAND hyprwire-scanner --client ${path}/${protoName}.xml
${CMAKE_CURRENT_SOURCE_DIR}/hw-protocols/
WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR})
target_sources(hyprctl PRIVATE hw-protocols/${protoName}-client.cpp
hw-protocols/${protoName}-client.hpp
hw-protocols/${protoName}-spec.hpp)
endfunction()
hyprprotocol(hw-protocols hyprpaper_core)
# binary
install(TARGETS hyprctl)

View file

@ -0,0 +1,172 @@
<?xml version="1.0" encoding="UTF-8"?>
<protocol name="hyprpaper_core" version="2">
<copyright>
BSD 3-Clause License
Copyright (c) 2025, Hypr Development
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
3. Neither the name of the copyright holder nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
</copyright>
<object name="hyprpaper_core_manager" version="2">
<description summary="manager object">
This is the core manager object for hyprpaper operations
</description>
<c2s name="get_wallpaper_object">
<description summary="Get a wallpaper object">
Creates a wallpaper object
</description>
<returns iface="hyprpaper_wallpaper"/>
</c2s>
<s2c name="add_monitor">
<description summary="New monitor added">
Emitted when a new monitor is added.
</description>
<arg name="monitor_name" type="varchar" summary="the monitor's name"/>
</s2c>
<s2c name="remove_monitor">
<description summary="A monitor was removed">
Emitted when a monitor is removed.
</description>
<arg name="monitor_name" type="varchar" summary="the monitor's name"/>
</s2c>
<c2s name="destroy" destructor="true">
<description summary="Destroy this object">
Destroys this object. Children remain alive until destroyed.
</description>
</c2s>
<c2s name="get_status_object" since="2">
<description summary="Get a status object">
Creates a status object
</description>
<returns iface="hyprpaper_status"/>
</c2s>
</object>
<enum name="wallpaper_fit_mode">
<value idx="0" name="stretch"/>
<value idx="1" name="cover"/>
<value idx="2" name="contain"/>
<value idx="3" name="tile"/>
</enum>
<enum name="wallpaper_errors">
<value idx="0" name="inert_wallpaper_object" description="attempted to use an inert wallpaper object"/>
</enum>
<enum name="applying_error">
<value idx="0" name="invalid_path" description="path provided was invalid"/>
<value idx="1" name="invalid_monitor" description="monitor provided was invalid"/>
<value idx="2" name="unknown_error" description="unknown error"/>
</enum>
<object name="hyprpaper_wallpaper" version="1">
<description summary="wallpaper object">
This is an object describing a wallpaper
</description>
<c2s name="path">
<description summary="Set a path">
Set a file path for the wallpaper. This has to be an absolute path from the fs root.
This is required.
</description>
<arg name="wallpaper" type="varchar" summary="path"/>
</c2s>
<c2s name="fit_mode">
<description summary="Set a fit mode">
Set a fit mode for the wallpaper. This is set to cover by default.
</description>
<arg name="fit_mode" type="enum" interface="wallpaper_fit_mode" summary="path"/>
</c2s>
<c2s name="monitor_name">
<description summary="Set the monitor name">
Set a monitor for the wallpaper. Setting this to empty (or not setting at all) will
treat this as a wildcard fallback.
See hyprpaper_core_manager.add_monitor and hyprpaper_core_manager.remove_monitor
for tracking monitor names.
</description>
<arg name="monitor_name" type="varchar" summary="monitor name"/>
</c2s>
<c2s name="apply">
<description summary="Apply this wallpaper">
Applies this object's state to the wallpaper state. Will emit .success on success,
and .failed on failure.
This object becomes inert after .succeess or .failed, the only valid operation
is to destroy it afterwards.
</description>
</c2s>
<s2c name="success">
<description summary="Operation succeeded">
Wallpaper was applied successfully.
</description>
</s2c>
<s2c name="failed">
<description summary="Operation failed">
Wallpaper was not applied. See the error field for more information.
</description>
<arg name="error" type="enum" interface="hyprpaper_wallpaper_application_error" summary="path"/>
</s2c>
<c2s name="destroy" destructor="true">
<description summary="Destroy this object">
Destroys this object.
</description>
</c2s>
</object>
<object name="hyprpaper_status" version="2">
<description summary="status object">
This is an object which will emit various status updates.
</description>
<s2c name="active_wallpaper">
<description summary="Active wallpaper state">
Sends the active wallpaper for a given monitor. This will be emitted
immediately after binding, and then every time the path changes.
</description>
<arg name="monitor" type="varchar" summary="monitor name"/>
<arg name="path" type="varchar" summary="wallpaper path"/>
</s2c>
<c2s name="destroy" destructor="true">
<description summary="Destroy this object">
Destroys this object.
</description>
</c2s>
</object>
</protocol>

View file

@ -74,11 +74,8 @@ flags:
const std::string_view HYPRPAPER_HELP = R"#(usage: hyprctl [flags] hyprpaper <request>
requests:
listactive Lists all active images
listloaded Lists all loaded images
preload <path> Preloads image
unload <path> Unloads image. Pass 'all' as path to unload all images
wallpaper Issue a wallpaper to call a config wallpaper dynamically
wallpaper Issue a wallpaper to call a config wallpaper dynamically.
Arguments are [mon],[path],[fit_mode]. Fit mode is optional.
flags:
See 'hyprctl --help')#";

View file

@ -0,0 +1,11 @@
#pragma once
#include <hyprutils/memory/SharedPtr.hpp>
#include <hyprutils/memory/UniquePtr.hpp>
#include <hyprutils/memory/Atomic.hpp>
using namespace Hyprutils::Memory;
#define SP CSharedPointer
#define WP CWeakPointer
#define UP CUniquePointer

View file

@ -0,0 +1,208 @@
#include "Hyprpaper.hpp"
#include "../helpers/Memory.hpp"
#include <optional>
#include <format>
#include <filesystem>
#include <print>
#include <hyprpaper_core-client.hpp>
#include <hyprutils/string/VarList2.hpp>
using namespace Hyprutils::String;
using namespace std::string_literals;
constexpr const char* SOCKET_NAME = ".hyprpaper.sock";
static SP<CCHyprpaperCoreImpl> g_coreImpl;
constexpr const uint32_t PROTOCOL_VERSION_SUPPORTED = 2;
//
static hyprpaperCoreWallpaperFitMode fitFromString(const std::string_view& sv) {
if (sv == "contain")
return HYPRPAPER_CORE_WALLPAPER_FIT_MODE_CONTAIN;
if (sv == "fit" || sv == "stretch")
return HYPRPAPER_CORE_WALLPAPER_FIT_MODE_STRETCH;
if (sv == "tile")
return HYPRPAPER_CORE_WALLPAPER_FIT_MODE_TILE;
return HYPRPAPER_CORE_WALLPAPER_FIT_MODE_COVER;
}
static std::expected<std::string, std::string> resolvePath(const std::string_view& sv) {
std::error_code ec;
auto can = std::filesystem::canonical(sv, ec);
if (ec)
return std::unexpected(std::format("invalid path: {}", ec.message()));
return can;
}
static std::expected<std::string, std::string> getFullPath(const std::string_view& sv) {
if (sv.empty())
return std::unexpected("empty path");
if (sv[0] == '~') {
static auto HOME = getenv("HOME");
if (!HOME || HOME[0] == '\0')
return std::unexpected("home path but no $HOME");
return resolvePath(std::string{HOME} + "/"s + std::string{sv.substr(1)});
}
return resolvePath(sv);
}
static std::expected<void, std::string> doWallpaper(const std::string_view& RHS) {
CVarList2 args(std::string{RHS}, 0, ',');
const std::string MONITOR = std::string{args[0]};
const auto& PATH_RAW = args[1];
const auto& FIT = args[2];
if (PATH_RAW.empty())
return std::unexpected("not enough args");
const auto RTDIR = getenv("XDG_RUNTIME_DIR");
if (!RTDIR || RTDIR[0] == '\0')
return std::unexpected("can't send: no XDG_RUNTIME_DIR");
const auto HIS = getenv("HYPRLAND_INSTANCE_SIGNATURE");
if (!HIS || HIS[0] == '\0')
return std::unexpected("can't send: no HYPRLAND_INSTANCE_SIGNATURE (not running under hyprland)");
const auto PATH = getFullPath(PATH_RAW);
if (!PATH)
return std::unexpected(std::format("bad path: {}", PATH_RAW));
auto socketPath = RTDIR + "/hypr/"s + HIS + "/"s + SOCKET_NAME;
auto socket = Hyprwire::IClientSocket::open(socketPath);
if (!socket)
return std::unexpected("can't send: failed to connect to hyprpaper (is it running?)");
g_coreImpl = makeShared<CCHyprpaperCoreImpl>(PROTOCOL_VERSION_SUPPORTED);
socket->addImplementation(g_coreImpl);
if (!socket->waitForHandshake())
return std::unexpected("can't send: wire handshake failed");
auto spec = socket->getSpec(g_coreImpl->protocol()->specName());
if (!spec)
return std::unexpected("can't send: hyprpaper doesn't have the spec?!");
auto manager = makeShared<CCHyprpaperCoreManagerObject>(socket->bindProtocol(g_coreImpl->protocol(), std::min(PROTOCOL_VERSION_SUPPORTED, spec->specVer())));
if (!manager)
return std::unexpected("wire error: couldn't create manager");
auto wallpaper = makeShared<CCHyprpaperWallpaperObject>(manager->sendGetWallpaperObject());
if (!wallpaper)
return std::unexpected("wire error: couldn't create wallpaper object");
bool canExit = false;
std::optional<std::string> err;
wallpaper->setFailed([&canExit, &err](uint32_t code) {
canExit = true;
switch (code) {
case HYPRPAPER_CORE_APPLYING_ERROR_INVALID_PATH: err = std::format("failed to set wallpaper: Invalid path", code); break;
case HYPRPAPER_CORE_APPLYING_ERROR_INVALID_MONITOR: err = std::format("failed to set wallpaper: Invalid monitor", code); break;
default: err = std::format("failed to set wallpaper: unknown error, code {}", code); break;
}
});
wallpaper->setSuccess([&canExit]() { canExit = true; });
wallpaper->sendPath(PATH->c_str());
wallpaper->sendMonitorName(MONITOR.c_str());
if (!FIT.empty())
wallpaper->sendFitMode(fitFromString(FIT));
wallpaper->sendApply();
while (!canExit) {
socket->dispatchEvents(true);
}
if (err)
return std::unexpected(*err);
return {};
}
static std::expected<void, std::string> doListActive() {
const auto RTDIR = getenv("XDG_RUNTIME_DIR");
if (!RTDIR || RTDIR[0] == '\0')
return std::unexpected("can't send: no XDG_RUNTIME_DIR");
const auto HIS = getenv("HYPRLAND_INSTANCE_SIGNATURE");
if (!HIS || HIS[0] == '\0')
return std::unexpected("can't send: no HYPRLAND_INSTANCE_SIGNATURE (not running under hyprland)");
auto socketPath = RTDIR + "/hypr/"s + HIS + "/"s + SOCKET_NAME;
auto socket = Hyprwire::IClientSocket::open(socketPath);
if (!socket)
return std::unexpected("can't send: failed to connect to hyprpaper (is it running?)");
g_coreImpl = makeShared<CCHyprpaperCoreImpl>(PROTOCOL_VERSION_SUPPORTED);
socket->addImplementation(g_coreImpl);
if (!socket->waitForHandshake())
return std::unexpected("can't send: wire handshake failed");
auto spec = socket->getSpec(g_coreImpl->protocol()->specName());
if (!spec)
return std::unexpected("can't send: hyprpaper doesn't have the spec?!");
if (spec->specVer() < 2)
return std::unexpected("can't send: hyprpaper protocol version too low (hyprpaper too old)");
auto manager = makeShared<CCHyprpaperCoreManagerObject>(socket->bindProtocol(g_coreImpl->protocol(), std::min(PROTOCOL_VERSION_SUPPORTED, spec->specVer())));
if (!manager)
return std::unexpected("wire error: couldn't create manager");
auto status = makeShared<CCHyprpaperStatusObject>(manager->sendGetStatusObject());
status->setActiveWallpaper([](const char* mon, const char* wp) { std::println("{}: {}", mon, wp); });
socket->roundtrip();
return {};
}
std::expected<void, std::string> Hyprpaper::makeHyprpaperRequest(const std::string_view& rq) {
if (!rq.contains(' '))
return std::unexpected("Invalid request");
if (!rq.starts_with("/hyprpaper "))
return std::unexpected("Invalid request");
std::string_view LHS, RHS;
auto spacePos = rq.find(' ', 12);
LHS = rq.substr(11, spacePos - 11);
RHS = rq.substr(spacePos + 1);
if (LHS == "wallpaper")
return doWallpaper(RHS);
else if (LHS == "listactive")
return doListActive();
else
return std::unexpected("invalid hyprpaper request");
return {};
}

View file

@ -0,0 +1,8 @@
#pragma once
#include <expected>
#include <string>
namespace Hyprpaper {
std::expected<void, std::string> makeHyprpaperRequest(const std::string_view& rq);
};

View file

@ -31,6 +31,7 @@ using namespace Hyprutils::String;
using namespace Hyprutils::Memory;
#include "Strings.hpp"
#include "hyprpaper/Hyprpaper.hpp"
std::string instanceSignature;
bool quiet = false;
@ -227,23 +228,23 @@ int request(std::string_view arg, int minArgs = 0, bool needRoll = false) {
constexpr size_t BUFFER_SIZE = 8192;
char buffer[BUFFER_SIZE] = {0};
sizeWritten = read(SERVERSOCKET, buffer, BUFFER_SIZE);
if (sizeWritten < 0) {
if (errno == EWOULDBLOCK)
log("Hyprland IPC didn't respond in time\n");
log("Couldn't read (6)");
return 6;
}
reply += std::string(buffer, sizeWritten);
while (sizeWritten == BUFFER_SIZE) {
// read all data until server closes the connection
// this handles partial writes on the server side under high load
while (true) {
sizeWritten = read(SERVERSOCKET, buffer, BUFFER_SIZE);
if (sizeWritten < 0) {
if (errno == EWOULDBLOCK)
log("Hyprland IPC didn't respond in time\n");
log("Couldn't read (6)");
return 6;
}
if (sizeWritten == 0) {
// server closed connection, we're done
break;
}
reply += std::string(buffer, sizeWritten);
}
@ -305,10 +306,6 @@ int requestIPC(std::string_view filename, std::string_view arg) {
return 0;
}
int requestHyprpaper(std::string_view arg) {
return requestIPC(".hyprpaper.sock", arg);
}
int requestHyprsunset(std::string_view arg) {
return requestIPC(".hyprsunset.sock", arg);
}
@ -500,9 +497,12 @@ int main(int argc, char** argv) {
if (fullRequest.contains("/--batch"))
batchRequest(fullRequest, json);
else if (fullRequest.contains("/hyprpaper"))
exitStatus = requestHyprpaper(fullRequest);
else if (fullRequest.contains("/hyprsunset"))
else if (fullRequest.contains("/hyprpaper")) {
auto result = Hyprpaper::makeHyprpaperRequest(fullRequest);
if (!result)
log(std::format("error: {}", result.error()));
exitStatus = !result;
} else if (fullRequest.contains("/hyprsunset"))
exitStatus = requestHyprsunset(fullRequest);
else if (fullRequest.contains("/switchxkblayout"))
exitStatus = request(fullRequest, 2);

View file

@ -4,4 +4,5 @@ Name: Hyprland
URL: https://github.com/hyprwm/Hyprland
Description: Hyprland header files
Version: @HYPRLAND_VERSION@
Cflags: -I${prefix} -I${prefix}/hyprland/protocols -I${prefix}/hyprland
Requires: aquamarine >= @AQUAMARINE_MINIMUM_VERSION@, hyprcursor >= @HYPRCURSOR_MINIMUM_VERSION@, hyprgraphics >= @HYPRGRAPHICS_MINIMUM_VERSION@, hyprlang >= @HYPRLANG_MINIMUM_VERSION@, hyprutils >= @HYPRUTILS_MINIMUM_VERSION@, libdrm, egl, cairo, xkbcommon >= @XKBCOMMON_MINIMUM_VERSION@, libinput >= @LIBINPUT_MINIMUM_VERSION@, wayland-server >= @WAYLAND_SERVER_MINIMUM_VERSION@@PKGCONFIG_XWAYLAND_DEPENDENCIES@
Cflags: -I${prefix} -I${prefix}/hyprland/protocols -I${prefix}/hyprland -I${prefix}/hyprland/src

View file

@ -11,16 +11,17 @@ set(CMAKE_CXX_STANDARD 23)
pkg_check_modules(hyprpm_deps REQUIRED IMPORTED_TARGET tomlplusplus hyprutils>=0.7.0)
find_package(glaze QUIET)
find_package(glaze 7...<8 QUIET)
if (NOT glaze_FOUND)
set(GLAZE_VERSION v5.1.1)
message(STATUS "glaze dependency not found, retrieving ${GLAZE_VERSION} with FetchContent")
set(GLAZE_VERSION v7.2.0)
message(STATUS "hyprpm: glaze dependency not found, retrieving ${GLAZE_VERSION} with FetchContent")
include(FetchContent)
FetchContent_Declare(
glaze
GIT_REPOSITORY https://github.com/stephenberry/glaze.git
GIT_TAG ${GLAZE_VERSION}
GIT_SHALLOW TRUE
EXCLUDE_FROM_ALL
)
FetchContent_MakeAvailable(glaze)
endif()

View file

@ -93,6 +93,7 @@ void DataState::addNewPluginRepo(const SPluginRepository& repo) {
auto DATA = toml::table{
{"repository", toml::table{
{"name", repo.name},
{"author", repo.author},
{"hash", repo.hash},
{"url", repo.url},
{"rev", repo.rev}
@ -122,31 +123,32 @@ void DataState::addNewPluginRepo(const SPluginRepository& repo) {
Debug::die("{}", failureString("Failed to write plugin state"));
}
bool DataState::pluginRepoExists(const std::string& urlOrName) {
bool DataState::pluginRepoExists(const SPluginRepoIdentifier identifier) {
ensureStateStoreExists();
for (const auto& stateFile : getPluginStates()) {
const auto STATE = toml::parse_file(stateFile.c_str());
const auto NAME = STATE["repository"]["name"].value_or("");
const auto URL = STATE["repository"]["url"].value_or("");
const auto STATE = toml::parse_file(stateFile.c_str());
const auto NAME = STATE["repository"]["name"].value_or("");
const auto AUTHOR = STATE["repository"]["author"].value_or("");
const auto URL = STATE["repository"]["url"].value_or("");
if (URL == urlOrName || NAME == urlOrName)
if (identifier.matches(URL, NAME, AUTHOR))
return true;
}
return false;
}
void DataState::removePluginRepo(const std::string& urlOrName) {
void DataState::removePluginRepo(const SPluginRepoIdentifier identifier) {
ensureStateStoreExists();
for (const auto& stateFile : getPluginStates()) {
const auto STATE = toml::parse_file(stateFile.c_str());
const auto NAME = STATE["repository"]["name"].value_or("");
const auto URL = STATE["repository"]["url"].value_or("");
if (URL == urlOrName || NAME == urlOrName) {
const auto STATE = toml::parse_file(stateFile.c_str());
const auto NAME = STATE["repository"]["name"].value_or("");
const auto AUTHOR = STATE["repository"]["author"].value_or("");
const auto URL = STATE["repository"]["url"].value_or("");
if (identifier.matches(URL, NAME, AUTHOR)) {
// unload the plugins!!
for (const auto& file : std::filesystem::directory_iterator(stateFile.parent_path())) {
if (!file.path().string().ends_with(".so"))
@ -181,7 +183,7 @@ void DataState::updateGlobalState(const SGlobalState& state) {
// clang-format off
auto DATA = toml::table{
{"state", toml::table{
{"hash", state.headersHashCompiled},
{"hash", state.headersAbiCompiled},
{"dont_warn_install", state.dontWarnInstall}
}}
};
@ -206,8 +208,8 @@ SGlobalState DataState::getGlobalState() {
auto DATA = toml::parse_file(stateFile.c_str());
SGlobalState state;
state.headersHashCompiled = DATA["state"]["hash"].value_or("");
state.dontWarnInstall = DATA["state"]["dont_warn_install"].value_or(false);
state.headersAbiCompiled = DATA["state"]["hash"].value_or("");
state.dontWarnInstall = DATA["state"]["dont_warn_install"].value_or(false);
return state;
}
@ -219,16 +221,18 @@ std::vector<SPluginRepository> DataState::getAllRepositories() {
for (const auto& stateFile : getPluginStates()) {
const auto STATE = toml::parse_file(stateFile.c_str());
const auto NAME = STATE["repository"]["name"].value_or("");
const auto URL = STATE["repository"]["url"].value_or("");
const auto REV = STATE["repository"]["rev"].value_or("");
const auto HASH = STATE["repository"]["hash"].value_or("");
const auto NAME = STATE["repository"]["name"].value_or("");
const auto AUTHOR = STATE["repository"]["author"].value_or("");
const auto URL = STATE["repository"]["url"].value_or("");
const auto REV = STATE["repository"]["rev"].value_or("");
const auto HASH = STATE["repository"]["hash"].value_or("");
SPluginRepository repo;
repo.hash = HASH;
repo.name = NAME;
repo.url = URL;
repo.rev = REV;
repo.hash = HASH;
repo.name = NAME;
repo.author = AUTHOR;
repo.url = URL;
repo.rev = REV;
for (const auto& [key, val] : STATE) {
if (key == "repository")
@ -247,7 +251,7 @@ std::vector<SPluginRepository> DataState::getAllRepositories() {
return repos;
}
bool DataState::setPluginEnabled(const std::string& name, bool enabled) {
bool DataState::setPluginEnabled(const SPluginRepoIdentifier identifier, bool enabled) {
ensureStateStoreExists();
for (const auto& stateFile : getPluginStates()) {
@ -256,8 +260,17 @@ bool DataState::setPluginEnabled(const std::string& name, bool enabled) {
if (key == "repository")
continue;
if (key.str() != name)
continue;
switch (identifier.type) {
case IDENTIFIER_NAME:
if (key.str() != identifier.name)
continue;
break;
case IDENTIFIER_AUTHOR_NAME:
if (STATE["repository"]["author"] != identifier.author || key.str() != identifier.name)
continue;
break;
default: return false;
}
const auto FAILED = STATE[key]["failed"].value_or(false);

View file

@ -5,8 +5,8 @@
#include "Plugin.hpp"
struct SGlobalState {
std::string headersHashCompiled = "";
bool dontWarnInstall = false;
std::string headersAbiCompiled = "";
bool dontWarnInstall = false;
};
namespace DataState {
@ -15,11 +15,11 @@ namespace DataState {
std::vector<std::filesystem::path> getPluginStates();
void ensureStateStoreExists();
void addNewPluginRepo(const SPluginRepository& repo);
void removePluginRepo(const std::string& urlOrName);
bool pluginRepoExists(const std::string& urlOrName);
void removePluginRepo(const SPluginRepoIdentifier identifier);
bool pluginRepoExists(const SPluginRepoIdentifier identifier);
void updateGlobalState(const SGlobalState& state);
void purgeAllCache();
SGlobalState getGlobalState();
bool setPluginEnabled(const std::string& name, bool enabled);
bool setPluginEnabled(const SPluginRepoIdentifier identifier, bool enabled);
std::vector<SPluginRepository> getAllRepositories();
};
};

View file

@ -0,0 +1,48 @@
#include "Plugin.hpp"
SPluginRepoIdentifier SPluginRepoIdentifier::fromUrl(const std::string& url) {
return SPluginRepoIdentifier{.type = IDENTIFIER_URL, .url = url};
}
SPluginRepoIdentifier SPluginRepoIdentifier::fromName(const std::string& name) {
return SPluginRepoIdentifier{.type = IDENTIFIER_NAME, .name = name};
}
SPluginRepoIdentifier SPluginRepoIdentifier::fromAuthorName(const std::string& author, const std::string& name) {
return SPluginRepoIdentifier{.type = IDENTIFIER_AUTHOR_NAME, .name = name, .author = author};
}
SPluginRepoIdentifier SPluginRepoIdentifier::fromString(const std::string& string) {
if (string.find(':') != std::string::npos) {
return SPluginRepoIdentifier{.type = IDENTIFIER_URL, .url = string};
} else {
auto slashPos = string.find('/');
if (slashPos != std::string::npos) {
std::string author = string.substr(0, slashPos);
std::string name = string.substr(slashPos + 1, string.size() - slashPos - 1);
return SPluginRepoIdentifier{.type = IDENTIFIER_AUTHOR_NAME, .name = name, .author = author};
} else {
return SPluginRepoIdentifier{.type = IDENTIFIER_NAME, .name = string};
}
}
}
std::string SPluginRepoIdentifier::toString() const {
switch (type) {
case IDENTIFIER_NAME: return name;
case IDENTIFIER_AUTHOR_NAME: return author + '/' + name;
case IDENTIFIER_URL: return url;
}
return "";
}
bool SPluginRepoIdentifier::matches(const std::string& url, const std::string& name, const std::string& author) const {
switch (type) {
case IDENTIFIER_URL: return this->url == url;
case IDENTIFIER_NAME: return this->name == name;
case IDENTIFIER_AUTHOR_NAME: return this->author == author && this->name == name;
}
return false;
}

View file

@ -14,6 +14,27 @@ struct SPluginRepository {
std::string url;
std::string rev;
std::string name;
std::string author;
std::vector<SPlugin> plugins;
std::string hash;
};
};
enum ePluginRepoIdentifierType {
IDENTIFIER_URL,
IDENTIFIER_NAME,
IDENTIFIER_AUTHOR_NAME
};
struct SPluginRepoIdentifier {
ePluginRepoIdentifierType type;
std::string url = "";
std::string name = "";
std::string author = "";
static SPluginRepoIdentifier fromString(const std::string& string);
static SPluginRepoIdentifier fromUrl(const std::string& Url);
static SPluginRepoIdentifier fromName(const std::string& name);
static SPluginRepoIdentifier fromAuthorName(const std::string& author, const std::string& name);
std::string toString() const;
bool matches(const std::string& url, const std::string& name, const std::string& author) const;
};

View file

@ -11,6 +11,7 @@
#include <cstdio>
#include <iostream>
#include <filesystem>
#include <string>
#include <print>
#include <fstream>
#include <algorithm>
@ -78,40 +79,33 @@ SHyprlandVersion CPluginManager::getHyprlandVersion(bool running) {
else
onceInstalled = true;
const auto HLVERCALL = running ? NHyprlandSocket::send("/version") : execAndGet("Hyprland --version");
if (m_bVerbose)
std::println("{}", verboseString("{} version returned: {}", running ? "running" : "installed", HLVERCALL));
const auto HLVERCALL = running ? NHyprlandSocket::send("j/version") : execAndGet("Hyprland --version-json");
if (!HLVERCALL.contains("Tag:")) {
std::println(stderr, "\n{}", failureString("You don't seem to be running Hyprland."));
auto jsonQuery = glz::read_json<glz::generic>(HLVERCALL);
if (!jsonQuery) {
std::println("{}", failureString("failed to get the current hyprland version. Are you running hyprland?"));
return SHyprlandVersion{};
}
std::string hlcommit = HLVERCALL.substr(HLVERCALL.find("at commit") + 10);
hlcommit = hlcommit.substr(0, hlcommit.find_first_of(' '));
auto hlbranch = (*jsonQuery)["branch"].get_string();
auto hlcommit = (*jsonQuery)["commit"].get_string();
auto abiHash = (*jsonQuery)["abiHash"].get_string();
auto hldate = (*jsonQuery)["commit_date"].get_string();
auto hlcommits = (*jsonQuery)["commits"].get_string();
std::string hlbranch = HLVERCALL.substr(HLVERCALL.find("from branch") + 12);
hlbranch = hlbranch.substr(0, hlbranch.find(" at commit "));
auto flags = (*jsonQuery)["flags"].get_array();
bool isNix = std::ranges::any_of(flags, [](const auto& f) { return f.is_string() && f.get_string() == std::string_view{"nix"}; });
std::string hldate = HLVERCALL.substr(HLVERCALL.find("Date: ") + 6);
hldate = hldate.substr(0, hldate.find('\n'));
std::string hlcommits;
if (HLVERCALL.contains("commits:")) {
hlcommits = HLVERCALL.substr(HLVERCALL.find("commits:") + 9);
hlcommits = hlcommits.substr(0, hlcommits.find(' '));
}
int commits = 0;
size_t commits = 0;
try {
commits = std::stoi(hlcommits);
commits = std::stoull(hlcommits);
} catch (...) { ; }
if (m_bVerbose)
std::println("{}", verboseString("parsed commit {} at branch {} on {}, commits {}", hlcommit, hlbranch, hldate, commits));
std::println("{}", verboseString("parsed commit {} at branch {} on {}, commits {}, nix: {}", hlcommit, hlbranch, hldate, commits, isNix));
auto ver = SHyprlandVersion{hlbranch, hlcommit, hldate, commits};
auto ver = SHyprlandVersion{hlbranch, hlcommit, hldate, abiHash, commits, isNix};
if (running)
verRunning = ver;
@ -137,16 +131,24 @@ bool CPluginManager::createSafeDirectory(const std::string& path) {
return true;
}
bool CPluginManager::validArg(const std::string& s) {
return !s.contains("'") && !s.ends_with("\\") && !s.starts_with("\\");
}
bool CPluginManager::addNewPluginRepo(const std::string& url, const std::string& rev) {
const auto HLVER = getHyprlandVersion();
if (!hasDeps()) {
std::println(stderr, "\n{}",
failureString("Could not clone the plugin repository. Dependencies not satisfied. Hyprpm requires: cmake, meson, cpio, pkg-config, git, g++, gcc"));
if (!validArg(url) || !validArg(rev)) {
std::println(stderr, "\n{}", failureString("url or rev invalid"));
return false;
}
if (DataState::pluginRepoExists(url)) {
if (!hasDeps()) {
std::println(stderr, "\n{}", failureString("Could not clone the plugin repository. Dependencies not satisfied. Hyprpm requires: cmake, cpio, pkg-config, git, g++, gcc"));
return false;
}
if (DataState::pluginRepoExists(SPluginRepoIdentifier::fromUrl(url))) {
std::println(stderr, "\n{}", failureString("Could not clone the plugin repository. Repository already installed."));
return false;
}
@ -161,7 +163,7 @@ bool CPluginManager::addNewPluginRepo(const std::string& url, const std::string&
DataState::updateGlobalState(GLOBALSTATE);
}
if (GLOBALSTATE.headersHashCompiled.empty()) {
if (GLOBALSTATE.headersAbiCompiled.empty()) {
std::println("\n{}", failureString("Cannot find headers in the global state. Try running hyprpm update first."));
return false;
}
@ -205,7 +207,7 @@ bool CPluginManager::addNewPluginRepo(const std::string& url, const std::string&
progress.printMessageAbove(infoString("Cloning {}", url));
std::string ret = execAndGet(std::format("cd {} && git clone --recursive {} {}", getTempRoot(), url, USERNAME));
std::string ret = execAndGet(std::format("cd {} && git clone --recursive '{}' {}", getTempRoot(), url, USERNAME));
if (!std::filesystem::exists(m_szWorkingPluginDirectory + "/.git")) {
std::println(stderr, "\n{}", failureString("Could not clone the plugin repository. shell returned:\n{}", ret));
@ -312,8 +314,14 @@ bool CPluginManager::addNewPluginRepo(const std::string& url, const std::string&
progress.printMessageAbove(infoString("Building {}", p.name));
for (auto const& bs : p.buildSteps) {
const std::string& cmd = std::format("cd {} && PKG_CONFIG_PATH=\"{}/share/pkgconfig\" {}", m_szWorkingPluginDirectory, DataState::getHeadersPath(), bs);
out += " -> " + cmd + "\n" + execAndGet(cmd) + "\n";
const auto CMD_RAW = nixDevelopIfNeeded(std::format("cd {} && PKG_CONFIG_PATH=\"{}\" {}", m_szWorkingPluginDirectory, getPkgConfigPath(), bs), HLVER);
if (!CMD_RAW) {
progress.printMessageAbove(failureString("Failed to build {}: {}", p.name, CMD_RAW.error()));
break;
}
out += " -> " + *CMD_RAW + "\n" + execAndGet(*CMD_RAW) + "\n";
}
if (m_bVerbose)
@ -343,10 +351,13 @@ bool CPluginManager::addNewPluginRepo(const std::string& url, const std::string&
std::string repohash = execAndGet("cd " + m_szWorkingPluginDirectory + " && git rev-parse HEAD");
if (repohash.length() > 0)
repohash.pop_back();
repo.name = pManifest->m_repository.name.empty() ? url.substr(url.find_last_of('/') + 1) : pManifest->m_repository.name;
repo.url = url;
repo.rev = rev;
repo.hash = repohash;
auto lastSlash = url.find_last_of('/');
auto secondLastSlash = url.find_last_of('/', lastSlash - 1);
repo.name = pManifest->m_repository.name.empty() ? url.substr(lastSlash + 1) : pManifest->m_repository.name;
repo.author = url.substr(secondLastSlash + 1, lastSlash - secondLastSlash - 1);
repo.url = url;
repo.rev = rev;
repo.hash = repohash;
for (auto const& p : pManifest->m_plugins) {
repo.plugins.push_back(SPlugin{p.name, m_szWorkingPluginDirectory + "/" + p.output, false, p.failed});
}
@ -366,13 +377,13 @@ bool CPluginManager::addNewPluginRepo(const std::string& url, const std::string&
return true;
}
bool CPluginManager::removePluginRepo(const std::string& urlOrName) {
if (!DataState::pluginRepoExists(urlOrName)) {
bool CPluginManager::removePluginRepo(const SPluginRepoIdentifier identifier) {
if (!DataState::pluginRepoExists(identifier)) {
std::println(stderr, "\n{}", failureString("Could not remove the repository. Repository is not installed."));
return false;
}
std::cout << Colors::YELLOW << "!" << Colors::RESET << Colors::RED << " removing a plugin repository: " << Colors::RESET << urlOrName << "\n "
std::cout << Colors::YELLOW << "!" << Colors::RESET << Colors::RED << " removing a plugin repository: " << Colors::RESET << identifier.toString() << "\n "
<< "Are you sure? [Y/n] ";
std::fflush(stdout);
std::string input;
@ -383,7 +394,7 @@ bool CPluginManager::removePluginRepo(const std::string& urlOrName) {
return false;
}
DataState::removePluginRepo(urlOrName);
DataState::removePluginRepo(identifier);
return true;
}
@ -395,7 +406,7 @@ eHeadersErrors CPluginManager::headersValid() {
return HEADERS_MISSING;
// find headers commit
const std::string& cmd = std::format("PKG_CONFIG_PATH=\"{}/share/pkgconfig\" pkgconf --cflags --keep-system-cflags hyprland", DataState::getHeadersPath());
const std::string& cmd = std::format("PKG_CONFIG_PATH=\"{}\" pkgconf --cflags --keep-system-cflags hyprland", getPkgConfigPath());
auto headers = execAndGet(cmd);
if (!headers.contains("-I/"))
@ -444,17 +455,22 @@ eHeadersErrors CPluginManager::headersValid() {
if (hash != HLVER.hash)
return HEADERS_MISMATCHED;
// check ABI hash too
const auto GLOBALSTATE = DataState::getGlobalState();
if (GLOBALSTATE.headersAbiCompiled != HLVER.abiHash)
return HEADERS_ABI_MISMATCH;
return HEADERS_OK;
}
bool CPluginManager::updateHeaders(bool force) {
DataState::ensureStateStoreExists();
const auto HLVER = getHyprlandVersion(false);
if (!hasDeps()) {
std::println("\n{}", failureString("Could not update. Dependencies not satisfied. Hyprpm requires: cmake, meson, cpio, pkg-config, git, g++, gcc"));
std::println("\n{}", failureString("Could not update. Dependencies not satisfied. Hyprpm requires: cmake, cpio, pkg-config, git, g++, gcc"));
return false;
}
@ -496,11 +512,11 @@ bool CPluginManager::updateHeaders(bool force) {
progress.printMessageAbove(verboseString("will shallow since: {}", SHALLOW_DATE));
std::string ret =
execAndGet(std::format("cd {} && git clone --recursive {} hyprland-{}{}", getTempRoot(), HL_URL, USERNAME, (bShallow ? " --shallow-since='" + SHALLOW_DATE + "'" : "")));
execAndGet(std::format("cd {} && git clone --recursive '{}' hyprland-{}{}", getTempRoot(), HL_URL, USERNAME, (bShallow ? " --shallow-since='" + SHALLOW_DATE + "'" : "")));
if (!std::filesystem::exists(WORKINGDIR)) {
progress.printMessageAbove(failureString("Clone failed. Retrying without shallow."));
ret = execAndGet(std::format("cd {} && git clone --recursive {} hyprland-{}", getTempRoot(), HL_URL, USERNAME));
ret = execAndGet(std::format("cd {} && git clone --recursive '{}' hyprland-{}", getTempRoot(), HL_URL, USERNAME));
}
if (!std::filesystem::exists(WORKINGDIR + "/.git")) {
@ -543,8 +559,17 @@ bool CPluginManager::updateHeaders(bool force) {
if (m_bVerbose)
progress.printMessageAbove(verboseString("setting PREFIX for cmake to {}", DataState::getHeadersPath()));
ret = execAndGet(std::format("cd {} && cmake --no-warn-unused-cli -DCMAKE_BUILD_TYPE:STRING=Release -DCMAKE_INSTALL_PREFIX:STRING=\"{}\" -S . -B ./build -G Ninja", WORKINGDIR,
DataState::getHeadersPath()));
const auto CONFIGURE_CMD =
nixDevelopIfNeeded(std::format("cd {} && cmake --no-warn-unused-cli -DCMAKE_BUILD_TYPE:STRING=Release -DCMAKE_INSTALL_PREFIX:STRING=\"{}\" -S . -B ./build", WORKINGDIR,
DataState::getHeadersPath()),
HLVER);
if (!CONFIGURE_CMD) {
std::println(stderr, "\n{}", failureString("Could not configure hyprland: {}", CONFIGURE_CMD.error()));
return false;
}
ret = execAndGet(*CONFIGURE_CMD);
if (m_bVerbose)
progress.printMessageAbove(verboseString("cmake returned: {}", ret));
@ -589,14 +614,15 @@ bool CPluginManager::updateHeaders(bool force) {
std::filesystem::remove_all(WORKINGDIR);
auto HEADERSVALID = headersValid();
if (HEADERSVALID == HEADERS_OK) {
if (HEADERSVALID == HEADERS_OK || HEADERSVALID == HEADERS_MISMATCHED || HEADERSVALID == HEADERS_ABI_MISMATCH) {
progress.printMessageAbove(successString("installed headers"));
progress.m_iSteps = 5;
progress.m_szCurrentMessage = "Done!";
progress.print();
auto GLOBALSTATE = DataState::getGlobalState();
GLOBALSTATE.headersHashCompiled = HLVER.hash;
auto GLOBALSTATE = DataState::getGlobalState();
GLOBALSTATE.headersAbiCompiled = HLVER.abiHash;
DataState::updateGlobalState(GLOBALSTATE);
std::print("\n");
@ -631,7 +657,7 @@ bool CPluginManager::updatePlugins(bool forceUpdateAll) {
const auto HLVER = getHyprlandVersion(false);
CProgressBar progress;
progress.m_iMaxSteps = REPOS.size() * 2 + 2;
progress.m_iMaxSteps = (REPOS.size() * 2) + 2;
progress.m_iSteps = 0;
progress.m_szCurrentMessage = "Updating repositories";
progress.print();
@ -652,7 +678,7 @@ bool CPluginManager::updatePlugins(bool forceUpdateAll) {
progress.printMessageAbove(infoString("Cloning {}", repo.url));
std::string ret = execAndGet(std::format("cd {} && git clone --recursive {} {}", getTempRoot(), repo.url, USERNAME));
std::string ret = execAndGet(std::format("cd {} && git clone --recursive '{}' {}", getTempRoot(), repo.url, USERNAME));
if (!std::filesystem::exists(m_szWorkingPluginDirectory + "/.git")) {
std::println("{}", failureString("could not clone repo: shell returned: {}", ret));
@ -662,7 +688,7 @@ bool CPluginManager::updatePlugins(bool forceUpdateAll) {
if (!repo.rev.empty()) {
progress.printMessageAbove(infoString("Plugin has revision set, resetting: {}", repo.rev));
std::string ret = execAndGet("git -C " + m_szWorkingPluginDirectory + " reset --hard --recurse-submodules " + repo.rev);
std::string ret = execAndGet("git -C " + m_szWorkingPluginDirectory + " reset --hard --recurse-submodules \'" + repo.rev + "\'");
if (ret.compare(0, 6, "fatal:") == 0) {
std::println(stderr, "\n{}", failureString("could not check out revision {}: shell returned:\n{}", repo.rev, ret));
@ -741,8 +767,14 @@ bool CPluginManager::updatePlugins(bool forceUpdateAll) {
progress.printMessageAbove(infoString("Building {}", p.name));
for (auto const& bs : p.buildSteps) {
const std::string& cmd = std::format("cd {} && PKG_CONFIG_PATH=\"{}/share/pkgconfig\" {}", m_szWorkingPluginDirectory, DataState::getHeadersPath(), bs);
out += " -> " + cmd + "\n" + execAndGet(cmd) + "\n";
const auto CMD_RAW = nixDevelopIfNeeded(std::format("cd {} && PKG_CONFIG_PATH=\"{}\" {}", m_szWorkingPluginDirectory, getPkgConfigPath(), bs), HLVER);
if (!CMD_RAW) {
progress.printMessageAbove(failureString("Failed to build {}: {}", p.name, CMD_RAW.error()));
break;
}
out += " -> " + *CMD_RAW + "\n" + execAndGet(*CMD_RAW) + "\n";
}
if (m_bVerbose)
@ -772,10 +804,10 @@ bool CPluginManager::updatePlugins(bool forceUpdateAll) {
repohash.pop_back();
newrepo.hash = repohash;
for (auto const& p : pManifest->m_plugins) {
const auto OLDPLUGINIT = std::find_if(repo.plugins.begin(), repo.plugins.end(), [&](const auto& other) { return other.name == p.name; });
newrepo.plugins.push_back(SPlugin{p.name, m_szWorkingPluginDirectory + "/" + p.output, OLDPLUGINIT != repo.plugins.end() ? OLDPLUGINIT->enabled : false});
const auto OLDPLUGINIT = std::ranges::find_if(repo.plugins, [&](const auto& other) { return other.name == p.name; });
newrepo.plugins.emplace_back(SPlugin{p.name, m_szWorkingPluginDirectory + "/" + p.output, OLDPLUGINIT != repo.plugins.end() ? OLDPLUGINIT->enabled : false});
}
DataState::removePluginRepo(newrepo.name);
DataState::removePluginRepo(SPluginRepoIdentifier::fromName(newrepo.name));
DataState::addNewPluginRepo(newrepo);
std::filesystem::remove_all(m_szWorkingPluginDirectory);
@ -787,8 +819,8 @@ bool CPluginManager::updatePlugins(bool forceUpdateAll) {
progress.m_szCurrentMessage = "Updating global state...";
progress.print();
auto GLOBALSTATE = DataState::getGlobalState();
GLOBALSTATE.headersHashCompiled = HLVER.hash;
auto GLOBALSTATE = DataState::getGlobalState();
GLOBALSTATE.headersAbiCompiled = HLVER.abiHash;
DataState::updateGlobalState(GLOBALSTATE);
progress.m_iSteps++;
@ -800,17 +832,23 @@ bool CPluginManager::updatePlugins(bool forceUpdateAll) {
return true;
}
bool CPluginManager::enablePlugin(const std::string& name) {
bool ret = DataState::setPluginEnabled(name, true);
bool CPluginManager::enablePlugin(const SPluginRepoIdentifier identifier) {
bool ret = false;
switch (identifier.type) {
case IDENTIFIER_NAME:
case IDENTIFIER_AUTHOR_NAME: ret = DataState::setPluginEnabled(identifier, true); break;
default: return false;
}
if (ret)
std::println("{}", successString("Enabled {}", name));
std::println("{}", successString("Enabled {}", identifier.name));
return ret;
}
bool CPluginManager::disablePlugin(const std::string& name) {
bool ret = DataState::setPluginEnabled(name, false);
bool CPluginManager::disablePlugin(const SPluginRepoIdentifier identifier) {
bool ret = DataState::setPluginEnabled(identifier, false);
if (ret)
std::println("{}", successString("Disabled {}", name));
std::println("{}", successString("Disabled {}", identifier.name));
return ret;
}
@ -828,7 +866,7 @@ ePluginLoadStateReturn CPluginManager::ensurePluginsLoadState(bool forceReload)
}
const auto HYPRPMPATH = DataState::getDataStatePath();
const auto json = glz::read_json<glz::json_t::array_t>(NHyprlandSocket::send("j/plugins list"));
const auto json = glz::read_json<glz::generic::array_t>(NHyprlandSocket::send("j/plugins list"));
if (!json) {
std::println(stderr, "PluginManager: couldn't parse plugin list output");
return LOADSTATE_FAIL;
@ -893,7 +931,7 @@ ePluginLoadStateReturn CPluginManager::ensurePluginsLoadState(bool forceReload)
if (!p.enabled)
continue;
if (!forceReload && std::find_if(loadedPlugins.begin(), loadedPlugins.end(), [&](const auto& other) { return other == p.name; }) != loadedPlugins.end())
if (!forceReload && std::ranges::find_if(loadedPlugins, [&](const auto& other) { return other == p.name; }) != loadedPlugins.end())
continue;
if (!loadUnloadPlugin(HYPRPMPATH / repoForName(p.name) / p.filename, true)) {
@ -913,9 +951,9 @@ bool CPluginManager::loadUnloadPlugin(const std::string& path, bool load) {
auto state = DataState::getGlobalState();
auto HLVER = getHyprlandVersion(true);
if (state.headersHashCompiled != HLVER.hash) {
if (state.headersAbiCompiled != HLVER.abiHash) {
if (load)
std::println("{}", infoString("Running Hyprland version ({}) differs from plugin state ({}), please restart Hyprland.", HLVER.hash, state.headersHashCompiled));
std::println("{}", infoString("Running Hyprland version ({}) differs from plugin state ({}), please restart Hyprland.", HLVER.hash, state.headersAbiCompiled));
return false;
}
@ -931,7 +969,7 @@ void CPluginManager::listAllPlugins() {
const auto REPOS = DataState::getAllRepositories();
for (auto const& r : REPOS) {
std::println("{}", infoString("Repository {}:", r.name));
std::println("{}", infoString("Repository {} (by {}):", r.name, r.author));
for (auto const& p : r.plugins) {
std::println(" │ Plugin {}", p.name);
@ -956,6 +994,7 @@ std::string CPluginManager::headerError(const eHeadersErrors err) {
case HEADERS_MISMATCHED: return failureString("Headers version mismatch. Please run hyprpm update to fix those.\n");
case HEADERS_NOT_HYPRLAND: return failureString("It doesn't seem you are running on hyprland.\n");
case HEADERS_MISSING: return failureString("Headers missing. Please run hyprpm update to fix those.\n");
case HEADERS_ABI_MISMATCH: return failureString("ABI is mismatched. Please run hyprpm update to fix that.\n");
case HEADERS_DUPLICATED: {
return failureString("Headers duplicated!!! This is a very bad sign.\n"
"This could be due to e.g. installing hyprland manually while a system package of hyprland is also installed.\n"
@ -980,8 +1019,11 @@ std::string CPluginManager::headerErrorShort(const eHeadersErrors err) {
}
bool CPluginManager::hasDeps() {
if (!m_bNoNix && getHyprlandVersion().isNix)
return true; // dep check not needed if we are on nix
bool hasAllDeps = true;
std::vector<std::string> deps = {"meson", "cpio", "cmake", "pkg-config", "g++", "gcc", "git"};
std::vector<std::string> deps = {"cpio", "cmake", "pkg-config", "g++", "gcc", "git"};
for (auto const& d : deps) {
if (!execAndGet("command -v " + d).contains("/")) {
@ -992,3 +1034,92 @@ bool CPluginManager::hasDeps() {
return hasAllDeps;
}
const std::string& CPluginManager::getPkgConfigPath() {
static const auto str = std::format("{}/share/pkgconfig:$PKG_CONFIG_PATH", DataState::getHeadersPath());
return str;
}
static std::expected<std::string, std::string> getNixDevelopFromPath(const std::string& argv0) {
std::string fullStorePath;
if (argv0.starts_with("/")) {
// we can use this directly
fullStorePath = argv0;
} else {
// use hyprpm, find in path
auto exe = NSys::findInPath("hyprpm");
if (!exe)
return std::unexpected("hyprpm not found in PATH");
fullStorePath = *exe;
}
if (fullStorePath.empty() || !fullStorePath.ends_with("/bin/hyprpm"))
return std::unexpected("couldn't get a real path for hyprpm (1)");
// canonicalize to get the real nix-store path
std::error_code ec;
fullStorePath = std::filesystem::canonical(fullStorePath, ec);
if (ec || fullStorePath.empty() || !fullStorePath.starts_with("/nix"))
return std::unexpected("couldn't get a real path for hyprpm");
fullStorePath = fullStorePath.substr(0, fullStorePath.length() - std::string_view{"/bin/hyprpm"}.length());
auto deriver = trim(execAndGet(std::format("echo \"$(nix-store --query --deriver '{}')\"", fullStorePath)));
if (deriver.starts_with("unknown"))
return std::unexpected("couldn't nix deriver");
return deriver;
}
static std::expected<std::string, std::string> getNixDevelopFromProfile() {
const auto NIX_PROFILE_STR = execAndGet("nix profile list --json");
auto rawJson = glz::read_json<glz::generic>(NIX_PROFILE_STR);
if (!rawJson)
return std::unexpected("failed to parse nix profile list --json");
auto& json = *rawJson;
if (!json.contains("elements") || !json["elements"].is_object())
return std::unexpected("nix profile list --json returned a wonky json");
if (!json["elements"].contains("hyprland") && !json["elements"].contains("Hyprland"))
return std::unexpected("nix profile list --json doesn't contain Hyprland (did you uninstall?)");
auto& hyprlandJson = json["elements"].contains("hyprland") ? json["elements"]["hyprland"] : json["elements"]["Hyprland"];
if (!hyprlandJson.contains("originalUrl"))
return std::unexpected("nix profile list --json's hyprland doesn't contain originalUrl?");
return hyprlandJson["originalUrl"].get_string();
}
std::expected<std::string, std::string> CPluginManager::nixDevelopIfNeeded(const std::string& cmd, const SHyprlandVersion& ver) {
if (m_bNoNix || !ver.isNix)
return cmd;
// Escape single quotes
std::string newCmd = cmd;
replaceInString(newCmd, "'", "\\'");
auto NIX_DEVELOP = getNixDevelopFromPath(m_szArgv0);
if (NIX_DEVELOP)
return std::format("nix develop '{}' --command bash -c $'{}'", *NIX_DEVELOP, newCmd);
else if (m_bVerbose)
std::println("{}", verboseString("Failed nix from path: {}", NIX_DEVELOP.error()));
NIX_DEVELOP = getNixDevelopFromProfile();
if (NIX_DEVELOP)
return std::format("nix develop '{}' --command bash -c $'{}'", *NIX_DEVELOP, newCmd);
else if (m_bVerbose)
std::println("{}", verboseString("Failed nix from profile: {}", NIX_DEVELOP.error()));
return std::unexpected("hyprland is nix, but hyprpm failed to obtain a nix develop shell for build cmd");
}

View file

@ -2,8 +2,10 @@
#include <cstdint>
#include <memory>
#include <optional>
#include <string>
#include <utility>
#include <expected>
#include "Plugin.hpp"
enum eHeadersErrors {
HEADERS_OK = 0,
@ -11,6 +13,7 @@ enum eHeadersErrors {
HEADERS_MISSING,
HEADERS_CORRUPTED,
HEADERS_MISMATCHED,
HEADERS_ABI_MISMATCH,
HEADERS_DUPLICATED
};
@ -36,7 +39,9 @@ struct SHyprlandVersion {
std::string branch;
std::string hash;
std::string date;
std::string abiHash;
int commits = 0;
bool isNix = false;
};
class CPluginManager {
@ -44,7 +49,7 @@ class CPluginManager {
CPluginManager();
bool addNewPluginRepo(const std::string& url, const std::string& rev);
bool removePluginRepo(const std::string& urlOrName);
bool removePluginRepo(const SPluginRepoIdentifier identifier);
eHeadersErrors headersValid();
bool updateHeaders(bool force = false);
@ -52,8 +57,8 @@ class CPluginManager {
void listAllPlugins();
bool enablePlugin(const std::string& name);
bool disablePlugin(const std::string& name);
bool enablePlugin(const SPluginRepoIdentifier identifier);
bool disablePlugin(const SPluginRepoIdentifier identifier);
ePluginLoadStateReturn ensurePluginsLoadState(bool forceReload = false);
bool loadUnloadPlugin(const std::string& path, bool load);
@ -61,20 +66,26 @@ class CPluginManager {
void notify(const eNotifyIcons icon, uint32_t color, int durationMs, const std::string& message);
const std::string& getPkgConfigPath();
bool hasDeps();
bool m_bVerbose = false;
bool m_bNoShallow = false;
std::string m_szCustomHlUrl, m_szUsername;
bool m_bNoNix = false;
std::string m_szCustomHlUrl, m_szUsername, m_szArgv0;
// will delete recursively if exists!!
bool createSafeDirectory(const std::string& path);
private:
std::string headerError(const eHeadersErrors err);
std::string headerErrorShort(const eHeadersErrors err);
std::string headerError(const eHeadersErrors err);
std::string headerErrorShort(const eHeadersErrors err);
bool validArg(const std::string& s);
std::string m_szWorkingPluginDirectory;
std::expected<std::string, std::string> nixDevelopIfNeeded(const std::string& cmd, const SHyprlandVersion& ver);
std::string m_szWorkingPluginDirectory;
};
inline std::unique_ptr<CPluginManager> g_pPluginManager;

View file

@ -35,9 +35,13 @@ static std::string validSubinsAsStr() {
}
static bool executableExistsInPath(const std::string& exe) {
return NSys::findInPath(exe).has_value();
}
std::optional<std::string> NSys::findInPath(const std::string& exe) {
const char* PATHENV = std::getenv("PATH");
if (!PATHENV)
return false;
return std::nullopt;
CVarList paths(PATHENV, 0, ':', true);
std::error_code ec;
@ -52,10 +56,10 @@ static bool executableExistsInPath(const std::string& exe) {
if (ec)
continue;
if ((perms & std::filesystem::perms::others_exec) != std::filesystem::perms::none)
return true;
return candidate.string();
}
return false;
return std::nullopt;
}
static std::string subin() {

View file

@ -1,11 +1,13 @@
#pragma once
#include <string>
#include <optional>
namespace NSys {
bool isSuperuser();
int getUID();
int getEUID();
bool isSuperuser();
int getUID();
int getEUID();
std::optional<std::string> findInPath(const std::string& exe);
// NOLINTNEXTLINE
namespace root {
@ -20,4 +22,4 @@ namespace NSys {
// Do not use this unless absolutely necessary!
std::string runAsSuperuserUnsafe(const std::string& cmd);
};
};
};

View file

@ -13,25 +13,26 @@ using namespace Hyprutils::Utils;
constexpr std::string_view HELP = R"#(┏ hyprpm, a Hyprland Plugin Manager
add [url] [git rev] Install a new plugin repository from git. Git revision
is optional, when set, commit locks are ignored.
remove [url/name] Remove an installed plugin repository.
enable [name] Enable a plugin.
disable [name] Disable a plugin.
update Check and update all plugins if needed.
reload Reload hyprpm state. Ensure all enabled plugins are loaded.
list List all installed plugins.
purge-cache Remove the entire hyprpm cache, built plugins, hyprpm settings and headers.
add <url> [git rev] Install a new plugin repository from git. Git revision
is optional, when set, commit locks are ignored.
remove <url|name|author/name> Remove an installed plugin repository.
enable <name|author/name> Enable a plugin.
disable <name|author/name> Disable a plugin.
update Check and update all plugins if needed.
reload Reload hyprpm state. Ensure all enabled plugins are loaded.
list List all installed plugins.
purge-cache Remove the entire hyprpm cache, built plugins, hyprpm settings and headers.
Flags:
--notify | -n Send a hyprland notification confirming successful plugin load.
Warnings/Errors trigger notifications regardless of this flag.
--help | -h Show this menu.
--verbose | -v Enable too much logging.
--force | -f Force an operation ignoring checks (e.g. update -f).
--no-shallow | -s Disable shallow cloning of Hyprland sources.
--hl-url | Pass a custom hyprland source url.
--no-nix | Disable `nix develop` for build commands, even if Hyprland is nix.
--notify | -n Send a hyprland notification confirming successful plugin load.
Warnings/Errors trigger notifications regardless of this flag.
--help | -h Show this menu.
--verbose | -v Enable too much logging.
--force | -f Force an operation ignoring checks (e.g. update -f).
--no-shallow | -s Disable shallow cloning of Hyprland sources.
--hl-url | Pass a custom hyprland source url.
)#";
@ -47,7 +48,7 @@ int main(int argc, char** argv, char** envp) {
}
std::vector<std::string> command;
bool notify = false, verbose = false, force = false, noShallow = false;
bool notify = false, verbose = false, force = false, noShallow = false, noNix = false;
std::string customHlUrl;
for (int i = 1; i < argc; ++i) {
@ -63,6 +64,8 @@ int main(int argc, char** argv, char** envp) {
g_pPluginManager->notify(ICON_INFO, 0, 10000, "[hyprpm] -n flag is deprecated, see hyprpm --help.");
} else if (ARGS[i] == "--verbose" || ARGS[i] == "-v") {
verbose = true;
} else if (ARGS[i] == "--no-nix") {
noNix = true;
} else if (ARGS[i] == "--no-shallow" || ARGS[i] == "-s") {
noShallow = true;
} else if (ARGS[i] == "--hl-url") {
@ -91,7 +94,9 @@ int main(int argc, char** argv, char** envp) {
g_pPluginManager = std::make_unique<CPluginManager>();
g_pPluginManager->m_bVerbose = verbose;
g_pPluginManager->m_bNoShallow = noShallow;
g_pPluginManager->m_bNoNix = noNix;
g_pPluginManager->m_szCustomHlUrl = customHlUrl;
g_pPluginManager->m_szArgv0 = argv[0];
if (command[0] == "add") {
if (command.size() < 2) {
@ -106,7 +111,7 @@ int main(int argc, char** argv, char** envp) {
const auto HLVER = g_pPluginManager->getHyprlandVersion();
auto GLOBALSTATE = DataState::getGlobalState();
if (GLOBALSTATE.headersHashCompiled != HLVER.hash) {
if (GLOBALSTATE.headersAbiCompiled != HLVER.abiHash) {
std::println(stderr, "{}", failureString("Headers outdated, please run hyprpm update."));
return 1;
}
@ -126,7 +131,7 @@ int main(int argc, char** argv, char** envp) {
NSys::root::cacheSudo();
CScopeGuard x([] { NSys::root::dropSudo(); });
return g_pPluginManager->removePluginRepo(command[1]) ? 0 : 1;
return g_pPluginManager->removePluginRepo(SPluginRepoIdentifier::fromString(command[1])) ? 0 : 1;
} else if (command[0] == "update") {
NSys::root::cacheSudo();
CScopeGuard x([] { NSys::root::dropSudo(); });
@ -137,7 +142,7 @@ int main(int argc, char** argv, char** envp) {
if (headers) {
const auto HLVER = g_pPluginManager->getHyprlandVersion(false);
auto GLOBALSTATE = DataState::getGlobalState();
const auto COMPILEDOUTDATED = HLVER.hash != GLOBALSTATE.headersHashCompiled;
const auto COMPILEDOUTDATED = HLVER.abiHash != GLOBALSTATE.headersAbiCompiled;
bool ret1 = g_pPluginManager->updatePlugins(!headersValid || force || COMPILEDOUTDATED);
@ -160,7 +165,7 @@ int main(int argc, char** argv, char** envp) {
return 1;
}
if (!g_pPluginManager->enablePlugin(command[1])) {
if (!g_pPluginManager->enablePlugin(SPluginRepoIdentifier::fromString(command[1]))) {
std::println(stderr, "{}", failureString("Couldn't enable plugin (missing?)"));
return 1;
}
@ -181,7 +186,7 @@ int main(int argc, char** argv, char** envp) {
return 1;
}
if (!g_pPluginManager->disablePlugin(command[1])) {
if (!g_pPluginManager->disablePlugin(SPluginRepoIdentifier::fromString(command[1]))) {
std::println(stderr, "{}", failureString("Couldn't disable plugin (missing?)"));
return 1;
}

View file

@ -48,7 +48,7 @@ pkg_check_modules(
IMPORTED_TARGET
hyprutils>=0.8.0
wayland-client
wayland-protocols
wayland-protocols>=1.47
)
pkg_get_variable(WAYLAND_PROTOCOLS_DIR wayland-protocols pkgdatadir)
@ -96,6 +96,9 @@ endfunction()
protocolnew("staging/pointer-warp" "pointer-warp-v1" false)
protocolnew("stable/xdg-shell" "xdg-shell" false)
protocolnew("unstable/keyboard-shortcuts-inhibit" "keyboard-shortcuts-inhibit-unstable-v1" false)
clientNew("pointer-warp" PROTOS "pointer-warp-v1" "xdg-shell")
clientNew("pointer-scroll" PROTOS "xdg-shell")
clientNew("child-window" PROTOS "xdg-shell")
clientNew("shortcut-inhibitor" PROTOS "xdg-shell" "keyboard-shortcuts-inhibit-unstable-v1")

View file

@ -0,0 +1,336 @@
#include <print>
#include <poll.h>
#include <sys/mman.h>
#include <fcntl.h>
#include <stdio.h>
#include <unistd.h>
#include <wayland-client.h>
#include <wayland.hpp>
#include <xdg-shell.hpp>
#include <hyprutils/memory/SharedPtr.hpp>
#include <hyprutils/math/Vector2D.hpp>
using Hyprutils::Math::Vector2D;
using namespace Hyprutils::Memory;
struct SWlState {
wl_display* display;
CSharedPointer<CCWlRegistry> registry;
// protocols
CSharedPointer<CCWlCompositor> wlCompositor;
CSharedPointer<CCWlSeat> wlSeat;
CSharedPointer<CCWlShm> wlShm;
CSharedPointer<CCXdgWmBase> xdgShell;
// shm/buffer stuff
CSharedPointer<CCWlShmPool> shmPool;
CSharedPointer<CCWlBuffer> shmBuf;
CSharedPointer<CCWlBuffer> shmBuf2;
int shmFd = 0;
size_t shmBufSize = 0;
bool xrgb8888_support = false;
// surface/toplevel stuff
CSharedPointer<CCWlSurface> surf;
CSharedPointer<CCXdgSurface> xdgSurf;
CSharedPointer<CCXdgToplevel> xdgToplevel;
Vector2D geom;
// pointer
CSharedPointer<CCWlPointer> pointer;
uint32_t enterSerial = 0;
};
bool debug, shouldExit, started;
template <typename... Args>
//NOLINTNEXTLINE
static void clientLog(std::format_string<Args...> fmt, Args&&... args) {
std::string text = std::vformat(fmt.get(), std::make_format_args(args...));
std::println("{}", text);
std::fflush(stdout);
}
template <typename... Args>
//NOLINTNEXTLINE
static void debugLog(std::format_string<Args...> fmt, Args&&... args) {
std::string text = std::vformat(fmt.get(), std::make_format_args(args...));
if (!debug)
return;
std::println("{}", text);
std::fflush(stdout);
}
static bool bindRegistry(SWlState& state) {
state.registry = makeShared<CCWlRegistry>((wl_proxy*)wl_display_get_registry(state.display));
state.registry->setGlobal([&](CCWlRegistry* r, uint32_t id, const char* name, uint32_t version) {
const std::string NAME = name;
if (NAME == "wl_compositor") {
debugLog(" > binding to global: {} (version {}) with id {}", name, version, id);
state.wlCompositor = makeShared<CCWlCompositor>((wl_proxy*)wl_registry_bind((wl_registry*)state.registry->resource(), id, &wl_compositor_interface, 6));
} else if (NAME == "wl_shm") {
debugLog(" > binding to global: {} (version {}) with id {}", name, version, id);
state.wlShm = makeShared<CCWlShm>((wl_proxy*)wl_registry_bind((wl_registry*)state.registry->resource(), id, &wl_shm_interface, 1));
} else if (NAME == "wl_seat") {
debugLog(" > binding to global: {} (version {}) with id {}", name, version, id);
state.wlSeat = makeShared<CCWlSeat>((wl_proxy*)wl_registry_bind((wl_registry*)state.registry->resource(), id, &wl_seat_interface, 9));
} else if (NAME == "xdg_wm_base") {
debugLog(" > binding to global: {} (version {}) with id {}", name, version, id);
state.xdgShell = makeShared<CCXdgWmBase>((wl_proxy*)wl_registry_bind((wl_registry*)state.registry->resource(), id, &xdg_wm_base_interface, 1));
}
});
state.registry->setGlobalRemove([](CCWlRegistry* r, uint32_t id) { debugLog("Global {} removed", id); });
wl_display_roundtrip(state.display);
if (!state.wlCompositor || !state.wlShm || !state.wlSeat || !state.xdgShell) {
clientLog("Failed to get protocols from Hyprland");
return false;
}
return true;
}
static bool createShm(SWlState& state, Vector2D geom) {
if (!state.xrgb8888_support)
return false;
size_t stride = geom.x * 4;
size_t size = geom.y * stride;
if (!state.shmPool) {
const char* name = "/wl-shm-pointer-warp";
state.shmFd = shm_open(name, O_RDWR | O_CREAT | O_EXCL, 0600);
if (state.shmFd < 0)
return false;
if (shm_unlink(name) < 0 || ftruncate(state.shmFd, size * 2) < 0) {
close(state.shmFd);
return false;
}
state.shmPool = makeShared<CCWlShmPool>(state.wlShm->sendCreatePool(state.shmFd, size * 2));
if (!state.shmPool->resource()) {
close(state.shmFd);
state.shmFd = -1;
state.shmPool.reset();
return false;
}
state.shmBufSize = size;
} else if (size > state.shmBufSize) {
if (ftruncate(state.shmFd, size) < 0) {
close(state.shmFd);
state.shmFd = -1;
state.shmPool.reset();
return false;
}
state.shmPool->sendResize(size * 2);
state.shmBufSize = size;
}
auto buf = makeShared<CCWlBuffer>(state.shmPool->sendCreateBuffer(0, geom.x, geom.y, stride, WL_SHM_FORMAT_XRGB8888));
if (!buf->resource())
return false;
if (state.shmBuf) {
state.shmBuf->sendDestroy();
state.shmBuf.reset();
}
state.shmBuf = buf;
return true;
}
static bool setupToplevel(SWlState& state) {
state.wlShm->setFormat([&](CCWlShm* p, uint32_t format) {
if (format == WL_SHM_FORMAT_XRGB8888)
state.xrgb8888_support = true;
});
state.xdgShell->setPing([&](CCXdgWmBase* p, uint32_t serial) { state.xdgShell->sendPong(serial); });
state.surf = makeShared<CCWlSurface>(state.wlCompositor->sendCreateSurface());
if (!state.surf->resource())
return false;
state.xdgSurf = makeShared<CCXdgSurface>(state.xdgShell->sendGetXdgSurface(state.surf->resource()));
if (!state.xdgSurf->resource())
return false;
state.xdgToplevel = makeShared<CCXdgToplevel>(state.xdgSurf->sendGetToplevel());
if (!state.xdgToplevel->resource())
return false;
state.xdgToplevel->setClose([&](CCXdgToplevel* p) { exit(0); });
state.xdgToplevel->setConfigure([&](CCXdgToplevel* p, int32_t w, int32_t h, wl_array* arr) {
state.geom = {1280, 720};
if (!createShm(state, state.geom))
exit(-1);
});
state.xdgSurf->setConfigure([&](CCXdgSurface* p, uint32_t serial) {
if (!state.shmBuf)
debugLog("xdgSurf configure but no buf made yet?");
state.xdgSurf->sendSetWindowGeometry(0, 0, state.geom.x, state.geom.y);
state.surf->sendAttach(state.shmBuf.get(), 0, 0);
state.surf->sendCommit();
state.xdgSurf->sendAckConfigure(serial);
if (!started) {
started = true;
clientLog("started");
}
});
state.xdgToplevel->sendSetTitle("child-test parent");
state.xdgToplevel->sendSetAppId("child-test-parent");
state.surf->sendAttach(nullptr, 0, 0);
state.surf->sendCommit();
return true;
}
static bool setupSeat(SWlState& state) {
state.pointer = makeShared<CCWlPointer>(state.wlSeat->sendGetPointer());
if (!state.pointer->resource())
return false;
state.pointer->setEnter([&](CCWlPointer* p, uint32_t serial, wl_proxy* surf, wl_fixed_t x, wl_fixed_t y) {
debugLog("Got pointer enter event, serial {}, x {}, y {}", serial, x, y);
state.enterSerial = serial;
});
state.pointer->setLeave([&](CCWlPointer* p, uint32_t serial, wl_proxy* surf) { debugLog("Got pointer leave event, serial {}", serial); });
state.pointer->setMotion([&](CCWlPointer* p, uint32_t serial, wl_fixed_t x, wl_fixed_t y) { debugLog("Got pointer motion event, serial {}, x {}, y {}", serial, x, y); });
return true;
}
struct SChildWindow {
CSharedPointer<CCWlSurface> surface;
CSharedPointer<CCXdgSurface> xSurface;
CSharedPointer<CCXdgToplevel> toplevel;
};
static void parseRequest(SWlState& state, std::string str, SChildWindow& window) {
if (str.starts_with("exit")) {
shouldExit = true;
return;
}
size_t index = str.find_first_of('\n');
str = str.substr(0, index);
if (str == "toplevel") {
window.surface = makeShared<CCWlSurface>(state.wlCompositor->sendCreateSurface());
window.xSurface = makeShared<CCXdgSurface>(state.xdgShell->sendGetXdgSurface(window.surface->resource()));
window.xSurface->setConfigure([&](CCXdgSurface* p, uint32_t serial) {
if (!state.shmBuf)
debugLog("xdgSurf configure but no buf made yet?");
window.xSurface->sendSetWindowGeometry(0, 0, state.geom.x, state.geom.y);
window.surface->sendAttach(state.shmBuf2.get(), 0, 0);
window.surface->sendCommit();
window.xSurface->sendAckConfigure(serial);
});
window.toplevel = makeShared<CCXdgToplevel>(window.xSurface->sendGetToplevel());
window.toplevel->setConfigure([&](CCXdgToplevel* p, int32_t w, int32_t h, wl_array* arr) {
size_t stride = 1280 * 4;
size_t size = 720 * stride;
auto buf = makeShared<CCWlBuffer>(state.shmPool->sendCreateBuffer(size, state.geom.x, state.geom.y, stride, WL_SHM_FORMAT_XRGB8888));
if (!buf->resource())
clientLog("Failed to create child buffer");
if (state.shmBuf2) {
state.shmBuf2->sendDestroy();
state.shmBuf2.reset();
}
state.shmBuf2 = buf;
});
window.toplevel->sendSetTitle("child-test child");
window.toplevel->sendSetAppId("child-test-child");
window.toplevel->sendSetParent(state.xdgToplevel.get());
window.surface->sendAttach(nullptr, 0, 0);
window.surface->sendCommit();
clientLog("child started");
return;
}
}
int main(int argc, char** argv) {
if (argc != 1 && argc != 2)
clientLog("Only the \"--debug\" switch is allowed, it turns on debug logs.");
if (argc == 2 && std::string{argv[1]} == "--debug")
debug = true;
SWlState state;
SChildWindow window;
// WAYLAND_DISPLAY env should be set to the correct one
state.display = wl_display_connect(nullptr);
if (!state.display) {
clientLog("Failed to connect to wayland display");
return -1;
}
if (!bindRegistry(state) || !setupSeat(state) || !setupToplevel(state))
return -1;
std::array<char, 1024> readBuf;
readBuf.fill(0);
wl_display_flush(state.display);
struct pollfd fds[2] = {{.fd = wl_display_get_fd(state.display), .events = POLLIN | POLLOUT}, {.fd = STDIN_FILENO, .events = POLLIN}};
while (!shouldExit && poll(fds, 2, 0) != -1) {
if (fds[0].revents & POLLIN) {
wl_display_flush(state.display);
if (wl_display_prepare_read(state.display) == 0) {
wl_display_read_events(state.display);
wl_display_dispatch_pending(state.display);
} else
wl_display_dispatch(state.display);
int ret = 0;
do {
ret = wl_display_dispatch_pending(state.display);
wl_display_flush(state.display);
} while (ret > 0);
}
if (fds[1].revents & POLLIN) {
ssize_t bytesRead = read(fds[1].fd, readBuf.data(), 1023);
if (bytesRead == -1)
continue;
readBuf[bytesRead] = 0;
parseRequest(state, std::string{readBuf.data()}, window);
}
}
wl_display* display = state.display;
state = {};
window = {};
wl_display_disconnect(display);
return 0;
}

View file

@ -2,6 +2,7 @@
#include <sys/poll.h>
#include <sys/mman.h>
#include <fcntl.h>
#include <unistd.h>
#include <print>
#include <format>
#include <string>

View file

@ -2,6 +2,7 @@
#include <sys/poll.h>
#include <sys/mman.h>
#include <fcntl.h>
#include <unistd.h>
#include <print>
#include <format>
#include <string>

View file

@ -0,0 +1,297 @@
#include <sys/poll.h>
#include <sys/mman.h>
#include <fcntl.h>
#include <print>
#include <wayland-client.h>
#include <wayland.hpp>
#include <xdg-shell.hpp>
#include <keyboard-shortcuts-inhibit-unstable-v1.hpp>
#include <hyprutils/memory/SharedPtr.hpp>
#include <hyprutils/math/Vector2D.hpp>
using Hyprutils::Math::Vector2D;
using namespace Hyprutils::Memory;
struct SWlState {
wl_display* display;
CSharedPointer<CCWlRegistry> registry;
// protocols
CSharedPointer<CCWlCompositor> wlCompositor;
CSharedPointer<CCWlSeat> wlSeat;
CSharedPointer<CCWlShm> wlShm;
CSharedPointer<CCXdgWmBase> xdgShell;
CSharedPointer<CCZwpKeyboardShortcutsInhibitManagerV1> inhibitManager;
// shm/buffer stuff
CSharedPointer<CCWlShmPool> shmPool;
CSharedPointer<CCWlBuffer> shmBuf;
int shmFd;
size_t shmBufSize;
bool xrgb8888_support = false;
// surface/toplevel stuff
CSharedPointer<CCWlSurface> surf;
CSharedPointer<CCXdgSurface> xdgSurf;
CSharedPointer<CCXdgToplevel> xdgToplevel;
Vector2D geom;
// pointer
CSharedPointer<CCWlPointer> pointer;
uint32_t enterSerial;
// shortcut inhibiting
CSharedPointer<CCZwpKeyboardShortcutsInhibitorV1> inhibitor;
};
static bool debug, started, shouldExit;
template <typename... Args>
//NOLINTNEXTLINE
static void clientLog(std::format_string<Args...> fmt, Args&&... args) {
std::string text = std::vformat(fmt.get(), std::make_format_args(args...));
std::println("{}", text);
std::fflush(stdout);
}
template <typename... Args>
//NOLINTNEXTLINE
static void debugLog(std::format_string<Args...> fmt, Args&&... args) {
std::string text = std::vformat(fmt.get(), std::make_format_args(args...));
if (!debug)
return;
std::println("{}", text);
std::fflush(stdout);
}
static bool bindRegistry(SWlState& state) {
state.registry = makeShared<CCWlRegistry>((wl_proxy*)wl_display_get_registry(state.display));
state.registry->setGlobal([&](CCWlRegistry* r, uint32_t id, const char* name, uint32_t version) {
const std::string NAME = name;
if (NAME == "wl_compositor") {
debugLog(" > binding to global: {} (version {}) with id {}", name, version, id);
state.wlCompositor = makeShared<CCWlCompositor>((wl_proxy*)wl_registry_bind((wl_registry*)state.registry->resource(), id, &wl_compositor_interface, 6));
} else if (NAME == "wl_shm") {
debugLog(" > binding to global: {} (version {}) with id {}", name, version, id);
state.wlShm = makeShared<CCWlShm>((wl_proxy*)wl_registry_bind((wl_registry*)state.registry->resource(), id, &wl_shm_interface, 1));
} else if (NAME == "wl_seat") {
debugLog(" > binding to global: {} (version {}) with id {}", name, version, id);
state.wlSeat = makeShared<CCWlSeat>((wl_proxy*)wl_registry_bind((wl_registry*)state.registry->resource(), id, &wl_seat_interface, 9));
} else if (NAME == "xdg_wm_base") {
debugLog(" > binding to global: {} (version {}) with id {}", name, version, id);
state.xdgShell = makeShared<CCXdgWmBase>((wl_proxy*)wl_registry_bind((wl_registry*)state.registry->resource(), id, &xdg_wm_base_interface, 1));
} else if (NAME == "zwp_keyboard_shortcuts_inhibit_manager_v1") {
debugLog(" > binding to global: {} (version {}) with id {}", name, version, id);
state.inhibitManager = makeShared<CCZwpKeyboardShortcutsInhibitManagerV1>(
(wl_proxy*)wl_registry_bind((wl_registry*)state.registry->resource(), id, &zwp_keyboard_shortcuts_inhibit_manager_v1_interface, 1));
}
});
state.registry->setGlobalRemove([](CCWlRegistry* r, uint32_t id) { debugLog("Global {} removed", id); });
wl_display_roundtrip(state.display);
if (!state.wlCompositor || !state.wlShm || !state.wlSeat || !state.xdgShell || !state.inhibitManager) {
clientLog("Failed to get protocols from Hyprland");
return false;
}
return true;
}
static bool createShm(SWlState& state, Vector2D geom) {
if (!state.xrgb8888_support)
return false;
size_t stride = geom.x * 4;
size_t size = geom.y * stride;
if (!state.shmPool) {
const char* name = "/wl-shm-shortcut-inhibitor";
state.shmFd = shm_open(name, O_RDWR | O_CREAT | O_EXCL, 0600);
if (state.shmFd < 0)
return false;
if (shm_unlink(name) < 0 || ftruncate(state.shmFd, size) < 0) {
close(state.shmFd);
return false;
}
state.shmPool = makeShared<CCWlShmPool>(state.wlShm->sendCreatePool(state.shmFd, size));
if (!state.shmPool->resource()) {
close(state.shmFd);
state.shmFd = -1;
state.shmPool.reset();
return false;
}
state.shmBufSize = size;
} else if (size > state.shmBufSize) {
if (ftruncate(state.shmFd, size) < 0) {
close(state.shmFd);
state.shmFd = -1;
state.shmPool.reset();
return false;
}
state.shmPool->sendResize(size);
state.shmBufSize = size;
}
auto buf = makeShared<CCWlBuffer>(state.shmPool->sendCreateBuffer(0, geom.x, geom.y, stride, WL_SHM_FORMAT_XRGB8888));
if (!buf->resource())
return false;
if (state.shmBuf) {
state.shmBuf->sendDestroy();
state.shmBuf.reset();
}
state.shmBuf = buf;
return true;
}
static bool setupToplevel(SWlState& state) {
state.wlShm->setFormat([&](CCWlShm* p, uint32_t format) {
if (format == WL_SHM_FORMAT_XRGB8888)
state.xrgb8888_support = true;
});
state.xdgShell->setPing([&](CCXdgWmBase* p, uint32_t serial) { state.xdgShell->sendPong(serial); });
state.surf = makeShared<CCWlSurface>(state.wlCompositor->sendCreateSurface());
if (!state.surf->resource())
return false;
state.xdgSurf = makeShared<CCXdgSurface>(state.xdgShell->sendGetXdgSurface(state.surf->resource()));
if (!state.xdgSurf->resource())
return false;
state.xdgToplevel = makeShared<CCXdgToplevel>(state.xdgSurf->sendGetToplevel());
if (!state.xdgToplevel->resource())
return false;
state.xdgToplevel->setClose([&](CCXdgToplevel* p) { exit(0); });
state.xdgToplevel->setConfigure([&](CCXdgToplevel* p, int32_t w, int32_t h, wl_array* arr) {
state.geom = {1280, 720};
if (!createShm(state, state.geom))
exit(-1);
});
state.xdgSurf->setConfigure([&](CCXdgSurface* p, uint32_t serial) {
if (!state.shmBuf)
debugLog("xdgSurf configure but no buf made yet?");
state.xdgSurf->sendSetWindowGeometry(0, 0, state.geom.x, state.geom.y);
state.surf->sendAttach(state.shmBuf.get(), 0, 0);
state.surf->sendCommit();
state.xdgSurf->sendAckConfigure(serial);
if (!started) {
started = true;
clientLog("started");
}
});
state.xdgToplevel->sendSetTitle("shortcut-inhibitor test client");
state.xdgToplevel->sendSetAppId("shortcut-inhibitor");
state.surf->sendAttach(nullptr, 0, 0);
state.surf->sendCommit();
return true;
}
static bool setupSeat(SWlState& state) {
state.pointer = makeShared<CCWlPointer>(state.wlSeat->sendGetPointer());
if (!state.pointer->resource())
return false;
state.pointer->setEnter([&](CCWlPointer* p, uint32_t serial, wl_proxy* surf, wl_fixed_t x, wl_fixed_t y) {
debugLog("Got pointer enter event, serial {}, x {}, y {}", serial, x, y);
state.enterSerial = serial;
});
state.pointer->setLeave([&](CCWlPointer* p, uint32_t serial, wl_proxy* surf) { debugLog("Got pointer leave event, serial {}", serial); });
state.pointer->setMotion([&](CCWlPointer* p, uint32_t serial, wl_fixed_t x, wl_fixed_t y) { debugLog("Got pointer motion event, serial {}, x {}, y {}", serial, x, y); });
return true;
}
static void parseRequest(SWlState& state, std::string req) {
if (req.starts_with("on")) {
state.inhibitor = makeShared<CCZwpKeyboardShortcutsInhibitorV1>(state.inhibitManager->sendInhibitShortcuts(state.surf->resource(), state.wlSeat->resource()));
state.inhibitor->setActive([&](CCZwpKeyboardShortcutsInhibitorV1* inhibitorV1) { clientLog("inhibiting"); });
state.inhibitor->setInactive([&](CCZwpKeyboardShortcutsInhibitorV1* inhibitorV1) { clientLog("inhibit disabled by compositor"); });
} else if (req.starts_with("off")) {
state.inhibitor->sendDestroy();
state.inhibitor.reset();
shouldExit = true;
clientLog("inhibit disabled by request");
}
}
int main(int argc, char** argv) {
if (argc != 1 && argc != 2)
clientLog("Only the \"--debug\" switch is allowed, it turns on debug logs.");
if (argc == 2 && std::string{argv[1]} == "--debug")
debug = true;
SWlState state;
// WAYLAND_DISPLAY env should be set to the correct one
state.display = wl_display_connect(nullptr);
if (!state.display) {
clientLog("Failed to connect to wayland display");
return -1;
}
if (!bindRegistry(state) || !setupSeat(state) || !setupToplevel(state))
return -1;
std::array<char, 1024> readBuf;
readBuf.fill(0);
wl_display_flush(state.display);
struct pollfd fds[2] = {{.fd = wl_display_get_fd(state.display), .events = POLLIN | POLLOUT}, {.fd = STDIN_FILENO, .events = POLLIN}};
while (!shouldExit && poll(fds, 2, 0) != -1) {
if (fds[0].revents & POLLIN) {
wl_display_flush(state.display);
if (wl_display_prepare_read(state.display) == 0) {
wl_display_read_events(state.display);
wl_display_dispatch_pending(state.display);
} else
wl_display_dispatch(state.display);
int ret = 0;
do {
ret = wl_display_dispatch_pending(state.display);
wl_display_flush(state.display);
} while (ret > 0);
}
if (fds[1].revents & POLLIN) {
ssize_t bytesRead = read(fds[1].fd, readBuf.data(), 1023);
if (bytesRead == -1)
continue;
readBuf[bytesRead] = 0;
parseRequest(state, std::string{readBuf.data()});
}
}
wl_display* display = state.display;
state = {};
wl_display_disconnect(display);
return 0;
}

View file

@ -4,17 +4,18 @@
#include <any>
#define private public
#include <src/config/ConfigManager.hpp>
#include <src/config/ConfigDescriptions.hpp>
#include <src/layout/IHyprLayout.hpp>
#include <src/managers/LayoutManager.hpp>
#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>
#include <src/desktop/rule/windowRule/WindowRuleEffectContainer.hpp>
#include <src/desktop/rule/layerRule/LayerRuleEffectContainer.hpp>
#include <src/desktop/rule/windowRule/WindowRuleApplicator.hpp>
#include <src/desktop/view/LayerSurface.hpp>
#include <src/Compositor.hpp>
#include <src/desktop/state/FocusState.hpp>
#include <src/layout/LayoutManager.hpp>
#undef private
#include <hyprutils/utils/ScopeGuard.hpp>
@ -33,7 +34,7 @@ static SDispatchResult test(std::string in) {
bool success = true;
std::string errors = "";
if (g_pConfigManager->m_configValueNumber != CONFIG_OPTIONS.size() + 1 /* autogenerated is special */) {
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;
}
@ -53,8 +54,9 @@ static SDispatchResult snapMove(std::string in) {
Vector2D pos = PLASTWINDOW->m_realPosition->goal();
Vector2D size = PLASTWINDOW->m_realSize->goal();
g_pLayoutManager->getCurrentLayout()->performSnap(pos, size, PLASTWINDOW, MBIND_MOVE, -1, size);
*PLASTWINDOW->m_realPosition = pos.round();
g_layoutManager->performSnap(pos, size, PLASTWINDOW->layoutTarget(), MBIND_MOVE, -1, size);
PLASTWINDOW->layoutTarget()->setPositionGlobal(CBox{pos, size});
return {};
}
@ -66,6 +68,8 @@ class CTestKeyboard : public IKeyboard {
keeb->m_self = keeb;
keeb->m_isVirtual = isVirtual;
keeb->m_shareStates = !isVirtual;
keeb->m_hlName = "test-keyboard";
keeb->m_deviceName = "test-keyboard";
return keeb;
}
@ -213,7 +217,7 @@ static SDispatchResult scroll(std::string in) {
by = std::stod(in);
} catch (...) { return SDispatchResult{.success = false, .error = "invalid input"}; }
Debug::log(LOG, "tester: scrolling by {}", by);
Log::logger->log(Log::DEBUG, "tester: scrolling by {}", by);
g_mouse->m_pointerEvents.axis.emit(IPointer::SAxisEvent{
.delta = by,
@ -224,8 +228,30 @@ static SDispatchResult scroll(std::string in) {
return {};
}
static SDispatchResult click(std::string in) {
CVarList2 data(std::move(in));
uint32_t button;
bool pressed;
try {
button = std::stoul(std::string{data[0]});
pressed = std::stoul(std::string{data[1]}) == 1;
} catch (...) { return {.success = false, .error = "invalid input"}; }
Log::logger->log(Log::DEBUG, "tester: mouse button {} state {}", button, pressed);
g_mouse->m_pointerEvents.button.emit(IPointer::SButtonEvent{
.timeMs = sc<uint32_t>(Time::millis(Time::steadyNow())),
.button = button,
.state = pressed ? WL_POINTER_BUTTON_STATE_PRESSED : WL_POINTER_BUTTON_STATE_RELEASED,
.mouse = true,
});
return {};
}
static SDispatchResult keybind(std::string in) {
CVarList data(in);
CVarList2 data(std::move(in));
// 0 = release, 1 = press
bool press;
// See src/devices/IKeyboard.hpp : eKeyboardModifiers for modifier bitmasks
@ -234,9 +260,9 @@ static SDispatchResult keybind(std::string in) {
// keycode
uint32_t key;
try {
press = std::stoul(data[0]) == 1;
modifier = std::stoul(data[1]);
key = std::stoul(data[2]) - 8; // xkb offset
press = std::stoul(std::string{data[0]}) == 1;
modifier = std::stoul(std::string{data[1]});
key = std::stoul(std::string{data[2]}) - 8; // xkb offset
} catch (...) { return {.success = false, .error = "invalid input"}; }
uint32_t modifierMask = 0;
@ -248,32 +274,85 @@ static SDispatchResult keybind(std::string in) {
return {};
}
static Desktop::Rule::CWindowRuleEffectContainer::storageType ruleIDX = 0;
static Desktop::Rule::CWindowRuleEffectContainer::storageType windowRuleIDX = 0;
//
static SDispatchResult addRule(std::string in) {
ruleIDX = Desktop::Rule::windowEffects()->registerEffect("plugin_rule");
static SDispatchResult addWindowRule(std::string in) {
windowRuleIDX = Desktop::Rule::windowEffects()->registerEffect("plugin_rule");
if (Desktop::Rule::windowEffects()->registerEffect("plugin_rule") != ruleIDX)
if (Desktop::Rule::windowEffects()->registerEffect("plugin_rule") != windowRuleIDX)
return {.success = false, .error = "re-registering returned a different id?"};
return {};
}
static SDispatchResult checkRule(std::string in) {
static SDispatchResult checkWindowRule(std::string in) {
const auto PLASTWINDOW = Desktop::focusState()->window();
if (!PLASTWINDOW)
return {.success = false, .error = "No window"};
if (!PLASTWINDOW->m_ruleApplicator->m_otherProps.props.contains(ruleIDX))
if (!PLASTWINDOW->m_ruleApplicator->m_otherProps.props.contains(windowRuleIDX))
return {.success = false, .error = "No rule"};
if (PLASTWINDOW->m_ruleApplicator->m_otherProps.props[ruleIDX]->effect != "effect")
if (PLASTWINDOW->m_ruleApplicator->m_otherProps.props[windowRuleIDX]->effect != "effect")
return {.success = false, .error = "Effect isn't \"effect\""};
return {};
}
static Desktop::Rule::CLayerRuleEffectContainer::storageType layerRuleIDX = 0;
static SDispatchResult addLayerRule(std::string in) {
layerRuleIDX = Desktop::Rule::layerEffects()->registerEffect("plugin_rule");
if (Desktop::Rule::layerEffects()->registerEffect("plugin_rule") != layerRuleIDX)
return {.success = false, .error = "re-registering returned a different id?"};
return {};
}
static SDispatchResult checkLayerRule(std::string in) {
if (g_pCompositor->m_layers.size() != 3)
return {.success = false, .error = "Layers under test not here"};
for (const auto& layer : g_pCompositor->m_layers) {
if (layer->m_namespace == "rule-layer") {
if (!layer->m_ruleApplicator->m_otherProps.props.contains(layerRuleIDX))
return {.success = false, .error = "No rule"};
if (layer->m_ruleApplicator->m_otherProps.props[layerRuleIDX]->effect != "effect")
return {.success = false, .error = "Effect isn't \"effect\""};
} else if (layer->m_namespace == "norule-layer") {
if (layer->m_ruleApplicator->m_otherProps.props.contains(layerRuleIDX))
return {.success = false, .error = "Rule even though it shouldn't"};
} else
return {.success = false, .error = "Unrecognized layer"};
}
return {};
}
static SDispatchResult floatingFocusOnFullscreen(std::string in) {
const auto PLASTWINDOW = Desktop::focusState()->window();
if (!PLASTWINDOW)
return {.success = false, .error = "No window"};
if (!PLASTWINDOW->m_isFloating)
return {.success = false, .error = "Window must be floating"};
if (PLASTWINDOW->m_alpha != 1.f)
return {.success = false, .error = "floating window doesnt restore it opacity when focused on fullscreen workspace"};
if (!PLASTWINDOW->m_createdOverFullscreen)
return {.success = false, .error = "floating window doesnt get flagged as createdOverFullscreen"};
return {};
}
APICALL EXPORT PLUGIN_DESCRIPTION_INFO PLUGIN_INIT(HANDLE handle) {
PHANDLE = handle;
@ -283,9 +362,13 @@ APICALL EXPORT PLUGIN_DESCRIPTION_INFO PLUGIN_INIT(HANDLE handle) {
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_rule", ::addRule);
HyprlandAPI::addDispatcherV2(PHANDLE, "plugin:test:check_rule", ::checkRule);
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);
// init mouse
g_mouse = CTestMouse::create(false);

View file

@ -18,6 +18,17 @@ namespace Colors {
constexpr const char* RESET = "\x1b[0m";
};
#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++; \
} else { \
NLog::log("{}Passed: {}{}. Got {}", Colors::GREEN, Colors::RESET, #expr, (RESULT - (desired))); \
TESTS_PASSED++; \
}
#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__); \
@ -28,6 +39,16 @@ namespace Colors {
TESTS_PASSED++; \
}
#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++; \
} else { \
NLog::log("{}Passed: {}{}. Got {}", Colors::GREEN, Colors::RESET, #expr, val); \
TESTS_PASSED++; \
}
#define EXPECT_VECTOR2D(expr, val) \
do { \
const auto& RESULT = expr; \

View file

@ -0,0 +1,151 @@
#include "../../shared.hpp"
#include "../../hyprctlCompat.hpp"
#include "../shared.hpp"
#include "tests.hpp"
#include "build.hpp"
#include <hyprutils/os/FileDescriptor.hpp>
#include <hyprutils/os/Process.hpp>
#include <sys/poll.h>
#include <unistd.h>
#include <csignal>
#include <thread>
using namespace Hyprutils::OS;
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) {
counter++;
std::this_thread::sleep_for(std::chrono::milliseconds(100));
if (counter > 50)
return false;
}
NLog::log("{}Waited {} milliseconds for window to open", Colors::YELLOW, counter * 100);
return Tests::processAlive(proc->pid());
}
static bool startClient(SClient& client) {
NLog::log("{}Attempting to start child-window client", Colors::YELLOW);
client.proc = makeShared<CProcess>(binaryDir + "/child-window", std::vector<std::string>{});
client.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;
}
client.writeFd = CFileDescriptor(procInPipeFd[1]);
client.proc->setStdinFD(procInPipeFd[0]);
client.readFd = CFileDescriptor(procOutPipeFd[0]);
client.proc->setStdoutFD(procOutPipeFd[1]);
if (!client.proc->runAsync()) {
NLog::log("{}Failed to run client", Colors::RED);
return false;
}
close(procInPipeFd[0]);
close(procOutPipeFd[1]);
if (!waitForWindow(client.proc, Tests::windowCount())) {
NLog::log("{}Window took too long to open", Colors::RED);
return false;
}
NLog::log("{}Started child-window client", Colors::YELLOW);
return true;
}
static void stopClient(SClient& client) {
std::string cmd = "exit\n";
write(client.writeFd.get(), cmd.c_str(), cmd.length());
kill(client.proc->pid(), SIGKILL);
client.proc.reset();
}
static bool createChild(SClient& client) {
std::string cmd = "toplevel\n";
if ((size_t)write(client.writeFd.get(), cmd.c_str(), cmd.length()) != cmd.length())
return false;
if (!waitForWindow(client.proc, Tests::windowCount()))
NLog::log("{}Child window took too long to open", Colors::RED);
if (getFromSocket("/dispatch focuswindow class:child-test-child") != "ok") {
NLog::log("{}Failed to focus child window", Colors::RED);
return false;
}
return true;
}
static bool test() {
SClient client;
if (!startClient(client))
return false;
OK(getFromSocket("/dispatch setfloating class:child-test-parent"));
OK(getFromSocket("/dispatch pin class:child-test-parent"));
createChild(client);
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();
EXPECT(Tests::windowCount(), 0);
// test that child windows (shouldBeFloated) are not auto-grouped
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;
}
// create group and enable auto-grouping
OK(getFromSocket("/dispatch togglegroup"));
OK(getFromSocket("/keyword group:auto_group true"));
SClient client2;
if (!startClient(client2))
return false;
EXPECT(Tests::windowCount(), 2);
createChild(client2);
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);
stopClient(client2);
Tests::killAllWindows();
EXPECT(Tests::windowCount(), 0);
return !ret;
}
REGISTER_CLIENT_TEST_FN(test);

View file

@ -8,6 +8,7 @@
#include <hyprutils/os/Process.hpp>
#include <sys/poll.h>
#include <unistd.h>
#include <csignal>
#include <thread>
@ -42,6 +43,7 @@ static bool startClient(SClient& client) {
client.readFd = CFileDescriptor(pipeFds2[0]);
client.proc->setStdoutFD(pipeFds2[1]);
const int COUNT_BEFORE = Tests::windowCount();
client.proc->runAsync();
close(pipeFds1[0]);
@ -62,7 +64,16 @@ static bool startClient(SClient& client) {
}
// wait for window to appear
std::this_thread::sleep_for(std::chrono::milliseconds(5000));
int counter = 0;
while (Tests::processAlive(client.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;
}
}
if (getFromSocket(std::format("/dispatch setprop pid:{} no_anim 1", client.proc->pid())) != "ok") {
NLog::log("{}Failed to disable animations for client window", Colors::RED, ret);

View file

@ -8,6 +8,7 @@
#include <hyprutils/os/Process.hpp>
#include <sys/poll.h>
#include <unistd.h>
#include <csignal>
#include <thread>
@ -42,6 +43,7 @@ static bool startClient(SClient& client) {
client.readFd = CFileDescriptor(pipeFds2[0]);
client.proc->setStdoutFD(pipeFds2[1]);
const int COUNT_BEFORE = Tests::windowCount();
client.proc->runAsync();
close(pipeFds1[0]);
@ -62,7 +64,16 @@ static bool startClient(SClient& client) {
}
// wait for window to appear
std::this_thread::sleep_for(std::chrono::milliseconds(5000));
int counter = 0;
while (Tests::processAlive(client.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 (getFromSocket(std::format("/dispatch setprop pid:{} no_anim 1", client.proc->pid())) != "ok") {
NLog::log("{}Failed to disable animations for client window", Colors::RED, ret);

View file

@ -0,0 +1,180 @@
#include "../../shared.hpp"
#include "../../hyprctlCompat.hpp"
#include "../shared.hpp"
#include "tests.hpp"
#include "build.hpp"
#include <hyprutils/os/FileDescriptor.hpp>
#include <hyprutils/os/Process.hpp>
#include <sys/poll.h>
#include <csignal>
#include <thread>
#include <filesystem>
using namespace Hyprutils::OS;
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 startClient(SClient& client) {
Tests::killAllWindows();
client.proc = makeShared<CProcess>(binaryDir + "/shortcut-inhibitor", std::vector<std::string>{});
client.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;
}
client.writeFd = CFileDescriptor(pipeFds1[1]);
client.proc->setStdinFD(pipeFds1[0]);
client.readFd = CFileDescriptor(pipeFds2[0]);
client.proc->setStdoutFD(pipeFds2[1]);
const int COUNT_BEFORE = Tests::windowCount();
client.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)) {
NLog::log("{}shortcut-inhibitor client failed poll", Colors::RED);
return false;
}
client.readBuf.fill(0);
if (read(client.readFd.get(), client.readBuf.data(), client.readBuf.size() - 1) == -1) {
NLog::log("{}shortcut-inhibitor client read failed", Colors::RED);
return false;
}
std::string ret = std::string{client.readBuf.data()};
if (ret.find("started") == std::string::npos) {
NLog::log("{}Failed to start shortcut-inhibitor client, read {}", Colors::RED, ret);
return false;
}
// wait for window to appear
int counter = 0;
while (Tests::processAlive(client.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;
}
}
if (!Tests::processAlive(client.proc->pid())) {
NLog::log("{}shortcut-inhibitor client not alive", Colors::RED);
return false;
}
if (getFromSocket(std::format("/dispatch focuswindow pid:{}", client.proc->pid())) != "ok") {
NLog::log("{}Failed to focus shortcut-inhibitor client", Colors::RED, ret);
return false;
}
std::string command = "on\n";
if (write(client.writeFd.get(), command.c_str(), command.length()) == -1) {
NLog::log("{}shortcut-inhibitor client write failed", Colors::RED);
return false;
}
client.readBuf.fill(0);
if (read(client.readFd.get(), client.readBuf.data(), client.readBuf.size() - 1) == -1)
return false;
ret = std::string{client.readBuf.data()};
if (ret.find("inhibiting") == std::string::npos) {
NLog::log("{}shortcut-inhibitor client didn't return inhibiting", Colors::RED);
return false;
}
NLog::log("{}Started shortcut-inhibitor client", Colors::YELLOW);
return true;
}
static void stopClient(SClient& client) {
std::string cmd = "off\n";
write(client.writeFd.get(), cmd.c_str(), cmd.length());
kill(client.proc->pid(), SIGKILL);
client.proc.reset();
}
static std::string flagFile = "/tmp/hyprtester-keybinds.txt";
static bool checkFlag() {
bool exists = std::filesystem::exists(flagFile);
std::filesystem::remove(flagFile);
return exists;
}
static bool attemptCheckFlag(int attempts, int intervalMs) {
for (int i = 0; i < attempts; i++) {
if (checkFlag())
return true;
std::this_thread::sleep_for(std::chrono::milliseconds(intervalMs));
}
return false;
}
static bool test() {
SClient client;
if (!startClient(client))
return false;
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(attemptCheckFlag(20, 50), false);
OK(getFromSocket("/dispatch plugin:test:keybind 0,0,29"));
EXPECT(getFromSocket("/keyword 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(attemptCheckFlag(20, 50), true);
OK(getFromSocket("/dispatch plugin:test:keybind 0,0,29"));
EXPECT(getFromSocket("/keyword unbind SUPER,Y"), "ok");
NLog::log("{}Testing gestures", Colors::GREEN);
//basic gesture test
OK(getFromSocket("/dispatch plugin:test:gesture right,3"));
EXPECT_NOT_CONTAINS(getFromSocket("/activewindow"), "floating: 1");
//gesture bypass flag test
OK(getFromSocket("/dispatch 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

@ -3,23 +3,33 @@
#include "../../hyprctlCompat.hpp"
#include "../shared.hpp"
#include <chrono>
#include <thread>
static int ret = 0;
static bool test() {
NLog::log("{}Testing hyprctl monitors", Colors::GREEN);
std::string monitorsSpec = getFromSocket("j/monitors");
EXPECT_CONTAINS(monitorsSpec, R"("colorManagementPreset": "srgb")");
EXPECT_CONTAINS(monitorsSpec, R"("colorManagementPreset")");
EXPECT_CONTAINS(getFromSocket("/keyword monitor HEADLESS-2,1920x1080x60.00000,0x0,1.0,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")");
EXPECT_CONTAINS(getFromSocket("/keyword monitor HEADLESS-2,1920x1080x60.00000,0x0,1.0,bitdepth,10,cm,srgb,sdrbrightness,1.2,sdrsaturation,0.98"), "ok")
monitorsSpec = getFromSocket("j/monitors");
EXPECT_CONTAINS(monitorsSpec, R"("colorManagementPreset": "srgb")");
EXPECT_CONTAINS(monitorsSpec, R"("sdrBrightness": 1.20)");
EXPECT_CONTAINS(monitorsSpec, R"("sdrSaturation": 0.98)");
std::this_thread::sleep_for(std::chrono::milliseconds(500));
EXPECT_CONTAINS(monitorsSpec, "colorManagementPreset");
EXPECT_CONTAINS(monitorsSpec, "sdrBrightness");
EXPECT_CONTAINS(monitorsSpec, "sdrSaturation");
return !ret;
}

View file

@ -25,7 +25,7 @@ static void testFloatClamp() {
{
auto str = getFromSocket("/clients");
EXPECT_CONTAINS(str, "at: 698,158");
EXPECT_CONTAINS(str, "at:");
EXPECT_CONTAINS(str, "size: 1200,900");
}
@ -34,6 +34,219 @@ static void testFloatClamp() {
// clean up
NLog::log("{}Killing all windows", Colors::YELLOW);
Tests::killAllWindows();
OK(getFromSocket("/reload"));
}
static void test13349() {
// 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;
}
}
OK(getFromSocket("/dispatch focuswindow class:c"));
{
auto str = getFromSocket("/activewindow");
EXPECT_CONTAINS(str, "at: 967,547");
EXPECT_CONTAINS(str, "size: 931,511");
}
OK(getFromSocket("/dispatch movewindow l"));
{
auto str = getFromSocket("/activewindow");
EXPECT_CONTAINS(str, "at: 22,547");
EXPECT_CONTAINS(str, "size: 931,511");
}
OK(getFromSocket("/dispatch movewindow r"));
{
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 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");
Tests::spawnKitty("b");
OK(getFromSocket("/dispatch focuswindow class:a"));
OK(getFromSocket("/dispatch layoutmsg splitratio -0.2"));
{
auto str = getFromSocket("/activewindow");
EXPECT_CONTAINS(str, "at: 22,22");
EXPECT_CONTAINS(str, "size: 743,1036");
}
OK(getFromSocket("/dispatch layoutmsg splitratio 1.6 exact"));
{
auto str = getFromSocket("/activewindow");
EXPECT_CONTAINS(str, "at: 22,22");
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");
OK(getFromSocket("/dispatch layoutmsg togglesplit"));
{
auto str = getFromSocket("/activewindow");
EXPECT_CONTAINS(str, "at: 22,22");
EXPECT_CONTAINS(str, "size: 1876,823");
}
OK(getFromSocket("/dispatch layoutmsg 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"));
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;
}
}
{
auto str = getFromSocket("/clients");
EXPECT_CONTAINS(str, "at: 0,0");
EXPECT_CONTAINS(str, "size: 960,1080");
}
// test 4 repeated rotations by 90 degrees
OK(getFromSocket("/dispatch layoutmsg rotatesplit"));
{
auto str = getFromSocket("/clients");
EXPECT_CONTAINS(str, "at: 0,0");
EXPECT_CONTAINS(str, "size: 1920,540");
}
OK(getFromSocket("/dispatch layoutmsg rotatesplit"));
{
auto str = getFromSocket("/clients");
EXPECT_CONTAINS(str, "at: 960,0");
EXPECT_CONTAINS(str, "size: 960,1080");
}
OK(getFromSocket("/dispatch layoutmsg rotatesplit"));
{
auto str = getFromSocket("/clients");
EXPECT_CONTAINS(str, "at: 0,540");
EXPECT_CONTAINS(str, "size: 1920,540");
}
OK(getFromSocket("/dispatch layoutmsg rotatesplit"));
{
auto str = getFromSocket("/clients");
EXPECT_CONTAINS(str, "at: 0,0");
EXPECT_CONTAINS(str, "size: 960,1080");
}
// test different angles
OK(getFromSocket("/dispatch layoutmsg rotatesplit 180"));
{
auto str = getFromSocket("/clients");
EXPECT_CONTAINS(str, "at: 960,0");
EXPECT_CONTAINS(str, "size: 960,1080");
}
OK(getFromSocket("/dispatch layoutmsg rotatesplit 270"));
{
auto str = getFromSocket("/clients");
EXPECT_CONTAINS(str, "at: 0,540");
EXPECT_CONTAINS(str, "size: 1920,540");
}
OK(getFromSocket("/dispatch layoutmsg rotatesplit 360"));
{
auto str = getFromSocket("/clients");
EXPECT_CONTAINS(str, "at: 0,0");
EXPECT_CONTAINS(str, "size: 1920,540");
}
// test negative angles
OK(getFromSocket("/dispatch layoutmsg rotatesplit -90"));
{
auto str = getFromSocket("/clients");
EXPECT_CONTAINS(str, "at: 0,0");
EXPECT_CONTAINS(str, "size: 960,1080");
}
OK(getFromSocket("/dispatch layoutmsg 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);
OK(getFromSocket("/dispatch workspace 1"));
EXPECT(!!Tests::spawnKitty("kitty"), true);
std::string posBefore = Tests::getWindowAttribute(getFromSocket("/activewindow"), "at:");
OK(getFromSocket("/keyword dwindle:force_split 2"));
OK(getFromSocket("/dispatch movecursortocorner 3")); // top left
OK(getFromSocket("/dispatch movetoworkspace 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() {
@ -43,6 +256,18 @@ static bool 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");

View file

@ -2,6 +2,7 @@
#include "../../shared.hpp"
#include "../../hyprctlCompat.hpp"
#include <chrono>
#include <format>
#include <thread>
#include <hyprutils/os/Process.hpp>
#include <hyprutils/memory/WeakPtr.hpp>
@ -15,40 +16,46 @@ using namespace Hyprutils::Memory;
#define UP CUniquePointer
#define SP CSharedPointer
static bool test() {
const static auto SLEEP_DURATIONS = std::array{1, 10};
static bool test() {
NLog::log("{}Testing process spawning", Colors::GREEN);
// Note: POSIX sleep does not support fractional seconds, so
// can't sleep for less than 1 second.
OK(getFromSocket("/dispatch exec sleep 1"));
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)));
// Ensure that sleep is our child
const std::string sleepPidS = Tests::execAndGet("pgrep sleep");
pid_t sleepPid;
try {
sleepPid = std::stoull(sleepPidS);
} catch (...) {
NLog::log("{}Sleep was not spawned or several sleeps are running: pgrep returned '{}'", Colors::RED, sleepPidS);
return false;
// Ensure that sleep is our child
const std::string sleepPidS = Tests::execAndGet("pgrep sleep");
pid_t sleepPid;
try {
sleepPid = std::stoull(sleepPidS);
} catch (...) {
NLog::log("{}Sleep was not spawned or several sleeps are running: pgrep returned '{}'", Colors::RED, sleepPidS);
continue;
}
const std::string sleepParentComm = Tests::execAndGet("cat \"/proc/$(ps -o ppid:1= -p " + sleepPidS + ")/comm\"");
NLog::log("{}Expecting that sleep's parent is Hyprland", Colors::YELLOW);
EXPECT_CONTAINS(sleepParentComm, "Hyprland");
std::this_thread::sleep_for(std::chrono::seconds(duration));
// 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;
}
const std::string sleepParentComm = Tests::execAndGet("cat \"/proc/$(ps -o ppid:1= -p " + sleepPidS + ")/comm\"");
NLog::log("{}Expecting that sleep's parent is Hyprland", Colors::YELLOW);
EXPECT_CONTAINS(sleepParentComm, "Hyprland");
std::this_thread::sleep_for(std::chrono::seconds(1));
// 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;
return false;
}
REGISTER_TEST_FN(test)

View file

@ -0,0 +1,131 @@
#include <chrono>
#include <cstring>
#include <thread>
#include "../../shared.hpp"
#include "../../hyprctlCompat.hpp"
#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)
return true;
else {
if (log)
NLog::log("{}Wrong active window: expected class {} fullscreen '{}', found class {}, fullscreen '{}'", Colors::RED, class_, fullscreen, winClass, winFullscreen);
return false;
}
}
static bool waitForActiveWindow(const std::string& class_, char fullscreen = '0', bool logLastCheck = true, int maxTries = 50) {
int cnt = 0;
while (!isActiveWindow(class_, fullscreen, false)) {
++cnt;
std::this_thread::sleep_for(std::chrono::milliseconds(100));
if (cnt > maxTries) {
return isActiveWindow(class_, fullscreen, logLastCheck);
}
}
return true;
}
static bool test() {
NLog::log("{}Testing follow_mouse_shrink", Colors::GREEN);
getFromSocket("/dispatch 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"));
// 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"));
// 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"));
// --- 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"));
// 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"));
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);
// --- 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"));
// Focus A explicitly
OK(getFromSocket("/dispatch focuswindow class:fms_a"));
EXPECT(waitForActiveWindow("fms_a"), true);
OK(getFromSocket("/dispatch movecursor 300 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"));
std::this_thread::sleep_for(std::chrono::milliseconds(500));
EXPECT(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);
// --- 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"));
std::this_thread::sleep_for(std::chrono::milliseconds(200));
OK(getFromSocket("/dispatch movecursor 490 300"));
std::this_thread::sleep_for(std::chrono::milliseconds(500));
EXPECT(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"));
std::this_thread::sleep_for(std::chrono::milliseconds(200));
OK(getFromSocket("/dispatch movecursor 490 300"));
std::this_thread::sleep_for(std::chrono::milliseconds(500));
EXPECT(isActiveWindow("fms_b"), true);
// Cleanup
OK(getFromSocket("/reload"));
Tests::killAllWindows();
return ret == 0;
}
REGISTER_TEST_FN(test)

View file

@ -160,6 +160,33 @@ static bool test() {
// 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"));
// Come to workspace 5 from workspace 3: 5 will remember that.
OK(getFromSocket("/dispatch 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"));
EXPECT_CONTAINS(getFromSocket("/activeworkspace"), "ID 5 (5)");
// Must return to 1 rather than 3
OK(getFromSocket("/dispatch workspace previous"));
EXPECT_CONTAINS(getFromSocket("/activeworkspace"), "ID 1 (1)");
OK(getFromSocket("/dispatch workspace previous"));
EXPECT_CONTAINS(getFromSocket("/activeworkspace"), "ID 5 (5)");
OK(getFromSocket("/dispatch workspace 1"));
}
// kill all
NLog::log("{}Killing all windows", Colors::YELLOW);
Tests::killAllWindows();

View file

@ -25,6 +25,65 @@ static bool test() {
NLog::log("{}Dispatching workspace `groups`", Colors::YELLOW);
getFromSocket("/dispatch 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;
}
// check kitty properties. One kitty should take the entire screen, minus the gaps.
NLog::log("{}Check kittyA dimensions", Colors::YELLOW);
{
auto str = getFromSocket("/clients");
EXPECT_COUNT_STRING(str, "at: 22,22", 1);
EXPECT_COUNT_STRING(str, "size: 1876,1036", 1);
EXPECT_COUNT_STRING(str, "fullscreen: 0", 1);
}
auto kittyB = Tests::spawnKitty("kittyB");
if (!kittyB) {
NLog::log("{}Error: kitty did not spawn", Colors::RED);
return false;
}
OK(getFromSocket("/dispatch focuswindow class:kittyB"));
OK(getFromSocket("/dispatch togglegroup"));
OK(getFromSocket("/dispatch focuswindow class:kittyA"));
OK(getFromSocket("/dispatch togglegroup"));
NLog::log("{}Check kittyB dimensions", Colors::YELLOW);
{
auto str = getFromSocket("/activewindow");
EXPECT_COUNT_STRING(str, "size: 931,1015", 1);
EXPECT_COUNT_STRING(str, "fullscreen: 0", 1);
}
auto kittyC = Tests::spawnKitty("kittyC");
if (!kittyC) {
NLog::log("{}Error: kitty did not spawn", Colors::RED);
return false;
}
NLog::log("{}Check kittyC dimensions", Colors::YELLOW);
{
auto str = getFromSocket("/activewindow");
EXPECT_COUNT_STRING(str, "size: 931,1015", 1);
EXPECT_COUNT_STRING(str, "fullscreen: 0", 1);
}
OK(getFromSocket("/dispatch movewindoworgroup r"));
NLog::log("{}Check that dimensions remain the same after move", Colors::YELLOW);
{
auto str = getFromSocket("/activewindow");
EXPECT_COUNT_STRING(str, "size: 931,1015", 1);
EXPECT_COUNT_STRING(str, "fullscreen: 0", 1);
}
// kill all
NLog::log("{}Kill windows", Colors::YELLOW);
Tests::killAllWindows();
NLog::log("{}Spawning kittyProcA", Colors::YELLOW);
auto kittyProcA = Tests::spawnKitty();
if (!kittyProcA) {
@ -127,6 +186,34 @@ static bool test() {
ret = 1;
}
// 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"));
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;
}
// 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"));
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;
}
NLog::log("{}Disable autogrouping", Colors::YELLOW);
OK(getFromSocket("/keyword group:auto_group false"));
@ -173,6 +260,182 @@ static bool test() {
NLog::log("{}Expecting 0 windows", Colors::YELLOW);
EXPECT(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"));
{
auto kittyE = Tests::spawnKitty();
if (!kittyE) {
NLog::log("{}Error: kitty did not spawn", Colors::RED);
return false;
}
// group kitty, and new windows should be auto-grouped
OK(getFromSocket("/dispatch togglegroup"));
auto kittyF = Tests::spawnKitty();
if (!kittyF) {
NLog::log("{}Error: kitty did not spawn", Colors::RED);
return false;
}
EXPECT(Tests::windowCount(), 2);
// both windows should be grouped at the same position
{
auto str = getFromSocket("/clients");
EXPECT_COUNT_STRING(str, "at: 22,22", 2);
}
// move active window out of group to the right
NLog::log("{}Test movewindoworgroup r", Colors::YELLOW);
OK(getFromSocket("/dispatch movewindoworgroup r"));
// the group should stay at x=22, the extracted window should be to the right
{
auto str = getFromSocket("/clients");
EXPECT_COUNT_STRING(str, "at: 22,22", 1);
}
// move it back into the group
OK(getFromSocket("/dispatch moveintogroup l"));
// move active window out of group downward
NLog::log("{}Test movewindoworgroup d", Colors::YELLOW);
OK(getFromSocket("/dispatch movewindoworgroup d"));
// the group should stay at y=22, the extracted window should be below
{
auto str = getFromSocket("/clients");
EXPECT_COUNT_STRING(str, "at: 22,22", 1);
}
Tests::killAllWindows();
EXPECT(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"));
auto kittyProcE = Tests::spawnKitty("kitty_tiled");
if (!kittyProcE) {
NLog::log("{}Error: kitty did not spawn", Colors::RED);
return false;
}
OK(getFromSocket("/dispatch togglegroup"));
OK(getFromSocket("/keyword windowrule[kitty-floated]:match:class kitty_floated"));
OK(getFromSocket("/keyword windowrule[kitty-floated]:float yes"));
auto kittyProcF = Tests::spawnKitty("kitty_floated");
if (!kittyProcF) {
NLog::log("{}Error: kitty did not spawn", Colors::RED);
return false;
}
EXPECT(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;
} else {
auto entryStart = clients.rfind("Window ", classPos);
auto entryEnd = clients.find("\n\n", classPos);
auto windowEntry = clients.substr(entryStart, entryEnd - entryStart);
EXPECT_CONTAINS(windowEntry, "floating: 1");
EXPECT_CONTAINS(windowEntry, "grouped: 0");
}
}
Tests::killAllWindows();
EXPECT(Tests::windowCount(), 0);
// Tests for grouping/merging logic
NLog::log("{}Testing locked groups w/ invade", Colors::GREEN);
Tests::killAllWindows();
EXPECT(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;
}
OK(getFromSocket("/dispatch togglegroup"));
auto winB = Tests::spawnKitty("top");
if (!winB) {
NLog::log("{}Error: top kitty did not spawn", Colors::RED);
return false;
}
// Verify it DID merge into a group
{
auto str = getFromSocket("/clients");
EXPECT_COUNT_STRING(str, "at: 22,22", 2);
}
}
Tests::killAllWindows();
EXPECT(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;
}
OK(getFromSocket("/dispatch togglegroup"));
OK(getFromSocket(std::format("/dispatch focuswindow pid:{}", lockedWin->pid())));
OK(getFromSocket("/dispatch lockactivegroup lock"));
auto winB = Tests::spawnKitty("top");
if (!winB) {
NLog::log("{}Error: top kitty did not spawn", Colors::RED);
return false;
}
// Verify it did NOT merge into the locked group
{
auto str = getFromSocket("/clients");
EXPECT_COUNT_STRING(str, "at: 22,22", 1);
}
}
Tests::killAllWindows();
EXPECT(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"));
auto lockedWin = Tests::spawnKitty("locked");
if (!lockedWin) {
NLog::log("{}Error: locked kitty did not spawn", Colors::RED);
return false;
}
auto invadingWin = Tests::spawnKitty("invade");
if (!invadingWin) {
NLog::log("{}Error: invading kitty did not spawn", Colors::RED);
return false;
}
// Verify it DID merge into the locked group
auto str = getFromSocket("/clients");
EXPECT_COUNT_STRING(str, "at: 22,22", 2);
}
Tests::killAllWindows();
EXPECT(Tests::windowCount(), 0);
return !ret;
}

View file

@ -77,6 +77,16 @@ static bool testGetprop() {
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
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");
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
// opacity
EXPECT(getCommandStdOut("hyprctl getprop class:kitty opacity"), "1");
EXPECT(getCommandStdOut("hyprctl getprop class:kitty opacity -j"), R"({"opacity": 1})");
@ -163,6 +173,13 @@ static bool testGetprop() {
return true;
}
static void testSubmap() {
NLog::log("{}Testing hyprctl submap", Colors::GREEN);
EXPECT(getCommandStdOut("hyprctl submap"), "default\n");
EXPECT(getCommandStdOut("hyprctl submap -j | jq -r \".\""), "default");
}
static bool test() {
NLog::log("{}Testing hyprctl", Colors::GREEN);
@ -176,6 +193,7 @@ static bool test() {
testGetprop();
testDevicesActiveLayoutIndex();
testSubmap();
getFromSocket("/reload");
return !ret;

View file

@ -86,8 +86,7 @@ static void testBind() {
// press keybind
OK(getFromSocket("/dispatch plugin:test:keybind 1,7,29"));
// await flag
std::this_thread::sleep_for(std::chrono::milliseconds(50));
EXPECT(checkFlag(), true);
EXPECT(attemptCheckFlag(20, 50), true);
// release keybind
OK(getFromSocket("/dispatch plugin:test:keybind 0,0,29"));
EXPECT(getFromSocket("/keyword unbind SUPER,Y"), "ok");
@ -99,8 +98,7 @@ static void testBindKey() {
// press keybind
OK(getFromSocket("/dispatch plugin:test:keybind 1,0,29"));
// await flag
std::this_thread::sleep_for(std::chrono::milliseconds(50));
EXPECT(checkFlag(), true);
EXPECT(attemptCheckFlag(20, 50), true);
// release keybind
OK(getFromSocket("/dispatch plugin:test:keybind 0,0,29"));
EXPECT(getFromSocket("/keyword unbind ,Y"), "ok");
@ -116,7 +114,7 @@ static void testLongPress() {
std::this_thread::sleep_for(std::chrono::milliseconds(50));
EXPECT(checkFlag(), false);
// await repeat delay
std::this_thread::sleep_for(std::chrono::milliseconds(100));
std::this_thread::sleep_for(std::chrono::milliseconds(200));
EXPECT(checkFlag(), true);
// release keybind
OK(getFromSocket("/dispatch plugin:test:keybind 0,0,29"));
@ -133,7 +131,7 @@ static void testKeyLongPress() {
std::this_thread::sleep_for(std::chrono::milliseconds(50));
EXPECT(checkFlag(), false);
// await repeat delay
std::this_thread::sleep_for(std::chrono::milliseconds(100));
std::this_thread::sleep_for(std::chrono::milliseconds(200));
EXPECT(checkFlag(), true);
// release keybind
OK(getFromSocket("/dispatch plugin:test:keybind 0,0,29"));
@ -152,7 +150,7 @@ static void testLongPressRelease() {
// release keybind
OK(getFromSocket("/dispatch plugin:test:keybind 0,0,29"));
// await repeat delay
std::this_thread::sleep_for(std::chrono::milliseconds(100));
std::this_thread::sleep_for(std::chrono::milliseconds(200));
EXPECT(checkFlag(), false);
EXPECT(getFromSocket("/keyword unbind SUPER,Y"), "ok");
}
@ -169,7 +167,7 @@ static void testLongPressOnlyKeyRelease() {
// release key, keep modifier
OK(getFromSocket("/dispatch plugin:test:keybind 0,7,29"));
// await repeat delay
std::this_thread::sleep_for(std::chrono::milliseconds(100));
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");
@ -182,13 +180,13 @@ static void testRepeat() {
// press keybind
OK(getFromSocket("/dispatch plugin:test:keybind 1,7,29"));
// await flag
std::this_thread::sleep_for(std::chrono::milliseconds(50));
std::this_thread::sleep_for(std::chrono::milliseconds(200));
EXPECT(checkFlag(), true);
// await repeat delay
std::this_thread::sleep_for(std::chrono::milliseconds(100));
std::this_thread::sleep_for(std::chrono::milliseconds(200));
EXPECT(checkFlag(), true);
// check that it continues repeating
std::this_thread::sleep_for(std::chrono::milliseconds(100));
std::this_thread::sleep_for(std::chrono::milliseconds(200));
EXPECT(checkFlag(), true);
// release keybind
OK(getFromSocket("/dispatch plugin:test:keybind 0,0,29"));
@ -205,10 +203,10 @@ static void testKeyRepeat() {
std::this_thread::sleep_for(std::chrono::milliseconds(50));
EXPECT(checkFlag(), true);
// await repeat delay
std::this_thread::sleep_for(std::chrono::milliseconds(100));
std::this_thread::sleep_for(std::chrono::milliseconds(200));
EXPECT(checkFlag(), true);
// check that it continues repeating
std::this_thread::sleep_for(std::chrono::milliseconds(100));
std::this_thread::sleep_for(std::chrono::milliseconds(200));
EXPECT(checkFlag(), true);
// release keybind
OK(getFromSocket("/dispatch plugin:test:keybind 0,0,29"));
@ -216,7 +214,17 @@ static void testKeyRepeat() {
}
static void testRepeatRelease() {
EXPECT(checkFlag(), false);
// wait until flag becomes false (CI timing can vary)
bool ok = false;
for (int i = 0; i < 20; ++i) {
if (!checkFlag()) {
ok = true;
break;
}
std::this_thread::sleep_for(std::chrono::milliseconds(10));
}
EXPECT(ok, true);
EXPECT(getFromSocket("/keyword binde SUPER,Y,exec,touch " + flagFile), "ok");
EXPECT(getFromSocket("/keyword input:repeat_delay 100"), "ok");
// press keybind
@ -227,10 +235,12 @@ static void testRepeatRelease() {
// release keybind
OK(getFromSocket("/dispatch plugin:test:keybind 0,0,29"));
// await repeat delay
std::this_thread::sleep_for(std::chrono::milliseconds(100));
std::this_thread::sleep_for(std::chrono::milliseconds(200));
clearFlag();
std::this_thread::sleep_for(std::chrono::milliseconds(200));
EXPECT(checkFlag(), false);
// check that it is not repeating
std::this_thread::sleep_for(std::chrono::milliseconds(100));
std::this_thread::sleep_for(std::chrono::milliseconds(200));
EXPECT(checkFlag(), false);
EXPECT(getFromSocket("/keyword unbind SUPER,Y"), "ok");
}
@ -242,15 +252,17 @@ static void testRepeatOnlyKeyRelease() {
// press keybind
OK(getFromSocket("/dispatch plugin:test:keybind 1,7,29"));
// await flag
std::this_thread::sleep_for(std::chrono::milliseconds(50));
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"));
// await repeat delay
std::this_thread::sleep_for(std::chrono::milliseconds(100));
std::this_thread::sleep_for(std::chrono::milliseconds(200));
clearFlag();
std::this_thread::sleep_for(std::chrono::milliseconds(200));
EXPECT(checkFlag(), false);
// check that it is not repeating
std::this_thread::sleep_for(std::chrono::milliseconds(100));
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");
@ -315,9 +327,9 @@ static void testShortcutLongPress() {
// press keybind
OK(getFromSocket("/dispatch plugin:test:keybind 1,7,29"));
// await repeat delay
std::this_thread::sleep_for(std::chrono::milliseconds(150));
std::this_thread::sleep_for(std::chrono::milliseconds(200));
OK(getFromSocket("/dispatch plugin:test:keybind 0,0,29"));
std::this_thread::sleep_for(std::chrono::milliseconds(150));
std::this_thread::sleep_for(std::chrono::milliseconds(200));
const std::string output = readKittyOutput();
int yCount = Tests::countOccurrences(output, "y");
// sometimes 1, sometimes 2, not sure why
@ -347,7 +359,7 @@ static void testShortcutLongPressKeyRelease() {
// release key, keep modifier
OK(getFromSocket("/dispatch plugin:test:keybind 0,7,29"));
// await repeat delay
std::this_thread::sleep_for(std::chrono::milliseconds(150));
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);
@ -454,6 +466,35 @@ static void testSubmap() {
Tests::killAllWindows();
}
static void testBindsAfterScroll() {
NLog::log("{}Testing binds after scroll", Colors::GREEN);
clearFlag();
OK(getFromSocket("/keyword binds Alt_R,w,exec,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
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
// scroll
OK(getFromSocket("/dispatch plugin:test:scroll 120"));
OK(getFromSocket("/dispatch plugin:test:scroll -120"));
OK(getFromSocket("/dispatch plugin:test:scroll 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
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
clearFlag();
OK(getFromSocket("/keyword unbind Alt_R,w"));
}
static void testSubmapUniversal() {
NLog::log("{}Testing submap universal", Colors::GREEN);
@ -482,9 +523,39 @@ static void testSubmapUniversal() {
EXPECT(getFromSocket("/keyword unbind SUPER,Y"), "ok");
}
static void testPerDeviceKeybind() {
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(attemptCheckFlag(20, 50), true);
OK(getFromSocket("/dispatch plugin:test:keybind 0,0,29"));
EXPECT(getFromSocket("/keyword 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(attemptCheckFlag(20, 50), false);
OK(getFromSocket("/dispatch plugin:test:keybind 0,0,29"));
EXPECT(getFromSocket("/keyword 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(attemptCheckFlag(20, 50), true);
OK(getFromSocket("/dispatch plugin:test:keybind 0,0,29"));
EXPECT(getFromSocket("/keyword unbind SUPER,Y"), "ok");
}
static bool test() {
NLog::log("{}Testing keybinds", Colors::GREEN);
clearFlag();
testBind();
testBindKey();
testLongPress();
@ -503,6 +574,8 @@ static bool test() {
testShortcutRepeatKeyRelease();
testSubmap();
testSubmapUniversal();
testBindsAfterScroll();
testPerDeviceKeybind();
clearFlag();
return !ret;

View file

@ -0,0 +1,53 @@
#include "../../Log.hpp"
#include "../shared.hpp"
#include "tests.hpp"
#include "../../shared.hpp"
#include "../../hyprctlCompat.hpp"
#include <hyprutils/os/Process.hpp>
#include <hyprutils/memory/WeakPtr.hpp>
static int ret = 0;
using namespace Hyprutils::OS;
using namespace Hyprutils::Memory;
static bool spawnLayer(const std::string& namespace_) {
NLog::log("{}Spawning kitty layer {}", Colors::YELLOW, namespace_);
if (!Tests::spawnLayerKitty(namespace_)) {
NLog::log("{}Error: {} layer did not spawn", Colors::RED, namespace_);
return false;
}
return true;
}
static bool test() {
NLog::log("{}Testing plugin layerrules", Colors::GREEN);
if (!spawnLayer("rule-layer"))
return false;
OK(getFromSocket("/dispatch plugin:test:add_layer_rule"));
OK(getFromSocket("/reload"));
OK(getFromSocket("/keyword layerrule match:namespace rule-layer, plugin_rule effect"));
if (!spawnLayer("rule-layer"))
return false;
if (!spawnLayer("norule-layer"))
return false;
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;
}
REGISTER_TEST_FN(test)

View file

@ -0,0 +1,191 @@
#include "../shared.hpp"
#include "../../shared.hpp"
#include "../../hyprctlCompat.hpp"
#include "tests.hpp"
static int ret = 0;
static void swar() {
OK(getFromSocket("/keyword layout:single_window_aspect_ratio 1 1"));
Tests::spawnKitty();
{
auto str = getFromSocket("/activewindow");
EXPECT_CONTAINS(str, "at: 442,22");
EXPECT_CONTAINS(str, "size: 1036,1036");
}
Tests::spawnKitty();
OK(getFromSocket("/dispatch killwindow activewindow"));
Tests::waitUntilWindowsN(1);
{
auto str = getFromSocket("/activewindow");
EXPECT_CONTAINS(str, "at: 442,22");
EXPECT_CONTAINS(str, "size: 1036,1036");
}
// don't use swar on maximized
OK(getFromSocket("/dispatch fullscreen 1"));
{
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() {
Tests::spawnKitty();
Tests::spawnKitty();
Tests::spawnKitty();
// move the layout
OK(getFromSocket("/keyword monitor HEADLESS-2,1920x1080@60,1000x0,1"));
// shouldnt crash
OK(getFromSocket("/dispatch movefocus r"));
OK(getFromSocket("/reload"));
NLog::log("{}Killing all windows", Colors::YELLOW);
Tests::killAllWindows();
}
// Test if size + pos is preserved after fs cycle
static void testPosPreserve() {
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"));
{
auto str = getFromSocket("/activewindow");
EXPECT_CONTAINS(str, "at: 420,420");
EXPECT_CONTAINS(str, "size: 1337,69");
}
OK(getFromSocket("/dispatch fullscreen"));
OK(getFromSocket("/dispatch fullscreen"));
{
auto str = getFromSocket("/activewindow");
EXPECT_CONTAINS(str, "size: 1337,69");
}
OK(getFromSocket("/dispatch movewindow r"));
{
auto str = getFromSocket("/activewindow");
EXPECT_CONTAINS(str, "at: 581,420");
EXPECT_CONTAINS(str, "size: 1337,69");
}
OK(getFromSocket("/dispatch fullscreen"));
OK(getFromSocket("/dispatch 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() {
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"));
EXPECT(!!Tests::spawnKitty("kitty_A"), true);
EXPECT(!!Tests::spawnKitty("kitty_B"), true);
EXPECT(!!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 killactive"));
Tests::waitUntilWindowsN(2);
{
auto str = getFromSocket("/activewindow");
EXPECT(str.contains("class: kitty_B"), true);
}
OK(getFromSocket("/dispatch killactive"));
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);
OK(getFromSocket("/keyword general:layout master"));
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 focuswindow class:kitty_C"));
OK(getFromSocket("/keyword 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,7 +3,53 @@
#include "../../hyprctlCompat.hpp"
#include "tests.hpp"
static int ret = 0;
static int ret = 0;
// reqs 1 master 3 slaves
static void testOrientations() {
OK(getFromSocket("/keyword master:orientation top"));
// top
{
auto str = getFromSocket("/activewindow");
EXPECT_CONTAINS(str, "at: 22,22");
EXPECT_CONTAINS(str, "size: 1876");
}
// cycle = top, right, bottom, center, left
// right
OK(getFromSocket("/dispatch layoutmsg orientationnext"));
{
auto str = getFromSocket("/activewindow");
EXPECT_CONTAINS(str, "at: 873,22");
EXPECT_CONTAINS(str, "size: 1025,1036");
}
// bottom
OK(getFromSocket("/dispatch layoutmsg orientationnext"));
{
auto str = getFromSocket("/activewindow");
EXPECT_CONTAINS(str, "at: 22,495");
EXPECT_CONTAINS(str, "size: 1876");
}
// center
OK(getFromSocket("/dispatch layoutmsg orientationnext"));
{
auto str = getFromSocket("/activewindow");
EXPECT_CONTAINS(str, "at: 450,22");
EXPECT_CONTAINS(str, "size: 1020,1036");
}
// left
OK(getFromSocket("/dispatch layoutmsg orientationnext"));
{
auto str = getFromSocket("/activewindow");
EXPECT_CONTAINS(str, "at: 22,22");
EXPECT_CONTAINS(str, "size: 1025,1036");
}
}
static void focusMasterPrevious() {
// setup
@ -44,11 +90,74 @@ static void focusMasterPrevious() {
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() {
// Master will re-send data to fullscreen / maximized windows, which can interfere with misc:on_focus_under_fullscreen
// check that it doesn't.
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;
}
}
OK(getFromSocket("/dispatch focuswindow class:master"));
OK(getFromSocket("/dispatch fullscreen 1"));
{
auto str = getFromSocket("/activewindow");
EXPECT_CONTAINS(str, "at: 22,22");
EXPECT_CONTAINS(str, "size: 1876,1036");
EXPECT_CONTAINS(str, "class: master");
}
OK(getFromSocket("/keyword misc:on_focus_under_fullscreen 1"));
Tests::spawnKitty("new_master");
{
auto str = getFromSocket("/activewindow");
EXPECT_CONTAINS(str, "at: 22,22");
EXPECT_CONTAINS(str, "size: 1876,1036");
EXPECT_CONTAINS(str, "class: new_master");
EXPECT_CONTAINS(str, "fullscreen: 1");
}
OK(getFromSocket("/keyword misc:on_focus_under_fullscreen 0"));
Tests::spawnKitty("ignored");
{
auto str = getFromSocket("/activewindow");
EXPECT_CONTAINS(str, "at: 22,22");
EXPECT_CONTAINS(str, "size: 1876,1036");
EXPECT_CONTAINS(str, "class: new_master");
EXPECT_CONTAINS(str, "fullscreen: 1");
}
OK(getFromSocket("/keyword misc:on_focus_under_fullscreen 2"));
Tests::spawnKitty("vaxwashere");
{
auto str = getFromSocket("/activewindow");
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);
@ -60,6 +169,9 @@ static bool 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"));

View file

@ -18,6 +18,121 @@ using namespace Hyprutils::Memory;
#define UP CUniquePointer
#define SP CSharedPointer
// Uncomment once test vm can run hyprland-dialog
// 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"));
//
// NLog::log("{}ANR dialog: regular workspaces", Colors::YELLOW);
// {
// OK(getFromSocket("/dispatch workspace 2"));
//
// auto kitty = Tests::spawnKitty("bad_kitty");
//
// if (!kitty) {
// ret = 1;
// return;
// }
//
// {
// auto str = getFromSocket("/activewindow");
// EXPECT_CONTAINS(str, "workspace: 2");
// }
//
// OK(getFromSocket("/dispatch workspace 1"));
//
// ::kill(kitty->pid(), SIGSTOP);
// Tests::waitUntilWindowsN(2);
//
// {
// auto str = getFromSocket("/activeworkspace");
// EXPECT_CONTAINS(str, "windows: 0");
// }
//
// {
// OK(getFromSocket("/dispatch focuswindow class:hyprland-dialog"))
// auto str = getFromSocket("/activewindow");
// EXPECT_CONTAINS(str, "workspace: 2");
// }
// }
//
// Tests::killAllWindows();
//
// NLog::log("{}ANR dialog: named workspaces", Colors::YELLOW);
// {
// OK(getFromSocket("/dispatch workspace name:yummy"));
//
// auto kitty = Tests::spawnKitty("bad_kitty");
//
// if (!kitty) {
// ret = 1;
// return;
// }
//
// {
// auto str = getFromSocket("/activewindow");
// EXPECT_CONTAINS(str, "yummy");
// }
//
// OK(getFromSocket("/dispatch workspace 1"));
//
// ::kill(kitty->pid(), SIGSTOP);
// Tests::waitUntilWindowsN(2);
//
// {
// auto str = getFromSocket("/activeworkspace");
// EXPECT_CONTAINS(str, "windows: 0");
// }
//
// {
// OK(getFromSocket("/dispatch focuswindow class:hyprland-dialog"))
// auto str = getFromSocket("/activewindow");
// EXPECT_CONTAINS(str, "yummy");
// }
// }
//
// Tests::killAllWindows();
//
// NLog::log("{}ANR dialog: special workspaces", Colors::YELLOW);
// {
// OK(getFromSocket("/dispatch workspace special:apple"));
//
// auto kitty = Tests::spawnKitty("bad_kitty");
//
// if (!kitty) {
// ret = 1;
// return;
// }
//
// {
// auto str = getFromSocket("/activewindow");
// EXPECT_CONTAINS(str, "special:apple");
// }
//
// OK(getFromSocket("/dispatch togglespecialworkspace apple"));
// OK(getFromSocket("/dispatch workspace 1"));
//
// ::kill(kitty->pid(), SIGSTOP);
// Tests::waitUntilWindowsN(2);
//
// {
// auto str = getFromSocket("/activeworkspace");
// EXPECT_CONTAINS(str, "windows: 0");
// }
//
// {
// OK(getFromSocket("/dispatch focuswindow class:hyprland-dialog"))
// auto str = getFromSocket("/activewindow");
// EXPECT_CONTAINS(str, "special:apple");
// }
// }
//
// OK(getFromSocket("/reload"));
// Tests::killAllWindows();
// }
static bool test() {
NLog::log("{}Testing config: misc:", Colors::GREEN);
@ -62,6 +177,7 @@ static bool test() {
{
auto str = getFromSocket("/activewindow");
EXPECT_CONTAINS(str, "fullscreen: 2");
EXPECT_CONTAINS(str, "fullscreenClient: 2");
EXPECT_CONTAINS(str, "kitty_A");
}
@ -70,6 +186,7 @@ static bool test() {
{
auto str = getFromSocket("/activewindow");
EXPECT_CONTAINS(str, "fullscreen: 2");
EXPECT_CONTAINS(str, "fullscreenClient: 2");
EXPECT_CONTAINS(str, "kitty_A");
}
@ -79,6 +196,7 @@ static bool test() {
// should be ignored as per focus_under_fullscreen 0
auto str = getFromSocket("/activewindow");
EXPECT_CONTAINS(str, "fullscreen: 2");
EXPECT_CONTAINS(str, "fullscreenClient: 2");
EXPECT_CONTAINS(str, "kitty_A");
}
@ -89,6 +207,7 @@ static bool test() {
{
auto str = getFromSocket("/activewindow");
EXPECT_CONTAINS(str, "fullscreen: 2");
EXPECT_CONTAINS(str, "fullscreenClient: 2");
EXPECT_CONTAINS(str, "kitty_C");
}
@ -99,6 +218,7 @@ static bool test() {
{
auto str = getFromSocket("/activewindow");
EXPECT_CONTAINS(str, "fullscreen: 0");
EXPECT_CONTAINS(str, "fullscreenClient: 0");
EXPECT_CONTAINS(str, "kitty_D");
}
@ -118,6 +238,7 @@ static bool test() {
{
auto str = getFromSocket("/activewindow");
EXPECT_CONTAINS(str, "fullscreen: 2");
EXPECT_CONTAINS(str, "fullscreenClient: 2");
}
OK(getFromSocket("/dispatch killwindow activewindow"));
@ -126,6 +247,7 @@ static bool test() {
{
auto str = getFromSocket("/activewindow");
EXPECT_CONTAINS(str, "fullscreen: 0");
EXPECT_CONTAINS(str, "fullscreenClient: 0");
}
Tests::spawnKitty("kitty_B");
@ -138,6 +260,7 @@ static bool test() {
{
auto str = getFromSocket("/activewindow");
EXPECT_CONTAINS(str, "fullscreen: 2");
EXPECT_CONTAINS(str, "fullscreenClient: 2");
}
Tests::killAllWindows();
@ -153,6 +276,7 @@ static bool test() {
{
auto str = getFromSocket("/activewindow");
EXPECT_CONTAINS(str, "fullscreen: 2");
EXPECT_CONTAINS(str, "fullscreenClient: 2");
}
OK(getFromSocket("/dispatch fullscreen 0 unset"));
@ -160,6 +284,7 @@ static bool test() {
{
auto str = getFromSocket("/activewindow");
EXPECT_CONTAINS(str, "fullscreen: 0");
EXPECT_CONTAINS(str, "fullscreenClient: 0");
}
OK(getFromSocket("/dispatch fullscreen 1 toggle"));
@ -167,6 +292,7 @@ static bool test() {
{
auto str = getFromSocket("/activewindow");
EXPECT_CONTAINS(str, "fullscreen: 1");
EXPECT_CONTAINS(str, "fullscreenClient: 1");
}
OK(getFromSocket("/dispatch fullscreen 1 toggle"));
@ -174,6 +300,23 @@ static bool test() {
{
auto str = getFromSocket("/activewindow");
EXPECT_CONTAINS(str, "fullscreen: 0");
EXPECT_CONTAINS(str, "fullscreenClient: 0");
}
OK(getFromSocket("/dispatch fullscreenstate 3 3 set"));
{
auto str = getFromSocket("/activewindow");
EXPECT_CONTAINS(str, "fullscreen: 3");
EXPECT_CONTAINS(str, "fullscreenClient: 3");
}
OK(getFromSocket("/dispatch fullscreenstate 3 3 set"));
{
auto str = getFromSocket("/activewindow");
EXPECT_CONTAINS(str, "fullscreen: 3");
EXPECT_CONTAINS(str, "fullscreenClient: 3");
}
OK(getFromSocket("/dispatch fullscreenstate 2 2 set"));
@ -181,6 +324,7 @@ static bool test() {
{
auto str = getFromSocket("/activewindow");
EXPECT_CONTAINS(str, "fullscreen: 2");
EXPECT_CONTAINS(str, "fullscreenClient: 2");
}
OK(getFromSocket("/dispatch fullscreenstate 2 2 set"));
@ -188,6 +332,7 @@ static bool test() {
{
auto str = getFromSocket("/activewindow");
EXPECT_CONTAINS(str, "fullscreen: 2");
EXPECT_CONTAINS(str, "fullscreenClient: 2");
}
OK(getFromSocket("/dispatch fullscreenstate 2 2 toggle"));
@ -195,6 +340,7 @@ static bool test() {
{
auto str = getFromSocket("/activewindow");
EXPECT_CONTAINS(str, "fullscreen: 0");
EXPECT_CONTAINS(str, "fullscreenClient: 0");
}
OK(getFromSocket("/dispatch fullscreenstate 2 2 toggle"));
@ -202,6 +348,7 @@ static bool test() {
{
auto str = getFromSocket("/activewindow");
EXPECT_CONTAINS(str, "fullscreen: 2");
EXPECT_CONTAINS(str, "fullscreenClient: 2");
}
// Ensure that the process autostarted in the config does not

View file

@ -0,0 +1,111 @@
#include "tests.hpp"
#include "../../shared.hpp"
#include "../../hyprctlCompat.hpp"
#include <print>
#include <thread>
#include <chrono>
#include <hyprutils/os/Process.hpp>
#include <hyprutils/memory/WeakPtr.hpp>
#include <csignal>
#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);
NLog::log("{}Dispatching workspace `moveintoorcreategroup`", Colors::YELLOW);
getFromSocket("/dispatch workspace name:moveintoorcreategroup");
OK(getFromSocket("/keyword 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;
}
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;
}
NLog::log("{}Expecting 2 windows", Colors::YELLOW);
EXPECT(Tests::windowCount(), 2);
{
auto str = getFromSocket("/clients");
EXPECT_CONTAINS(str, "grouped: 0");
}
OK(getFromSocket("/dispatch focuswindow class:kitty_A"));
NLog::log("{}Move kittyA into group with kittyB (creates group)", Colors::YELLOW);
OK(getFromSocket("/dispatch moveintoorcreategroup r"));
{
auto str = getFromSocket("/clients");
EXPECT_CONTAINS(str, "grouped:");
}
NLog::log("{}Verify active window is kitty_A (the moved window)", Colors::YELLOW);
{
auto str = getFromSocket("/activewindow");
EXPECT_CONTAINS(str, "kitty_A");
}
NLog::log("{}Kill windows", Colors::YELLOW);
Tests::killAllWindows();
NLog::log("{}Expecting 0 windows", Colors::YELLOW);
EXPECT(Tests::windowCount(), 0);
NLog::log("{}Testing moveintoorcreategroup into existing group", Colors::YELLOW);
NLog::log("{}Spawning kittyC", Colors::YELLOW);
auto kittyC = Tests::spawnKitty("kitty_C");
NLog::log("{}Spawning kittyD", Colors::YELLOW);
auto kittyD = Tests::spawnKitty("kitty_D");
NLog::log("{}Spawning kittyE", Colors::YELLOW);
auto kittyE = Tests::spawnKitty("kitty_E");
NLog::log("{}Expecting 3 windows", Colors::YELLOW);
EXPECT(Tests::windowCount(), 3);
OK(getFromSocket("/dispatch focuswindow class:kitty_D"));
OK(getFromSocket("/dispatch togglegroup"));
OK(getFromSocket("/dispatch focuswindow class:kitty_E"));
NLog::log("{}Move kittyE into existing group with kittyD", Colors::YELLOW);
OK(getFromSocket("/dispatch moveintoorcreategroup l"));
{
auto str = getFromSocket("/clients");
EXPECT_CONTAINS(str, "grouped:");
}
NLog::log("{}Verify active window is kitty_E (the moved window)", Colors::YELLOW);
{
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

@ -0,0 +1,258 @@
#include "../shared.hpp"
#include "../../shared.hpp"
#include "../../hyprctlCompat.hpp"
#include "tests.hpp"
static int ret = 0;
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;
}
}
OK(getFromSocket("/dispatch focuswindow class:a"));
OK(getFromSocket("/dispatch movefocus r"));
{
auto str = getFromSocket("/activewindow");
EXPECT_CONTAINS(str, "class: b");
}
OK(getFromSocket("/dispatch movefocus r"));
{
auto str = getFromSocket("/activewindow");
EXPECT_CONTAINS(str, "class: c");
}
OK(getFromSocket("/dispatch movefocus r"));
{
auto str = getFromSocket("/activewindow");
EXPECT_CONTAINS(str, "class: d");
}
OK(getFromSocket("/dispatch movewindow l"));
{
auto str = getFromSocket("/activewindow");
EXPECT_CONTAINS(str, "class: d");
}
OK(getFromSocket("/dispatch movefocus u"));
{
auto str = getFromSocket("/activewindow");
EXPECT_CONTAINS(str, "class: c");
}
// clean up
NLog::log("{}Killing all windows", Colors::YELLOW);
Tests::killAllWindows();
}
static void testFocusWrapping() {
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;
}
}
// set wrap_focus to true
OK(getFromSocket("/keyword scrolling:wrap_focus true"));
OK(getFromSocket("/dispatch focuswindow class:a"));
OK(getFromSocket("/dispatch layoutmsg focus l"));
{
auto str = getFromSocket("/activewindow");
EXPECT_CONTAINS(str, "class: d");
}
OK(getFromSocket("/dispatch layoutmsg focus r"));
{
auto str = getFromSocket("/activewindow");
EXPECT_CONTAINS(str, "class: a");
}
// set wrap_focus to false
OK(getFromSocket("/keyword scrolling:wrap_focus false"));
OK(getFromSocket("/dispatch focuswindow class:a"));
OK(getFromSocket("/dispatch layoutmsg focus l"));
{
auto str = getFromSocket("/activewindow");
EXPECT_CONTAINS(str, "class: a");
}
OK(getFromSocket("/dispatch focuswindow class:d"));
OK(getFromSocket("/dispatch layoutmsg focus r"));
{
auto str = getFromSocket("/activewindow");
EXPECT_CONTAINS(str, "class: d");
}
// clean up
NLog::log("{}Killing all windows", Colors::YELLOW);
Tests::killAllWindows();
}
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;
}
}
// set wrap_swapcol to true
OK(getFromSocket("/keyword scrolling:wrap_swapcol true"));
OK(getFromSocket("/dispatch focuswindow class:a"));
OK(getFromSocket("/dispatch layoutmsg swapcol l"));
OK(getFromSocket("/dispatch layoutmsg focus l"));
{
auto str = getFromSocket("/activewindow");
EXPECT_CONTAINS(str, "class: c");
}
// clean up
NLog::log("{}Killing all windows", Colors::YELLOW);
Tests::killAllWindows();
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;
}
}
OK(getFromSocket("/dispatch focuswindow class:d"));
OK(getFromSocket("/dispatch layoutmsg swapcol r"));
OK(getFromSocket("/dispatch layoutmsg focus r"));
{
auto str = getFromSocket("/activewindow");
EXPECT_CONTAINS(str, "class: b");
}
// clean up
NLog::log("{}Killing all windows", Colors::YELLOW);
Tests::killAllWindows();
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;
}
}
// set wrap_swapcol to false
OK(getFromSocket("/keyword scrolling:wrap_swapcol false"));
OK(getFromSocket("/dispatch focuswindow class:a"));
OK(getFromSocket("/dispatch layoutmsg swapcol l"));
OK(getFromSocket("/dispatch layoutmsg focus r"));
{
auto str = getFromSocket("/activewindow");
EXPECT_CONTAINS(str, "class: b");
}
OK(getFromSocket("/dispatch focuswindow class:d"));
OK(getFromSocket("/dispatch layoutmsg swapcol r"));
OK(getFromSocket("/dispatch layoutmsg focus l"));
{
auto str = getFromSocket("/activewindow");
EXPECT_CONTAINS(str, "class: c");
}
// clean up
NLog::log("{}Killing all windows", Colors::YELLOW);
Tests::killAllWindows();
}
static bool testWindowRule() {
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"));
if (!Tests::spawnKitty("kitty_scroll")) {
NLog::log("{}Failed to spawn kitty with win class `kitty_scroll`", Colors::RED);
return false;
}
if (!Tests::spawnKitty("kitty_scroll")) {
NLog::log("{}Failed to spawn kitty with win class `kitty_scroll`", Colors::RED);
return false;
}
EXPECT(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

@ -1,3 +1,4 @@
#include <unistd.h>
#include <cmath>
#include <chrono>
#include <cstdlib>
@ -6,6 +7,7 @@
#include <thread>
#include <hyprutils/os/Process.hpp>
#include <hyprutils/memory/WeakPtr.hpp>
#include <hyprutils/string/VarList2.hpp>
#include "../../shared.hpp"
#include "../../hyprctlCompat.hpp"
@ -23,15 +25,26 @@ static bool spawnKitty(const std::string& class_, const std::vector<std::string>
return true;
}
static std::string getWindowAttribute(const std::string& winInfo, const std::string& attr) {
auto pos = winInfo.find(attr);
if (pos == std::string::npos) {
NLog::log("{}Wrong window attribute", Colors::RED);
ret = 1;
return "Wrong window attribute";
/// Spawns a kitty and creates a file and returns its name. The removal of the file triggers
/// activation of the spawned kitty window.
///
/// On failure, returns an empty string, possibly leaving a temporary file.
static std::string spawnKittyActivating(const std::string& class_ = "kitty_activating") {
// `XXXXXX` is what `mkstemp` expects to find in the string
std::string tmpFilename = (std::filesystem::temp_directory_path() / "XXXXXX").string();
int fd = mkstemp(tmpFilename.data());
if (fd < 0) {
NLog::log("{}Error: could not create tmp file: errno {}", Colors::RED, errno);
return "";
}
auto pos2 = winInfo.find('\n', pos);
return winInfo.substr(pos, pos2 - pos);
(void)close(fd);
bool ok =
spawnKitty(class_, {"-o", "allow_remote_control=yes", "--", "/bin/sh", "-c", "while [ -f \"" + tmpFilename + "\" ]; do :; done; kitten @ focus-window; sleep infinity"});
if (!ok) {
NLog::log("{}Error: failed to spawn kitty", Colors::RED);
return "";
}
return tmpFilename;
}
static std::string getWindowAddress(const std::string& winInfo) {
@ -68,10 +81,10 @@ static void testSwapWindow() {
// Test swapwindow by direction
{
getFromSocket("/dispatch focuswindow class:kitty_A");
auto pos = getWindowAttribute(getFromSocket("/activewindow"), "at:");
NLog::log("{}Testing kitty_A {}, swapwindow with direction 'l'", Colors::YELLOW, pos);
auto pos = Tests::getWindowAttribute(getFromSocket("/activewindow"), "at:");
NLog::log("{}Testing kitty_A {}, swapwindow with direction 'r'", Colors::YELLOW, pos);
OK(getFromSocket("/dispatch swapwindow l"));
OK(getFromSocket("/dispatch swapwindow r"));
OK(getFromSocket("/dispatch focuswindow class:kitty_B"));
EXPECT_CONTAINS(getFromSocket("/activewindow"), std::format("{}", pos));
@ -80,7 +93,7 @@ static void testSwapWindow() {
// Test swapwindow by class
{
getFromSocket("/dispatch focuswindow class:kitty_A");
auto pos = getWindowAttribute(getFromSocket("/activewindow"), "at:");
auto pos = Tests::getWindowAttribute(getFromSocket("/activewindow"), "at:");
NLog::log("{}Testing kitty_A {}, swapwindow with class:kitty_B", Colors::YELLOW, pos);
OK(getFromSocket("/dispatch swapwindow class:kitty_B"));
@ -94,7 +107,7 @@ static void testSwapWindow() {
getFromSocket("/dispatch focuswindow class:kitty_B");
auto addr = getWindowAddress(getFromSocket("/activewindow"));
getFromSocket("/dispatch focuswindow class:kitty_A");
auto pos = getWindowAttribute(getFromSocket("/activewindow"), "at:");
auto pos = Tests::getWindowAttribute(getFromSocket("/activewindow"), "at:");
NLog::log("{}Testing kitty_A {}, swapwindow with address:0x{}(kitty_B)", Colors::YELLOW, pos, addr);
OK(getFromSocket(std::format("/dispatch swapwindow address:0x{}", addr)));
@ -117,7 +130,7 @@ static void testSwapWindow() {
{
getFromSocket("/dispatch focuswindow class:kitty_B");
auto addr = getWindowAddress(getFromSocket("/activewindow"));
auto ws = getWindowAttribute(getFromSocket("/activewindow"), "workspace:");
auto ws = Tests::getWindowAttribute(getFromSocket("/activewindow"), "workspace:");
NLog::log("{}Sending address:0x{}(kitty_B) to workspace \"swapwindow2\"", Colors::YELLOW, addr);
OK(getFromSocket("/dispatch movetoworkspacesilent name:swapwindow2"));
@ -196,10 +209,10 @@ static void testGroupRules() {
Tests::killAllWindows();
}
static bool isActiveWindow(const std::string& class_, char fullscreen, bool log = true) {
static bool isActiveWindow(const std::string& class_, char fullscreen = '0', bool log = true) {
std::string activeWin = getFromSocket("/activewindow");
auto winClass = getWindowAttribute(activeWin, "class:");
auto winFullscreen = getWindowAttribute(activeWin, "fullscreen:").back();
auto winClass = Tests::getWindowAttribute(activeWin, "class:");
auto winFullscreen = Tests::getWindowAttribute(activeWin, "fullscreen:").back();
if (winClass.substr(strlen("class: ")) == class_ && winFullscreen == fullscreen)
return true;
else {
@ -209,13 +222,13 @@ static bool isActiveWindow(const std::string& class_, char fullscreen, bool log
}
}
static bool waitForActiveWindow(const std::string& class_, char fullscreen, int maxTries = 50) {
static bool waitForActiveWindow(const std::string& class_, char fullscreen = '0', bool logLastCheck = true, int maxTries = 50) {
int cnt = 0;
while (!isActiveWindow(class_, fullscreen, false)) {
++cnt;
std::this_thread::sleep_for(std::chrono::milliseconds(100));
if (cnt > maxTries) {
return isActiveWindow(class_, fullscreen, true);
return isActiveWindow(class_, fullscreen, logLastCheck);
}
}
return true;
@ -231,24 +244,6 @@ static bool testWindowFocusOnFullscreenConflict() {
OK(getFromSocket("/keyword misc:focus_on_activate true"));
auto spawnKittyActivating = [] -> std::string {
// `XXXXXX` is what `mkstemp` expects to find in the string
std::string tmpFilename = (std::filesystem::temp_directory_path() / "XXXXXX").string();
int fd = mkstemp(tmpFilename.data());
if (fd < 0) {
NLog::log("{}Error: could not create tmp file: errno {}", Colors::RED, errno);
return "";
}
(void)close(fd);
bool ok = spawnKitty("kitty_activating",
{"-o", "allow_remote_control=yes", "--", "/bin/sh", "-c", "while [ -f \"" + tmpFilename + "\" ]; do :; done; kitten @ focus-window; sleep infinity"});
if (!ok) {
NLog::log("{}Error: failed to spawn kitty", Colors::RED);
return "";
}
return tmpFilename;
};
// Unfullscreen on conflict
{
OK(getFromSocket("/keyword misc:on_focus_under_fullscreen 2"));
@ -343,6 +338,365 @@ static bool testWindowFocusOnFullscreenConflict() {
return true;
}
static void testMaximizeSize() {
NLog::log("{}Testing maximize size", Colors::GREEN);
EXPECT(spawnKitty("kitty_A"), true);
// check kitty properties. Maximizing shouldnt change its size
{
auto str = getFromSocket("/clients");
EXPECT(str.contains("at: 22,22"), true);
EXPECT(str.contains("size: 1876,1036"), true);
EXPECT(str.contains("fullscreen: 0"), true);
}
OK(getFromSocket("/dispatch fullscreen 1"));
{
auto str = getFromSocket("/clients");
EXPECT(str.contains("at: 22,22"), true);
EXPECT(str.contains("size: 1876,1036"), true);
EXPECT(str.contains("fullscreen: 1"), true);
}
NLog::log("{}Killing all windows", Colors::YELLOW);
Tests::killAllWindows();
NLog::log("{}Expecting 0 windows", Colors::YELLOW);
EXPECT(Tests::windowCount(), 0);
}
static void testFloatingFocusOnFullscreen() {
NLog::log("{}Testing floating focus on fullscreen", Colors::GREEN);
EXPECT(spawnKitty("kitty_A"), true);
OK(getFromSocket("/dispatch togglefloating"));
EXPECT(spawnKitty("kitty_B"), true);
OK(getFromSocket("/dispatch fullscreen 1"));
OK(getFromSocket("/dispatch cyclenext"));
OK(getFromSocket("/dispatch plugin:test:floating_focus_on_fullscreen"));
NLog::log("{}Killing all windows", Colors::YELLOW);
Tests::killAllWindows();
NLog::log("{}Expecting 0 windows", Colors::YELLOW);
EXPECT(Tests::windowCount(), 0);
}
static void testGroupFallbackFocus() {
NLog::log("{}Testing group fallback focus", Colors::GREEN);
EXPECT(spawnKitty("kitty_A"), true);
OK(getFromSocket("/dispatch togglegroup"));
EXPECT(spawnKitty("kitty_B"), true);
EXPECT(spawnKitty("kitty_C"), true);
EXPECT(spawnKitty("kitty_D"), true);
{
auto str = getFromSocket("/activewindow");
EXPECT(str.contains("class: kitty_D"), true);
}
OK(getFromSocket("/dispatch focuswindow class:kitty_B"));
OK(getFromSocket("/dispatch focuswindow class:kitty_D"));
OK(getFromSocket("/dispatch killactive"));
Tests::waitUntilWindowsN(3);
// Focus must return to the last focus, in this case B.
{
auto str = getFromSocket("/activewindow");
EXPECT(str.contains("class: kitty_B"), true);
}
NLog::log("{}Killing all windows", Colors::YELLOW);
Tests::killAllWindows();
NLog::log("{}Expecting 0 windows", Colors::YELLOW);
EXPECT(Tests::windowCount(), 0);
}
static void testBringActiveToTopMouseMovement() {
NLog::log("{}Testing bringactivetotop mouse movement", Colors::GREEN);
Tests::killAllWindows();
OK(getFromSocket("/keyword input:follow_mouse 2"));
OK(getFromSocket("/keyword input:float_switch_override_focus 0"));
EXPECT(spawnKitty("a"), true);
OK(getFromSocket("/dispatch setfloating"));
OK(getFromSocket("/dispatch movewindowpixel exact 500 300,activewindow"));
OK(getFromSocket("/dispatch resizewindowpixel exact 400 400,activewindow"));
EXPECT(spawnKitty("b"), true);
OK(getFromSocket("/dispatch setfloating"));
OK(getFromSocket("/dispatch movewindowpixel exact 500 300,activewindow"));
OK(getFromSocket("/dispatch resizewindowpixel exact 400 400,activewindow"));
auto getTopWindow = []() -> std::string {
auto clients = getFromSocket("/clients");
return (clients.rfind("class: a") > clients.rfind("class: b")) ? "a" : "b";
};
EXPECT(getTopWindow(), std::string("b"));
OK(getFromSocket("/dispatch movecursor 700 500"));
OK(getFromSocket("/dispatch focuswindow class:a"));
EXPECT_CONTAINS(getFromSocket("/activewindow"), "class: a");
OK(getFromSocket("/dispatch bringactivetotop"));
EXPECT(getTopWindow(), std::string("a"));
OK(getFromSocket("/dispatch plugin:test:click 272,1"));
OK(getFromSocket("/dispatch plugin:test:click 272,0"));
EXPECT(getTopWindow(), std::string("a"));
Tests::killAllWindows();
}
static void testInitialFloatSize() {
NLog::log("{}Testing initial float size", Colors::GREEN);
Tests::killAllWindows();
OK(getFromSocket("/keyword windowrule match:class kitty, float yes"));
OK(getFromSocket("/keyword input:float_switch_override_focus 0"));
EXPECT(spawnKitty("kitty"), true);
{
// Kitty by default opens as 640x400, if this changes this test will break
auto str = getFromSocket("/clients");
EXPECT(str.contains("size: 640,400"), true);
}
OK(getFromSocket("/reload"));
Tests::killAllWindows();
OK(getFromSocket("/dispatch exec [float yes]kitty"));
Tests::waitUntilWindowsN(1);
{
// Kitty by default opens as 640x400, if this changes this test will break
auto str = getFromSocket("/clients");
EXPECT(str.contains("size: 640,400"), true);
EXPECT(str.contains("floating: 1"), true);
}
Tests::killAllWindows();
}
/// Tests that the `focus_on_activate` effect of window rules always overrides
/// the `misc:focus_on_activate` variable.
static bool testWindowRuleFocusOnActivate() {
OK(getFromSocket("/reload"));
if (!spawnKitty("kitty_default")) {
NLog::log("{}Error: failed to spawn kitty", Colors::RED);
return false;
}
// Do not focus anyone automatically
///////////OK(getFromSocket("/keyword windowrule match:class .*, no_initial_focus true"));
// `focus_on_activate off` takes over
{
OK(getFromSocket("/keyword misc:focus_on_activate true"));
OK(getFromSocket("/keyword windowrule match:class kitty_antifocus, focus_on_activate off"));
const std::string removeToActivate = spawnKittyActivating("kitty_antifocus");
if (removeToActivate.empty()) {
return false;
}
EXPECT(waitForActiveWindow("kitty_antifocus"), true);
OK(getFromSocket("/dispatch focuswindow class:kitty_default"));
EXPECT(isActiveWindow("kitty_default"), true);
std::filesystem::remove(removeToActivate);
// The focus should NOT transition, since the window rule explicitly forbids that
EXPECT(waitForActiveWindow("kitty_antifocus", '0', false), false);
}
// `focus_on_activate on` takes over
{
OK(getFromSocket("/keyword misc:focus_on_activate false"));
OK(getFromSocket("/keyword windowrule match:class kitty_superfocus, focus_on_activate on"));
const std::string removeToActivate = spawnKittyActivating("kitty_superfocus");
if (removeToActivate.empty()) {
return false;
}
EXPECT(waitForActiveWindow("kitty_superfocus"), true);
OK(getFromSocket("/dispatch focuswindow class:kitty_default"));
EXPECT(isActiveWindow("kitty_default"), true);
std::filesystem::remove(removeToActivate);
// Now that we requested activation, the focus SHOULD transition to kitty_superfocus, according to the window rule
EXPECT(waitForActiveWindow("kitty_superfocus"), true);
}
NLog::log("{}Reloading config", Colors::YELLOW);
OK(getFromSocket("/reload"));
NLog::log("{}Killing all windows", Colors::YELLOW);
Tests::killAllWindows();
NLog::log("{}Expecting 0 windows", Colors::YELLOW);
EXPECT(Tests::windowCount(), 0);
return true;
}
// tests if a pinned window contains the valid workspace after change
static bool testPinnedWorkspacesValid() {
OK(getFromSocket("/reload"));
getFromSocket("/dispatch workspace 1337");
if (!spawnKitty("kitty")) {
NLog::log("{}Error: failed to spawn kitty", Colors::RED);
return false;
}
OK(getFromSocket("/dispatch setfloating class:kitty"));
OK(getFromSocket("/dispatch pin class:kitty"));
{
auto str = getFromSocket("/activewindow");
EXPECT(str.contains("workspace: 1337"), true);
EXPECT(str.contains("pinned: 1"), true);
}
getFromSocket("/dispatch workspace 1338");
{
auto str = getFromSocket("/activewindow");
EXPECT(str.contains("workspace: 1338"), true);
EXPECT(str.contains("pinned: 1"), true);
}
OK(getFromSocket("/dispatch settiled class:kitty"))
{
auto str = getFromSocket("/activewindow");
EXPECT(str.contains("workspace: 1338"), true);
EXPECT(str.contains("pinned: 0"), true);
}
NLog::log("{}Reloading config", Colors::YELLOW);
OK(getFromSocket("/reload"));
NLog::log("{}Killing all windows", Colors::YELLOW);
Tests::killAllWindows();
NLog::log("{}Expecting 0 windows", Colors::YELLOW);
EXPECT(Tests::windowCount(), 0);
return true;
}
static bool testWindowRuleWorkspaceEmpty() {
NLog::log("{}Testing windowrule workspace empty", Colors::YELLOW);
OK(getFromSocket("/reload"));
OK(getFromSocket("/keyword windowrule match:class kitty_A, workspace empty"));
OK(getFromSocket("/keyword windowrule match:class kitty_B, workspace emptyn"));
getFromSocket("/dispatch workspace 3");
if (!spawnKitty("kitty")) {
NLog::log("{}Error: failed to spawn kitty", Colors::RED);
return false;
}
{
auto str = getFromSocket("/activewindow");
EXPECT(str.contains("workspace: 3"), true);
}
if (!spawnKitty("kitty_A")) {
NLog::log("{}Error: failed to spawn kitty", Colors::RED);
return false;
}
{
auto str = getFromSocket("/activewindow");
EXPECT(str.contains("workspace: 1"), true);
}
getFromSocket("/dispatch workspace 3");
if (!spawnKitty("kitty_B")) {
NLog::log("{}Error: failed to spawn kitty", Colors::RED);
return false;
}
{
auto str = getFromSocket("/activewindow");
EXPECT(str.contains("workspace: 4"), true);
}
Tests::killAllWindows();
return true;
}
static bool testContentRules() {
NLog::log("{}Testing content window rules", Colors::YELLOW);
// kill me PLEASE
OK(getFromSocket("/keyword windowrule match:class kitty_content_string, content game"));
OK(getFromSocket("/keyword windowrule match:class kitty_content_numbers, content 3"));
OK(getFromSocket("/keyword windowrule match:content game, border_size 10"));
OK(getFromSocket("/keyword windowrule match:content 3, opacity 0.5"));
const auto testProps = []() {
EXPECT_CONTAINS(getFromSocket("/getprop active border_size"), "10");
EXPECT_CONTAINS(getFromSocket("/getprop active opacity"), "0.5");
};
if (!spawnKitty("kitty_content_string"))
return false;
waitForActiveWindow("kitty_content_string");
testProps();
Tests::killAllWindows();
EXPECT(Tests::windowCount(), 0);
if (!spawnKitty("kitty_content_numbers"))
return false;
waitForActiveWindow("kitty_content_numbers");
testProps();
Tests::killAllWindows();
EXPECT(Tests::windowCount(), 0);
return true;
}
static bool test14038() {
NLog::log("{}Testing #14038 crash", Colors::YELLOW);
if (!spawnKitty("kitty_14038"))
return false;
OK(getFromSocket("/dispatch movetoworkspacesilent special:a,class:kitty_14038"));
OK(getFromSocket("/dispatch togglefloating class:kitty_14038"));
OK(getFromSocket("/dispatch pin class:kitty_14038"));
OK(getFromSocket("/dispatch togglefloating class:kitty_14038"));
// this should not crash hyprland. If we are alive, we good.
Tests::killAllWindows();
EXPECT(Tests::windowCount(), 0);
return true;
}
static bool test() {
NLog::log("{}Testing windows", Colors::GREEN);
@ -364,22 +718,40 @@ static bool test() {
NLog::log("{}Testing window split ratios", Colors::YELLOW);
{
const double RATIO = 1.25;
const double PERCENT = RATIO / 2.0 * 100.0;
const int GAPSIN = 5;
const int GAPSOUT = 20;
const int BORDERS = 2 * 2;
const int WTRIM = BORDERS + GAPSIN + GAPSOUT;
const int HEIGHT = 1080 - (BORDERS + (GAPSOUT * 2));
const int WIDTH1 = std::round(1920.0 / 2.0 * (2 - RATIO)) - WTRIM;
const int WIDTH2 = std::round(1920.0 / 2.0 * RATIO) - WTRIM;
const double INITIAL_RATIO = 1.25;
const int GAPSIN = 5;
const int GAPSOUT = 20;
const int BORDERSIZE = 2;
const int BORDERS = BORDERSIZE * 2;
const int MONITOR_W = 1920;
const int MONITOR_H = 1080;
const float totalAvailableHeight = MONITOR_H - (GAPSOUT * 2);
const int HEIGHT = std::floor(totalAvailableHeight) - BORDERS;
const float availableWidthForSplit = MONITOR_W - (GAPSOUT * 2) - GAPSIN;
auto calculateFinalWidth = [&](double boxWidth, bool isLeftWindow) {
double gapLeft = isLeftWindow ? GAPSOUT : GAPSIN;
double gapRight = isLeftWindow ? GAPSIN : GAPSOUT;
return std::floor(boxWidth - gapLeft - gapRight - BORDERS);
};
double geomBoxWidthA_R1 = (availableWidthForSplit * INITIAL_RATIO / 2.0) + GAPSOUT + (GAPSIN / 2.0);
double geomBoxWidthB_R1 = MONITOR_W - geomBoxWidthA_R1;
const int WIDTH1 = calculateFinalWidth(geomBoxWidthB_R1, false);
const double INVERTED_RATIO = 0.75;
double geomBoxWidthA_R2 = (availableWidthForSplit * INVERTED_RATIO / 2.0) + GAPSOUT + (GAPSIN / 2.0);
double geomBoxWidthB_R2 = MONITOR_W - geomBoxWidthA_R2;
const int WIDTH2 = calculateFinalWidth(geomBoxWidthB_R2, false);
const int WIDTH_A_FINAL = calculateFinalWidth(geomBoxWidthA_R2, true);
OK(getFromSocket("/keyword dwindle:default_split_ratio 1.25"));
if (!spawnKitty("kitty_B"))
return false;
NLog::log("{}Expecting kitty_B to take up roughly {}% of screen width", Colors::YELLOW, 100 - PERCENT);
NLog::log("{}Expecting kitty_B size: {},{}", Colors::YELLOW, WIDTH1, HEIGHT);
EXPECT_CONTAINS(getFromSocket("/activewindow"), std::format("size: {},{}", WIDTH1, HEIGHT));
OK(getFromSocket("/dispatch killwindow activewindow"));
@ -391,12 +763,38 @@ static bool test() {
if (!spawnKitty("kitty_B"))
return false;
NLog::log("{}Expecting kitty_B to take up roughly {}% of screen width", Colors::YELLOW, PERCENT);
EXPECT_CONTAINS(getFromSocket("/activewindow"), std::format("size: {},{}", WIDTH2, HEIGHT));
try {
NLog::log("{}Expecting kitty_B size: {},{}", Colors::YELLOW, WIDTH2, HEIGHT);
OK(getFromSocket("/dispatch focuswindow class:kitty_A"));
NLog::log("{}Expecting kitty_A to have the same width as the previous kitty_B", Colors::YELLOW);
EXPECT_CONTAINS(getFromSocket("/activewindow"), std::format("size: {},{}", WIDTH1, HEIGHT));
{
auto data = getFromSocket("/activewindow");
data = data.substr(data.find("size:") + 5);
data = data.substr(0, data.find('\n'));
Hyprutils::String::CVarList2 sizes(std::move(data), 0, ',');
EXPECT_MAX_DELTA(std::stoi(std::string{sizes[0]}), WIDTH2, 2);
EXPECT_MAX_DELTA(std::stoi(std::string{sizes[1]}), HEIGHT, 2);
}
OK(getFromSocket("/dispatch focuswindow class:kitty_A"));
NLog::log("{}Expecting kitty_A size: {},{}", Colors::YELLOW, WIDTH_A_FINAL, HEIGHT);
{
auto data = getFromSocket("/activewindow");
data = data.substr(data.find("size:") + 5);
data = data.substr(0, data.find('\n'));
Hyprutils::String::CVarList2 sizes(std::move(data), 0, ',');
EXPECT_MAX_DELTA(std::stoi(std::string{sizes[0]}), WIDTH_A_FINAL, 2);
EXPECT_MAX_DELTA(std::stoi(std::string{sizes[1]}), HEIGHT, 2);
}
} catch (...) {
NLog::log("{}Exception thrown", Colors::RED);
EXPECT(false, true);
}
OK(getFromSocket("/keyword dwindle:default_split_ratio 1"));
}
@ -412,14 +810,14 @@ static bool test() {
EXPECT(Tests::windowCount(), 3);
NLog::log("{}Checking props of xeyes", Colors::YELLOW);
// check some window props of xeyes, try to tile them
// check some window props of xeyes, try to float it
{
auto str = getFromSocket("/clients");
EXPECT_CONTAINS(str, "floating: 1");
getFromSocket("/dispatch settiled class:XEyes");
EXPECT_NOT_CONTAINS(str, "floating: 1");
getFromSocket("/dispatch setfloating class:XEyes");
std::this_thread::sleep_for(std::chrono::milliseconds(200));
str = getFromSocket("/clients");
EXPECT_NOT_CONTAINS(str, "floating: 1");
EXPECT_CONTAINS(str, "floating: 1");
}
// kill all
@ -477,10 +875,10 @@ static bool test() {
EXPECT_CONTAINS(dwindle, "size: 1500,500");
EXPECT_CONTAINS(dwindle, "at: 210,290");
if (!spawnKitty("kitty_maxsize"))
return false;
EXPECT_CONTAINS(getFromSocket("/activewindow"), "size: 1200,500");
// Fuck this test, it's fucking stupid - vax
// if (!spawnKitty("kitty_maxsize"))
// return false;
// EXPECT_CONTAINS(getFromSocket("/activewindow"), "size: 1200,500");
Tests::killAllWindows();
EXPECT(Tests::windowCount(), 0);
@ -497,8 +895,69 @@ static bool test() {
if (!spawnKitty("kitty_maxsize"))
return false;
// FIXME: I can't be arsed.
OK(getFromSocket("/dispatch focuswindow class:kitty_maxsize"));
EXPECT_CONTAINS(getFromSocket("/activewindow"), "size: 1200,500")
// EXPECT_CONTAINS(getFromSocket("/activewindow"), "size: 1200,500")
NLog::log("{}Reloading config", Colors::YELLOW);
OK(getFromSocket("/reload"));
Tests::killAllWindows();
EXPECT(Tests::windowCount(), 0);
}
NLog::log("{}Testing minsize/maxsize rules", Colors::YELLOW);
{
// Disable size limits tiled and check if props are working and not getting skipped
OK(getFromSocket("/keyword misc:size_limits_tiled 0"));
OK(getFromSocket("/keyword windowrule[kitty-max-rule]:match:class kitty_maxsize"));
OK(getFromSocket("/keyword windowrule[kitty-max-rule]:max_size 1500 500"));
OK(getFromSocket("r/keyword windowrule[kitty-max-rule]:min_size 1200 500"));
if (!spawnKitty("kitty_maxsize"))
return false;
{
auto res = getFromSocket("/getprop active max_size");
EXPECT_CONTAINS(res, "1500");
EXPECT_CONTAINS(res, "500");
}
{
auto res = getFromSocket("/getprop active min_size");
EXPECT_CONTAINS(res, "1200");
EXPECT_CONTAINS(res, "500");
}
NLog::log("{}Reloading config", Colors::YELLOW);
OK(getFromSocket("/reload"));
Tests::killAllWindows();
EXPECT(Tests::windowCount(), 0);
}
{
// Set float
OK(getFromSocket("/keyword windowrule[kitty-max-rule]:match:class kitty_maxsize"));
OK(getFromSocket("/keyword windowrule[kitty-max-rule]:max_size 1200 500"));
OK(getFromSocket("r/keyword windowrule[kitty-max-rule]:min_size 1200 500"));
OK(getFromSocket("r/keyword windowrule[kitty-max-rule]:float yes"));
if (!spawnKitty("kitty_maxsize"))
return false;
{
auto res = getFromSocket("/getprop active max_size");
EXPECT_CONTAINS(res, "1200");
EXPECT_CONTAINS(res, "500");
}
{
auto res = getFromSocket("/getprop active min_size");
EXPECT_CONTAINS(res, "1200");
EXPECT_CONTAINS(res, "500");
}
{
auto res = getFromSocket("/activewindow");
EXPECT_CONTAINS(res, "size: 1200,500");
}
NLog::log("{}Reloading config", Colors::YELLOW);
OK(getFromSocket("/reload"));
@ -542,6 +1001,23 @@ static bool test() {
Tests::killAllWindows();
OK(getFromSocket("/keyword windowrule[border-magic-kitty]:match:class border_kitty"));
OK(getFromSocket("/keyword windowrule[border-magic-kitty]:border_color rgba(c6ff00ff) rgba(ff0000ee) 45deg"));
if (!spawnKitty("border_kitty"))
return false;
OK(getFromSocket("/dispatch focuswindow class:border_kitty"));
{
auto str = getFromSocket("/getprop active active_border_color");
EXPECT_CONTAINS(str, "ffc6ff00");
EXPECT_CONTAINS(str, "eeff0000");
EXPECT_CONTAINS(str, "45deg");
}
Tests::killAllWindows();
if (!spawnKitty("tag_kitty"))
return false;
@ -625,7 +1101,8 @@ static bool test() {
Tests::killAllWindows();
// test expression rules
OK(getFromSocket("/keyword windowrule match:class expr_kitty, float yes, size monitor_w*0.5 monitor_h*0.5, move 20+(monitor_w*0.1) monitor_h*0.5"));
OK(getFromSocket("/keyword windowrule match:class expr_kitty, float yes, size monitor_w*0.5 monitor_h*0.5, min_size monitor_w*0.25 monitor_h*0.25, "
"max_size monitor_w*0.75 monitor_h*0.75, move 20+(monitor_w*0.1) monitor_h*0.5"));
if (!spawnKitty("expr_kitty"))
return false;
@ -635,12 +1112,20 @@ static bool test() {
EXPECT_CONTAINS(str, "floating: 1");
EXPECT_CONTAINS(str, "at: 212,540");
EXPECT_CONTAINS(str, "size: 960,540");
auto min = getFromSocket("/getprop active min_size");
EXPECT_CONTAINS(min, "480");
EXPECT_CONTAINS(min, "270");
auto max = getFromSocket("/getprop active max_size");
EXPECT_CONTAINS(max, "1440");
EXPECT_CONTAINS(max, "810");
}
OK(getFromSocket("/reload"));
Tests::killAllWindows();
OK(getFromSocket("/dispatch plugin:test:add_rule"));
OK(getFromSocket("/dispatch plugin:test:add_window_rule"));
OK(getFromSocket("/reload"));
OK(getFromSocket("/keyword windowrule match:class plugin_kitty, plugin_rule effect"));
@ -648,12 +1133,12 @@ static bool test() {
if (!spawnKitty("plugin_kitty"))
return false;
OK(getFromSocket("/dispatch plugin:test:check_rule"));
OK(getFromSocket("/dispatch plugin:test:check_window_rule"));
OK(getFromSocket("/reload"));
Tests::killAllWindows();
OK(getFromSocket("/dispatch plugin:test:add_rule"));
OK(getFromSocket("/dispatch plugin:test:add_window_rule"));
OK(getFromSocket("/reload"));
OK(getFromSocket("/keyword windowrule[test-plugin-rule]:match:class plugin_kitty"));
@ -662,12 +1147,22 @@ static bool test() {
if (!spawnKitty("plugin_kitty"))
return false;
OK(getFromSocket("/dispatch plugin:test:check_rule"));
OK(getFromSocket("/dispatch plugin:test:check_window_rule"));
OK(getFromSocket("/reload"));
Tests::killAllWindows();
testGroupRules();
testMaximizeSize();
testFloatingFocusOnFullscreen();
testBringActiveToTopMouseMovement();
testGroupFallbackFocus();
testInitialFloatSize();
testWindowRuleFocusOnActivate();
testPinnedWorkspacesValid();
testWindowRuleWorkspaceEmpty();
testContentRules();
test14038();
NLog::log("{}Reloading config", Colors::YELLOW);
OK(getFromSocket("/reload"));

View file

@ -6,6 +6,7 @@
#include <chrono>
#include <hyprutils/os/Process.hpp>
#include <hyprutils/memory/WeakPtr.hpp>
#include <hyprutils/utils/ScopeGuard.hpp>
#include <csignal>
#include <cerrno>
#include "../shared.hpp"
@ -14,10 +15,413 @@ static int ret = 0;
using namespace Hyprutils::OS;
using namespace Hyprutils::Memory;
using namespace Hyprutils::Utils;
#define UP CUniquePointer
#define SP CSharedPointer
static bool testSpecialWorkspaceFullscreen() {
NLog::log("{}Testing special workspace fullscreen detection", Colors::YELLOW);
CScopeGuard guard = {[&]() {
NLog::log("{}Cleaning up special workspace fullscreen test", Colors::YELLOW);
// 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");
Tests::killAllWindows();
OK(getFromSocket("/reload"));
}};
getFromSocket("/dispatch workspace 1");
EXPECT(Tests::windowCount(), 0);
NLog::log("{}Test 1: Fullscreen detection on special workspace", Colors::YELLOW);
OK(getFromSocket("/dispatch workspace special:test_fs_special"));
if (!Tests::spawnKitty("kitty_special"))
return false;
{
auto str = getFromSocket("/activewindow");
EXPECT_CONTAINS(str, "class: kitty_special");
EXPECT_CONTAINS(str, "(special:test_fs_special)");
}
OK(getFromSocket("/dispatch fullscreen 0"));
{
auto str = getFromSocket("/activewindow");
EXPECT_CONTAINS(str, "fullscreen: 2");
}
{
auto str = getFromSocket("/monitors");
EXPECT_CONTAINS(str, "(special:test_fs_special)");
}
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");
if (!Tests::spawnKitty("kitty_regular"))
return false;
OK(getFromSocket("/dispatch fullscreen 0"));
{
auto str = getFromSocket("/activewindow");
EXPECT_CONTAINS(str, "class: kitty_regular");
EXPECT_CONTAINS(str, "fullscreen: 2");
}
OK(getFromSocket("/dispatch togglespecialworkspace test_fs_special"));
OK(getFromSocket("/dispatch focuswindow class:kitty_special"));
{
auto str = getFromSocket("/activewindow");
EXPECT_CONTAINS(str, "class: kitty_special");
}
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"));
{
auto str = getFromSocket("/activewindow");
EXPECT_CONTAINS(str, "class: kitty_regular");
EXPECT_CONTAINS(str, "fullscreen: 2");
}
{
auto str = getFromSocket("/monitors");
EXPECT_CONTAINS(str, "special workspace: 0 ()");
}
return true;
}
static bool testAsymmetricGaps() {
NLog::log("{}Testing asymmetric gap splits", Colors::YELLOW);
{
CScopeGuard guard = {[&]() {
NLog::log("{}Cleaning up asymmetric gap test", Colors::YELLOW);
Tests::killAllWindows();
OK(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"));
NLog::log("{}Testing default split (force_split = 0)", Colors::YELLOW);
OK(getFromSocket("r/keyword dwindle:force_split 0"));
if (!Tests::spawnKitty("gaps_kitty_A") || !Tests::spawnKitty("gaps_kitty_B"))
return false;
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");
Tests::killAllWindows();
EXPECT(Tests::windowCount(), 0);
NLog::log("{}Testing force_split = 1", Colors::YELLOW);
OK(getFromSocket("r/keyword dwindle:force_split 1"));
if (!Tests::spawnKitty("gaps_kitty_A") || !Tests::spawnKitty("gaps_kitty_B"))
return false;
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 horizontal split (C left of B)", Colors::YELLOW);
OK(getFromSocket("/dispatch focuswindow class:gaps_kitty_B"));
if (!Tests::spawnKitty("gaps_kitty_C"))
return false;
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");
Tests::killAllWindows();
EXPECT(Tests::windowCount(), 0);
NLog::log("{}Testing force_split = 2", Colors::YELLOW);
OK(getFromSocket("r/keyword dwindle:force_split 2"));
if (!Tests::spawnKitty("gaps_kitty_A") || !Tests::spawnKitty("gaps_kitty_B"))
return false;
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 horizontal split (C right of A)", Colors::YELLOW);
OK(getFromSocket("/dispatch focuswindow class:gaps_kitty_A"));
if (!Tests::spawnKitty("gaps_kitty_C"))
return false;
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");
}
// kill all
NLog::log("{}Killing all windows", Colors::YELLOW);
Tests::killAllWindows();
return true;
}
static void testWorkspaceHistoryMultiMon() {
NLog::log("{}Testing multimon workspace history tracker", Colors::YELLOW);
// Initial state:
OK(getFromSocket("/dispatch focusmonitor HEADLESS-2"));
OK(getFromSocket("/dispatch workspace 10"));
Tests::spawnKitty();
OK(getFromSocket("/dispatch workspace 11"));
Tests::spawnKitty();
OK(getFromSocket("/dispatch focusmonitor HEADLESS-3"));
OK(getFromSocket("/dispatch workspace 12"));
Tests::spawnKitty();
OK(getFromSocket("/dispatch focusmonitor HEADLESS-2"));
{
auto str = getFromSocket("/activeworkspace");
EXPECT_CONTAINS(str, "workspace ID 11");
}
OK(getFromSocket("/dispatch workspace previous_per_monitor"));
{
auto str = getFromSocket("/activeworkspace");
EXPECT_CONTAINS(str, "workspace ID 10");
}
NLog::log("{}Killing all windows", Colors::YELLOW);
Tests::killAllWindows();
}
static void testMultimonBAF() {
NLog::log("{}Testing multimon back and forth", Colors::YELLOW);
OK(getFromSocket("/keyword binds:workspace_back_and_forth 1"));
OK(getFromSocket("/dispatch focusmonitor HEADLESS-2"));
OK(getFromSocket("/dispatch workspace 1"));
Tests::spawnKitty();
OK(getFromSocket("/dispatch workspace 2"));
Tests::spawnKitty();
OK(getFromSocket("/dispatch focusmonitor HEADLESS-3"));
OK(getFromSocket("/dispatch workspace 3"));
Tests::spawnKitty();
OK(getFromSocket("/dispatch workspace 3"));
{
auto str = getFromSocket("/activeworkspace");
EXPECT_CONTAINS(str, "workspace ID 2 ");
}
OK(getFromSocket("/dispatch workspace 4"));
OK(getFromSocket("/dispatch workspace 4"));
{
auto str = getFromSocket("/activeworkspace");
EXPECT_CONTAINS(str, "workspace ID 2 ");
}
OK(getFromSocket("/dispatch workspace 2"));
{
auto str = getFromSocket("/activeworkspace");
EXPECT_CONTAINS(str, "workspace ID 4 ");
}
OK(getFromSocket("/dispatch workspace 3"));
OK(getFromSocket("/dispatch 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"));
{
auto str = getFromSocket("/activeworkspace");
EXPECT_CONTAINS(str, "workspace ID 3 ");
}
Tests::killAllWindows();
}
static void testMultimonFocus() {
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"));
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;
}
}
OK(getFromSocket("/dispatch focuswindow class:a"));
OK(getFromSocket("/dispatch movefocus r"));
{
auto str = getFromSocket("/activeworkspace");
EXPECT_CONTAINS(str, "workspace ID 7 ");
}
{
auto str = getFromSocket("/activewindow");
EXPECT_CONTAINS(str, "class: b");
}
OK(getFromSocket("/dispatch movefocus r"));
{
auto str = getFromSocket("/activeworkspace");
EXPECT_CONTAINS(str, "workspace ID 8 ");
}
Tests::spawnKitty("c");
{
auto str = getFromSocket("/activewindow");
EXPECT_CONTAINS(str, "class: c");
}
{
auto str = getFromSocket("/activeworkspace");
EXPECT_CONTAINS(str, "workspace ID 8 ");
}
OK(getFromSocket("/dispatch movefocus l"));
{
auto str = getFromSocket("/activewindow");
EXPECT_CONTAINS(str, "class: b");
}
{
auto str = getFromSocket("/activeworkspace");
EXPECT_CONTAINS(str, "workspace ID 7 ");
}
OK(getFromSocket("/dispatch movewindow r"));
{
auto str = getFromSocket("/activewindow");
EXPECT_CONTAINS(str, "class: b");
}
{
auto str = getFromSocket("/activeworkspace");
EXPECT_CONTAINS(str, "workspace ID 8 ");
}
OK(getFromSocket("/dispatch movefocus r"));
{
auto str = getFromSocket("/activewindow");
EXPECT_CONTAINS(str, "class: c");
}
{
auto str = getFromSocket("/activeworkspace");
EXPECT_CONTAINS(str, "workspace ID 8 ");
}
OK(getFromSocket("/dispatch movefocus l"));
OK(getFromSocket("/keyword general:no_focus_fallback true"));
OK(getFromSocket("/keyword binds:window_direction_monitor_fallback false"));
EXPECT_NOT(getFromSocket("/dispatch movefocus l"), "ok");
{
auto str = getFromSocket("/activewindow");
EXPECT_CONTAINS(str, "class: b");
}
{
auto str = getFromSocket("/activeworkspace");
EXPECT_CONTAINS(str, "workspace ID 8 ");
}
OK(getFromSocket("/dispatch movewindow l"));
{
auto str = getFromSocket("/activewindow");
EXPECT_CONTAINS(str, "class: b");
}
{
auto str = getFromSocket("/activeworkspace");
EXPECT_CONTAINS(str, "workspace ID 8 ");
}
OK(getFromSocket("/reload"));
Tests::killAllWindows();
}
static void testDynamicWsEffects() {
// test dynamic workspace effects, they shouldn't lag
OK(getFromSocket("/dispatch workspace 69"));
Tests::spawnKitty("bitch");
OK(getFromSocket("r/keyword workspace 69,bordersize:20"));
OK(getFromSocket("r/keyword workspace 69,rounding:false"));
EXPECT(getFromSocket("/getprop class:bitch border_size"), "20");
EXPECT(getFromSocket("/getprop class:bitch rounding"), "0");
OK(getFromSocket("/reload"));
Tests::killAllWindows();
}
static bool test() {
NLog::log("{}Testing workspaces", Colors::GREEN);
@ -352,13 +756,21 @@ static bool test() {
EXPECT_CONTAINS(str, "class: kitty_B");
}
// destroy the headless output
OK(getFromSocket("/output remove HEADLESS-3"));
// kill all
NLog::log("{}Killing all windows", Colors::YELLOW);
Tests::killAllWindows();
testMultimonBAF();
testMultimonFocus();
testWorkspaceHistoryMultiMon();
// destroy the headless output
OK(getFromSocket("/output remove HEADLESS-3"));
testSpecialWorkspaceFullscreen();
testAsymmetricGaps();
testDynamicWsEffects();
NLog::log("{}Expecting 0 windows", Colors::YELLOW);
EXPECT(Tests::windowCount(), 0);

View file

@ -3,6 +3,7 @@
#include <cerrno>
#include <thread>
#include <print>
#include <fstream>
#include "../shared.hpp"
#include "../hyprctlCompat.hpp"
@ -39,6 +40,38 @@ CUniquePointer<CProcess> Tests::spawnKitty(const std::string& class_, const std:
return kitty;
}
CUniquePointer<CProcess> Tests::spawnLayerKitty(const std::string& namespace_, const std::vector<std::string> args) {
std::vector<std::string> programArgs = args;
if (!namespace_.empty()) {
programArgs.insert(programArgs.begin(), "--class");
programArgs.insert(programArgs.begin() + 1, namespace_);
}
programArgs.insert(programArgs.begin(), "+kitten");
programArgs.insert(programArgs.begin() + 1, "panel");
CUniquePointer<CProcess> kitty = makeUnique<CProcess>("kitty", programArgs);
kitty->addEnv("WAYLAND_DISPLAY", WLDISPLAY);
kitty->runAsync();
std::this_thread::sleep_for(std::chrono::milliseconds(10));
// wait while the layer spawns
int counter = 0;
while (processAlive(kitty->pid()) && countOccurrences(getFromSocket("/layers"), std::format("pid: {}", kitty->pid())) == 0) {
counter++;
std::this_thread::sleep_for(std::chrono::milliseconds(100));
if (counter > 50)
return nullptr;
}
if (!processAlive(kitty->pid()))
return nullptr;
return kitty;
}
bool Tests::processAlive(pid_t pid) {
errno = 0;
int ret = kill(pid, 0);
@ -96,6 +129,38 @@ void Tests::waitUntilWindowsN(int n) {
}
}
int Tests::layerCount() {
return countOccurrences(getFromSocket("/layers"), "namespace: ");
}
bool Tests::killAllLayers() {
auto str = getFromSocket("/layers");
auto pos = str.find("pid: ");
while (pos != std::string::npos) {
auto pid = stoi(str.substr(pos + 5, str.find('\n', pos)));
kill(pid, 15);
// we need to wait for a bit because for some reason otherwise we'll end up
// with layers with pid -1 if they are all removed at the same time
std::this_thread::sleep_for(std::chrono::milliseconds(100));
pos = str.find("pid: ", pos + 5);
}
int counter = 0;
while (Tests::layerCount() != 0) {
counter++;
std::this_thread::sleep_for(std::chrono::milliseconds(100));
if (counter > 50) {
std::println("{}Timed out waiting for layers to close", Colors::RED);
return false;
}
}
return true;
}
std::string Tests::execAndGet(const std::string& cmd) {
CProcess proc("/bin/sh", {"-c", cmd});
@ -105,3 +170,24 @@ std::string Tests::execAndGet(const std::string& cmd) {
return proc.stdOut();
}
bool Tests::writeFile(const std::string& name, const std::string& contents) {
std::ofstream of(name, std::ios::trunc);
if (!of.good())
return false;
of << contents;
of.close();
return true;
}
std::string Tests::getWindowAttribute(const std::string& winInfo, const std::string& attr) {
auto pos = winInfo.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);
}

View file

@ -9,10 +9,15 @@
//NOLINTNEXTLINE
namespace Tests {
Hyprutils::Memory::CUniquePointer<Hyprutils::OS::CProcess> spawnKitty(const std::string& class_ = "", const std::vector<std::string> args = {});
Hyprutils::Memory::CUniquePointer<Hyprutils::OS::CProcess> spawnLayerKitty(const std::string& namespace_ = "", const std::vector<std::string> args = {});
bool processAlive(pid_t pid);
int windowCount();
int countOccurrences(const std::string& in, const std::string& what);
bool killAllWindows();
void waitUntilWindowsN(int n);
int layerCount();
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);
};

View file

@ -179,6 +179,17 @@ 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
@ -239,7 +250,7 @@ bind = $mainMod, E, exec, $fileManager
bind = $mainMod, V, togglefloating,
bind = $mainMod, R, exec, $menu
bind = $mainMod, P, pseudo, # dwindle
bind = $mainMod, J, togglesplit, # dwindle
bind = $mainMod, J, layoutmsg, togglesplit, # dwindle
# Move focus with mainMod + arrow keys
bind = $mainMod, left, movefocus, l
@ -398,3 +409,5 @@ 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

View file

@ -11,7 +11,9 @@
cairo,
epoll-shim,
git,
glaze,
glaze-hyprland,
glslang,
gtest,
hyprcursor,
hyprgraphics,
hyprland-protocols,
@ -19,10 +21,18 @@
hyprlang,
hyprutils,
hyprwayland-scanner,
hyprwire,
lcms2,
libGL,
libdrm,
libexecinfo,
libinput,
libxcb,
libxcb-errors,
libxcb-render-util,
libxcb-wm,
libxdmcp,
libxcursor,
libxkbcommon,
libuuid,
libgbm,
@ -36,9 +46,9 @@
wayland,
wayland-protocols,
wayland-scanner,
xorg,
xwayland,
debug ? false,
withTests ? debug,
enableXWayland ? true,
withSystemd ? lib.meta.availableOn stdenv.hostPlatform systemd,
wrapRuntimeDeps ? true,
@ -52,12 +62,23 @@
hidpiXWayland ? false,
legacyRenderer ? false,
withHyprtester ? false,
}: let
}:
let
inherit (builtins) foldl' readFile;
inherit (lib.asserts) assertMsg;
inherit (lib.attrsets) mapAttrsToList;
inherit (lib.lists) flatten concatLists optional optionals;
inherit (lib.strings) makeBinPath optionalString cmakeBool trim;
inherit (lib.lists)
flatten
concatLists
optional
optionals
;
inherit (lib.strings)
makeBinPath
optionalString
cmakeBool
trim
;
fs = lib.fileset;
adapters = flatten [
@ -67,162 +88,184 @@
customStdenv = foldl' (acc: adapter: adapter acc) stdenv adapters;
in
assert assertMsg (!nvidiaPatches) "The option `nvidiaPatches` has been removed.";
assert assertMsg (!enableNvidiaPatches) "The option `enableNvidiaPatches` has been removed.";
assert assertMsg (!hidpiXWayland) "The option `hidpiXWayland` has been removed. Please refer https://wiki.hypr.land/Configuring/XWayland";
assert assertMsg (!legacyRenderer) "The option `legacyRenderer` has been removed. Legacy renderer is no longer supported.";
assert assertMsg (!withHyprtester) "The option `withHyprtester` has been removed. Hyprtester is always built now.";
customStdenv.mkDerivation (finalAttrs: {
pname = "hyprland${optionalString debug "-debug"}";
inherit version;
assert assertMsg (!nvidiaPatches) "The option `nvidiaPatches` has been removed.";
assert assertMsg (!enableNvidiaPatches) "The option `enableNvidiaPatches` has been removed.";
assert assertMsg (!hidpiXWayland)
"The option `hidpiXWayland` has been removed. Please refer https://wiki.hypr.land/Configuring/XWayland";
assert assertMsg (
!legacyRenderer
) "The option `legacyRenderer` has been removed. Legacy renderer is no longer supported.";
assert assertMsg (
!withHyprtester
) "The option `withHyprtester` has been removed. Hyprtester is always built now.";
customStdenv.mkDerivation (finalAttrs: {
pname = "hyprland${optionalString debug "-debug"}";
inherit version withTests;
src = fs.toSource {
root = ../.;
fileset =
fs.intersection
# allows non-flake builds to only include files tracked by git
(fs.gitTracked ../.)
(fs.unions (flatten [
src = fs.toSource {
root = ../.;
fileset =
fs.intersection
# allows non-flake builds to only include files tracked by git
(fs.gitTracked ../.)
(
fs.unions (flatten [
../assets/hyprland-portals.conf
../assets/install
../hyprctl
../hyprland.pc.in
../hyprtester
../hyprpm
../LICENSE
../protocols
../src
../start
../systemd
../VERSION
(fs.fileFilter (file: file.hasExt "1") ../docs)
(fs.fileFilter (file: file.hasExt "conf" || file.hasExt "desktop") ../example)
(fs.fileFilter (file: file.hasExt "conf" || file.hasExt "in") ../example)
(fs.fileFilter (file: file.hasExt "sh") ../scripts)
(fs.fileFilter (file: file.name == "CMakeLists.txt") ../.)
]));
};
(optional withTests [
../tests
../hyprtester
])
])
);
};
postPatch = ''
# Fix hardcoded paths to /usr installation
sed -i "s#/usr#$out#" src/render/OpenGL.cpp
postPatch = ''
# Fix hardcoded paths to /usr installation
sed -i "s#/usr#$out#" src/render/OpenGL.cpp
# Remove extra @PREFIX@ to fix pkg-config paths
sed -i "s#@PREFIX@/##g" hyprland.pc.in
'';
# Remove extra @PREFIX@ to fix some paths
sed -i "s#@PREFIX@/##g" hyprland.pc.in
sed -i "s#@PREFIX@/##g" example/hyprland.desktop.in
'';
env = {
GIT_COMMITS = revCount;
GIT_COMMIT_DATE = date;
GIT_COMMIT_HASH = commit;
GIT_DIRTY = if (commit == "") then "clean" else "dirty";
GIT_TAG = "v${trim (readFile "${finalAttrs.src}/VERSION")}";
};
env = {
GIT_COMMITS = revCount;
GIT_COMMIT_DATE = date;
GIT_COMMIT_HASH = commit;
GIT_DIRTY = if (commit == "") then "clean" else "dirty";
GIT_TAG = "v${trim (readFile "${finalAttrs.src}/VERSION")}";
};
depsBuildBuild = [
pkg-config
];
depsBuildBuild = [
pkg-config
];
nativeBuildInputs = [
hyprwayland-scanner
makeWrapper
cmake
pkg-config
];
nativeBuildInputs = [
hyprwayland-scanner
hyprwire
makeWrapper
cmake
pkg-config
];
outputs = [
"out"
"man"
"dev"
];
outputs = [
"out"
"man"
"dev"
];
buildInputs = concatLists [
[
aquamarine
cairo
git
glaze
hyprcursor
hyprgraphics
hyprland-protocols
hyprlang
hyprutils
libdrm
libGL
libinput
libuuid
libxkbcommon
libgbm
muparser
pango
pciutils
re2
tomlplusplus
udis86-hyprland
wayland
wayland-protocols
wayland-scanner
xorg.libXcursor
]
(optionals customStdenv.hostPlatform.isBSD [epoll-shim])
(optionals customStdenv.hostPlatform.isMusl [libexecinfo])
(optionals enableXWayland [
xorg.libxcb
xorg.libXdmcp
xorg.xcbutilerrors
xorg.xcbutilrenderutil
xorg.xcbutilwm
xwayland
])
(optional withSystemd systemd)
];
buildInputs = concatLists [
[
aquamarine
cairo
git
glaze-hyprland
glslang
gtest
hyprcursor
hyprgraphics
hyprland-protocols
hyprlang
hyprutils
hyprwire
lcms2
libdrm
libgbm
libGL
libinput
libuuid
libxcursor
libxkbcommon
muparser
pango
pciutils
re2
tomlplusplus
udis86-hyprland
wayland
wayland-protocols
wayland-scanner
]
(optionals customStdenv.hostPlatform.isBSD [ epoll-shim ])
(optionals customStdenv.hostPlatform.isMusl [ libexecinfo ])
(optionals enableXWayland [
libxcb
libxcb-errors
libxcb-render-util
libxcb-wm
libxdmcp
xwayland
])
(optional withSystemd systemd)
];
strictDeps = true;
strictDeps = true;
cmakeBuildType =
if debug
then "Debug"
else "RelWithDebInfo";
cmakeBuildType = if debug then "Debug" else "RelWithDebInfo";
# we want as much debug info as possible
dontStrip = debug;
# we want as much debug info as possible
dontStrip = debug;
cmakeFlags = mapAttrsToList cmakeBool {
"NO_XWAYLAND" = !enableXWayland;
"LEGACY_RENDERER" = legacyRenderer;
"NO_SYSTEMD" = !withSystemd;
"CMAKE_DISABLE_PRECOMPILE_HEADERS" = true;
"NO_UWSM" = true;
"NO_HYPRPM" = true;
"TRACY_ENABLE" = false;
"BUILD_HYPRTESTER" = true;
};
cmakeFlags = mapAttrsToList cmakeBool {
"BUILT_WITH_NIX" = true;
"NO_XWAYLAND" = !enableXWayland;
"LEGACY_RENDERER" = legacyRenderer;
"NO_SYSTEMD" = !withSystemd;
"CMAKE_DISABLE_PRECOMPILE_HEADERS" = true;
"NO_UWSM" = !withSystemd;
"TRACY_ENABLE" = false;
"WITH_TESTS" = withTests;
};
preConfigure = ''
substituteInPlace hyprtester/CMakeLists.txt --replace-fail \
"\''${CMAKE_CURRENT_BINARY_DIR}" \
"${placeholder "out"}/bin"
'';
preConfigure = ''
substituteInPlace hyprtester/CMakeLists.txt --replace-fail \
"\''${CMAKE_CURRENT_BINARY_DIR}" \
"${placeholder "out"}/bin"
'';
postInstall = ''
${optionalString wrapRuntimeDeps ''
wrapProgram $out/bin/Hyprland \
--suffix PATH : ${makeBinPath [
postInstall = ''
${optionalString wrapRuntimeDeps ''
wrapProgram $out/bin/Hyprland \
--suffix PATH : ${
makeBinPath [
binutils
hyprland-guiutils
pciutils
pkgconf
]}
''}
]
}
''}
install hyprtester/pointer-warp -t $out/bin
install hyprtester/pointer-scroll -t $out/bin
'';
${optionalString withTests ''
install hyprtester/pointer-warp -t $out/bin
install hyprtester/pointer-scroll -t $out/bin
install hyprtester/shortcut-inhibitor -t $out/bin
install hyprland_gtests -t $out/bin
install hyprtester/child-window -t $out/bin
''}
'';
passthru.providedSessions = ["hyprland"];
passthru.providedSessions = [ "hyprland" ] ++ optionals withSystemd [ "hyprland-uwsm" ];
meta = {
homepage = "https://github.com/hyprwm/Hyprland";
description = "Dynamic tiling Wayland compositor that doesn't sacrifice on its looks";
license = lib.licenses.bsd3;
platforms = lib.platforms.linux;
mainProgram = "Hyprland";
};
})
meta = {
homepage = "https://github.com/hyprwm/Hyprland";
description = "Dynamic tiling Wayland compositor that doesn't sacrifice on its looks";
license = lib.licenses.bsd3;
platforms = lib.platforms.linux;
mainProgram = "Hyprland";
};
})

View file

@ -2,7 +2,7 @@
writeShellApplication,
deadnix,
statix,
alejandra,
nixfmt,
llvmPackages_19,
fd,
}:
@ -11,7 +11,7 @@ writeShellApplication {
runtimeInputs = [
deadnix
statix
alejandra
nixfmt
llvmPackages_19.clang-tools
fd
];
@ -24,14 +24,14 @@ writeShellApplication {
nix_format() {
if [ "$*" = 0 ]; then
fd '.*\.nix' . -E "$excludes" -x statix fix -- {} \;
fd '.*\.nix' . -E "$excludes" -X deadnix -e -- {} \; -X alejandra {} \;
fd '.*\.nix' . -E "$excludes" -X deadnix -e -- {} \; -X nixfmt {} \;
elif [ -d "$1" ]; then
fd '.*\.nix' "$1" -E "$excludes" -i -x statix fix -- {} \;
fd '.*\.nix' "$1" -E "$excludes" -i -X deadnix -e -- {} \; -X alejandra {} \;
fd '.*\.nix' "$1" -E "$excludes" -i -X deadnix -e -- {} \; -X nixfmt {} \;
else
statix fix -- "$1"
deadnix -e "$1"
alejandra "$1"
nixfmt "$1"
fi
}

View file

@ -1,13 +1,15 @@
self: {
config,
self:
{
lib,
pkgs,
...
}: let
}:
let
inherit (pkgs.stdenv.hostPlatform) system;
package = self.packages.${system}.default;
in {
in
{
config = {
wayland.windowManager.hyprland.package = lib.mkDefault package;
};

View file

@ -1,4 +1,5 @@
lib: let
lib:
let
inherit (lib)
attrNames
filterAttrs
@ -17,7 +18,7 @@ lib: let
This function takes a nested attribute set and converts it into Hyprland-compatible
configuration syntax, supporting top, bottom, and regular command sections.
Commands are flattened using the `flattenAttrs` function, and attributes are formatted as
`key = value` pairs. Lists are expanded as duplicate keys to match Hyprland's expected format.
@ -81,44 +82,51 @@ lib: let
:::
*/
toHyprlang = {
topCommandsPrefixes ? ["$" "bezier"],
bottomCommandsPrefixes ? [],
}: attrs: let
toHyprlang' = attrs: let
# Specially configured `toKeyValue` generator with support for duplicate keys
# and a legible key-value separator.
mkCommands = generators.toKeyValue {
mkKeyValue = generators.mkKeyValueDefault {} " = ";
listsAsDuplicateKeys = true;
indent = ""; # No indent, since we don't have nesting
};
toHyprlang =
{
topCommandsPrefixes ? [
"$"
"bezier"
],
bottomCommandsPrefixes ? [ ],
}:
attrs:
let
toHyprlang' =
attrs:
let
# Specially configured `toKeyValue` generator with support for duplicate keys
# and a legible key-value separator.
mkCommands = generators.toKeyValue {
mkKeyValue = generators.mkKeyValueDefault { } " = ";
listsAsDuplicateKeys = true;
indent = ""; # No indent, since we don't have nesting
};
# Flatten the attrset, combining keys in a "path" like `"a:b:c" = "x"`.
# Uses `flattenAttrs` with a colon separator.
commands = flattenAttrs (p: k: "${p}:${k}") attrs;
# Flatten the attrset, combining keys in a "path" like `"a:b:c" = "x"`.
# Uses `flattenAttrs` with a colon separator.
commands = flattenAttrs (p: k: "${p}:${k}") attrs;
# General filtering function to check if a key starts with any prefix in a given list.
filterCommands = list: n:
foldl (acc: prefix: acc || hasPrefix prefix n) false list;
# General filtering function to check if a key starts with any prefix in a given list.
filterCommands = list: n: foldl (acc: prefix: acc || hasPrefix prefix n) false list;
# Partition keys into top commands and the rest
result = partition (filterCommands topCommandsPrefixes) (attrNames commands);
topCommands = filterAttrs (n: _: builtins.elem n result.right) commands;
remainingCommands = removeAttrs commands result.right;
# Partition keys into top commands and the rest
result = partition (filterCommands topCommandsPrefixes) (attrNames commands);
topCommands = filterAttrs (n: _: builtins.elem n result.right) commands;
remainingCommands = removeAttrs commands result.right;
# Partition remaining commands into bottom commands and regular commands
result2 = partition (filterCommands bottomCommandsPrefixes) result.wrong;
bottomCommands = filterAttrs (n: _: builtins.elem n result2.right) remainingCommands;
regularCommands = removeAttrs remainingCommands result2.right;
# Partition remaining commands into bottom commands and regular commands
result2 = partition (filterCommands bottomCommandsPrefixes) result.wrong;
bottomCommands = filterAttrs (n: _: builtins.elem n result2.right) remainingCommands;
regularCommands = removeAttrs remainingCommands result2.right;
in
# Concatenate strings from mapping `mkCommands` over top, regular, and bottom commands.
concatMapStrings mkCommands [
topCommands
regularCommands
bottomCommands
];
in
# Concatenate strings from mapping `mkCommands` over top, regular, and bottom commands.
concatMapStrings mkCommands [
topCommands
regularCommands
bottomCommands
];
in
toHyprlang' attrs;
/**
@ -131,7 +139,7 @@ lib: let
Configuration:
* `pred` - A function `(string -> string -> string)` defining how keys should be concatenated.
# Inputs
Structured function argument:
@ -139,7 +147,7 @@ lib: let
: pred (required)
: A function that determines how parent and child keys should be combined into a single key.
It takes a `prefix` (parent key) and `key` (current key) and returns the joined key.
Value:
: The nested attribute set to be flattened.
@ -174,26 +182,21 @@ lib: let
```
:::
*/
flattenAttrs = pred: attrs: let
flattenAttrs' = prefix: attrs:
builtins.foldl' (
acc: key: let
value = attrs.${key};
newKey =
if prefix == ""
then key
else pred prefix key;
in
acc
// (
if builtins.isAttrs value
then flattenAttrs' newKey value
else {"${newKey}" = value;}
)
) {} (builtins.attrNames attrs);
in
flattenAttrs =
pred: attrs:
let
flattenAttrs' =
prefix: attrs:
builtins.foldl' (
acc: key:
let
value = attrs.${key};
newKey = if prefix == "" then key else pred prefix key;
in
acc // (if builtins.isAttrs value then flattenAttrs' newKey value else { "${newKey}" = value; })
) { } (builtins.attrNames attrs);
in
flattenAttrs' "" attrs;
in
{

View file

@ -1,18 +1,21 @@
inputs: {
inputs:
{
config,
lib,
pkgs,
...
}: let
}:
let
inherit (pkgs.stdenv.hostPlatform) system;
selflib = import ./lib.nix lib;
cfg = config.programs.hyprland;
in {
in
{
options = {
programs.hyprland = {
plugins = lib.mkOption {
type = with lib.types; listOf (either package path);
default = [];
default = [ ];
description = ''
List of Hyprland plugins to use. Can either be packages or
absolute plugin paths.
@ -20,23 +23,25 @@ in {
};
settings = lib.mkOption {
type = with lib.types; let
valueType =
nullOr (oneOf [
bool
int
float
str
path
(attrsOf valueType)
(listOf valueType)
])
// {
description = "Hyprland configuration value";
};
in
type =
with lib.types;
let
valueType =
nullOr (oneOf [
bool
int
float
str
path
(attrsOf valueType)
(listOf valueType)
])
// {
description = "Hyprland configuration value";
};
in
valueType;
default = {};
default = { };
description = ''
Hyprland configuration written in Nix. Entries with the same key
should be written as lists. Variables' and colors' names should be
@ -92,8 +97,15 @@ in {
topPrefixes = lib.mkOption {
type = with lib.types; listOf str;
default = ["$" "bezier"];
example = ["$" "bezier" "source"];
default = [
"$"
"bezier"
];
example = [
"$"
"bezier"
"source"
];
description = ''
List of prefix of attributes to put at the top of the config.
'';
@ -101,8 +113,8 @@ in {
bottomPrefixes = lib.mkOption {
type = with lib.types; listOf str;
default = [];
example = ["source"];
default = [ ];
example = [ "source" ];
description = ''
List of prefix of attributes to put at the bottom of the config.
'';
@ -117,35 +129,36 @@ in {
};
}
(lib.mkIf cfg.enable {
environment.etc."xdg/hypr/hyprland.conf" = let
shouldGenerate = cfg.extraConfig != "" || cfg.settings != {} || cfg.plugins != [];
environment.etc."xdg/hypr/hyprland.conf" =
let
shouldGenerate = cfg.extraConfig != "" || cfg.settings != { } || cfg.plugins != [ ];
pluginsToHyprlang = plugins:
selflib.toHyprlang {
topCommandsPrefixes = cfg.topPrefixes;
bottomCommandsPrefixes = cfg.bottomPrefixes;
}
{
"exec-once" = let
mkEntry = entry:
if lib.types.package.check entry
then "${entry}/lib/lib${entry.pname}.so"
else entry;
hyprctl = lib.getExe' config.programs.hyprland.package "hyprctl";
in
map (p: "${hyprctl} plugin load ${mkEntry p}") cfg.plugins;
};
in
lib.mkIf shouldGenerate {
text =
lib.optionalString (cfg.plugins != [])
(pluginsToHyprlang cfg.plugins)
+ lib.optionalString (cfg.settings != {})
(selflib.toHyprlang {
pluginsToHyprlang =
_plugins:
selflib.toHyprlang
{
topCommandsPrefixes = cfg.topPrefixes;
bottomCommandsPrefixes = cfg.bottomPrefixes;
}
cfg.settings)
{
"exec-once" =
let
mkEntry =
entry: if lib.types.package.check entry then "${entry}/lib/lib${entry.pname}.so" else entry;
hyprctl = lib.getExe' config.programs.hyprland.package "hyprctl";
in
map (p: "${hyprctl} plugin load ${mkEntry p}") cfg.plugins;
};
in
lib.mkIf shouldGenerate {
text =
lib.optionalString (cfg.plugins != [ ]) (pluginsToHyprlang cfg.plugins)
+ lib.optionalString (cfg.settings != { }) (
selflib.toHyprlang {
topCommandsPrefixes = cfg.topPrefixes;
bottomCommandsPrefixes = cfg.bottomPrefixes;
} cfg.settings
)
+ lib.optionalString (cfg.extraConfig != "") cfg.extraConfig;
};
})

View file

@ -2,22 +2,29 @@
self,
lib,
inputs,
}: let
mkDate = longDate: (lib.concatStringsSep "-" [
(builtins.substring 0 4 longDate)
(builtins.substring 4 2 longDate)
(builtins.substring 6 2 longDate)
]);
}:
let
mkDate =
longDate:
(lib.concatStringsSep "-" [
(builtins.substring 0 4 longDate)
(builtins.substring 4 2 longDate)
(builtins.substring 6 2 longDate)
]);
ver = lib.removeSuffix "\n" (builtins.readFile ../VERSION);
in {
in
{
# Contains what a user is most likely to care about:
# Hyprland itself, XDPH and the Share Picker.
default = lib.composeManyExtensions (with self.overlays; [
hyprland-packages
hyprland-extras
]);
default = lib.composeManyExtensions (
with self.overlays;
[
hyprland
hyprland-extras
]
);
# Packages for variations of Hyprland, dependencies included.
# Packages for variations of Hyprland, all dependencies included.
hyprland-packages = lib.composeManyExtensions [
# Dependencies
inputs.aquamarine.overlays.default
@ -28,61 +35,68 @@ in {
inputs.hyprlang.overlays.default
inputs.hyprutils.overlays.default
inputs.hyprwayland-scanner.overlays.default
self.overlays.udis86
inputs.hyprwire.overlays.default
# Hyprland packages themselves
(final: _prev: let
self.overlays.hyprland
];
# Hyprland with its internal dependencies.
hyprland = lib.composeManyExtensions (with self.overlays; [
udis86
glaze
hyprland-no-deps
]);
# Hyprland without any dependencies.
hyprland-no-deps =
final: _prev:
let
date = mkDate (self.lastModifiedDate or "19700101");
version = "${ver}+date=${date}_${self.shortRev or "dirty"}";
in {
in
{
hyprland = final.callPackage ./default.nix {
stdenv = final.gcc15Stdenv;
commit = self.rev or "";
revCount = self.sourceInfo.revCount or "";
inherit date version;
};
hyprland-unwrapped = final.hyprland.override {wrapRuntimeDeps = false;};
hyprland-with-hyprtester =
builtins.trace ''
hyprland-with-hyprtester was removed. Please use the hyprland package.
Hyprtester is always built now.
''
final.hyprland;
hyprland-unwrapped = final.hyprland.override { wrapRuntimeDeps = false; };
hyprland-with-tests = final.hyprland.override { withTests = true; };
hyprland-with-hyprtester = builtins.trace ''
hyprland-with-hyprtester was removed. Please use the hyprland package.
Hyprtester is always built now.
'' final.hyprland;
# deprecated packages
hyprland-legacy-renderer =
builtins.trace ''
hyprland-legacy-renderer was removed. Please use the hyprland package.
Legacy renderer is no longer supported.
''
final.hyprland;
hyprland-legacy-renderer = builtins.trace ''
hyprland-legacy-renderer was removed. Please use the hyprland package.
Legacy renderer is no longer supported.
'' final.hyprland;
hyprland-nvidia =
builtins.trace ''
hyprland-nvidia was removed. Please use the hyprland package.
Nvidia patches are no longer needed.
''
final.hyprland;
hyprland-nvidia = builtins.trace ''
hyprland-nvidia was removed. Please use the hyprland package.
Nvidia patches are no longer needed.
'' final.hyprland;
hyprland-hidpi =
builtins.trace ''
hyprland-hidpi was removed. Please use the hyprland package.
For more information, refer to https://wiki.hypr.land/Configuring/XWayland.
''
final.hyprland;
})
];
hyprland-hidpi = builtins.trace ''
hyprland-hidpi was removed. Please use the hyprland package.
For more information, refer to https://wiki.hypr.land/Configuring/XWayland.
'' final.hyprland;
};
# Debug
hyprland-debug = lib.composeManyExtensions [
# Dependencies
self.overlays.hyprland-packages
(final: prev: {
aquamarine = prev.aquamarine.override {debug = true;};
hyprutils = prev.hyprutils.override {debug = true;};
hyprland-debug = prev.hyprland.override {debug = true;};
(_final: prev: {
aquamarine = prev.aquamarine.override { debug = true; };
hyprutils = prev.hyprutils.override { debug = true; };
hyprland-debug = prev.hyprland.override { debug = true; };
})
];
@ -96,15 +110,26 @@ in {
# this version is the one used in the git submodule, and allows us to
# fetch the source without '?submodules=1'
udis86 = final: prev: {
udis86-hyprland = prev.udis86.overrideAttrs (_self: _super: {
src = final.fetchFromGitHub {
owner = "canihavesomecoffee";
repo = "udis86";
rev = "5336633af70f3917760a6d441ff02d93477b0c86";
hash = "sha256-HifdUQPGsKQKQprByeIznvRLONdOXeolOsU5nkwIv3g=";
};
udis86-hyprland = prev.udis86.overrideAttrs (
_self: _super: {
src = final.fetchFromGitHub {
owner = "canihavesomecoffee";
repo = "udis86";
rev = "5336633af70f3917760a6d441ff02d93477b0c86";
hash = "sha256-HifdUQPGsKQKQprByeIznvRLONdOXeolOsU5nkwIv3g=";
};
patches = [];
});
patches = [ ];
}
);
};
# Even though glaze itself disables it by default, nixpkgs sets ENABLE_SSL set to true.
# Since we don't include openssl, the build failes without the `enableSSL = false;` override
glaze = _final: prev: {
glaze-hyprland = prev.glaze.override {
enableSSL = false;
enableInterop = false;
};
};
}

View file

@ -1,89 +1,107 @@
inputs: pkgs: let
inputs: pkgs:
let
flake = inputs.self.packages.${pkgs.stdenv.hostPlatform.system};
hyprland = flake.hyprland;
in {
hyprland = flake.hyprland-with-tests;
in
{
tests = pkgs.testers.runNixOSTest {
name = "hyprland-tests";
nodes.machine = {pkgs, ...}: {
environment.systemPackages = with pkgs; [
# Programs needed for tests
jq
kitty
wl-clipboard
xorg.xeyes
];
nodes.machine =
{ pkgs, ... }:
{
environment.systemPackages = with pkgs; [
# Programs needed for tests
jq
kitty
wl-clipboard
xeyes
];
# Enabled by default for some reason
services.speechd.enable = false;
# Enabled by default for some reason
services.speechd.enable = false;
environment.variables = {
"AQ_TRACE" = "1";
"HYPRLAND_TRACE" = "1";
"XDG_RUNTIME_DIR" = "/tmp";
"XDG_CACHE_HOME" = "/tmp";
"KITTY_CONFIG_DIRECTORY" = "/etc/kitty";
};
environment.etc."kitty/kitty.conf".text = ''
confirm_os_window_close 0
'';
programs.hyprland = {
enable = true;
package = hyprland;
# We don't need portals in this test, so we don't set portalPackage
};
# Test configuration
environment.etc."test.conf".source = "${hyprland}/share/hypr/test.conf";
# Disable portals
xdg.portal.enable = pkgs.lib.mkForce false;
# Autologin root into tty
services.getty.autologinUser = "alice";
system.stateVersion = "24.11";
users.users.alice = {
isNormalUser = true;
};
virtualisation = {
cores = 4;
# Might crash with less
memorySize = 8192;
resolution = {
x = 1920;
y = 1080;
environment.variables = {
"AQ_TRACE" = "1";
"HYPRLAND_TRACE" = "1";
"XDG_RUNTIME_DIR" = "/tmp";
"XDG_CACHE_HOME" = "/tmp";
"KITTY_CONFIG_DIRECTORY" = "/etc/kitty";
};
# Doesn't seem to do much, thought it would fix XWayland crashing
qemu.options = ["-vga none -device virtio-gpu-pci"];
environment.etc."kitty/kitty.conf".text = ''
confirm_os_window_close 0
remember_window_size no
initial_window_width 640
initial_window_height 400
'';
programs.hyprland = {
enable = true;
package = hyprland;
# We don't need portals in this test, so we don't set portalPackage
};
# Test configuration
environment.etc."test.conf".source = "${hyprland}/share/hypr/test.conf";
# Disable portals
xdg.portal.enable = pkgs.lib.mkForce false;
# Autologin root into tty
services.getty.autologinUser = "alice";
system.stateVersion = "24.11";
users.users.alice = {
isNormalUser = true;
};
virtualisation = {
cores = 4;
# Might crash with less
memorySize = 8192;
resolution = {
x = 1920;
y = 1080;
};
# Doesn't seem to do much, thought it would fix XWayland crashing
qemu.options = [ "-vga none -device virtio-gpu-pci" ];
};
};
};
testScript = ''
# Wait for tty to be up
machine.wait_for_unit("multi-user.target")
# Run gtests
print("Running gtests")
exit_status, _out = machine.execute("su - alice -c 'hyprland_gtests 2>&1 | tee /tmp/gtestslog; exit ''${PIPESTATUS[0]}'")
machine.execute(f'echo {exit_status} > /tmp/exit_status_gtests')
# Print logs for visibility in CI
_, out = machine.execute("cat /tmp/gtestslog")
print(f"gtests log:\n{out}")
# 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]}'")
print(f"Hyprtester exited with {exit_status}")
# Copy logs to host
machine.execute('cp "$(find /tmp/hypr -name *.log | head -1)" /tmp/hyprlog')
machine.execute(f'echo {exit_status} > /tmp/exit_status')
machine.copy_from_vm("/tmp/testerlog")
machine.copy_from_vm("/tmp/hyprlog")
machine.copy_from_vm("/tmp/exit_status")
# Print logs for visibility in CI
_, out = machine.execute("cat /tmp/testerlog")
print(f"Hyprtester log:\n{out}")
# Copy logs to host
machine.execute('cp "$(find /tmp/hypr -name *.log | head -1)" /tmp/hyprlog')
machine.execute(f'echo {exit_status} > /tmp/exit_status')
machine.copy_from_vm("/tmp/gtestslog")
machine.copy_from_vm("/tmp/testerlog")
machine.copy_from_vm("/tmp/hyprlog")
machine.copy_from_vm("/tmp/exit_status")
# Finally - shutdown
machine.shutdown()
'';

View file

@ -1,366 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<protocol name="frog_color_management_v1">
<copyright>
Copyright © 2023 Joshua Ashton for Valve Software
Copyright © 2023 Xaver Hugl
Permission is hereby granted, free of charge, to any person obtaining a
copy of this software and associated documentation files (the "Software"),
to deal in the Software without restriction, including without limitation
the rights to use, copy, modify, merge, publish, distribute, sublicense,
and/or sell copies of the Software, and to permit persons to whom the
Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice (including the next
paragraph) shall be included in all copies or substantial portions of the
Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE.
</copyright>
<description summary="experimental color management protocol">
The aim of this color management extension is to get HDR games working quickly,
and have an easy way to test implementations in the wild before the upstream
protocol is ready to be merged.
For that purpose it's intentionally limited and cut down and does not serve
all uses cases.
</description>
<interface name="frog_color_management_factory_v1" version="1">
<description summary="color management factory">
The color management factory singleton creates color managed surface objects.
</description>
<request name="destroy" type="destructor"></request>
<request name="get_color_managed_surface">
<description summary="create color management interface for surface">
</description>
<arg name="surface" type="object" interface="wl_surface"
summary="target surface" />
<arg name="callback" type="new_id" interface="frog_color_managed_surface"
summary="new color managed surface object" />
</request>
</interface>
<interface name="frog_color_managed_surface" version="1">
<description summary="color managed surface">
Interface for changing surface color management and HDR state.
An implementation must: support every part of the version
of the frog_color_managed_surface interface it exposes.
Including all known enums associated with a given version.
</description>
<request name="destroy" type="destructor">
<description summary="destroy color managed surface">
Destroying the color managed surface resets all known color
state for the surface back to 'undefined' implementation-specific
values.
</description>
</request>
<enum name="transfer_function">
<description summary="known transfer functions">
Extended information on the transfer functions described
here can be found in the Khronos Data Format specification:
https://registry.khronos.org/DataFormat/specs/1.3/dataformat.1.3.html
</description>
<entry name="undefined" value="0"
summary="specifies undefined, implementation-specific handling of the surface's transfer function." />
<entry name="srgb" value="1"
summary="specifies the sRGB non-linear EOTF. An implementation may: display this as Gamma 2.2 for the purposes of being consistent with content rendering across displays, rendering_intent and user expectations." />
<entry name="gamma_22" value="2" summary="specifies gamma 2.2 power curve as the EOTF" />
<entry name="st2084_pq" value="3"
summary="specifies the SMPTE ST2084 Perceptual Quantizer (PQ) EOTF" />
<entry name="scrgb_linear" value="4"
summary="specifies the scRGB (extended sRGB) linear EOTF. Note: Primaries outside the gamut triangle specified can be expressed with negative values for this transfer function." />
</enum>
<request name="set_known_transfer_function">
<description summary="sets a known transfer function for a surface" />
<arg name="transfer_function" type="uint" enum="transfer_function"
summary="transfer function for the surface" />
</request>
<enum name="primaries">
<description summary="known primaries" />
<entry name="undefined" value="0"
summary="specifies undefined, implementation-specific handling" />
<entry name="rec709" value="1" summary="specifies Rec.709/sRGB primaries with D65 white point" />
<entry name="rec2020" value="2"
summary="specifies Rec.2020/HDR10 primaries with D65 white point" />
</enum>
<request name="set_known_container_color_volume">
<description summary="sets the container color volume (primaries) for a surface" />
<arg name="primaries" type="uint" enum="primaries" summary="primaries for the surface" />
</request>
<enum name="render_intent">
<description summary="known render intents">
Extended information on render intents described
here can be found in ICC.1:2022:
https://www.color.org/specification/ICC.1-2022-05.pdf
</description>
<entry name="perceptual" value="0" summary="perceptual" />
</enum>
<request name="set_render_intent">
<description summary="sets the render intent for a surface">
NOTE: On a surface with "perceptual" (default) render intent, handling of the container's
color volume
is implementation-specific, and may differ between different transfer functions it is paired
with:
ie. sRGB + 709 rendering may have it's primaries widened to more of the available display's
gamut
to be be more pleasing for the viewer.
Compared to scRGB Linear + 709 being treated faithfully as 709
(including utilizing negatives out of the 709 gamut triangle)
</description>
<arg name="render_intent" type="uint" enum="render_intent"
summary="render intent for the surface" />
</request>
<request name="set_hdr_metadata">
<description summary="set HDR metadata for a surface">
Forwards HDR metadata from the client to the compositor.
HDR Metadata Infoframe as per CTA 861.G spec.
Usage of this HDR metadata is implementation specific and
outside of the scope of this protocol.
</description>
<arg name="mastering_display_primary_red_x" type="uint">
<description summary="red primary x coordinate">
Mastering Red Color Primary X Coordinate of the Data.
Coded as unsigned 16-bit values in units of
0.00002, where 0x0000 represents zero and 0xC350
represents 1.0000.
</description>
</arg>
<arg name="mastering_display_primary_red_y" type="uint">
<description summary="red primary y coordinate">
Mastering Red Color Primary Y Coordinate of the Data.
Coded as unsigned 16-bit values in units of
0.00002, where 0x0000 represents zero and 0xC350
represents 1.0000.
</description>
</arg>
<arg name="mastering_display_primary_green_x" type="uint">
<description summary="green primary x coordinate">
Mastering Green Color Primary X Coordinate of the Data.
Coded as unsigned 16-bit values in units of
0.00002, where 0x0000 represents zero and 0xC350
represents 1.0000.
</description>
</arg>
<arg name="mastering_display_primary_green_y" type="uint">
<description summary="green primary y coordinate">
Mastering Green Color Primary Y Coordinate of the Data.
Coded as unsigned 16-bit values in units of
0.00002, where 0x0000 represents zero and 0xC350
represents 1.0000.
</description>
</arg>
<arg name="mastering_display_primary_blue_x" type="uint">
<description summary="blue primary x coordinate">
Mastering Blue Color Primary X Coordinate of the Data.
Coded as unsigned 16-bit values in units of
0.00002, where 0x0000 represents zero and 0xC350
represents 1.0000.
</description>
</arg>
<arg name="mastering_display_primary_blue_y" type="uint">
<description summary="blue primary y coordinate">
Mastering Blue Color Primary Y Coordinate of the Data.
Coded as unsigned 16-bit values in units of
0.00002, where 0x0000 represents zero and 0xC350
represents 1.0000.
</description>
</arg>
<arg name="mastering_white_point_x" type="uint">
<description summary="white point x coordinate">
Mastering White Point X Coordinate of the Data.
These are coded as unsigned 16-bit values in units of
0.00002, where 0x0000 represents zero and 0xC350
represents 1.0000.
</description>
</arg>
<arg name="mastering_white_point_y" type="uint">
<description summary="white point y coordinate">
Mastering White Point Y Coordinate of the Data.
These are coded as unsigned 16-bit values in units of
0.00002, where 0x0000 represents zero and 0xC350
represents 1.0000.
</description>
</arg>
<arg name="max_display_mastering_luminance" type="uint">
<description summary="max display mastering luminance">
Max Mastering Display Luminance.
This value is coded as an unsigned 16-bit value in units of 1 cd/m2,
where 0x0001 represents 1 cd/m2 and 0xFFFF represents 65535 cd/m2.
</description>
</arg>
<arg name="min_display_mastering_luminance" type="uint">
<description summary="min display mastering luminance">
Min Mastering Display Luminance.
This value is coded as an unsigned 16-bit value in units of
0.0001 cd/m2, where 0x0001 represents 0.0001 cd/m2 and 0xFFFF
represents 6.5535 cd/m2.
</description>
</arg>
<arg name="max_cll" type="uint">
<description summary="max content light level">
Max Content Light Level.
This value is coded as an unsigned 16-bit value in units of 1 cd/m2,
where 0x0001 represents 1 cd/m2 and 0xFFFF represents 65535 cd/m2.
</description>
</arg>
<arg name="max_fall" type="uint">
<description summary="max frame average light level">
Max Frame Average Light Level.
This value is coded as an unsigned 16-bit value in units of 1 cd/m2,
where 0x0001 represents 1 cd/m2 and 0xFFFF represents 65535 cd/m2.
</description>
</arg>
</request>
<event name="preferred_metadata">
<description summary="preferred metadata for a surface">
Current preferred metadata for a surface.
The application should use this information to tone-map its buffers
to this target before committing.
This metadata does not necessarily correspond to any physical output, but
rather what the compositor thinks would be best for a given surface.
</description>
<arg name="transfer_function" type="uint" enum="transfer_function">
<description summary="output's current transfer function">
Specifies a known transfer function that corresponds to the
output the surface is targeting.
</description>
</arg>
<arg name="output_display_primary_red_x" type="uint">
<description summary="red primary x coordinate">
Output Red Color Primary X Coordinate of the Data.
Coded as unsigned 16-bit values in units of
0.00002, where 0x0000 represents zero and 0xC350
represents 1.0000.
</description>
</arg>
<arg name="output_display_primary_red_y" type="uint">
<description summary="red primary y coordinate">
Output Red Color Primary Y Coordinate of the Data.
Coded as unsigned 16-bit values in units of
0.00002, where 0x0000 represents zero and 0xC350
represents 1.0000.
</description>
</arg>
<arg name="output_display_primary_green_x" type="uint">
<description summary="green primary x coordinate">
Output Green Color Primary X Coordinate of the Data.
Coded as unsigned 16-bit values in units of
0.00002, where 0x0000 represents zero and 0xC350
represents 1.0000.
</description>
</arg>
<arg name="output_display_primary_green_y" type="uint">
<description summary="green primary y coordinate">
Output Green Color Primary Y Coordinate of the Data.
Coded as unsigned 16-bit values in units of
0.00002, where 0x0000 represents zero and 0xC350
represents 1.0000.
</description>
</arg>
<arg name="output_display_primary_blue_x" type="uint">
<description summary="blue primary x coordinate">
Output Blue Color Primary X Coordinate of the Data.
Coded as unsigned 16-bit values in units of
0.00002, where 0x0000 represents zero and 0xC350
represents 1.0000.
</description>
</arg>
<arg name="output_display_primary_blue_y" type="uint">
<description summary="blue primary y coordinate">
Output Blue Color Primary Y Coordinate of the Data.
Coded as unsigned 16-bit values in units of
0.00002, where 0x0000 represents zero and 0xC350
represents 1.0000.
</description>
</arg>
<arg name="output_white_point_x" type="uint">
<description summary="white point x coordinate">
Output White Point X Coordinate of the Data.
These are coded as unsigned 16-bit values in units of
0.00002, where 0x0000 represents zero and 0xC350
represents 1.0000.
</description>
</arg>
<arg name="output_white_point_y" type="uint">
<description summary="white point y coordinate">
Output White Point Y Coordinate of the Data.
These are coded as unsigned 16-bit values in units of
0.00002, where 0x0000 represents zero and 0xC350
represents 1.0000.
</description>
</arg>
<arg name="max_luminance" type="uint">
<description summary="maximum luminance">
Max Output Luminance
The max luminance in nits that the output is capable of rendering in small areas.
Content should: not exceed this value to avoid clipping.
This value is coded as an unsigned 16-bit value in units of 1 cd/m2,
where 0x0001 represents 1 cd/m2 and 0xFFFF represents 65535 cd/m2.
</description>
</arg>
<arg name="min_luminance" type="uint">
<description summary="minimum luminance">
Min Output Luminance
The min luminance that the output is capable of rendering.
Content should: not exceed this value to avoid clipping.
This value is coded as an unsigned 16-bit value in units of
0.0001 cd/m2, where 0x0001 represents 0.0001 cd/m2 and 0xFFFF
represents 6.5535 cd/m2.
</description>
</arg>
<arg name="max_full_frame_luminance" type="uint">
<description summary="maximum full frame luminance">
Max Full Frame Luminance
The max luminance in nits that the output is capable of rendering for the
full frame sustained.
This value is coded as an unsigned 16-bit value in units of 1 cd/m2,
where 0x0001 represents 1 cd/m2 and 0xFFFF represents 65535 cd/m2.
</description>
</arg>
</event>
</interface>
</protocol>

File diff suppressed because it is too large Load diff

View file

@ -15,7 +15,7 @@ echo 'static const std::map<std::string, std::string> SHADERS = {' >> ./src/rend
for filename in `ls ${SHADERS_SRC}`; do
echo "-- ${filename}"
{ echo 'R"#('; cat ${SHADERS_SRC}/${filename}; echo ')#"'; } > ./src/render/shaders/${filename}.inc
{ echo -n 'R"#('; cat ${SHADERS_SRC}/${filename}; echo ')#"'; } > ./src/render/shaders/${filename}.inc
echo "{\"${filename}\"," >> ./src/render/shaders/Shaders.hpp
echo "#include \"./${filename}.inc\"" >> ./src/render/shaders/Shaders.hpp
echo "}," >> ./src/render/shaders/Shaders.hpp

File diff suppressed because it is too large Load diff

View file

@ -4,11 +4,13 @@
#include <ranges>
#include "helpers/math/Direction.hpp"
#include "managers/XWaylandManager.hpp"
#include "managers/KeybindManager.hpp"
#include "managers/SessionLockManager.hpp"
#include "desktop/Window.hpp"
#include "protocols/types/ColorManagement.hpp"
#include "desktop/view/Window.hpp"
#include "helpers/cm/ColorManagement.hpp"
#include "config/shared/workspace/WorkspaceRule.hpp"
#include <aquamarine/backend/Backend.hpp>
#include <aquamarine/output/Output.hpp>
@ -40,6 +42,7 @@ class CCompositor {
} m_drmRenderNode;
bool m_initialized = false;
bool m_safeMode = false;
SP<Aquamarine::CBackend> m_aqBackend;
std::string m_hyprTempDataRoot = "";
@ -55,6 +58,7 @@ class CCompositor {
std::vector<PHLLS> m_layers;
std::vector<PHLWINDOWREF> m_windowsFadingOut;
std::vector<PHLLSREF> m_surfacesFadingOut;
std::vector<SP<Desktop::View::IView>> m_otherViews;
std::unordered_map<std::string, MONITORID> m_monitorIDMap;
std::unordered_map<std::string, WORKSPACEID> m_seenMonitorWorkspaceMap; // map of seen monitor names to workspace IDs
@ -65,6 +69,7 @@ class CCompositor {
void cleanup();
void bumpNofile();
void restoreNofile();
bool setWatchdogFd(int fd);
bool m_readyToProcess = false;
bool m_sessionActive = true;
@ -94,7 +99,7 @@ class CCompositor {
PHLMONITOR getMonitorFromVector(const Vector2D&);
void removeWindowFromVectorSafe(PHLWINDOW);
bool monitorExists(PHLMONITOR);
PHLWINDOW vectorToWindowUnified(const Vector2D&, uint8_t properties, PHLWINDOW pIgnoreWindow = nullptr);
PHLWINDOW vectorToWindowUnified(const Vector2D&, uint16_t properties, PHLWINDOW pIgnoreWindow = nullptr);
SP<CWLSurfaceResource> vectorToLayerSurface(const Vector2D&, std::vector<PHLLSREF>*, Vector2D*, PHLLS*, bool aboveLockscreen = false);
SP<CWLSurfaceResource> vectorToLayerPopupSurface(const Vector2D&, PHLMONITOR monitor, Vector2D*, PHLLS*);
SP<CWLSurfaceResource> vectorWindowToSurface(const Vector2D&, PHLWINDOW, Vector2D& sl);
@ -110,16 +115,16 @@ class CCompositor {
bool isWindowActive(PHLWINDOW);
void changeWindowZOrder(PHLWINDOW, bool);
void cleanupFadingOut(const MONITORID& monid);
PHLWINDOW getWindowInDirection(PHLWINDOW, char);
PHLWINDOW getWindowInDirection(const CBox& box, PHLWORKSPACE pWorkspace, char dir, PHLWINDOW ignoreWindow = nullptr, bool useVectorAngles = false);
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);
WORKSPACEID getNextAvailableNamedWorkspace();
bool isPointOnAnyMonitor(const Vector2D&);
bool isPointOnReservedArea(const Vector2D& point, const PHLMONITOR monitor = nullptr);
CBox calculateX11WorkArea();
PHLMONITOR getMonitorInDirection(const char&);
PHLMONITOR getMonitorInDirection(PHLMONITOR, const char&);
std::optional<CBox> calculateX11WorkArea();
PHLMONITOR getMonitorInDirection(Math::eDirection);
PHLMONITOR getMonitorInDirection(PHLMONITOR, Math::eDirection);
void updateAllWindowsAnimatedDecorationValues();
MONITORID getNextAvailableMonitorID(std::string const& name);
void moveWorkspaceToMonitor(PHLWORKSPACE, PHLMONITOR, bool noWarpCursor = false);
@ -128,7 +133,7 @@ class CCompositor {
bool workspaceIDOutOfBounds(const WORKSPACEID&);
void setWindowFullscreenInternal(const PHLWINDOW PWINDOW, const eFullscreenMode MODE);
void setWindowFullscreenClient(const PHLWINDOW PWINDOW, const eFullscreenMode MODE);
void setWindowFullscreenState(const PHLWINDOW PWINDOW, const SFullscreenState state);
void setWindowFullscreenState(const PHLWINDOW PWINDOW, const Desktop::View::SFullscreenState state);
void changeWindowFullscreenModeClient(const PHLWINDOW PWINDOW, const eFullscreenMode MODE, const bool ON);
PHLWINDOW getX11Parent(PHLWINDOW);
void scheduleFrameForMonitor(PHLMONITOR, Aquamarine::IOutput::scheduleFrameReason reason = Aquamarine::IOutput::AQ_SCHEDULE_CLIENT_UNKNOWN);
@ -156,31 +161,37 @@ class CCompositor {
void setPreferredTransformForSurface(SP<CWLSurfaceResource> pSurface, wl_output_transform transform);
void updateSuspendedStates();
void onNewMonitor(SP<Aquamarine::IOutput> output);
void ensurePersistentWorkspacesPresent(const std::vector<SWorkspaceRule>& rules, PHLWORKSPACE pWorkspace = nullptr);
void ensurePersistentWorkspacesPresent(const std::vector<Config::CWorkspaceRule>& rules, PHLWORKSPACE pWorkspace = nullptr);
void ensurePersistentWorkspacesPresent(PHLWORKSPACE pWorkspace = nullptr);
void ensureWorkspacesOnAssignedMonitors();
std::optional<unsigned int> getVTNr();
bool isVRRActiveOnAnyMonitor() const;
NColorManagement::SImageDescription getPreferredImageDescription();
NColorManagement::PImageDescription getPreferredImageDescription();
NColorManagement::PImageDescription getHDRImageDescription();
bool shouldChangePreferredImageDescription();
bool supportsDrmSyncobjTimeline() const;
std::string m_explicitConfigPath;
private:
void initAllSignals();
void removeAllSignals();
void cleanEnvironment();
void setRandomSplash();
void initManagers(eManagersInitStage stage);
void prepareFallbackOutput();
void createLockFile();
void removeLockFile();
void setMallocThreshold();
void initAllSignals();
void removeAllSignals();
void cleanEnvironment();
void setRandomSplash();
void initManagers(eManagersInitStage stage);
void prepareFallbackOutput();
void createLockFile();
void removeLockFile();
void setMallocThreshold();
void openSafeModeBox();
uint64_t m_hyprlandPID = 0;
wl_event_source* m_critSigSource = nullptr;
rlimit m_originalNofile = {};
uint64_t m_hyprlandPID = 0;
wl_event_source* m_critSigSource = nullptr;
rlimit m_originalNofile = {};
Hyprutils::OS::CFileDescriptor m_watchdogWriteFd;
std::vector<PHLWORKSPACEREF> m_workspaces;
std::vector<PHLWORKSPACEREF> m_workspaces;
};
inline UP<CCompositor> g_pCompositor;

View file

@ -38,10 +38,6 @@ enum eInputType : uint8_t {
INPUT_TYPE_MOTION
};
struct SCallbackInfo {
bool cancelled = false; /* on cancellable events, will cancel the event. */
};
enum eHyprCtlOutputFormat : uint8_t {
FORMAT_NORMAL = 0,
FORMAT_JSON
@ -62,5 +58,3 @@ struct SDispatchResult {
using WINDOWID = int64_t;
using MONITORID = int64_t;
using WORKSPACEID = int64_t;
using HOOK_CALLBACK_FN = std::function<void(void*, SCallbackInfo&, std::any)>;

View file

@ -1,180 +0,0 @@
#pragma once
#include "../defines.hpp"
#include "../helpers/varlist/VarList.hpp"
#include <vector>
#include <map>
enum eConfigValueDataTypes : int8_t {
CVD_TYPE_INVALID = -1,
CVD_TYPE_GRADIENT = 0,
CVD_TYPE_CSS_VALUE = 1,
CVD_TYPE_FONT_WEIGHT = 2,
};
class ICustomConfigValueData {
public:
virtual ~ICustomConfigValueData() = default;
virtual eConfigValueDataTypes getDataType() = 0;
virtual std::string toString() = 0;
};
class CGradientValueData : public ICustomConfigValueData {
public:
CGradientValueData() = default;
CGradientValueData(CHyprColor col) {
m_colors.push_back(col);
updateColorsOk();
};
virtual ~CGradientValueData() = default;
virtual eConfigValueDataTypes getDataType() {
return CVD_TYPE_GRADIENT;
}
void reset(CHyprColor col) {
m_colors.clear();
m_colors.emplace_back(col);
m_angle = 0;
updateColorsOk();
}
void updateColorsOk() {
m_colorsOkLabA.clear();
for (auto& c : m_colors) {
const auto OKLAB = c.asOkLab();
m_colorsOkLabA.emplace_back(OKLAB.l);
m_colorsOkLabA.emplace_back(OKLAB.a);
m_colorsOkLabA.emplace_back(OKLAB.b);
m_colorsOkLabA.emplace_back(c.a);
}
}
/* Vector containing the colors */
std::vector<CHyprColor> m_colors;
/* Vector containing pure colors for shoving into opengl */
std::vector<float> m_colorsOkLabA;
/* Float corresponding to the angle (rad) */
float m_angle = 0;
//
bool operator==(const CGradientValueData& other) const {
if (other.m_colors.size() != m_colors.size() || m_angle != other.m_angle)
return false;
for (size_t i = 0; i < m_colors.size(); ++i)
if (m_colors[i] != other.m_colors[i])
return false;
return true;
}
virtual std::string toString() {
std::string result;
for (auto& c : m_colors) {
result += std::format("{:x} ", c.getAsHex());
}
result += std::format("{}deg", sc<int>(m_angle * 180.0 / M_PI));
return result;
}
};
class CCssGapData : public ICustomConfigValueData {
public:
CCssGapData() : m_top(0), m_right(0), m_bottom(0), m_left(0) {};
CCssGapData(int64_t global) : m_top(global), m_right(global), m_bottom(global), m_left(global) {};
CCssGapData(int64_t vertical, int64_t horizontal) : m_top(vertical), m_right(horizontal), m_bottom(vertical), m_left(horizontal) {};
CCssGapData(int64_t top, int64_t horizontal, int64_t bottom) : m_top(top), m_right(horizontal), m_bottom(bottom), m_left(horizontal) {};
CCssGapData(int64_t top, int64_t right, int64_t bottom, int64_t left) : m_top(top), m_right(right), m_bottom(bottom), m_left(left) {};
/* Css like directions */
int64_t m_top;
int64_t m_right;
int64_t m_bottom;
int64_t m_left;
void parseGapData(CVarList2 varlist) {
const auto toInt = [](std::string_view string) -> int { return std::stoi(std::string(string)); };
switch (varlist.size()) {
case 1: {
*this = CCssGapData(toInt(varlist[0]));
break;
}
case 2: {
*this = CCssGapData(toInt(varlist[0]), toInt(varlist[1]));
break;
}
case 3: {
*this = CCssGapData(toInt(varlist[0]), toInt(varlist[1]), toInt(varlist[2]));
break;
}
case 4: {
*this = CCssGapData(toInt(varlist[0]), toInt(varlist[1]), toInt(varlist[2]), toInt(varlist[3]));
break;
}
default: {
Debug::log(WARN, "Too many arguments provided for gaps.");
*this = CCssGapData(toInt(varlist[0]), toInt(varlist[1]), toInt(varlist[2]), toInt(varlist[3]));
break;
}
}
}
void reset(int64_t global) {
m_top = global;
m_right = global;
m_bottom = global;
m_left = global;
}
virtual eConfigValueDataTypes getDataType() {
return CVD_TYPE_CSS_VALUE;
}
virtual std::string toString() {
return std::format("{} {} {} {}", m_top, m_right, m_bottom, m_left);
}
};
class CFontWeightConfigValueData : public ICustomConfigValueData {
public:
CFontWeightConfigValueData() = default;
CFontWeightConfigValueData(const char* weight) {
parseWeight(weight);
}
int64_t m_value = 400; // default to normal weight
virtual eConfigValueDataTypes getDataType() {
return CVD_TYPE_FONT_WEIGHT;
}
virtual std::string toString() {
return std::format("{}", m_value);
}
void parseWeight(const std::string& strWeight) {
auto lcWeight{strWeight};
std::ranges::transform(strWeight, lcWeight.begin(), ::tolower);
// values taken from Pango weight enums
const auto WEIGHTS = std::map<std::string, int>{
{"thin", 100}, {"ultralight", 200}, {"light", 300}, {"semilight", 350}, {"book", 380}, {"normal", 400},
{"medium", 500}, {"semibold", 600}, {"bold", 700}, {"ultrabold", 800}, {"heavy", 900}, {"ultraheavy", 1000},
};
auto weight = WEIGHTS.find(lcWeight);
if (weight != WEIGHTS.end())
m_value = weight->second;
else {
int w_i = std::stoi(strWeight);
if (w_i < 100 || w_i > 1000)
m_value = 400;
}
}
};

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -1,347 +1,63 @@
#pragma once
#include <hyprutils/animation/AnimationConfig.hpp>
#define CONFIG_MANAGER_H
#include <string>
#include <filesystem>
#include <expected>
#include <map>
#include <unordered_map>
#include "../defines.hpp"
#include <variant>
#include <vector>
#include <optional>
#include <functional>
#include <xf86drmMode.h>
#include "../helpers/Monitor.hpp"
#include "../desktop/Window.hpp"
#include "ConfigDataValues.hpp"
#include "../SharedDefs.hpp"
#include "../helpers/Color.hpp"
#include "../desktop/DesktopTypes.hpp"
#include "./shared/Types.hpp"
#include "../helpers/memory/Memory.hpp"
#include "../managers/XWaylandManager.hpp"
#include "../managers/KeybindManager.hpp"
#include <hyprlang.hpp>
namespace Config {
#define HANDLE void*
class CConfigManager;
struct SWorkspaceRule {
std::string monitor = "";
std::string workspaceString = "";
std::string workspaceName = "";
WORKSPACEID workspaceId = -1;
bool isDefault = false;
bool isPersistent = false;
std::optional<CCssGapData> gapsIn;
std::optional<CCssGapData> gapsOut;
std::optional<CCssGapData> floatGaps = gapsOut;
std::optional<int64_t> borderSize;
std::optional<bool> decorate;
std::optional<bool> noRounding;
std::optional<bool> noBorder;
std::optional<bool> noShadow;
std::optional<std::string> onCreatedEmptyRunCmd;
std::optional<std::string> defaultName;
std::map<std::string, std::string> layoutopts;
};
struct SMonitorAdditionalReservedArea {
int top = 0;
int bottom = 0;
int left = 0;
int right = 0;
};
struct SPluginKeyword {
HANDLE handle = nullptr;
std::string name = "";
Hyprlang::PCONFIGHANDLERFUNC fn = nullptr;
};
struct SPluginVariable {
HANDLE handle = nullptr;
std::string name = "";
};
enum eConfigOptionType : uint8_t {
CONFIG_OPTION_BOOL = 0,
CONFIG_OPTION_INT = 1, /* e.g. 0/1/2*/
CONFIG_OPTION_FLOAT = 2,
CONFIG_OPTION_STRING_SHORT = 3, /* e.g. "auto" */
CONFIG_OPTION_STRING_LONG = 4, /* e.g. a command */
CONFIG_OPTION_COLOR = 5,
CONFIG_OPTION_CHOICE = 6, /* e.g. "one", "two", "three" */
CONFIG_OPTION_GRADIENT = 7,
CONFIG_OPTION_VECTOR = 8,
};
enum eConfigOptionFlags : uint8_t {
CONFIG_OPTION_FLAG_PERCENTAGE = (1 << 0),
};
struct SConfigOptionDescription {
struct SBoolData {
bool value = false;
struct SConfigOptionReply {
// <type>* const*
void* const* dataptr = nullptr;
const std::type_info* type = nullptr;
bool setByUser = false;
};
struct SRangeData {
int value = 0, min = 0, max = 2;
enum eConfigManagerType : uint8_t {
CONFIG_LEGACY = 0,
CONFIG_LUA
};
struct SFloatData {
float value = 0, min = 0, max = 100;
class IConfigManager {
protected:
IConfigManager() = default;
public:
virtual ~IConfigManager() = default;
virtual eConfigManagerType type() = 0;
virtual void init() = 0;
virtual void reload() = 0;
virtual std::string verify() = 0;
virtual int getDeviceInt(const std::string&, const std::string&, const std::string& fallback = "") = 0;
virtual float getDeviceFloat(const std::string&, const std::string&, const std::string& fallback = "") = 0;
virtual Vector2D getDeviceVec(const std::string&, const std::string&, const std::string& fallback = "") = 0;
virtual std::string getDeviceString(const std::string&, const std::string&, const std::string& fallback = "") = 0;
virtual bool deviceConfigExplicitlySet(const std::string&, const std::string&) = 0;
virtual bool deviceConfigExists(const std::string&) = 0;
virtual SConfigOptionReply getConfigValue(const std::string&) = 0;
virtual std::string getMainConfigPath() = 0;
virtual std::string currentConfigPath() = 0;
virtual std::string getConfigString() = 0;
virtual const std::vector<std::string>& getConfigPaths() = 0;
virtual bool configVerifPassed() = 0;
virtual std::string getErrors() = 0;
virtual std::expected<void, std::string> generateDefaultConfig(const std::filesystem::path&, bool safeMode = false) = 0;
virtual void handlePluginLoads() = 0;
};
struct SStringData {
std::string value;
};
bool initConfigManager();
struct SColorData {
CHyprColor color;
};
struct SChoiceData {
int firstIndex = 0;
std::string choices; // comma-separated
};
struct SGradientData {
std::string gradient;
};
struct SVectorData {
Vector2D vec, min, max;
};
std::string value; // e.g. general:gaps_in
std::string description;
std::string specialCategory; // if value is special (e.g. device:abc) value will be abc and special device
bool specialKey = false;
eConfigOptionType type = CONFIG_OPTION_BOOL;
uint32_t flags = 0; // eConfigOptionFlags
std::string jsonify() const;
//
std::variant<SBoolData, SRangeData, SFloatData, SStringData, SColorData, SChoiceData, SGradientData, SVectorData> data;
};
struct SFirstExecRequest {
std::string exec = "";
bool withRules = false;
};
struct SFloatCache {
size_t hash;
SFloatCache(PHLWINDOW window, bool initial) {
// Base hash from class/title
size_t baseHash = initial ? (std::hash<std::string>{}(window->m_initialClass) ^ (std::hash<std::string>{}(window->m_initialTitle) << 1)) :
(std::hash<std::string>{}(window->m_class) ^ (std::hash<std::string>{}(window->m_title) << 1));
// Use empty string as default tag value
std::string tagValue = "";
if (auto xdgTag = window->xdgTag())
tagValue = xdgTag.value();
// Combine hashes
hash = baseHash ^ (std::hash<std::string>{}(tagValue) << 2);
}
bool operator==(const SFloatCache& other) const {
return hash == other.hash;
}
};
namespace std {
template <>
struct hash<SFloatCache> {
size_t operator()(const SFloatCache& id) const {
return id.hash;
}
};
}
class CMonitorRuleParser {
public:
CMonitorRuleParser(const std::string& name);
const std::string& name();
SMonitorRule& rule();
std::optional<std::string> getError();
bool parseMode(const std::string& value);
bool parsePosition(const std::string& value, bool isFirst = false);
bool parseScale(const std::string& value);
bool parseTransform(const std::string& value);
bool parseBitdepth(const std::string& value);
bool parseCM(const std::string& value);
bool parseSDRBrightness(const std::string& value);
bool parseSDRSaturation(const std::string& value);
bool parseVRR(const std::string& value);
void setDisabled();
void setMirror(const std::string& value);
bool setReserved(const SMonitorAdditionalReservedArea& value);
private:
SMonitorRule m_rule;
std::string m_error = "";
};
class CConfigManager {
public:
CConfigManager();
void init();
void reload();
std::string verify();
int getDeviceInt(const std::string&, const std::string&, const std::string& fallback = "");
float getDeviceFloat(const std::string&, const std::string&, const std::string& fallback = "");
Vector2D getDeviceVec(const std::string&, const std::string&, const std::string& fallback = "");
std::string getDeviceString(const std::string&, const std::string&, const std::string& fallback = "");
bool deviceConfigExplicitlySet(const std::string&, const std::string&);
bool deviceConfigExists(const std::string&);
Hyprlang::CConfigValue* getConfigValueSafeDevice(const std::string& dev, const std::string& val, const std::string& fallback);
void* const* getConfigValuePtr(const std::string&);
Hyprlang::CConfigValue* getHyprlangConfigValuePtr(const std::string& name, const std::string& specialCat = "");
std::string getMainConfigPath();
std::string getConfigString();
SMonitorRule getMonitorRuleFor(const PHLMONITOR);
SWorkspaceRule getWorkspaceRuleFor(PHLWORKSPACE workspace);
std::string getDefaultWorkspaceFor(const std::string&);
PHLMONITOR getBoundMonitorForWS(const std::string&);
std::string getBoundMonitorStringForWS(const std::string&);
const std::vector<SWorkspaceRule>& getAllWorkspaceRules();
void ensurePersistentWorkspacesPresent();
const std::vector<SConfigOptionDescription>& getAllDescriptions();
std::unordered_map<std::string, SMonitorAdditionalReservedArea> m_mAdditionalReservedAreas;
const std::unordered_map<std::string, SP<Hyprutils::Animation::SAnimationPropertyConfig>>& getAnimationConfig();
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);
// no-op when done.
void dispatchExecOnce();
void dispatchExecShutdown();
void performMonitorReload();
void ensureMonitorStatus();
void ensureVRR(PHLMONITOR pMonitor = nullptr);
bool shouldUseSoftwareCursors(PHLMONITOR pMonitor);
void updateWatcher();
std::string parseKeyword(const std::string&, const std::string&);
void addParseError(const std::string&);
SP<Hyprutils::Animation::SAnimationPropertyConfig> getAnimationPropertyConfig(const std::string&);
void handlePluginLoads();
std::string getErrors();
// keywords
std::optional<std::string> handleRawExec(const std::string&, const std::string&);
std::optional<std::string> handleExec(const std::string&, const std::string&);
std::optional<std::string> handleExecOnce(const std::string&, const std::string&);
std::optional<std::string> handleExecRawOnce(const std::string&, const std::string&);
std::optional<std::string> handleExecShutdown(const std::string&, const std::string&);
std::optional<std::string> handleMonitor(const std::string&, const std::string&);
std::optional<std::string> handleBind(const std::string&, const std::string&);
std::optional<std::string> handleUnbind(const std::string&, const std::string&);
std::optional<std::string> handleWorkspaceRules(const std::string&, const std::string&);
std::optional<std::string> handleBezier(const std::string&, const std::string&);
std::optional<std::string> handleAnimation(const std::string&, const std::string&);
std::optional<std::string> handleSource(const std::string&, const std::string&);
std::optional<std::string> handleSubmap(const std::string&, const std::string&);
std::optional<std::string> handleBindWS(const std::string&, const std::string&);
std::optional<std::string> handleEnv(const std::string&, const std::string&);
std::optional<std::string> handlePlugin(const std::string&, const std::string&);
std::optional<std::string> handlePermission(const std::string&, const std::string&);
std::optional<std::string> handleGesture(const std::string&, const std::string&);
std::optional<std::string> handleWindowrule(const std::string&, const std::string&);
std::optional<std::string> handleLayerrule(const std::string&, const std::string&);
std::optional<std::string> handleMonitorv2(const std::string& output);
Hyprlang::CParseResult handleMonitorv2();
std::optional<std::string> addRuleFromConfigKey(const std::string& name);
std::optional<std::string> addLayerRuleFromConfigKey(const std::string& name);
Hyprlang::CParseResult reloadRules();
std::string m_configCurrentPath;
bool m_wantsMonitorReload = false;
bool m_noMonitorReload = false;
bool m_isLaunchingExecOnce = false; // For exec-once to skip initial ws tracking
bool m_lastConfigVerificationWasSuccessful = true;
void storeFloatingSize(PHLWINDOW window, const Vector2D& size);
std::optional<Vector2D> getStoredFloatingSize(PHLWINDOW window);
private:
UP<Hyprlang::CConfig> m_config;
std::vector<std::string> m_configPaths;
Hyprutils::Animation::CAnimationConfigTree m_animationTree;
SSubmap m_currentSubmap;
std::vector<std::string> m_declaredPlugins;
std::vector<SPluginKeyword> m_pluginKeywords;
std::vector<SPluginVariable> m_pluginVariables;
std::vector<SP<Desktop::Rule::IRule>> m_keywordRules;
bool m_isFirstLaunch = true; // For exec-once
std::vector<SMonitorRule> m_monitorRules;
std::vector<SWorkspaceRule> m_workspaceRules;
bool m_firstExecDispatched = false;
bool m_manualCrashInitiated = false;
std::vector<SFirstExecRequest> m_firstExecRequests; // bool is for if with rules
std::vector<std::string> m_finalExecRequests;
std::vector<std::pair<std::string, std::string>> m_failedPluginConfigValues; // for plugin values of unloaded plugins
std::string m_configErrors = "";
uint32_t m_configValueNumber = 0;
// internal methods
void setDefaultAnimationVars();
std::optional<std::string> resetHLConfig();
std::optional<std::string> generateConfig(std::string configPath);
std::optional<std::string> verifyConfigExists();
void reloadRuleConfigs();
void postConfigReload(const Hyprlang::CParseResult& result);
SWorkspaceRule mergeWorkspaceRules(const SWorkspaceRule&, const SWorkspaceRule&);
void registerConfigVar(const char* name, const Hyprlang::INT& val);
void registerConfigVar(const char* name, const Hyprlang::FLOAT& val);
void registerConfigVar(const char* name, const Hyprlang::VEC2& val);
void registerConfigVar(const char* name, const Hyprlang::STRING& val);
void registerConfigVar(const char* name, Hyprlang::CUSTOMTYPE&& val);
std::unordered_map<SFloatCache, Vector2D> m_mStoredFloatingSizes;
friend struct SConfigOptionDescription;
friend class CMonitorRuleParser;
};
inline UP<CConfigManager> g_pConfigManager;
UP<IConfigManager>& mgr();
};

View file

@ -1,15 +1,12 @@
#include "ConfigValue.hpp"
#include "ConfigManager.hpp"
#include "../macros.hpp"
void local__configValuePopulate(void* const** p, const std::string& val) {
const auto PVHYPRLANG = g_pConfigManager->getHyprlangConfigValuePtr(val);
*p = PVHYPRLANG->getDataStaticPtr();
const auto BIGP = Config::mgr()->getConfigValue(val);
*p = BIGP.dataptr;
}
std::type_index local__configValueTypeIdx(const std::string& val) {
const auto PVHYPRLANG = g_pConfigManager->getHyprlangConfigValuePtr(val);
const auto ANY = PVHYPRLANG->getValue();
return std::type_index(ANY.type());
const auto BIGP = Config::mgr()->getConfigValue(val);
return std::type_index(*BIGP.type);
}

View file

@ -1,33 +0,0 @@
#pragma once
#include "../helpers/memory/Memory.hpp"
#include <vector>
#include <string>
#include <functional>
#include <hyprutils/os/FileDescriptor.hpp>
class CConfigWatcher {
public:
CConfigWatcher();
~CConfigWatcher() = default;
struct SConfigWatchEvent {
std::string file;
};
Hyprutils::OS::CFileDescriptor& getInotifyFD();
void setWatchList(const std::vector<std::string>& paths);
void setOnChange(const std::function<void(const SConfigWatchEvent&)>& fn);
void onInotifyEvent();
private:
struct SInotifyWatch {
int wd = -1;
std::string file;
};
std::function<void(const SConfigWatchEvent&)> m_watchCallback;
std::vector<SInotifyWatch> m_watches;
Hyprutils::OS::CFileDescriptor m_inotifyFd;
};
inline UP<CConfigWatcher> g_pConfigWatcher = makeUnique<CConfigWatcher>();

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,153 @@
#pragma once
#include <hyprutils/animation/AnimationConfig.hpp>
#include <vector>
#include <optional>
#include <xf86drmMode.h>
#include "../../desktop/DesktopTypes.hpp"
#include "../../desktop/rule/Rule.hpp"
#include "../../helpers/memory/Memory.hpp"
#include "../../managers/KeybindManager.hpp"
#include "../ConfigManager.hpp"
#include <hyprlang.hpp>
#define HANDLE void*
namespace Config::Supplementary {
struct SConfigOptionDescription;
};
namespace Config::Legacy {
class CConfigManager;
struct SPluginKeyword {
HANDLE handle = nullptr;
std::string name = "";
Hyprlang::PCONFIGHANDLERFUNC fn = nullptr;
};
struct SPluginVariable {
HANDLE handle = nullptr;
std::string name = "";
};
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;
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);
std::string parseKeyword(const std::string&, const std::string&);
Hyprlang::CConfigValue* getHyprlangConfigValuePtr(const std::string& name, const std::string& specialCat = "");
// keywords
std::optional<std::string> handleRawExec(const std::string&, const std::string&);
std::optional<std::string> handleExec(const std::string&, const std::string&);
std::optional<std::string> handleExecOnce(const std::string&, const std::string&);
std::optional<std::string> handleExecRawOnce(const std::string&, const std::string&);
std::optional<std::string> handleExecShutdown(const std::string&, const std::string&);
std::optional<std::string> handleMonitor(const std::string&, const std::string&);
std::optional<std::string> handleBind(const std::string&, const std::string&);
std::optional<std::string> handleUnbind(const std::string&, const std::string&);
std::optional<std::string> handleWorkspaceRules(const std::string&, const std::string&);
std::optional<std::string> handleBezier(const std::string&, const std::string&);
std::optional<std::string> handleAnimation(const std::string&, const std::string&);
std::optional<std::string> handleSource(const std::string&, const std::string&);
std::optional<std::string> handleSubmap(const std::string&, const std::string&);
std::optional<std::string> handleBindWS(const std::string&, const std::string&);
std::optional<std::string> handleEnv(const std::string&, const std::string&);
std::optional<std::string> handlePlugin(const std::string&, const std::string&);
std::optional<std::string> handlePermission(const std::string&, const std::string&);
std::optional<std::string> handleGesture(const std::string&, const std::string&);
std::optional<std::string> handleWindowrule(const std::string&, const std::string&);
std::optional<std::string> handleLayerrule(const std::string&, const std::string&);
std::optional<std::string> handleMonitorv2(const std::string& output);
Hyprlang::CParseResult handleMonitorv2();
std::optional<std::string> addRuleFromConfigKey(const std::string& name);
std::optional<std::string> addLayerRuleFromConfigKey(const std::string& name);
Hyprlang::CParseResult reloadRules();
std::string m_configCurrentPath;
bool m_isLaunchingExecOnce = false; // For exec-once to skip initial ws tracking
bool m_lastConfigVerificationWasSuccessful = true;
private:
// internal methods
std::optional<std::string> resetHLConfig();
std::optional<std::string> verifyConfigExists();
void reloadRuleConfigs();
void postConfigReload(const Hyprlang::CParseResult& result);
void registerConfigVar(const char* name, const Hyprlang::INT& val);
void registerConfigVar(const char* name, const Hyprlang::FLOAT& val);
void registerConfigVar(const char* name, const Hyprlang::VEC2& val);
void registerConfigVar(const char* name, const Hyprlang::STRING& val);
void registerConfigVar(const char* name, Hyprlang::CUSTOMTYPE&& val);
Hyprlang::CConfigValue* getConfigValueSafeDevice(const std::string& dev, const std::string& val, const std::string& fallback);
UP<Hyprlang::CConfig> m_config;
std::vector<std::string> m_configPaths;
std::string m_mainConfigPath;
SSubmap m_currentSubmap;
std::vector<std::string> m_declaredPlugins;
std::vector<SPluginKeyword> m_pluginKeywords;
std::vector<SPluginVariable> m_pluginVariables;
std::vector<SP<Desktop::Rule::IRule>> m_keywordRules;
bool m_isFirstLaunch = true; // For exec-once
bool m_firstExecDispatched = false;
bool m_manualCrashInitiated = false;
std::vector<std::pair<std::string, std::string>> m_failedPluginConfigValues; // for plugin values of unloaded plugins
std::string m_configErrors = "";
uint32_t m_configValueNumber = 0;
friend struct Config::Supplementary::SConfigOptionDescription;
friend class CMonitorRuleParser;
};
WP<CConfigManager> mgr();
}

View file

@ -0,0 +1,15 @@
#pragma once
#include "../../helpers/math/Math.hpp"
#include <string>
namespace Config {
class ICustomConfigValueData;
typedef int64_t INTEGER;
typedef float FLOAT;
typedef Vector2D VEC2;
typedef std::string STRING;
typedef ICustomConfigValueData* COMPLEX;
};

View file

@ -0,0 +1,79 @@
#include "AnimationTree.hpp"
using namespace Config;
UP<CAnimationTreeController>& Config::animationTree() {
static UP<CAnimationTreeController> p = makeUnique<CAnimationTreeController>();
return p;
}
CAnimationTreeController::CAnimationTreeController() {
reset();
}
void CAnimationTreeController::reset() {
m_animationTree.createNode("__internal_fadeCTM");
m_animationTree.createNode("global");
// global
m_animationTree.createNode("windows", "global");
m_animationTree.createNode("layers", "global");
m_animationTree.createNode("fade", "global");
m_animationTree.createNode("border", "global");
m_animationTree.createNode("borderangle", "global");
m_animationTree.createNode("workspaces", "global");
m_animationTree.createNode("zoomFactor", "global");
m_animationTree.createNode("monitorAdded", "global");
// layer
m_animationTree.createNode("layersIn", "layers");
m_animationTree.createNode("layersOut", "layers");
// windows
m_animationTree.createNode("windowsIn", "windows");
m_animationTree.createNode("windowsOut", "windows");
m_animationTree.createNode("windowsMove", "windows");
// fade
m_animationTree.createNode("fadeIn", "fade");
m_animationTree.createNode("fadeOut", "fade");
m_animationTree.createNode("fadeSwitch", "fade");
m_animationTree.createNode("fadeShadow", "fade");
m_animationTree.createNode("fadeGlow", "fade");
m_animationTree.createNode("fadeDim", "fade");
m_animationTree.createNode("fadeLayers", "fade");
m_animationTree.createNode("fadeLayersIn", "fadeLayers");
m_animationTree.createNode("fadeLayersOut", "fadeLayers");
m_animationTree.createNode("fadePopups", "fade");
m_animationTree.createNode("fadePopupsIn", "fadePopups");
m_animationTree.createNode("fadePopupsOut", "fadePopups");
m_animationTree.createNode("fadeDpms", "fade");
// workspaces
m_animationTree.createNode("workspacesIn", "workspaces");
m_animationTree.createNode("workspacesOut", "workspaces");
m_animationTree.createNode("specialWorkspace", "workspaces");
m_animationTree.createNode("specialWorkspaceIn", "specialWorkspace");
m_animationTree.createNode("specialWorkspaceOut", "specialWorkspace");
// init the root nodes
m_animationTree.setConfigForNode("global", 1, 8.f, "default");
m_animationTree.setConfigForNode("__internal_fadeCTM", 1, 5.f, "linear");
m_animationTree.setConfigForNode("borderangle", 0, 1, "default");
}
const std::unordered_map<std::string, SP<Hyprutils::Animation::SAnimationPropertyConfig>>& CAnimationTreeController::getAnimationConfig() {
return m_animationTree.getFullConfig();
}
SP<Hyprutils::Animation::SAnimationPropertyConfig> CAnimationTreeController::getAnimationPropertyConfig(const std::string& name) {
return m_animationTree.getConfig(name);
}
void CAnimationTreeController::setConfigForNode(const std::string& name, bool enabled, float speed, const std::string& bezier, const std::string& style) {
m_animationTree.setConfigForNode(name, enabled, speed, bezier, style);
}
bool CAnimationTreeController::nodeExists(const std::string& name) {
return m_animationTree.nodeExists(name);
}

View file

@ -0,0 +1,29 @@
#pragma once
#include <string>
#include "../../../helpers/memory/Memory.hpp"
#include <hyprutils/animation/AnimationConfig.hpp>
namespace Config {
class CAnimationTreeController {
public:
CAnimationTreeController();
const std::unordered_map<std::string, SP<Hyprutils::Animation::SAnimationPropertyConfig>>& getAnimationConfig();
SP<Hyprutils::Animation::SAnimationPropertyConfig> getAnimationPropertyConfig(const std::string&);
//
void reset();
//
void setConfigForNode(const std::string& name, bool enabled, float speed, const std::string& bezier, const std::string& style = "");
bool nodeExists(const std::string& name);
private:
Hyprutils::Animation::CAnimationConfigTree m_animationTree;
};
UP<CAnimationTreeController>& animationTree();
};

View file

@ -0,0 +1,22 @@
#pragma once
#include <cstdint>
#include <string>
namespace Config {
enum eConfigValueDataTypes : int8_t {
CVD_TYPE_INVALID = -1,
CVD_TYPE_GRADIENT = 0,
CVD_TYPE_CSS_VALUE = 1,
CVD_TYPE_FONT_WEIGHT = 2,
};
class IComplexConfigValue {
public:
virtual ~IComplexConfigValue() = default;
virtual eConfigValueDataTypes getDataType() = 0;
virtual std::string toString() = 0;
};
}

View file

@ -0,0 +1,176 @@
#pragma once
#include "ComplexDataType.hpp"
#include "../../../helpers/Color.hpp"
#include <hyprutils/string/VarList2.hpp>
#include <vector>
#include <format>
#include <map>
#include <algorithm>
namespace Config {
class CGradientValueData : public IComplexConfigValue {
public:
CGradientValueData() = default;
CGradientValueData(CHyprColor col) {
m_colors.push_back(col);
updateColorsOk();
};
CGradientValueData(std::vector<CHyprColor>&& cols, float ang) : m_colors(std::move(cols)), m_angle(ang) {
updateColorsOk();
};
virtual ~CGradientValueData() = default;
virtual eConfigValueDataTypes getDataType() {
return CVD_TYPE_GRADIENT;
}
void reset(CHyprColor col) {
m_colors.clear();
m_colors.emplace_back(col);
m_angle = 0;
updateColorsOk();
}
void updateColorsOk() {
m_colorsOkLabA.clear();
for (auto& c : m_colors) {
const auto OKLAB = c.asOkLab();
m_colorsOkLabA.emplace_back(OKLAB.l);
m_colorsOkLabA.emplace_back(OKLAB.a);
m_colorsOkLabA.emplace_back(OKLAB.b);
m_colorsOkLabA.emplace_back(c.a);
}
}
/* Vector containing the colors */
std::vector<CHyprColor> m_colors;
/* Vector containing pure colors for shoving into opengl */
std::vector<float> m_colorsOkLabA;
/* Float corresponding to the angle (rad) */
float m_angle = 0;
//
bool operator==(const CGradientValueData& other) const {
if (other.m_colors.size() != m_colors.size() || m_angle != other.m_angle)
return false;
for (size_t i = 0; i < m_colors.size(); ++i)
if (m_colors[i] != other.m_colors[i])
return false;
return true;
}
virtual std::string toString() {
std::string result;
for (auto& c : m_colors) {
result += std::format("{:x} ", c.getAsHex());
}
result += std::format("{}deg", sc<int>(m_angle * 180.0 / M_PI));
return result;
}
};
class CCssGapData : public IComplexConfigValue {
public:
CCssGapData() : m_top(0), m_right(0), m_bottom(0), m_left(0) {};
CCssGapData(int64_t global) : m_top(global), m_right(global), m_bottom(global), m_left(global) {};
CCssGapData(int64_t vertical, int64_t horizontal) : m_top(vertical), m_right(horizontal), m_bottom(vertical), m_left(horizontal) {};
CCssGapData(int64_t top, int64_t horizontal, int64_t bottom) : m_top(top), m_right(horizontal), m_bottom(bottom), m_left(horizontal) {};
CCssGapData(int64_t top, int64_t right, int64_t bottom, int64_t left) : m_top(top), m_right(right), m_bottom(bottom), m_left(left) {};
/* Css like directions */
int64_t m_top;
int64_t m_right;
int64_t m_bottom;
int64_t m_left;
void parseGapData(Hyprutils::String::CVarList2 varlist) {
const auto toInt = [](std::string_view string) -> int { return std::stoi(std::string(string)); };
switch (varlist.size()) {
case 1: {
*this = CCssGapData(toInt(varlist[0]));
break;
}
case 2: {
*this = CCssGapData(toInt(varlist[0]), toInt(varlist[1]));
break;
}
case 3: {
*this = CCssGapData(toInt(varlist[0]), toInt(varlist[1]), toInt(varlist[2]));
break;
}
case 4: {
*this = CCssGapData(toInt(varlist[0]), toInt(varlist[1]), toInt(varlist[2]), toInt(varlist[3]));
break;
}
default: {
Log::logger->log(Log::WARN, "Too many arguments provided for gaps.");
*this = CCssGapData(toInt(varlist[0]), toInt(varlist[1]), toInt(varlist[2]), toInt(varlist[3]));
break;
}
}
}
void reset(int64_t global) {
m_top = global;
m_right = global;
m_bottom = global;
m_left = global;
}
virtual eConfigValueDataTypes getDataType() {
return CVD_TYPE_CSS_VALUE;
}
virtual std::string toString() {
return std::format("{} {} {} {}", m_top, m_right, m_bottom, m_left);
}
};
class CFontWeightConfigValueData : public IComplexConfigValue {
public:
CFontWeightConfigValueData() = default;
CFontWeightConfigValueData(const char* weight) {
parseWeight(weight);
}
int64_t m_value = 400; // default to normal weight
virtual eConfigValueDataTypes getDataType() {
return CVD_TYPE_FONT_WEIGHT;
}
virtual std::string toString() {
return std::format("{}", m_value);
}
void parseWeight(const std::string& strWeight) {
auto lcWeight{strWeight};
std::ranges::transform(strWeight, lcWeight.begin(), ::tolower);
// values taken from Pango weight enums
const auto WEIGHTS = std::map<std::string, int>{
{"thin", 100}, {"ultralight", 200}, {"light", 300}, {"semilight", 350}, {"book", 380}, {"normal", 400},
{"medium", 500}, {"semibold", 600}, {"bold", 700}, {"ultrabold", 800}, {"heavy", 900}, {"ultraheavy", 1000},
};
auto weight = WEIGHTS.find(lcWeight);
if (weight != WEIGHTS.end())
m_value = weight->second;
else {
int w_i = std::stoi(strWeight);
if (w_i < 100 || w_i > 1000)
m_value = 400;
}
}
};
}

View file

@ -3,24 +3,32 @@
#include <linux/limits.h>
#endif
#include <sys/inotify.h>
#include "../debug/Log.hpp"
#include "../../../debug/log/Logger.hpp"
#include "../../ConfigValue.hpp"
#include "../../ConfigManager.hpp"
#include <ranges>
#include <fcntl.h>
#include <unistd.h>
#include <filesystem>
using namespace Hyprutils::OS;
using namespace Config;
UP<CConfigWatcher>& Config::watcher() {
static UP<CConfigWatcher> p = makeUnique<CConfigWatcher>();
return p;
}
CConfigWatcher::CConfigWatcher() : m_inotifyFd(inotify_init()) {
if (!m_inotifyFd.isValid()) {
Debug::log(ERR, "CConfigWatcher couldn't open an inotify node. Config will not be automatically reloaded");
Log::logger->log(Log::ERR, "CConfigWatcher couldn't open an inotify node. Config will not be automatically reloaded");
return;
}
// TODO: make CFileDescriptor take F_GETFL, F_SETFL
const int FLAGS = fcntl(m_inotifyFd.get(), F_GETFL, 0);
if (fcntl(m_inotifyFd.get(), F_SETFL, FLAGS | O_NONBLOCK) < 0) {
Debug::log(ERR, "CConfigWatcher couldn't non-block inotify node. Config will not be automatically reloaded");
Log::logger->log(Log::ERR, "CConfigWatcher couldn't non-block inotify node. Config will not be automatically reloaded");
m_inotifyFd.reset();
return;
}
@ -30,6 +38,11 @@ CFileDescriptor& CConfigWatcher::getInotifyFD() {
return m_inotifyFd;
}
void CConfigWatcher::update() {
static const auto PDISABLEAUTORELOAD = CConfigValue<Hyprlang::INT>("misc:disable_autoreload");
setWatchList(*PDISABLEAUTORELOAD ? std::vector<std::string>{} : Config::mgr()->getConfigPaths());
}
void CConfigWatcher::setWatchList(const std::vector<std::string>& paths) {
// we clear all watches first, because whichever fired is now invalid
@ -78,19 +91,19 @@ void CConfigWatcher::onInotifyEvent() {
const auto* ev = rc<const inotify_event*>(buffer.data() + offset);
if (offset + sizeof(inotify_event) > sc<size_t>(bytesRead)) {
Debug::log(ERR, "CConfigWatcher: malformed inotify event, truncated header");
Log::logger->log(Log::ERR, "CConfigWatcher: malformed inotify event, truncated header");
break;
}
if (offset + sizeof(inotify_event) + ev->len > sc<size_t>(bytesRead)) {
Debug::log(ERR, "CConfigWatcher: malformed inotify event, truncated name field");
Log::logger->log(Log::ERR, "CConfigWatcher: malformed inotify event, truncated name field");
break;
}
const auto WD = std::ranges::find_if(m_watches, [wd = ev->wd](const auto& e) { return e.wd == wd; });
if (WD == m_watches.end())
Debug::log(ERR, "CConfigWatcher: got an event for wd {} which we don't have?!", ev->wd);
Log::logger->log(Log::ERR, "CConfigWatcher: got an event for wd {} which we don't have?!", ev->wd);
else
m_watchCallback(SConfigWatchEvent{
.file = WD->file,

View file

@ -0,0 +1,37 @@
#pragma once
#include "../../../helpers/memory/Memory.hpp"
#include <vector>
#include <string>
#include <functional>
#include <hyprutils/os/FileDescriptor.hpp>
namespace Config {
class CConfigWatcher {
public:
CConfigWatcher();
~CConfigWatcher() = default;
struct SConfigWatchEvent {
std::string file;
};
void update();
Hyprutils::OS::CFileDescriptor& getInotifyFD();
void setWatchList(const std::vector<std::string>& paths);
void setOnChange(const std::function<void(const SConfigWatchEvent&)>& fn);
void onInotifyEvent();
private:
struct SInotifyWatch {
int wd = -1;
std::string file;
};
std::function<void(const SConfigWatchEvent&)> m_watchCallback;
std::vector<SInotifyWatch> m_watches;
Hyprutils::OS::CFileDescriptor m_inotifyFd;
};
UP<CConfigWatcher>& watcher();
}

View file

@ -0,0 +1,62 @@
#pragma once
#include <string>
#include <optional>
#include <xf86drmMode.h>
#include "../../../helpers/math/Math.hpp"
#include "../../../helpers/cm/ColorManagement.hpp"
#include "../../../helpers/CMType.hpp"
#include "../../../helpers/TransferFunction.hpp"
#include "../../../desktop/reserved/ReservedArea.hpp"
namespace Config {
// Enum for the different types of auto directions, e.g. auto-left, auto-up.
enum eAutoDirs : uint8_t {
DIR_AUTO_NONE = 0, /* None will be treated as right. */
DIR_AUTO_UP,
DIR_AUTO_DOWN,
DIR_AUTO_LEFT,
DIR_AUTO_RIGHT,
DIR_AUTO_CENTER_UP,
DIR_AUTO_CENTER_DOWN,
DIR_AUTO_CENTER_LEFT,
DIR_AUTO_CENTER_RIGHT
};
class CMonitorRule {
public:
CMonitorRule() = default;
~CMonitorRule() = default;
eAutoDirs m_autoDir = DIR_AUTO_NONE;
std::string m_name = "";
Vector2D m_resolution = Vector2D(1280, 720);
Vector2D m_offset = Vector2D(0, 0);
float m_scale = 1;
float m_refreshRate = 60; // Hz
bool m_disabled = false;
wl_output_transform m_transform = WL_OUTPUT_TRANSFORM_NORMAL;
std::string m_mirrorOf = "";
bool m_enable10bit = false;
NCMType::eCMType m_cmType = NCMType::CM_SRGB;
NTransferFunction::eTF m_sdrEotf = NTransferFunction::TF_DEFAULT;
float m_sdrSaturation = 1.F; // SDR -> HDR
float m_sdrBrightness = 1.F; // SDR -> HDR
Desktop::CReservedArea m_reservedArea;
std::string m_iccFile;
int m_supportsWideColor = 0; // 0 - auto, 1 - force enable, -1 - force disable
int m_supportsHDR = 0; // 0 - auto, 1 - force enable, -1 - force disable
float m_sdrMinLuminance = 0.2F; // SDR -> HDR
int m_sdrMaxLuminance = 80; // SDR -> HDR
// Incorrect values will result in reduced luminance range or incorrect tonemapping. Shouldn't damage the HW. Use with care in case of a faulty monitor firmware.
float m_minLuminance = -1.F; // >= 0 overrides EDID
int m_maxLuminance = -1; // >= 0 overrides EDID
int m_maxAvgLuminance = -1; // >= 0 overrides EDID
drmModeModeInfo m_drmMode = {};
std::optional<int> m_vrr;
};
};

View file

@ -0,0 +1,252 @@
#include "MonitorRuleManager.hpp"
#include "../../../debug/log/Logger.hpp"
#include "../../../protocols/OutputManagement.hpp"
#include "../../../helpers/Monitor.hpp"
#include "../../../Compositor.hpp"
#include "../../../render/Renderer.hpp"
#include "../../../event/EventBus.hpp"
#include "../../../managers/eventLoop/EventLoopManager.hpp"
#include <ranges>
using namespace Config;
UP<CMonitorRuleManager>& Config::monitorRuleMgr() {
static UP<CMonitorRuleManager> p = makeUnique<CMonitorRuleManager>();
return p;
}
CMonitorRuleManager::CMonitorRuleManager() {
m_listeners.preChecksRender = Event::bus()->m_events.render.preChecks.listen([this](PHLMONITOR m) {
if (m_reloadScheduled)
performMonitorReload();
m_reloadScheduled = false;
});
}
void CMonitorRuleManager::clear() {
m_rules.clear();
}
void CMonitorRuleManager::add(CMonitorRule&& x) {
std::erase_if(m_rules, [&x](const auto& e) { return e.m_name == x.m_name; });
m_rules.emplace_back(std::move(x));
}
CMonitorRule CMonitorRuleManager::get(const PHLMONITOR PMONITOR) {
auto applyWlrOutputConfig = [PMONITOR](CMonitorRule rule) -> CMonitorRule {
const auto CONFIG = PROTO::outputManagement->getOutputStateFor(PMONITOR);
if (!CONFIG)
return rule;
Log::logger->log(Log::DEBUG, "CConfigManager::getMonitorRuleFor: found a wlr_output_manager override for {}", PMONITOR->m_name);
Log::logger->log(Log::DEBUG, " > overriding enabled: {} -> {}", !rule.m_disabled, !CONFIG->enabled);
rule.m_disabled = !CONFIG->enabled;
if ((CONFIG->committedProperties & OUTPUT_HEAD_COMMITTED_MODE) || (CONFIG->committedProperties & OUTPUT_HEAD_COMMITTED_CUSTOM_MODE)) {
Log::logger->log(Log::DEBUG, " > overriding mode: {:.0f}x{:.0f}@{:.2f}Hz -> {:.0f}x{:.0f}@{:.2f}Hz", rule.m_resolution.x, rule.m_resolution.y, rule.m_refreshRate,
CONFIG->resolution.x, CONFIG->resolution.y, CONFIG->refresh / 1000.F);
rule.m_resolution = CONFIG->resolution;
rule.m_refreshRate = CONFIG->refresh / 1000.F;
}
if (CONFIG->committedProperties & OUTPUT_HEAD_COMMITTED_POSITION) {
Log::logger->log(Log::DEBUG, " > overriding offset: {:.0f}, {:.0f} -> {:.0f}, {:.0f}", rule.m_offset.x, rule.m_offset.y, CONFIG->position.x, CONFIG->position.y);
rule.m_offset = CONFIG->position;
}
if (CONFIG->committedProperties & OUTPUT_HEAD_COMMITTED_TRANSFORM) {
Log::logger->log(Log::DEBUG, " > overriding transform: {} -> {}", sc<uint8_t>(rule.m_transform), sc<uint8_t>(CONFIG->transform));
rule.m_transform = CONFIG->transform;
}
if (CONFIG->committedProperties & OUTPUT_HEAD_COMMITTED_SCALE) {
Log::logger->log(Log::DEBUG, " > overriding scale: {} -> {}", sc<uint8_t>(rule.m_scale), sc<uint8_t>(CONFIG->scale));
rule.m_scale = CONFIG->scale;
}
if (CONFIG->committedProperties & OUTPUT_HEAD_COMMITTED_ADAPTIVE_SYNC) {
Log::logger->log(Log::DEBUG, " > overriding vrr: {} -> {}", rule.m_vrr.value_or(0), CONFIG->adaptiveSync);
rule.m_vrr = sc<int>(CONFIG->adaptiveSync);
}
return rule;
};
for (auto const& r : m_rules | std::views::reverse) {
if (PMONITOR->matchesStaticSelector(r.m_name))
return applyWlrOutputConfig(r);
}
Log::logger->log(Log::WARN, "No rule found for {}, trying to use the first.", PMONITOR->m_name);
for (auto const& r : m_rules) {
if (r.m_name.empty())
return applyWlrOutputConfig(r);
}
Log::logger->log(Log::WARN, "No rules configured. Using the default hardcoded one.");
CMonitorRule fallbackRule;
fallbackRule.m_autoDir = eAutoDirs::DIR_AUTO_RIGHT;
fallbackRule.m_name = "";
fallbackRule.m_resolution = Vector2D{};
fallbackRule.m_offset = Vector2D{-INT32_MAX, -INT32_MAX};
fallbackRule.m_scale = -1;
return applyWlrOutputConfig(fallbackRule);
}
const std::vector<CMonitorRule>& CMonitorRuleManager::all() {
return m_rules;
}
std::vector<CMonitorRule>& CMonitorRuleManager::allMut() {
return m_rules;
}
void CMonitorRuleManager::scheduleReload() {
if (m_reloadScheduled)
return;
m_reloadScheduled = true;
}
void CMonitorRuleManager::performMonitorReload() {
bool overAgain = false;
for (auto const& m : g_pCompositor->m_realMonitors) {
if (!m->m_output || m->m_isUnsafeFallback)
continue;
auto rule = get(m);
if (!m->applyMonitorRule(Config::CMonitorRule{rule})) {
overAgain = true;
break;
}
// ensure mirror
m->setMirror(rule.m_mirrorOf);
g_pHyprRenderer->arrangeLayersForMonitor(m->m_id);
}
if (overAgain)
performMonitorReload();
m_reloadScheduled = false;
Event::bus()->m_events.monitor.layoutChanged.emit();
}
void CMonitorRuleManager::ensureMonitorStatus() {
for (auto const& rm : g_pCompositor->m_realMonitors) {
if (!rm->m_output || rm->m_isUnsafeFallback)
continue;
auto rule = get(rm);
if (rule.m_disabled == rm->m_enabled)
rm->applyMonitorRule(std::move(rule));
}
}
void CMonitorRuleManager::ensureVRR(PHLMONITOR pMonitor) {
static auto PVRR = CConfigValue<Hyprlang::INT>("misc:vrr");
static auto ensureVRRForDisplay = [&](PHLMONITOR m) -> void {
if (!m->m_output || m->m_createdByUser)
return;
const auto USEVRR = m->m_activeMonitorRule.m_vrr.has_value() ? m->m_activeMonitorRule.m_vrr.value() : *PVRR;
if (USEVRR == 0) {
if (m->m_vrrActive) {
m->m_output->state->resetExplicitFences();
m->m_output->state->setAdaptiveSync(false);
if (!m->m_state.commit())
Log::logger->log(Log::ERR, "Couldn't commit output {} in ensureVRR -> false", m->m_output->name);
}
m->m_vrrActive = false;
return;
}
const auto PWORKSPACE = m->m_activeWorkspace;
if (USEVRR == 1) {
bool wantVRR = true;
if (PWORKSPACE && PWORKSPACE->m_hasFullscreenWindow && (PWORKSPACE->m_fullscreenMode & FSMODE_FULLSCREEN))
wantVRR = !PWORKSPACE->getFullscreenWindow()->m_ruleApplicator->noVRR().valueOrDefault();
if (wantVRR) {
if (!m->m_vrrActive) {
m->m_output->state->resetExplicitFences();
m->m_output->state->setAdaptiveSync(true);
if (!m->m_state.test()) {
Log::logger->log(Log::DEBUG, "Pending output {} does not accept VRR.", m->m_output->name);
m->m_output->state->setAdaptiveSync(false);
}
if (!m->m_state.commit())
Log::logger->log(Log::ERR, "Couldn't commit output {} in ensureVRR -> true", m->m_output->name);
}
m->m_vrrActive = true;
} else {
if (m->m_vrrActive) {
m->m_output->state->resetExplicitFences();
m->m_output->state->setAdaptiveSync(false);
if (!m->m_state.commit())
Log::logger->log(Log::ERR, "Couldn't commit output {} in ensureVRR -> false", m->m_output->name);
}
m->m_vrrActive = false;
}
return;
} else if (USEVRR == 2 || USEVRR == 3) {
if (!PWORKSPACE)
return; // ???
bool wantVRR = PWORKSPACE->m_hasFullscreenWindow && (PWORKSPACE->m_fullscreenMode & FSMODE_FULLSCREEN);
if (wantVRR && PWORKSPACE->getFullscreenWindow()->m_ruleApplicator->noVRR().valueOrDefault())
wantVRR = false;
if (wantVRR && USEVRR == 3) {
const auto contentType = PWORKSPACE->getFullscreenWindow()->getContentType();
wantVRR = contentType == NContentType::CONTENT_TYPE_GAME || contentType == NContentType::CONTENT_TYPE_VIDEO;
}
if (wantVRR) {
/* fullscreen */
m->m_vrrActive = true;
if (!m->m_output->state->state().adaptiveSync) {
m->m_output->state->setAdaptiveSync(true);
if (!m->m_state.test()) {
Log::logger->log(Log::DEBUG, "Pending output {} does not accept VRR.", m->m_output->name);
m->m_output->state->setAdaptiveSync(false);
}
}
} else {
m->m_vrrActive = false;
m->m_output->state->setAdaptiveSync(false);
}
}
};
if (pMonitor) {
ensureVRRForDisplay(pMonitor);
return;
}
for (auto const& m : g_pCompositor->m_monitors) {
ensureVRRForDisplay(m);
}
}

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