Compare commits

..

No commits in common. "main" and "v0.50.0" have entirely different histories.

479 changed files with 13823 additions and 28928 deletions

View file

@ -1,111 +1,4 @@
WarningsAsErrors: >
-*,
bugprone-*,
-bugprone-multi-level-implicit-pointer-conversion,
-bugprone-empty-catch,
-bugprone-unused-return-value,
-bugprone-reserved-identifier,
-bugprone-switch-missing-default-case,
-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,
-bugprone-assignment-in-if-condition,
concurrency-*,
-concurrency-mt-unsafe,
cppcoreguidelines-*,
-cppcoreguidelines-pro-type-const-cast,
-cppcoreguidelines-owning-memory,
-cppcoreguidelines-avoid-magic-numbers,
-cppcoreguidelines-pro-bounds-constant-array-index,
-cppcoreguidelines-avoid-const-or-ref-data-members,
-cppcoreguidelines-non-private-member-variables-in-classes,
-cppcoreguidelines-avoid-goto,
-cppcoreguidelines-pro-bounds-array-to-pointer-decay,
-cppcoreguidelines-avoid-do-while,
-cppcoreguidelines-avoid-non-const-global-variables,
-cppcoreguidelines-special-member-functions,
-cppcoreguidelines-explicit-virtual-functions,
-cppcoreguidelines-avoid-c-arrays,
-cppcoreguidelines-pro-bounds-pointer-arithmetic,
-cppcoreguidelines-narrowing-conversions,
-cppcoreguidelines-pro-type-union-access,
-cppcoreguidelines-pro-type-member-init,
-cppcoreguidelines-macro-usage,
-cppcoreguidelines-macro-to-enum,
-cppcoreguidelines-init-variables,
-cppcoreguidelines-pro-type-cstyle-cast,
-cppcoreguidelines-pro-type-vararg,
-cppcoreguidelines-pro-type-reinterpret-cast,
-google-global-names-in-headers,
-google-readability-casting,
google-runtime-operator,
misc-*,
-misc-use-internal-linkage,
-misc-unused-parameters,
-misc-no-recursion,
-misc-non-private-member-variables-in-classes,
-misc-include-cleaner,
-misc-use-anonymous-namespace,
-misc-const-correctness,
modernize-*,
-modernize-use-emplace,
-modernize-redundant-void-arg,
-modernize-use-starts-ends-with,
-modernize-use-designated-initializers,
-modernize-use-std-numbers,
-modernize-return-braced-init-list,
-modernize-use-trailing-return-type,
-modernize-use-using,
-modernize-use-override,
-modernize-avoid-c-arrays,
-modernize-macro-to-enum,
-modernize-loop-convert,
-modernize-use-nodiscard,
-modernize-pass-by-value,
-modernize-use-auto,
performance-*,
-performance-inefficient-vector-operation,
-performance-inefficient-string-concatenation,
-performance-enum-size,
-performance-move-const-arg,
-performance-avoid-endl,
-performance-unnecessary-value-param,
portability-std-allocator-const,
readability-*,
-readability-identifier-naming,
-readability-use-std-min-max,
-readability-math-missing-parentheses,
-readability-simplify-boolean-expr,
-readability-static-accessed-through-instance,
-readability-use-anyofallof,
-readability-enum-initial-value,
-readability-redundant-inline-specifier,
-readability-function-cognitive-complexity,
-readability-function-size,
-readability-identifier-length,
-readability-magic-numbers,
-readability-uppercase-literal-suffix,
-readability-braces-around-statements,
-readability-redundant-access-specifiers,
-readability-else-after-return,
-readability-container-data-pointer,
-readability-implicit-bool-conversion,
-readability-avoid-nested-conditional-operator,
-readability-redundant-member-init,
-readability-redundant-string-init,
-readability-avoid-const-params-in-decls,
-readability-named-parameter,
-readability-convert-member-functions-to-static,
-readability-qualified-auto,
-readability-make-member-function-const,
-readability-isolate-declaration,
-readability-inconsistent-declaration-parameter-name,
-clang-diagnostic-error,
WarningsAsErrors: '*'
HeaderFilterRegex: '.*\.hpp'
FormatStyle: file
Checks: >

View file

@ -24,7 +24,6 @@ runs:
glm \
glslang \
go \
gtest \
hyprlang \
hyprcursor \
jq \
@ -46,7 +45,6 @@ runs:
libxkbfile \
lld \
meson \
muparser \
ninja \
pango \
pixman \
@ -76,25 +74,16 @@ runs:
cmake --build ./build --config Release --target all -j`nproc 2>/dev/null || getconf NPROCESSORS_CONF`
cmake --install build
- name: Get hyprwire-git
- name: Get hyprgraphics-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
git clone https://github.com/hyprwm/hyprgraphics && cd hyprgraphics && cmake -DCMAKE_BUILD_TYPE:STRING=Release -DCMAKE_INSTALL_PREFIX:PATH=/usr -B build && cmake --build build --target hyprgraphics && cmake --install build
- name: Get hyprutils-git
shell: bash
run: |
git clone https://github.com/hyprwm/hyprutils && cd hyprutils && cmake -DCMAKE_BUILD_TYPE:STRING=Release -DCMAKE_INSTALL_PREFIX:PATH=/usr -B build && cmake --build build --target hyprutils && cmake --install build
- name: Get hyprgraphics-git
shell: bash
run: |
git clone https://github.com/hyprwm/hyprgraphics && cd hyprgraphics && cmake -DCMAKE_BUILD_TYPE:STRING=Release -DCMAKE_INSTALL_PREFIX:PATH=/usr -B build && cmake --build build --target hyprgraphics && cmake --install build
- name: Get aquamarine-git
shell: bash
run: |

4
.github/labeler.yml vendored
View file

@ -22,10 +22,6 @@ 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

@ -21,19 +21,22 @@ jobs:
- name: Build Hyprland
run: |
CFLAGS=-Werror CXXFLAGS=-Werror make nopch
CFLAGS=-Werror CXXFLAGS=-Werror make all
- name: Compress and package artifacts
run: |
mkdir x86_64-pc-linux-gnu
mkdir hyprland
mkdir hyprland/example
mkdir hyprland/assets
cp ./LICENSE hyprland/
cp build/Hyprland hyprland/
cp build/hyprctl/hyprctl hyprland/
cp build/hyprpm/hyprpm hyprland/
cp build/Hyprland hyprland/
cp -r example/ hyprland/
cp -r assets/ hyprland/
tar -cvJf Hyprland.tar.xz hyprland
tar -cvf Hyprland.tar.xz hyprland
- name: Release
uses: actions/upload-artifact@v4
@ -41,16 +44,86 @@ jobs:
name: Build archive
path: Hyprland.tar.xz
meson:
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork
name: "Build Hyprland with Meson (Arch)"
runs-on: ubuntu-latest
container:
image: archlinux
steps:
- name: Checkout repository actions
uses: actions/checkout@v4
with:
sparse-checkout: .github/actions
- name: Setup base
uses: ./.github/actions/setup_base
- name: Configure
run: meson setup build -Ddefault_library=static
- name: Compile
run: ninja -C build
no-pch:
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork
name: "Build Hyprland without precompiled headers (Arch)"
runs-on: ubuntu-latest
container:
image: archlinux
steps:
- name: Checkout repository actions
uses: actions/checkout@v4
with:
sparse-checkout: .github/actions
- name: Setup base
uses: ./.github/actions/setup_base
with:
INSTALL_XORG_PKGS: true
- name: Compile
run: make nopch
noxwayland:
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork
name: "Build Hyprland in pure Wayland (Arch)"
runs-on: ubuntu-latest
container:
image: archlinux
steps:
- name: Checkout repository actions
uses: actions/checkout@v4
with:
sparse-checkout: .github/actions
- name: Setup base
uses: ./.github/actions/setup_base
- name: Configure
run: mkdir -p build && cmake --no-warn-unused-cli -DCMAKE_BUILD_TYPE:STRING=Release -DNO_XWAYLAND:STRING=true -H./ -B./build -G Ninja
- name: Compile
run: make release
clang-format:
permissions: read-all
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork
name: "Code Style"
name: "Code Style (Arch)"
runs-on: ubuntu-latest
container:
image: archlinux
steps:
- name: Checkout repository
- name: Checkout repository actions
uses: actions/checkout@v4
with:
sparse-checkout: .github/actions
- name: Setup base
uses: ./.github/actions/setup_base
- name: Configure
run: meson setup build -Ddefault_library=static
- name: clang-format check
uses: jidicula/clang-format-action@v4.16.0
with:
exclude-regex: ^subprojects$
run: ninja -C build clang-format-check

View file

@ -4,23 +4,43 @@ jobs:
clang-format:
permissions: write-all
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork
name: "Code Style"
name: "Code Style (Arch)"
runs-on: ubuntu-latest
container:
image: archlinux
steps:
- name: Checkout repository
- name: Checkout repository actions
uses: actions/checkout@v4
with:
sparse-checkout: .github/actions
- name: Setup base
uses: ./.github/actions/setup_base
- name: Configure
run: meson setup build -Ddefault_library=static
- name: clang-format check
uses: jidicula/clang-format-action@v4.16.0
with:
exclude-regex: ^subprojects$
run: ninja -C build clang-format-check
- name: Create comment
- name: clang-format apply
if: ${{ failure() && github.event_name == 'pull_request' }}
run: ninja -C build clang-format
- name: Create patch
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
echo 'Please fix the formatting issues by running [`clang-format`](https://wiki.hyprland.org/Contributing-and-Debugging/PR-Guidelines/#code-style), or directly apply this patch:' > clang-format.patch
echo '<details>' >> clang-format.patch
echo '<summary>clang-format.patch</summary>' >> clang-format.patch
echo >> clang-format.patch
echo '```diff' >> clang-format.patch
git diff >> clang-format.patch
echo '```' >> clang-format.patch
echo >> clang-format.patch
echo '</details>' >> clang-format.patch
- name: Post comment
- name: Comment patch
if: ${{ failure() && github.event_name == 'pull_request' }}
uses: mshick/add-pr-comment@v2
with:

View file

@ -1,45 +0,0 @@
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

@ -13,7 +13,7 @@ jobs:
uses: ./.github/workflows/nix.yml
secrets: inherit
with:
command: nix build 'github:${{ github.repository }}?ref=${{ github.ref }}' -L --extra-substituters "https://hyprland.cachix.org"
command: nix build 'github:hyprwm/Hyprland?ref=${{ github.ref }}' -L --extra-substituters "https://hyprland.cachix.org"
xdph:
if: (github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork)
@ -21,9 +21,10 @@ jobs:
uses: ./.github/workflows/nix.yml
secrets: inherit
with:
command: nix build 'github:${{ github.repository }}?ref=${{ github.ref }}#xdg-desktop-portal-hyprland' -L --extra-substituters "https://hyprland.cachix.org"
command: nix build 'github:hyprwm/Hyprland?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

@ -20,13 +20,25 @@ jobs:
- 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 }}
# restore and save a cache using this key
primary-key: nix-${{ runner.os }}
# 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
# 1G = 1073741824
gc-max-store-size-linux: 5G
# do purge caches
purge: true
# purge all versions of the cache
purge-prefixes: nix-${{ runner.os }}
# created more than this number of seconds ago
purge-created: 0
# or, last accessed more than this number of seconds ago
# relative to the start of the `Post Restore and save Nix store` phase
purge-last-accessed: 0
# except any version with the key that is the same as the `primary-key`
purge-primary-key: never
- uses: cachix/cachix-action@v15
with:
@ -34,7 +46,7 @@ jobs:
authToken: "${{ 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"
run: nix build 'github:hyprwm/Hyprland?ref=${{ github.ref }}#checks.x86_64-linux.tests' -L --extra-substituters "https://hyprland.cachix.org"
- name: Check exit status
run: grep 0 result/exit_status

View file

@ -27,13 +27,25 @@ jobs:
- 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 }}
# restore and save a cache using this key
primary-key: nix-${{ runner.os }}-${{ hashFiles('**/*.nix', '**/flake.lock') }}
# if there's no cache hit, restore a cache by this prefix
restore-prefixes-first-match: nix-${{ runner.os }}
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
# 1G = 1073741824
gc-max-store-size-linux: 1G
# do purge caches
purge: true
# purge all versions of the cache
purge-prefixes: nix-${{ runner.os }}-
# created more than this number of seconds ago
purge-created: 0
# or, last accessed more than this number of seconds ago
# relative to the start of the `Post Restore and save Nix store` phase
purge-last-accessed: 0
# except any version with the key that is the same as the `primary-key`
purge-primary-key: never
- name: Update inputs
run: nix/update-inputs.sh

View file

@ -25,13 +25,25 @@ jobs:
- 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 }}
# restore and save a cache using this key
primary-key: nix-${{ runner.os }}
# 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
# 1G = 1073741824
gc-max-store-size-linux: 5G
# do purge caches
purge: true
# purge all versions of the cache
purge-prefixes: nix-${{ runner.os }}
# created more than this number of seconds ago
purge-created: 0
# or, last accessed more than this number of seconds ago
# relative to the start of the `Post Restore and save Nix store` phase
purge-last-accessed: 0
# except any version with the key that is the same as the `primary-key`
purge-primary-key: never
- uses: cachix/cachix-action@v15
with:

View file

@ -9,36 +9,17 @@ jobs:
source-tarball:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v5
- name: Checkout Hyprland
id: checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
submodules: recursive
- name: Populate git info in version.h.in
- name: Generate version
id: genversion
run: |
git fetch --tags --unshallow || true
COMMIT_HASH=$(git rev-parse HEAD)
BRANCH="${GITHUB_REF_NAME:-$(git rev-parse --abbrev-ref HEAD)}"
COMMIT_MSG=$(git show -s --format=%s | sed 's/[&/]/\\&/g')
COMMIT_DATE=$(git show -s --format=%cd --date=local)
GIT_DIRTY=$(git diff-index --quiet HEAD -- && echo "clean" || echo "dirty")
GIT_TAG=$(git describe --tags --always || echo "unknown")
GIT_COMMITS=$(git rev-list --count HEAD)
echo "Branch: $BRANCH"
echo "Tag: $GIT_TAG"
sed -i \
-e "s|@GIT_COMMIT_HASH@|$COMMIT_HASH|" \
-e "s|@GIT_BRANCH@|$BRANCH|" \
-e "s|@GIT_COMMIT_MESSAGE@|$COMMIT_MSG|" \
-e "s|@GIT_COMMIT_DATE@|$COMMIT_DATE|" \
-e "s|@GIT_DIRTY@|$GIT_DIRTY|" \
-e "s|@GIT_TAG@|$GIT_TAG|" \
-e "s|@GIT_COMMITS@|$GIT_COMMITS|" \
src/version.h.in
git fetch --unshallow || echo "failed unshallowing"
bash -c scripts/generateVersion.sh
- name: Create tarball with submodules
id: tar

View file

@ -1,139 +0,0 @@
name: AI Translation Check
on:
# pull_request_target:
# types:
# - opened
issue_comment:
types:
- created
permissions:
contents: read
pull-requests: write
issues: write
jobs:
review:
name: Review Translation
if: ${{ github.event_name == 'pull_request_target' || (github.event_name == 'issue_comment' && github.event.action == 'created' && github.event.issue.pull_request != null && github.event.comment.user.login == 'vaxerski' && github.event.comment.body == 'ai, please recheck' ) }}
runs-on: ubuntu-latest
env:
OPENAI_MODEL: gpt-5-mini
SYSTEM_PROMPT: |
You are a programmer and a translator. Your job is to review the attached patch for adding translation to a piece of software and make sure the submitted translation is not malicious, and that it makes sense. If the translation is not malicious, and doesn't contain obvious grammatical mistakes, say "Translation check OK". Otherwise, say "Translation check not ok" and list bad entries.
Examples of bad translations include obvious trolling (slurs, etc) or nonsense sentences. Meaningful improvements may be suggested, but if there are only minor improvements, just reply with "Translation check OK". Do not provide anything but the result and (if applicable) the bad entries or improvements.
AI_PROMPT: Translation patch below.
steps:
- name: Checkout source code
uses: actions/checkout@v5
- uses: dorny/paths-filter@v3
id: changes
with:
filters: |
i18n:
- 'src/i18n/**'
- name: Stop if i18n not changed
if: steps.changes.outputs.i18n != 'true'
run: echo "No i18n changes in this PR; skipping." && exit 0
- name: Determine PR number
id: pr
run: |
if [ "${{ github.event_name }}" = "pull_request_target" ]; then
echo "number=${{ github.event.pull_request.number }}" >> "$GITHUB_OUTPUT"
else
echo "number=${{ github.event.issue.number }}" >> "$GITHUB_OUTPUT"
fi
- name: Download combined PR diff
id: get_diff
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
PR_NUMBER: ${{ steps.pr.outputs.number }}
run: |
# Get the combined diff for the entire PR
curl -sSL \
-H "Authorization: token $GITHUB_TOKEN" \
-H "Accept: application/vnd.github.v3.diff" \
"https://api.github.com/repos/${{ github.repository }}/pulls/$PR_NUMBER" \
-o pr.diff
# Compute character length
LEN=$(wc -c < pr.diff | tr -d ' ')
echo "len=$LEN" >> "$GITHUB_OUTPUT"
if [ "$LEN" -gt 25000 ]; then
echo "too_long=true" >> "$GITHUB_OUTPUT"
else
echo "too_long=false" >> "$GITHUB_OUTPUT"
fi
echo "got diff:"
cat pr.diff
- name: Comment when diff length exceeded
if: steps.get_diff.outputs.too_long == 'true'
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
PR_NUMBER: ${{ steps.pr.outputs.number }}
run: |
jq -n --arg body "Diff length exceeded, can't query API" '{body: ("AI translation check result:\n\n" + $body)}' > body.json
curl -sS -X POST \
-H "Authorization: token $GITHUB_TOKEN" \
-H "Content-Type: application/json" \
"https://api.github.com/repos/${{ github.repository }}/issues/$PR_NUMBER/comments" \
--data @body.json
- name: Query OpenAI and post review
if: steps.get_diff.outputs.too_long == 'false'
env:
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
OPENAI_MODEL: ${{ env.OPENAI_MODEL }}
SYSTEM_PROMPT: ${{ env.SYSTEM_PROMPT }}
AI_PROMPT: ${{ env.AI_PROMPT }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
PR_NUMBER: ${{ steps.pr.outputs.number }}
run: |
# Prepare OpenAI chat request payload (embed diff safely)
jq -n \
--arg model "$OPENAI_MODEL" \
--arg sys "$SYSTEM_PROMPT" \
--arg prompt "$AI_PROMPT" \
--rawfile diff pr.diff \
'{model:$model,
messages:[
{role:"system", content:$sys},
{role:"user", content: ($prompt + "\n\n```diff\n" + $diff + "\n```")}
]
}' > payload.json
# Call OpenAI
curl -sS https://api.openai.com/v1/chat/completions \
-H "Authorization: Bearer $OPENAI_API_KEY" \
-H "Content-Type: application/json" \
-d @payload.json > response.json
# Extract response text
COMMENT=$(jq -r '.choices[0].message.content // empty' response.json)
if [ -z "$COMMENT" ]; then
COMMENT="AI did not return a response."
fi
# If failed, add a note
ADDITIONAL_NOTE=""
if [[ "$COMMENT" == *"not ok"* ]]; then
ADDITIONAL_NOTE=$(echo -ne "\n\nPlease note this check is a guideline, not a hard requirement. It is here to help you translate. If you disagree with some points, just state that. Any typos should be fixed.")
fi
# Post the review as a PR comment
jq -n --arg body "$COMMENT" --arg note "$ADDITIONAL_NOTE" '{body: ("AI translation check result:\n\n" + $body + $note)}' > body.json
echo "CURLing https://api.github.com/repos/${{ github.repository }}/issues/$PR_NUMBER/comments"
curl -sS -X POST \
-H "Authorization: token $GITHUB_TOKEN" \
-H "Content-Type: application/json" \
"https://api.github.com/repos/${{ github.repository }}/issues/$PR_NUMBER/comments" \
--data @body.json

2
.gitignore vendored
View file

@ -32,8 +32,6 @@ src/render/shaders/*.inc
src/render/shaders/Shaders.hpp
hyprctl/hyprctl
hyprctl/hw-protocols/*.c*
hyprctl/hw-protocols/*.h*
gmon.out
*.out

View file

@ -17,21 +17,18 @@ 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")
message(STATUS "Gathering git info")
# Get git info hash and branch
execute_process(COMMAND ./scripts/generateVersion.sh
WORKING_DIRECTORY ${CMAKE_SOURCE_DIR})
# Make shader files includable
execute_process(COMMAND ./scripts/generateShaderIncludes.sh
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()
WORKING_DIRECTORY ${CMAKE_SOURCE_DIR})
find_package(PkgConfig REQUIRED)
@ -39,23 +36,11 @@ find_package(PkgConfig REQUIRED)
# provide a .pc file and won't be detected this way
pkg_check_modules(udis_dep IMPORTED_TARGET udis86>=1.7.2)
# Find non-pkgconfig udis86, otherwise fallback to subproject
# Fallback to subproject
if(NOT udis_dep_FOUND)
find_library(udis_nopc udis86)
if(NOT("${udis_nopc}" MATCHES "udis_nopc-NOTFOUND"))
message(STATUS "Found udis86 at ${udis_nopc}")
else()
add_subdirectory("subprojects/udis86")
include_directories("subprojects/udis86")
message(STATUS "udis86 dependency not found, falling back to subproject")
endif()
endif()
find_library(librt rt)
if("${librt}" MATCHES "librt-NOTFOUND")
unset(LIBRT)
else()
set(LIBRT rt)
add_subdirectory("subprojects/udis86")
include_directories("subprojects/udis86")
message(STATUS "udis86 dependency not found, falling back to subproject")
endif()
if(CMAKE_BUILD_TYPE)
@ -86,11 +71,9 @@ 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,19 +85,15 @@ set(CXX_STANDARD_REQUIRED ON)
add_compile_options(
-Wall
-Wextra
-Wpedantic
-Wno-unused-parameter
-Wno-unused-value
-Wno-missing-field-initializers
-Wno-gnu-zero-variadic-macro-arguments
-Wno-narrowing
-Wno-pointer-arith
-Wno-clobbered
-Wpedantic
-fmacro-prefix-map=${CMAKE_SOURCE_DIR}/=)
# disable lto as it may break plugins
add_compile_options(-fno-lto)
set(CMAKE_EXECUTABLE_ENABLE_EXPORTS TRUE)
set(CMAKE_EXPORT_COMPILE_COMMANDS TRUE)
@ -125,153 +104,48 @@ find_package(Threads REQUIRED)
set(GLES_VERSION "GLES3")
find_package(OpenGL REQUIRED COMPONENTS ${GLES_VERSION})
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.0)
set(HYPRGRAPHICS_MINIMUM_VERSION 0.1.6)
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})
pkg_check_modules(aquamarine_dep REQUIRED IMPORTED_TARGET aquamarine>=0.9.0)
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.8.1)
pkg_check_modules(hyprgraphics_dep REQUIRED IMPORTED_TARGET hyprgraphics>=0.1.3)
string(REPLACE "." ";" AQ_VERSION_LIST ${aquamarine_dep_VERSION})
list(GET AQ_VERSION_LIST 0 AQ_VERSION_MAJOR)
list(GET AQ_VERSION_LIST 1 AQ_VERSION_MINOR)
list(GET AQ_VERSION_LIST 2 AQ_VERSION_PATCH)
set(AQUAMARINE_VERSION "${aquamarine_dep_VERSION}")
set(AQUAMARINE_VERSION_MAJOR "${AQ_VERSION_MAJOR}")
set(AQUAMARINE_VERSION_MINOR "${AQ_VERSION_MINOR}")
set(AQUAMARINE_VERSION_PATCH "${AQ_VERSION_PATCH}")
set(HYPRLANG_VERSION "${hyprlang_dep_VERSION}")
set(HYPRUTILS_VERSION "${hyprutils_dep_VERSION}")
set(HYPRCURSOR_VERSION "${hyprcursor_dep_VERSION}")
set(HYPRGRAPHICS_VERSION "${hyprgraphics_dep_VERSION}")
find_package(Git QUIET)
# Populate variables with env vars if present
set(GIT_COMMIT_HASH "$ENV{GIT_COMMIT_HASH}")
if(NOT GIT_COMMIT_HASH)
set(GIT_COMMIT_HASH "unknown")
endif()
set(GIT_BRANCH "$ENV{GIT_BRANCH}")
if(NOT GIT_BRANCH)
set(GIT_BRANCH "unknown")
endif()
set(GIT_COMMIT_MESSAGE "$ENV{GIT_COMMIT_MESSAGE}")
if(NOT GIT_COMMIT_MESSAGE)
set(GIT_COMMIT_MESSAGE "unknown")
endif()
set(GIT_COMMIT_DATE "$ENV{GIT_COMMIT_DATE}")
if(NOT GIT_COMMIT_DATE)
set(GIT_COMMIT_DATE "unknown")
endif()
set(GIT_DIRTY "$ENV{GIT_DIRTY}")
if(NOT GIT_DIRTY)
set(GIT_DIRTY "unknown")
endif()
set(GIT_TAG "$ENV{GIT_TAG}")
if(NOT GIT_TAG)
set(GIT_TAG "unknown")
endif()
set(GIT_COMMITS "$ENV{GIT_COMMITS}")
if(NOT GIT_COMMITS)
set(GIT_COMMITS "0")
endif()
if(Git_FOUND)
execute_process(
COMMAND ${GIT_EXECUTABLE} rev-parse --show-toplevel
WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
OUTPUT_VARIABLE GIT_TOPLEVEL
OUTPUT_STRIP_TRAILING_WHITESPACE
ERROR_QUIET
RESULT_VARIABLE GIT_TOPLEVEL_RESULT
)
if(GIT_TOPLEVEL_RESULT EQUAL 0)
message(STATUS "Detected git repository root: ${GIT_TOPLEVEL}")
execute_process(COMMAND ${GIT_EXECUTABLE} rev-parse HEAD
WORKING_DIRECTORY ${GIT_TOPLEVEL}
OUTPUT_VARIABLE GIT_COMMIT_HASH OUTPUT_STRIP_TRAILING_WHITESPACE)
execute_process(COMMAND ${GIT_EXECUTABLE} branch --show-current
WORKING_DIRECTORY ${GIT_TOPLEVEL}
OUTPUT_VARIABLE GIT_BRANCH OUTPUT_STRIP_TRAILING_WHITESPACE)
execute_process(COMMAND sh "-c" "${GIT_EXECUTABLE} show -s --format=%s --no-show-signature | sed \"s/\\\"/\'/g\""
WORKING_DIRECTORY ${GIT_TOPLEVEL}
OUTPUT_VARIABLE GIT_COMMIT_MESSAGE OUTPUT_STRIP_TRAILING_WHITESPACE)
execute_process(COMMAND ${GIT_EXECUTABLE} show -s --format=%cd --date=local --no-show-signature
WORKING_DIRECTORY ${GIT_TOPLEVEL}
OUTPUT_VARIABLE GIT_COMMIT_DATE OUTPUT_STRIP_TRAILING_WHITESPACE)
execute_process(COMMAND ${GIT_EXECUTABLE} diff-index --quiet HEAD --
WORKING_DIRECTORY ${GIT_TOPLEVEL}
RESULT_VARIABLE GIT_DIRTY_RESULT)
if(NOT GIT_DIRTY_RESULT EQUAL 0)
set(GIT_DIRTY "dirty")
else()
set(GIT_DIRTY "clean")
endif()
execute_process(COMMAND ${GIT_EXECUTABLE} describe --tags
WORKING_DIRECTORY ${GIT_TOPLEVEL}
OUTPUT_VARIABLE GIT_TAG OUTPUT_STRIP_TRAILING_WHITESPACE)
execute_process(COMMAND ${GIT_EXECUTABLE} rev-list --count HEAD
WORKING_DIRECTORY ${GIT_TOPLEVEL}
OUTPUT_VARIABLE GIT_COMMITS OUTPUT_STRIP_TRAILING_WHITESPACE)
else()
message(WARNING "No Git repository detected in ${CMAKE_SOURCE_DIR}")
endif()
endif()
configure_file(
${CMAKE_SOURCE_DIR}/src/version.h.in
${CMAKE_SOURCE_DIR}/src/version.h
@ONLY
)
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.90)
set(WAYLAND_SERVER_PROTOCOLS_MINIMUM_VERSION 1.45)
set(LIBINPUT_MINIMUM_VERSION 1.28)
add_compile_definitions(AQUAMARINE_VERSION="${aquamarine_dep_VERSION}")
add_compile_definitions(AQUAMARINE_VERSION_MAJOR=${AQ_VERSION_MAJOR})
add_compile_definitions(AQUAMARINE_VERSION_MINOR=${AQ_VERSION_MINOR})
add_compile_definitions(AQUAMARINE_VERSION_PATCH=${AQ_VERSION_PATCH})
add_compile_definitions(HYPRLANG_VERSION="${hyprlang_dep_VERSION}")
add_compile_definitions(HYPRUTILS_VERSION="${hyprutils_dep_VERSION}")
add_compile_definitions(HYPRCURSOR_VERSION="${hyprcursor_dep_VERSION}")
add_compile_definitions(HYPRGRAPHICS_VERSION="${hyprgraphics_dep_VERSION}")
pkg_check_modules(
deps
REQUIRED
IMPORTED_TARGET GLOBAL
xkbcommon>=${XKBCOMMON_MINIMUM_VERSION}
IMPORTED_TARGET
xkbcommon
uuid
wayland-server>=${WAYLAND_SERVER_MINIMUM_VERSION}
wayland-protocols>=${WAYLAND_SERVER_PROTOCOLS_MINIMUM_VERSION}
wayland-server>=1.22.90
wayland-protocols>=1.43
cairo
pango
pangocairo
pixman-1
xcursor
libdrm
libinput>=${LIBINPUT_MINIMUM_VERSION}
libinput>=1.28
gbm
gio-2.0
re2
muparser)
re2)
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)
@ -279,12 +153,7 @@ if(USE_TRACY)
message(STATUS "Tracy enabled, TRACY_CPP_FILES: " ${TRACY_CPP_FILES})
endif()
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})
add_executable(Hyprland ${SRCFILES} ${TRACY_CPP_FILES})
set(USE_GPROF OFF)
@ -294,8 +163,8 @@ if(CMAKE_BUILD_TYPE MATCHES Debug OR CMAKE_BUILD_TYPE MATCHES DEBUG)
if(WITH_ASAN)
message(STATUS "Enabling ASan")
target_link_libraries(hyprland_lib PUBLIC asan)
target_compile_options(hyprland_lib PUBLIC -fsanitize=address)
target_link_libraries(Hyprland asan)
target_compile_options(Hyprland PUBLIC -fsanitize=address)
endif()
if(USE_TRACY)
@ -305,7 +174,7 @@ if(CMAKE_BUILD_TYPE MATCHES Debug OR CMAKE_BUILD_TYPE MATCHES DEBUG)
option(TRACY_ON_DEMAND "" ON)
add_subdirectory(subprojects/tracy)
target_link_libraries(hyprland_lib PUBLIC Tracy::TracyClient)
target_link_libraries(Hyprland Tracy::TracyClient)
if(USE_TRACY_GPU)
message(STATUS "Tracy GPU Profiling is turned on")
@ -321,10 +190,6 @@ if(CMAKE_BUILD_TYPE MATCHES Debug OR CMAKE_BUILD_TYPE MATCHES DEBUG)
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")
@ -334,19 +199,19 @@ endif()
include(CheckLibraryExists)
check_library_exists(execinfo backtrace "" HAVE_LIBEXECINFO)
if(HAVE_LIBEXECINFO)
target_link_libraries(hyprland_lib PUBLIC execinfo)
target_link_libraries(Hyprland 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_lib PUBLIC PkgConfig::epoll)
target_link_libraries(Hyprland 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_lib PUBLIC PkgConfig::inotify)
target_link_libraries(Hyprland PkgConfig::inotify)
endif()
if(NO_XWAYLAND)
@ -354,7 +219,10 @@ if(NO_XWAYLAND)
add_compile_definitions(NO_XWAYLAND)
else()
message(STATUS "XWAYLAND Enabled (NO_XWAYLAND not defined) checking deps...")
set(XWAYLAND_DEPENDENCIES
pkg_check_modules(
xdeps
REQUIRED
IMPORTED_TARGET
xcb
xcb-render
xcb-xfixes
@ -362,21 +230,9 @@ else()
xcb-composite
xcb-res
xcb-errors)
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)
target_link_libraries(Hyprland PkgConfig::xdeps)
endif()
configure_file(hyprland.pc.in hyprland.pc @ONLY)
if(NO_SYSTEMD)
message(STATUS "SYSTEMD support is disabled...")
else()
@ -397,42 +253,30 @@ set(CPACK_PROJECT_NAME ${PROJECT_NAME})
set(CPACK_PROJECT_VERSION ${PROJECT_VERSION})
include(CPack)
if(CMAKE_DISABLE_PRECOMPILE_HEADERS)
message(STATUS "Not using precompiled headers")
else()
message(STATUS "Setting precompiled headers")
target_precompile_headers(hyprland_lib PRIVATE
$<$<COMPILE_LANGUAGE:CXX>:src/pch/pch.hpp>)
endif()
message(STATUS "Setting precompiled headers")
target_precompile_headers(Hyprland PRIVATE
$<$<COMPILE_LANGUAGE:CXX>:src/pch/pch.hpp>)
message(STATUS "Setting link libraries")
target_link_libraries(
hyprland_lib
PUBLIC
Hyprland
rt
PkgConfig::aquamarine_dep
PkgConfig::hyprlang_dep
PkgConfig::hyprutils_dep
PkgConfig::hyprcursor_dep
PkgConfig::hyprgraphics_dep
PkgConfig::deps
)
target_link_libraries(
Hyprland
${LIBRT}
hyprland_lib)
PkgConfig::deps)
if(udis_dep_FOUND)
target_link_libraries(hyprland_lib PUBLIC PkgConfig::udis_dep)
elseif(NOT("${udis_nopc}" MATCHES "udis_nopc-NOTFOUND"))
target_link_libraries(hyprland_lib PUBLIC ${udis_nopc})
target_link_libraries(Hyprland PkgConfig::udis_dep)
else()
target_link_libraries(hyprland_lib PUBLIC libudis86)
target_link_libraries(Hyprland 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)
@ -446,15 +290,10 @@ function(protocolnew protoPath protoName external)
COMMAND hyprwayland-scanner ${path}/${protoName}.xml
${CMAKE_SOURCE_DIR}/protocols/
WORKING_DIRECTORY ${CMAKE_SOURCE_DIR})
target_sources(hyprland_lib PRIVATE protocols/${protoName}.cpp
target_sources(Hyprland 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(
@ -464,17 +303,12 @@ function(protocolWayland)
hyprwayland-scanner --wayland-enums
${WAYLAND_SCANNER_PKGDATA_DIR}/wayland.xml ${CMAKE_SOURCE_DIR}/protocols/
WORKING_DIRECTORY ${CMAKE_SOURCE_DIR})
target_sources(hyprland_lib PRIVATE protocols/wayland.cpp protocols/wayland.hpp)
target_sources(Hyprland 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_lib PUBLIC OpenGL::EGL OpenGL::GL Threads::Threads)
target_link_libraries(Hyprland OpenGL::EGL OpenGL::GL Threads::Threads)
pkg_check_modules(hyprland_protocols_dep hyprland-protocols>=0.6.4)
if(hyprland_protocols_dep_FOUND)
@ -547,16 +381,11 @@ protocolnew("staging/color-management" "color-management-v1" false)
protocolnew("staging/xdg-toplevel-tag" "xdg-toplevel-tag-v1" false)
protocolnew("staging/xdg-system-bell" "xdg-system-bell-v1" false)
protocolnew("staging/ext-workspace" "ext-workspace-v1" false)
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)
protocolwayland()
# tools
add_subdirectory(hyprctl)
add_subdirectory(start)
if(NO_HYPRPM)
message(STATUS "hyprpm is disabled")
@ -565,6 +394,12 @@ else()
message(STATUS "hyprpm is enabled (NO_HYPRPM not defined)")
endif()
if(NO_TESTS)
message(STATUS "building tests is disabled")
else()
message(STATUS "building tests is enabled (NO_TESTS not defined)")
endif()
# binary and symlink
install(TARGETS Hyprland)
@ -583,6 +418,7 @@ add_compile_definitions(DATAROOTDIR="${CMAKE_INSTALL_FULL_DATAROOTDIR}")
# installable assets
file(GLOB_RECURSE INSTALLABLE_ASSETS "assets/install/*")
list(FILTER INSTALLABLE_ASSETS EXCLUDE REGEX "meson.build")
install(FILES ${INSTALLABLE_ASSETS}
DESTINATION ${CMAKE_INSTALL_DATAROOTDIR}/hypr)
@ -620,47 +456,15 @@ install(
PATTERN "*.hpp"
PATTERN "*.inc")
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)
message(STATUS "Testing is enabled")
if(TESTS)
enable_testing()
add_custom_target(tests)
add_dependencies(tests hyprland_gtests)
add_subdirectory(hyprtester)
add_test(
NAME "Main Test"
WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}/hyprtester
COMMAND hyprtester)
else()
message(STATUS "Testing is disabled")
add_dependencies(tests hyprtester)
endif()

View file

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

View file

@ -8,7 +8,7 @@ release:
cmake --build ./build --config Release --target all -j`nproc 2>/dev/null || getconf NPROCESSORS_CONF`
debug:
cmake --no-warn-unused-cli -DCMAKE_BUILD_TYPE:STRING=Debug -DTESTS=true -DCMAKE_INSTALL_PREFIX:STRING=${PREFIX} -S . -B ./build
cmake --no-warn-unused-cli -DCMAKE_BUILD_TYPE:STRING=Debug -DCMAKE_INSTALL_PREFIX:STRING=${PREFIX} -S . -B ./build
cmake --build ./build --config Debug --target all -j`nproc 2>/dev/null || getconf NPROCESSORS_CONF`
nopch:
@ -92,7 +92,3 @@ asan:
@echo "Hyprland done"
ASAN_OPTIONS="detect_odr_violation=0,log_path=asan.log" HYPRLAND_NO_CRASHREPORTER=1 ./build/Hyprland -c ~/.config/hypr/hyprland.conf
test:
$(MAKE) debug
./build/hyprtester/hyprtester -c hyprtester/test.conf -b ./build/Hyprland -p hyprtester/plugin/hyprtestplugin.so

View file

@ -4,7 +4,7 @@
<br>
[![Badge Workflow]][Workflow]
![Badge Workflow]
[![Badge License]][License]
![Badge Language]
[![Badge Pull Requests]][Pull Requests]
@ -111,7 +111,6 @@ easy IPC, much more QoL stuff than other compositors and more...
[Contribute]: https://wiki.hypr.land/Contributing-and-Debugging/
[Install]: https://wiki.hypr.land/Getting-Started/Installation/
[Quick Start]: https://wiki.hypr.land/Getting-Started/Master-Tutorial/
[Workflow]: https://github.com/hyprwm/Hyprland/actions/workflows/ci.yaml
[License]: LICENSE

View file

@ -1 +1 @@
0.52.0
0.50.0

View file

@ -0,0 +1,10 @@
globber = run_command('sh', '-c', 'find . -type f -not -name "*.build"', check: true)
files = globber.stdout().strip().split('\n')
foreach file : files
install_data(
file,
install_dir: join_paths(get_option('datadir'), 'hypr'),
install_tag: 'runtime',
)
endforeach

7
assets/meson.build Normal file
View file

@ -0,0 +1,7 @@
install_data(
'hyprland-portals.conf',
install_dir: join_paths(get_option('datadir'), 'xdg-desktop-portal'),
install_tag: 'runtime',
)
subdir('install')

2
docs/meson.build Normal file
View file

@ -0,0 +1,2 @@
install_man('Hyprland.1')
install_man('hyprctl.1')

View file

@ -27,7 +27,7 @@ monitor=,preferred,auto,auto
# Set programs that you use
$terminal = kitty
$fileManager = dolphin
$menu = hyprlauncher
$menu = wofi --show drun
#################
@ -125,33 +125,30 @@ decoration {
animations {
enabled = yes, please :)
# Default curves, see https://wiki.hypr.land/Configuring/Animations/#curves
# NAME, X0, Y0, X1, Y1
bezier = easeOutQuint, 0.23, 1, 0.32, 1
bezier = easeInOutCubic, 0.65, 0.05, 0.36, 1
bezier = linear, 0, 0, 1, 1
bezier = almostLinear, 0.5, 0.5, 0.75, 1
bezier = quick, 0.15, 0, 0.1, 1
# Default animations, see https://wiki.hypr.land/Configuring/Animations/ for more
# Default animations, see https://wiki.hypr.land/Configuring/Animations/
# NAME, ONOFF, SPEED, CURVE, [STYLE]
animation = global, 1, 10, default
animation = border, 1, 5.39, easeOutQuint
animation = windows, 1, 4.79, easeOutQuint
animation = windowsIn, 1, 4.1, easeOutQuint, popin 87%
animation = windowsOut, 1, 1.49, linear, popin 87%
animation = fadeIn, 1, 1.73, almostLinear
animation = fadeOut, 1, 1.46, almostLinear
animation = fade, 1, 3.03, quick
animation = layers, 1, 3.81, easeOutQuint
animation = layersIn, 1, 4, easeOutQuint, fade
animation = layersOut, 1, 1.5, linear, fade
animation = fadeLayersIn, 1, 1.79, almostLinear
animation = fadeLayersOut, 1, 1.39, almostLinear
animation = workspaces, 1, 1.94, almostLinear, fade
animation = workspacesIn, 1, 1.21, almostLinear, fade
animation = workspacesOut, 1, 1.94, almostLinear, fade
animation = zoomFactor, 1, 7, quick
bezier = easeOutQuint,0.23,1,0.32,1
bezier = easeInOutCubic,0.65,0.05,0.36,1
bezier = linear,0,0,1,1
bezier = almostLinear,0.5,0.5,0.75,1.0
bezier = quick,0.15,0,0.1,1
animation = global, 1, 10, default
animation = border, 1, 5.39, easeOutQuint
animation = windows, 1, 4.79, easeOutQuint
animation = windowsIn, 1, 4.1, easeOutQuint, popin 87%
animation = windowsOut, 1, 1.49, linear, popin 87%
animation = fadeIn, 1, 1.73, almostLinear
animation = fadeOut, 1, 1.46, almostLinear
animation = fade, 1, 3.03, quick
animation = layers, 1, 3.81, easeOutQuint
animation = layersIn, 1, 4, easeOutQuint, fade
animation = layersOut, 1, 1.5, linear, fade
animation = fadeLayersIn, 1, 1.79, almostLinear
animation = fadeLayersOut, 1, 1.39, almostLinear
animation = workspaces, 1, 1.94, almostLinear, fade
animation = workspacesIn, 1, 1.21, almostLinear, fade
animation = workspacesOut, 1, 1.94, almostLinear, fade
}
# Ref https://wiki.hypr.land/Configuring/Workspace-Rules/
@ -159,23 +156,10 @@ animations {
# uncomment all if you wish to use that.
# workspace = w[tv1], gapsout:0, gapsin:0
# workspace = f[1], gapsout:0, gapsin:0
# windowrule {
# name = no-gaps-wtv1
# match:float = false
# match:workspace = w[tv1]
#
# border_size = 0
# rounding = 0
# }
#
# windowrule {
# name = no-gaps-f1
# match:float = false
# match:workspace = f[1]
#
# border_size = 0
# rounding = 0
# }
# windowrule = bordersize 0, floating:0, onworkspace:w[tv1]
# windowrule = rounding 0, floating:0, onworkspace:w[tv1]
# windowrule = bordersize 0, floating:0, onworkspace:f[1]
# windowrule = rounding 0, floating:0, onworkspace:f[1]
# See https://wiki.hypr.land/Configuring/Dwindle-Layout/ for more
dwindle {
@ -216,8 +200,10 @@ input {
}
}
# See https://wiki.hypr.land/Configuring/Gestures
gesture = 3, horizontal, workspace
# https://wiki.hypr.land/Configuring/Variables/#gestures
gestures {
workspace_swipe = false
}
# Example per-device config
# See https://wiki.hypr.land/Configuring/Keywords/#per-device-input-configs for more
@ -237,7 +223,7 @@ $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, exec, command -v hyprshutdown >/dev/null 2>&1 && hyprshutdown || hyprctl dispatch exit
bind = $mainMod, M, exit,
bind = $mainMod, E, exec, $fileManager
bind = $mainMod, V, togglefloating,
bind = $mainMod, R, exec, $menu
@ -307,35 +293,11 @@ bindl = , XF86AudioPrev, exec, playerctl previous
# See https://wiki.hypr.land/Configuring/Window-Rules/ for more
# See https://wiki.hypr.land/Configuring/Workspace-Rules/ for workspace rules
# Example windowrules that are useful
# Example windowrule
# windowrule = float,class:^(kitty)$,title:^(kitty)$
windowrule {
# Ignore maximize requests from all apps. You'll probably like this.
name = suppress-maximize-events
match:class = .*
# Ignore maximize requests from apps. You'll probably like this.
windowrule = suppressevent maximize, class:.*
suppress_event = maximize
}
windowrule {
# Fix some dragging issues with XWayland
name = fix-xwayland-drags
match:class = ^$
match:title = ^$
match:xwayland = true
match:float = true
match:fullscreen = false
match:pin = false
no_focus = true
}
# Hyprland-run windowrule
windowrule {
name = move-hyprland-run
match:class = hyprland-run
move = 20 monitor_h-120
float = yes
}
# Fix some dragging issues with XWayland
windowrule = nofocus,class:^$,title:^$,xwayland:1,floating:1,fullscreen:0,pinned:0

View file

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

10
example/meson.build Normal file
View file

@ -0,0 +1,10 @@
install_data(
'hyprland.conf',
install_dir: join_paths(get_option('datadir'), 'hypr'),
install_tag: 'runtime',
)
install_data(
'hyprland.desktop',
install_dir: join_paths(get_option('datadir'), 'wayland-sessions'),
install_tag: 'runtime',
)

View file

@ -1,19 +1,16 @@
//
// Example blue light filter shader.
//
#version 300 es
//
precision mediump float;
in vec2 v_texcoord;
layout(location = 0) out vec4 fragColor;
varying vec2 v_texcoord;
uniform sampler2D tex;
void main() {
vec4 pixColor = texture(tex, v_texcoord);
vec4 pixColor = texture2D(tex, v_texcoord);
pixColor[2] *= 0.8;
fragColor = pixColor;
gl_FragColor = pixColor;
}

240
flake.lock generated
View file

@ -16,11 +16,11 @@
]
},
"locked": {
"lastModified": 1765900596,
"narHash": "sha256-+hn8v9jkkLP9m+o0Nm5SiEq10W0iWDSotH2XfjU45fA=",
"lastModified": 1751740947,
"narHash": "sha256-35040CHH7P3JGmhGVfEb2oJHL/A5mI2IXumhkxrBnao=",
"owner": "hyprwm",
"repo": "aquamarine",
"rev": "d83c97f8f5c0aae553c1489c7d9eff3eadcadace",
"rev": "dfc1db15a08c4cd234288f66e1199c653495301f",
"type": "github"
},
"original": {
@ -32,11 +32,11 @@
"flake-compat": {
"flake": false,
"locked": {
"lastModified": 1761588595,
"narHash": "sha256-XKUZz9zewJNUj46b4AJdiRZJAvSZ0Dqj2BNfXvFlJC4=",
"lastModified": 1696426674,
"narHash": "sha256-kvjfFW7WAETZlt09AgDn1MrtKzP7t90Vf7vypd3OL1U=",
"owner": "edolstra",
"repo": "flake-compat",
"rev": "f387cd2afec9419c8ee37694406ca490c3f34ee5",
"rev": "0f9255e01c2351cc7d116c072cb317785dd33b33",
"type": "github"
},
"original": {
@ -79,11 +79,11 @@
]
},
"locked": {
"lastModified": 1753964049,
"narHash": "sha256-lIqabfBY7z/OANxHoPeIrDJrFyYy9jAM4GQLzZ2feCM=",
"lastModified": 1749155331,
"narHash": "sha256-XR9fsI0zwLiFWfqi/pdS/VD+YNorKb3XIykgTg4l1nA=",
"owner": "hyprwm",
"repo": "hyprcursor",
"rev": "44e91d467bdad8dcf8bbd2ac7cf49972540980a5",
"rev": "45fcc10b4c282746d93ec406a740c43b48b4ef80",
"type": "github"
},
"original": {
@ -105,11 +105,11 @@
]
},
"locked": {
"lastModified": 1763733840,
"narHash": "sha256-JnET78yl5RvpGuDQy3rCycOCkiKoLr5DN1fPhRNNMco=",
"lastModified": 1751808145,
"narHash": "sha256-OXgL0XaKMmfX2rRQkt9SkJw+QNfv0jExlySt1D6O72g=",
"owner": "hyprwm",
"repo": "hyprgraphics",
"rev": "8f1bec691b2d198c60cccabca7a94add2df4ed1a",
"rev": "b841473a0bd4a1a74a0b64f1ec2ab199035c349f",
"type": "github"
},
"original": {
@ -118,45 +118,6 @@
"type": "github"
}
},
"hyprland-guiutils": {
"inputs": {
"aquamarine": [
"aquamarine"
],
"hyprgraphics": [
"hyprgraphics"
],
"hyprlang": [
"hyprlang"
],
"hyprtoolkit": "hyprtoolkit",
"hyprutils": [
"hyprutils"
],
"hyprwayland-scanner": [
"hyprwayland-scanner"
],
"nixpkgs": [
"nixpkgs"
],
"systems": [
"systems"
]
},
"locked": {
"lastModified": 1765643131,
"narHash": "sha256-CCGohW5EBIRy4B7vTyBMqPgsNcaNenVad/wszfddET0=",
"owner": "hyprwm",
"repo": "hyprland-guiutils",
"rev": "e50ae912813bdfa8372d62daf454f48d6df02297",
"type": "github"
},
"original": {
"owner": "hyprwm",
"repo": "hyprland-guiutils",
"type": "github"
}
},
"hyprland-protocols": {
"inputs": {
"nixpkgs": [
@ -167,11 +128,11 @@
]
},
"locked": {
"lastModified": 1765214753,
"narHash": "sha256-P9zdGXOzToJJgu5sVjv7oeOGPIIwrd9hAUAP3PsmBBs=",
"lastModified": 1749046714,
"narHash": "sha256-kymV5FMnddYGI+UjwIw8ceDjdeg7ToDVjbHCvUlhn14=",
"owner": "hyprwm",
"repo": "hyprland-protocols",
"rev": "3f3860b869014c00e8b9e0528c7b4ddc335c21ab",
"rev": "613878cb6f459c5e323aaafe1e6f388ac8a36330",
"type": "github"
},
"original": {
@ -180,6 +141,67 @@
"type": "github"
}
},
"hyprland-qt-support": {
"inputs": {
"hyprlang": [
"hyprland-qtutils",
"hyprlang"
],
"nixpkgs": [
"hyprland-qtutils",
"nixpkgs"
],
"systems": [
"hyprland-qtutils",
"systems"
]
},
"locked": {
"lastModified": 1749154592,
"narHash": "sha256-DO7z5CeT/ddSGDEnK9mAXm1qlGL47L3VAHLlLXoCjhE=",
"owner": "hyprwm",
"repo": "hyprland-qt-support",
"rev": "4c8053c3c888138a30c3a6c45c2e45f5484f2074",
"type": "github"
},
"original": {
"owner": "hyprwm",
"repo": "hyprland-qt-support",
"type": "github"
}
},
"hyprland-qtutils": {
"inputs": {
"hyprland-qt-support": "hyprland-qt-support",
"hyprlang": [
"hyprlang"
],
"hyprutils": [
"hyprland-qtutils",
"hyprlang",
"hyprutils"
],
"nixpkgs": [
"nixpkgs"
],
"systems": [
"systems"
]
},
"locked": {
"lastModified": 1750371812,
"narHash": "sha256-D868K1dVEACw17elVxRgXC6hOxY+54wIEjURztDWLk8=",
"owner": "hyprwm",
"repo": "hyprland-qtutils",
"rev": "b13c7481e37856f322177010bdf75fccacd1adc8",
"type": "github"
},
"original": {
"owner": "hyprwm",
"repo": "hyprland-qtutils",
"type": "github"
}
},
"hyprlang": {
"inputs": {
"hyprutils": [
@ -193,11 +215,11 @@
]
},
"locked": {
"lastModified": 1764612430,
"narHash": "sha256-54ltTSbI6W+qYGMchAgCR6QnC1kOdKXN6X6pJhOWxFg=",
"lastModified": 1750371198,
"narHash": "sha256-/iuJ1paQOBoSLqHflRNNGyroqfF/yvPNurxzcCT0cAE=",
"owner": "hyprwm",
"repo": "hyprlang",
"rev": "0d00dc118981531aa731150b6ea551ef037acddd",
"rev": "cee01452bca58d6cadb3224e21e370de8bc20f0b",
"type": "github"
},
"original": {
@ -206,51 +228,6 @@
"type": "github"
}
},
"hyprtoolkit": {
"inputs": {
"aquamarine": [
"hyprland-guiutils",
"aquamarine"
],
"hyprgraphics": [
"hyprland-guiutils",
"hyprgraphics"
],
"hyprlang": [
"hyprland-guiutils",
"hyprlang"
],
"hyprutils": [
"hyprland-guiutils",
"hyprutils"
],
"hyprwayland-scanner": [
"hyprland-guiutils",
"hyprwayland-scanner"
],
"nixpkgs": [
"hyprland-guiutils",
"nixpkgs"
],
"systems": [
"hyprland-guiutils",
"systems"
]
},
"locked": {
"lastModified": 1764592794,
"narHash": "sha256-7CcO+wbTJ1L1NBQHierHzheQGPWwkIQug/w+fhTAVuU=",
"owner": "hyprwm",
"repo": "hyprtoolkit",
"rev": "5cfe0743f0e608e1462972303778d8a0859ee63e",
"type": "github"
},
"original": {
"owner": "hyprwm",
"repo": "hyprtoolkit",
"type": "github"
}
},
"hyprutils": {
"inputs": {
"nixpkgs": [
@ -261,11 +238,11 @@
]
},
"locked": {
"lastModified": 1766160771,
"narHash": "sha256-roINUGikWRqqgKrD4iotKbGj3ZKJl3hjMz5l/SyKrHw=",
"lastModified": 1751888065,
"narHash": "sha256-F2SV9WGqgtRsXIdUrl3sRe0wXlQD+kRRZcSfbepjPJY=",
"owner": "hyprwm",
"repo": "hyprutils",
"rev": "5ac060bfcf2f12b3a6381156ebbc13826a05b09f",
"rev": "a8229739cf36d159001cfc203871917b83fdf917",
"type": "github"
},
"original": {
@ -284,11 +261,11 @@
]
},
"locked": {
"lastModified": 1763640274,
"narHash": "sha256-Uan1Nl9i4TF/kyFoHnTq1bd/rsWh4GAK/9/jDqLbY5A=",
"lastModified": 1751881472,
"narHash": "sha256-meB0SnXbwIe2trD041MLKEv6R7NZ759QwBcVIhlSBfE=",
"owner": "hyprwm",
"repo": "hyprwayland-scanner",
"rev": "f6cf414ca0e16a4d30198fd670ec86df3c89f671",
"rev": "8fb426b3e5452fd9169453fd6c10f8c14ca37120",
"type": "github"
},
"original": {
@ -297,39 +274,13 @@
"type": "github"
}
},
"hyprwire": {
"inputs": {
"hyprutils": [
"hyprutils"
],
"nixpkgs": [
"nixpkgs"
],
"systems": [
"systems"
]
},
"locked": {
"lastModified": 1766253200,
"narHash": "sha256-26qPwrd3od+xoYVywSB7hC2cz9ivN46VPLlrsXyGxvE=",
"owner": "hyprwm",
"repo": "hyprwire",
"rev": "1079777525b30a947c8d657fac158e00ae85de9d",
"type": "github"
},
"original": {
"owner": "hyprwm",
"repo": "hyprwire",
"type": "github"
}
},
"nixpkgs": {
"locked": {
"lastModified": 1766070988,
"narHash": "sha256-G/WVghka6c4bAzMhTwT2vjLccg/awmHkdKSd2JrycLc=",
"lastModified": 1751792365,
"narHash": "sha256-J1kI6oAj25IG4EdVlg2hQz8NZTBNYvIS0l4wpr9KcUo=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "c6245e83d836d0433170a16eb185cefe0572f8b8",
"rev": "1fd8bada0b6117e6c7eb54aad5813023eed37ccb",
"type": "github"
},
"original": {
@ -348,11 +299,11 @@
]
},
"locked": {
"lastModified": 1765911976,
"narHash": "sha256-t3T/xm8zstHRLx+pIHxVpQTiySbKqcQbK+r+01XVKc0=",
"lastModified": 1750779888,
"narHash": "sha256-wibppH3g/E2lxU43ZQHC5yA/7kIKLGxVEnsnVK1BtRg=",
"owner": "cachix",
"repo": "git-hooks.nix",
"rev": "b68b780b69702a090c8bb1b973bab13756cc7a27",
"rev": "16ec914f6fb6f599ce988427d9d94efddf25fe6d",
"type": "github"
},
"original": {
@ -366,12 +317,11 @@
"aquamarine": "aquamarine",
"hyprcursor": "hyprcursor",
"hyprgraphics": "hyprgraphics",
"hyprland-guiutils": "hyprland-guiutils",
"hyprland-protocols": "hyprland-protocols",
"hyprland-qtutils": "hyprland-qtutils",
"hyprlang": "hyprlang",
"hyprutils": "hyprutils",
"hyprwayland-scanner": "hyprwayland-scanner",
"hyprwire": "hyprwire",
"nixpkgs": "nixpkgs",
"pre-commit-hooks": "pre-commit-hooks",
"systems": "systems",
@ -415,11 +365,11 @@
]
},
"locked": {
"lastModified": 1761431178,
"narHash": "sha256-xzjC1CV3+wpUQKNF+GnadnkeGUCJX+vgaWIZsnz9tzI=",
"lastModified": 1751300244,
"narHash": "sha256-PFuv1TZVYvQhha0ac53E3YgdtmLShrN0t4T6xqHl0jE=",
"owner": "hyprwm",
"repo": "xdg-desktop-portal-hyprland",
"rev": "4b8801228ff958d028f588f0c2b911dbf32297f9",
"rev": "6115f3fdcb2c1a57b4a80a69f3c797e47607b90a",
"type": "github"
},
"original": {

View file

@ -35,15 +35,11 @@
inputs.systems.follows = "systems";
};
hyprland-guiutils = {
url = "github:hyprwm/hyprland-guiutils";
hyprland-qtutils = {
url = "github:hyprwm/hyprland-qtutils";
inputs.nixpkgs.follows = "nixpkgs";
inputs.systems.follows = "systems";
inputs.aquamarine.follows = "aquamarine";
inputs.hyprgraphics.follows = "hyprgraphics";
inputs.hyprutils.follows = "hyprutils";
inputs.hyprlang.follows = "hyprlang";
inputs.hyprwayland-scanner.follows = "hyprwayland-scanner";
};
hyprlang = {
@ -65,13 +61,6 @@
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";
@ -159,7 +148,7 @@
# hyprland-packages
hyprland
hyprland-unwrapped
hyprland-with-tests
hyprtester
# hyprland-extras
xdg-desktop-portal-hyprland
;
@ -187,7 +176,7 @@
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
# Recent versions of Hydra can aggregate jobsets from 'hydraJobs' intead 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,32 +5,11 @@ project(
DESCRIPTION "Control utility for Hyprland"
)
pkg_check_modules(hyprctl_deps REQUIRED IMPORTED_TARGET hyprutils>=0.2.4 hyprwire re2)
pkg_check_modules(hyprctl_deps REQUIRED IMPORTED_TARGET hyprutils>=0.2.4 re2)
file(GLOB_RECURSE HYPRCTL_SRCFILES CONFIGURE_DEPENDS "src/*.cpp" "hw-protocols/*.cpp" "include/*.hpp")
add_executable(hyprctl ${HYPRCTL_SRCFILES})
add_executable(hyprctl "main.cpp")
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

@ -49,7 +49,6 @@ commands:
the same format as in colors in config. Will reset
when Hyprland's config is reloaded
setprop ... Sets a window property
getprop ... Gets a window property
splash Get the current splash
switchxkblayout ... Sets the xkb layout index for a keyboard
systeminfo Get system info
@ -74,8 +73,11 @@ flags:
const std::string_view HYPRPAPER_HELP = R"#(usage: hyprctl [flags] hyprpaper <request>
requests:
wallpaper Issue a wallpaper to call a config wallpaper dynamically.
Arguments are [mon],[path],[fit_mode]. Fit mode is optional.
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
flags:
See 'hyprctl --help')#";
@ -157,18 +159,6 @@ lock:
flags:
See 'hyprctl --help')#";
const std::string_view GETPROP_HELP = R"#(usage: hyprctl [flags] getprop <regex> <property>
regex:
Regular expression by which a window will be searched
property:
See https://wiki.hypr.land/Configuring/Using-hyprctl/#setprop for list
of properties
flags:
See 'hyprctl --help')#";
const std::string_view SWITCHXKBLAYOUT_HELP = R"#(usage: [flags] switchxkblayout <device> <cmd>
device:

View file

@ -1,144 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<protocol name="hyprpaper_core" version="1">
<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="1">
<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>
</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>
</protocol>

View file

@ -48,7 +48,7 @@ function _hyprctl
set descriptions[22] "Focus the urgent window or the last window"
set descriptions[23] "Get the list of defined workspace rules"
set descriptions[24] "Move the active workspace to a monitor"
set descriptions[25] "Move window doesn't switch to the workspace"
set descriptions[25] "Move window doesnt switch to the workspace"
set descriptions[26] "Interact with hyprpaper if present"
set descriptions[29] "Swap the active window with the next or previous in a group"
set descriptions[30] "Move the cursor to the corner of the active window"

View file

@ -1,4 +1,4 @@
# This is a file fed to complgen to generate bash/fish/zsh completions
# This is a file feeded to complgen to generate bash/fish/zsh completions
# Repo: https://github.com/adaszko/complgen
# Generate completion scripts: "complgen aot --bash-script hyprctl.bash --fish-script hyprctl.fish --zsh-script hyprctl.zsh ./hyprctl.usage"
@ -111,7 +111,7 @@ hyprctl [<OPTIONS>]... <ARGUMENTS>
| (closewindow) "Close a specified window"
| (workspace) "Change the workspace"
| (movetoworkspace) "Move the focused window to a workspace"
| (movetoworkspacesilent) "Move window doesn't switch to the workspace"
| (movetoworkspacesilent) "Move window doesnt switch to the workspace"
| (togglefloating) "Toggle the current window's floating state"
| (setfloating) "Set the current window's floating state to true"
| (settiled) "Set the current window's floating state to false"

View file

@ -36,7 +36,7 @@ _hyprctl () {
descriptions[22]="Focus the urgent window or the last window"
descriptions[23]="Get the list of defined workspace rules"
descriptions[24]="Move the active workspace to a monitor"
descriptions[25]="Move window doesn't switch to the workspace"
descriptions[25]="Move window doesnt switch to the workspace"
descriptions[26]="Interact with hyprpaper if present"
descriptions[29]="Swap the active window with the next or previous in a group"
descriptions[30]="Move the cursor to the corner of the active window"

View file

@ -26,12 +26,9 @@
#include <filesystem>
#include <cstdarg>
#include <hyprutils/string/String.hpp>
#include <hyprutils/memory/Casts.hpp>
using namespace Hyprutils::String;
using namespace Hyprutils::Memory;
#include "Strings.hpp"
#include "hyprpaper/Hyprpaper.hpp"
std::string instanceSignature;
bool quiet = false;
@ -209,7 +206,7 @@ int request(std::string_view arg, int minArgs = 0, bool needRoll = false) {
strncpy(serverAddress.sun_path, socketPath.c_str(), sizeof(serverAddress.sun_path) - 1);
if (connect(SERVERSOCKET, rc<sockaddr*>(&serverAddress), SUN_LEN(&serverAddress)) < 0) {
if (connect(SERVERSOCKET, (sockaddr*)&serverAddress, SUN_LEN(&serverAddress)) < 0) {
log("Couldn't connect to " + socketPath + ". (4)");
return 4;
}
@ -275,7 +272,7 @@ int requestIPC(std::string_view filename, std::string_view arg) {
strncpy(serverAddress.sun_path, socketPath.c_str(), sizeof(serverAddress.sun_path) - 1);
if (connect(SERVERSOCKET, rc<sockaddr*>(&serverAddress), SUN_LEN(&serverAddress)) < 0) {
if (connect(SERVERSOCKET, (sockaddr*)&serverAddress, SUN_LEN(&serverAddress)) < 0) {
log("Couldn't connect to " + socketPath + ". (3)");
return 3;
}
@ -306,6 +303,10 @@ 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);
}
@ -424,8 +425,6 @@ int main(int argc, char** argv) {
std::println("{}", PLUGIN_HELP);
} else if (cmd == "setprop") {
std::println("{}", SETPROP_HELP);
} else if (cmd == "getprop") {
std::println("{}", GETPROP_HELP);
} else if (cmd == "switchxkblayout") {
std::println("{}", SWITCHXKBLAYOUT_HELP);
} else {
@ -476,7 +475,7 @@ int main(int argc, char** argv) {
const auto INSTANCES = instances();
if (INSTANCENO < 0 || sc<std::size_t>(INSTANCENO) >= INSTANCES.size()) {
if (INSTANCENO < 0 || static_cast<std::size_t>(INSTANCENO) >= INSTANCES.size()) {
log("no such instance\n");
return 1;
}
@ -497,12 +496,9 @@ int main(int argc, char** argv) {
if (fullRequest.contains("/--batch"))
batchRequest(fullRequest, json);
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"))
else if (fullRequest.contains("/hyprpaper"))
exitStatus = requestHyprpaper(fullRequest);
else if (fullRequest.contains("/hyprsunset"))
exitStatus = requestHyprsunset(fullRequest);
else if (fullRequest.contains("/switchxkblayout"))
exitStatus = request(fullRequest, 2);

27
hyprctl/meson.build Normal file
View file

@ -0,0 +1,27 @@
executable(
'hyprctl',
'main.cpp',
dependencies: [
dependency('hyprutils', version: '>= 0.1.1'),
dependency('re2', required: true)
],
install: true,
)
install_data(
'hyprctl.bash',
install_dir: join_paths(get_option('datadir'), 'bash-completion/completions'),
install_tag: 'runtime',
rename: 'hyprctl',
)
install_data(
'hyprctl.fish',
install_dir: join_paths(get_option('datadir'), 'fish/vendor_completions.d'),
install_tag: 'runtime',
)
install_data(
'hyprctl.zsh',
install_dir: join_paths(get_option('datadir'), 'zsh/site-functions'),
install_tag: 'runtime',
rename: '_hyprctl',
)

View file

@ -1,11 +0,0 @@
#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

@ -1,148 +0,0 @@
#include "Hyprpaper.hpp"
#include "../helpers/Memory.hpp"
#include <optional>
#include <format>
#include <filesystem>
#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 = 1;
//
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);
}
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 std::unexpected("Unknown hyprpaper request");
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>(1);
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(), PROTOCOL_VERSION_SUPPORTED));
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;
err = std::format("failed to set wallpaper, code {}", code);
});
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 {};
}

View file

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

View file

@ -4,5 +4,4 @@ Name: Hyprland
URL: https://github.com/hyprwm/Hyprland
Description: Hyprland header files
Version: @HYPRLAND_VERSION@
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

View file

@ -11,9 +11,9 @@ set(CMAKE_CXX_STANDARD 23)
pkg_check_modules(hyprpm_deps REQUIRED IMPORTED_TARGET tomlplusplus hyprutils>=0.7.0)
find_package(glaze 6.0.0 QUIET)
find_package(glaze QUIET)
if (NOT glaze_FOUND)
set(GLAZE_VERSION v6.1.0)
set(GLAZE_VERSION v5.1.1)
message(STATUS "glaze dependency not found, retrieving ${GLAZE_VERSION} with FetchContent")
include(FetchContent)
FetchContent_Declare(

View file

@ -29,8 +29,8 @@ function _hyprpm
set descriptions[6] "Show help menu"
set descriptions[7] "Check and update all plugins if needed"
set descriptions[8] "Install a new plugin repository from git"
set descriptions[9] "Enable too much logging"
set descriptions[10] "Enable too much logging"
set descriptions[9] "Enable too much loggin"
set descriptions[10] "Enable too much loggin"
set descriptions[11] "Force an operation ignoring checks (e.g. update -f)"
set descriptions[12] "Disable shallow cloning of Hyprland sources"
set descriptions[13] "Remove a plugin repository"

View file

@ -3,7 +3,7 @@ hyprpm [<FLAGS>]... <ARGUMENT>
<FLAGS> ::= (--notify | -n) "Send a hyprland notification for important events (e.g. load fail)"
| (--help | -h) "Show help menu"
| (--verbose | -v) "Enable too much logging"
| (--verbose | -v) "Enable too much loggin"
| (--force | -f) "Force an operation ignoring checks (e.g. update -f)"
| (--no-shallow | -s) "Disable shallow cloning of Hyprland sources"
;

View file

@ -19,8 +19,8 @@ _hyprpm () {
descriptions[6]="Show help menu"
descriptions[7]="Check and update all plugins if needed"
descriptions[8]="Install a new plugin repository from git"
descriptions[9]="Enable too much logging"
descriptions[10]="Enable too much logging"
descriptions[9]="Enable too much loggin"
descriptions[10]="Enable too much loggin"
descriptions[11]="Force an operation ignoring checks (e.g. update -f)"
descriptions[12]="Disable shallow cloning of Hyprland sources"
descriptions[13]="Remove a plugin repository"

View file

@ -18,9 +18,6 @@ static std::string getTempRoot() {
const auto STR = ENV + std::string{"/hyprpm/"};
if (!std::filesystem::exists(STR))
mkdir(STR.c_str(), S_IRWXU);
return STR;
}
@ -93,7 +90,6 @@ 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}
@ -123,32 +119,31 @@ void DataState::addNewPluginRepo(const SPluginRepository& repo) {
Debug::die("{}", failureString("Failed to write plugin state"));
}
bool DataState::pluginRepoExists(const SPluginRepoIdentifier identifier) {
bool DataState::pluginRepoExists(const std::string& urlOrName) {
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 AUTHOR = STATE["repository"]["author"].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 URL = STATE["repository"]["url"].value_or("");
if (identifier.matches(URL, NAME, AUTHOR))
if (URL == urlOrName || NAME == urlOrName)
return true;
}
return false;
}
void DataState::removePluginRepo(const SPluginRepoIdentifier identifier) {
void DataState::removePluginRepo(const std::string& urlOrName) {
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 AUTHOR = STATE["repository"]["author"].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 URL = STATE["repository"]["url"].value_or("");
if (URL == urlOrName || NAME == urlOrName) {
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"))
@ -183,7 +178,7 @@ void DataState::updateGlobalState(const SGlobalState& state) {
// clang-format off
auto DATA = toml::table{
{"state", toml::table{
{"hash", state.headersAbiCompiled},
{"hash", state.headersHashCompiled},
{"dont_warn_install", state.dontWarnInstall}
}}
};
@ -208,8 +203,8 @@ SGlobalState DataState::getGlobalState() {
auto DATA = toml::parse_file(stateFile.c_str());
SGlobalState state;
state.headersAbiCompiled = DATA["state"]["hash"].value_or("");
state.dontWarnInstall = DATA["state"]["dont_warn_install"].value_or(false);
state.headersHashCompiled = DATA["state"]["hash"].value_or("");
state.dontWarnInstall = DATA["state"]["dont_warn_install"].value_or(false);
return state;
}
@ -221,18 +216,16 @@ 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 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("");
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("");
SPluginRepository repo;
repo.hash = HASH;
repo.name = NAME;
repo.author = AUTHOR;
repo.url = URL;
repo.rev = REV;
repo.hash = HASH;
repo.name = NAME;
repo.url = URL;
repo.rev = REV;
for (const auto& [key, val] : STATE) {
if (key == "repository")
@ -251,7 +244,7 @@ std::vector<SPluginRepository> DataState::getAllRepositories() {
return repos;
}
bool DataState::setPluginEnabled(const SPluginRepoIdentifier identifier, bool enabled) {
bool DataState::setPluginEnabled(const std::string& name, bool enabled) {
ensureStateStoreExists();
for (const auto& stateFile : getPluginStates()) {
@ -260,17 +253,8 @@ bool DataState::setPluginEnabled(const SPluginRepoIdentifier identifier, bool en
if (key == "repository")
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;
}
if (key.str() != name)
continue;
const auto FAILED = STATE[key]["failed"].value_or(false);

View file

@ -5,8 +5,8 @@
#include "Plugin.hpp"
struct SGlobalState {
std::string headersAbiCompiled = "";
bool dontWarnInstall = false;
std::string headersHashCompiled = "";
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 SPluginRepoIdentifier identifier);
bool pluginRepoExists(const SPluginRepoIdentifier identifier);
void removePluginRepo(const std::string& urlOrName);
bool pluginRepoExists(const std::string& urlOrName);
void updateGlobalState(const SGlobalState& state);
void purgeAllCache();
SGlobalState getGlobalState();
bool setPluginEnabled(const SPluginRepoIdentifier identifier, bool enabled);
bool setPluginEnabled(const std::string& name, bool enabled);
std::vector<SPluginRepository> getAllRepositories();
};
};

View file

@ -6,9 +6,6 @@
#include <sys/un.h>
#include <unistd.h>
#include <cstring>
#include <hyprutils/memory/Casts.hpp>
using namespace Hyprutils::Memory;
static int getUID() {
const auto UID = getuid();
@ -49,7 +46,7 @@ std::string NHyprlandSocket::send(const std::string& cmd) {
strncpy(serverAddress.sun_path, socketPath.c_str(), sizeof(serverAddress.sun_path) - 1);
if (connect(SERVERSOCKET, rc<sockaddr*>(&serverAddress), SUN_LEN(&serverAddress)) < 0) {
if (connect(SERVERSOCKET, (sockaddr*)&serverAddress, SUN_LEN(&serverAddress)) < 0) {
std::println("{}", failureString("Couldn't connect to " + socketPath + ". (4)"));
return "";
}

View file

@ -1,48 +0,0 @@
#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,27 +14,6 @@ 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,7 +11,6 @@
#include <cstdio>
#include <iostream>
#include <filesystem>
#include <string>
#include <print>
#include <fstream>
#include <algorithm>
@ -27,10 +26,8 @@
#include <hyprutils/string/String.hpp>
#include <hyprutils/os/Process.hpp>
#include <hyprutils/memory/Casts.hpp>
using namespace Hyprutils::String;
using namespace Hyprutils::OS;
using namespace Hyprutils::Memory;
static std::string execAndGet(std::string cmd) {
cmd += " 2>&1";
@ -79,30 +76,40 @@ SHyprlandVersion CPluginManager::getHyprlandVersion(bool running) {
else
onceInstalled = true;
const auto HLVERCALL = running ? NHyprlandSocket::send("j/version") : execAndGet("Hyprland --version-json");
const auto HLVERCALL = running ? NHyprlandSocket::send("/version") : execAndGet("Hyprland --version");
if (m_bVerbose)
std::println("{}", verboseString("{} version returned: {}", running ? "running" : "installed", HLVERCALL));
auto jsonQuery = glz::read_json<glz::generic>(HLVERCALL);
if (!jsonQuery) {
std::println("{}", failureString("failed to get the current hyprland version. Are you running hyprland?"));
if (!HLVERCALL.contains("Tag:")) {
std::println(stderr, "\n{}", failureString("You don't seem to be running Hyprland."));
return SHyprlandVersion{};
}
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 hlcommit = HLVERCALL.substr(HLVERCALL.find("at commit") + 10);
hlcommit = hlcommit.substr(0, hlcommit.find_first_of(' '));
size_t commits = 0;
std::string hlbranch = HLVERCALL.substr(HLVERCALL.find("from branch") + 12);
hlbranch = hlbranch.substr(0, hlbranch.find(" at commit "));
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;
try {
commits = std::stoull(hlcommits);
commits = std::stoi(hlcommits);
} catch (...) { ; }
if (m_bVerbose)
std::println("{}", verboseString("parsed commit {} at branch {} on {}, commits {}", hlcommit, hlbranch, hldate, commits));
auto ver = SHyprlandVersion{hlbranch, hlcommit, hldate, abiHash, commits};
auto ver = SHyprlandVersion{hlbranch, hlcommit, hldate, commits};
if (running)
verRunning = ver;
@ -137,7 +144,7 @@ bool CPluginManager::addNewPluginRepo(const std::string& url, const std::string&
return false;
}
if (DataState::pluginRepoExists(SPluginRepoIdentifier::fromUrl(url))) {
if (DataState::pluginRepoExists(url)) {
std::println(stderr, "\n{}", failureString("Could not clone the plugin repository. Repository already installed."));
return false;
}
@ -145,14 +152,14 @@ bool CPluginManager::addNewPluginRepo(const std::string& url, const std::string&
auto GLOBALSTATE = DataState::getGlobalState();
if (!GLOBALSTATE.dontWarnInstall) {
std::println("{}!{} Disclaimer: {}", Colors::YELLOW, Colors::RED, Colors::RESET);
std::println("plugins, especially not official, have no guarantee of stability, availability or security.\n"
std::println("plugins, especially not official, have no guarantee of stability, availablity or security.\n"
"Run them at your own risk.\n"
"This message will not appear again.");
GLOBALSTATE.dontWarnInstall = true;
DataState::updateGlobalState(GLOBALSTATE);
}
if (GLOBALSTATE.headersAbiCompiled.empty()) {
if (GLOBALSTATE.headersHashCompiled.empty()) {
std::println("\n{}", failureString("Cannot find headers in the global state. Try running hyprpm update first."));
return false;
}
@ -334,13 +341,10 @@ 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();
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;
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;
for (auto const& p : pManifest->m_plugins) {
repo.plugins.push_back(SPlugin{p.name, m_szWorkingPluginDirectory + "/" + p.output, false, p.failed});
}
@ -360,13 +364,13 @@ bool CPluginManager::addNewPluginRepo(const std::string& url, const std::string&
return true;
}
bool CPluginManager::removePluginRepo(const SPluginRepoIdentifier identifier) {
if (!DataState::pluginRepoExists(identifier)) {
bool CPluginManager::removePluginRepo(const std::string& urlOrName) {
if (!DataState::pluginRepoExists(urlOrName)) {
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 << identifier.toString() << "\n "
std::cout << Colors::YELLOW << "!" << Colors::RESET << Colors::RED << " removing a plugin repository: " << Colors::RESET << urlOrName << "\n "
<< "Are you sure? [Y/n] ";
std::fflush(stdout);
std::string input;
@ -377,7 +381,7 @@ bool CPluginManager::removePluginRepo(const SPluginRepoIdentifier identifier) {
return false;
}
DataState::removePluginRepo(identifier);
DataState::removePluginRepo(urlOrName);
return true;
}
@ -438,16 +442,11 @@ 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);
@ -588,20 +587,19 @@ bool CPluginManager::updateHeaders(bool force) {
std::filesystem::remove_all(WORKINGDIR);
auto HEADERSVALID = headersValid();
if (HEADERSVALID == HEADERS_OK || HEADERSVALID == HEADERS_MISMATCHED || HEADERSVALID == HEADERS_ABI_MISMATCH) {
if (HEADERSVALID == HEADERS_OK) {
progress.printMessageAbove(successString("installed headers"));
progress.m_iSteps = 5;
progress.m_szCurrentMessage = "Done!";
progress.print();
auto GLOBALSTATE = DataState::getGlobalState();
GLOBALSTATE.headersAbiCompiled = HLVER.abiHash;
auto GLOBALSTATE = DataState::getGlobalState();
GLOBALSTATE.headersHashCompiled = HLVER.hash;
DataState::updateGlobalState(GLOBALSTATE);
std::print("\n");
} else {
progress.printMessageAbove(failureString("failed to install headers with error code {} ({})", sc<int>(HEADERSVALID), headerErrorShort(HEADERSVALID)));
progress.printMessageAbove(failureString("failed to install headers with error code {} ({})", (int)HEADERSVALID, headerErrorShort(HEADERSVALID)));
progress.printMessageAbove(infoString("if the problem persists, try running hyprpm purge-cache."));
progress.m_iSteps = 5;
progress.m_szCurrentMessage = "Failed";
@ -775,7 +773,7 @@ bool CPluginManager::updatePlugins(bool forceUpdateAll) {
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});
}
DataState::removePluginRepo(SPluginRepoIdentifier::fromName(newrepo.name));
DataState::removePluginRepo(newrepo.name);
DataState::addNewPluginRepo(newrepo);
std::filesystem::remove_all(m_szWorkingPluginDirectory);
@ -787,8 +785,8 @@ bool CPluginManager::updatePlugins(bool forceUpdateAll) {
progress.m_szCurrentMessage = "Updating global state...";
progress.print();
auto GLOBALSTATE = DataState::getGlobalState();
GLOBALSTATE.headersAbiCompiled = HLVER.abiHash;
auto GLOBALSTATE = DataState::getGlobalState();
GLOBALSTATE.headersHashCompiled = HLVER.hash;
DataState::updateGlobalState(GLOBALSTATE);
progress.m_iSteps++;
@ -800,23 +798,17 @@ bool CPluginManager::updatePlugins(bool forceUpdateAll) {
return 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;
}
bool CPluginManager::enablePlugin(const std::string& name) {
bool ret = DataState::setPluginEnabled(name, true);
if (ret)
std::println("{}", successString("Enabled {}", identifier.name));
std::println("{}", successString("Enabled {}", name));
return ret;
}
bool CPluginManager::disablePlugin(const SPluginRepoIdentifier identifier) {
bool ret = DataState::setPluginEnabled(identifier, false);
bool CPluginManager::disablePlugin(const std::string& name) {
bool ret = DataState::setPluginEnabled(name, false);
if (ret)
std::println("{}", successString("Disabled {}", identifier.name));
std::println("{}", successString("Disabled {}", name));
return ret;
}
@ -834,7 +826,7 @@ ePluginLoadStateReturn CPluginManager::ensurePluginsLoadState(bool forceReload)
}
const auto HYPRPMPATH = DataState::getDataStatePath();
const auto json = glz::read_json<glz::generic::array_t>(NHyprlandSocket::send("j/plugins list"));
const auto json = glz::read_json<glz::json_t::array_t>(NHyprlandSocket::send("j/plugins list"));
if (!json) {
std::println(stderr, "PluginManager: couldn't parse plugin list output");
return LOADSTATE_FAIL;
@ -919,9 +911,9 @@ bool CPluginManager::loadUnloadPlugin(const std::string& path, bool load) {
auto state = DataState::getGlobalState();
auto HLVER = getHyprlandVersion(true);
if (state.headersAbiCompiled != HLVER.abiHash) {
if (state.headersHashCompiled != HLVER.hash) {
if (load)
std::println("{}", infoString("Running Hyprland version ({}) differs from plugin state ({}), please restart Hyprland.", HLVER.hash, state.headersAbiCompiled));
std::println("{}", infoString("Running Hyprland version ({}) differs from plugin state ({}), please restart Hyprland.", HLVER.hash, state.headersHashCompiled));
return false;
}
@ -937,7 +929,7 @@ void CPluginManager::listAllPlugins() {
const auto REPOS = DataState::getAllRepositories();
for (auto const& r : REPOS) {
std::println("{}", infoString("Repository {} (by {}):", r.name, r.author));
std::println("{}", infoString("Repository {}:", r.name));
for (auto const& p : r.plugins) {
std::println(" │ Plugin {}", p.name);
@ -953,7 +945,7 @@ void CPluginManager::listAllPlugins() {
}
void CPluginManager::notify(const eNotifyIcons icon, uint32_t color, int durationMs, const std::string& message) {
NHyprlandSocket::send("/notify " + std::to_string(icon) + " " + std::to_string(durationMs) + " " + std::to_string(color) + " " + message);
NHyprlandSocket::send("/notify " + std::to_string((int)icon) + " " + std::to_string(durationMs) + " " + std::to_string(color) + " " + message);
}
std::string CPluginManager::headerError(const eHeadersErrors err) {
@ -962,7 +954,6 @@ 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"

View file

@ -1,11 +1,8 @@
#pragma once
#include <cstdint>
#include <memory>
#include <optional>
#include <string>
#include <utility>
#include "Plugin.hpp"
enum eHeadersErrors {
HEADERS_OK = 0,
@ -13,7 +10,6 @@ enum eHeadersErrors {
HEADERS_MISSING,
HEADERS_CORRUPTED,
HEADERS_MISMATCHED,
HEADERS_ABI_MISMATCH,
HEADERS_DUPLICATED
};
@ -39,7 +35,6 @@ struct SHyprlandVersion {
std::string branch;
std::string hash;
std::string date;
std::string abiHash;
int commits = 0;
};
@ -48,7 +43,7 @@ class CPluginManager {
CPluginManager();
bool addNewPluginRepo(const std::string& url, const std::string& rev);
bool removePluginRepo(const SPluginRepoIdentifier identifier);
bool removePluginRepo(const std::string& urlOrName);
eHeadersErrors headersValid();
bool updateHeaders(bool force = false);
@ -56,8 +51,8 @@ class CPluginManager {
void listAllPlugins();
bool enablePlugin(const SPluginRepoIdentifier identifier);
bool disablePlugin(const SPluginRepoIdentifier identifier);
bool enablePlugin(const std::string& name);
bool disablePlugin(const std::string& name);
ePluginLoadStateReturn ensurePluginsLoadState(bool forceReload = false);
bool loadUnloadPlugin(const std::string& path, bool load);

View file

@ -8,7 +8,6 @@
#include <print>
#include <filesystem>
#include <algorithm>
#include <sstream>
#include <hyprutils/os/Process.hpp>
#include <hyprutils/string/VarList.hpp>

View file

@ -13,25 +13,25 @@ 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|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.
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.
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.
--notify | -n Send a hyprland notification for important events (including both successes and fail events).
--notify-fail | -nn Send a hyprland notification for fail events only.
--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 +47,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, notifyFail = false, verbose = false, force = false, noShallow = false;
std::string customHlUrl;
for (int i = 1; i < argc; ++i) {
@ -58,9 +58,7 @@ int main(int argc, char** argv, char** envp) {
} else if (ARGS[i] == "--notify" || ARGS[i] == "-n") {
notify = true;
} else if (ARGS[i] == "--notify-fail" || ARGS[i] == "-nn") {
// TODO: Deprecated since v.053.0. Remove in version>0.56.0
std::println(stderr, "{}", failureString("Deprececated flag."));
g_pPluginManager->notify(ICON_INFO, 0, 10000, "[hyprpm] -n flag is deprecated, see hyprpm --help.");
notifyFail = notify = true;
} else if (ARGS[i] == "--verbose" || ARGS[i] == "-v") {
verbose = true;
} else if (ARGS[i] == "--no-shallow" || ARGS[i] == "-s") {
@ -106,7 +104,7 @@ int main(int argc, char** argv, char** envp) {
const auto HLVER = g_pPluginManager->getHyprlandVersion();
auto GLOBALSTATE = DataState::getGlobalState();
if (GLOBALSTATE.headersAbiCompiled != HLVER.abiHash) {
if (GLOBALSTATE.headersHashCompiled != HLVER.hash) {
std::println(stderr, "{}", failureString("Headers outdated, please run hyprpm update."));
return 1;
}
@ -126,7 +124,7 @@ int main(int argc, char** argv, char** envp) {
NSys::root::cacheSudo();
CScopeGuard x([] { NSys::root::dropSudo(); });
return g_pPluginManager->removePluginRepo(SPluginRepoIdentifier::fromString(command[1])) ? 0 : 1;
return g_pPluginManager->removePluginRepo(command[1]) ? 0 : 1;
} else if (command[0] == "update") {
NSys::root::cacheSudo();
CScopeGuard x([] { NSys::root::dropSudo(); });
@ -137,7 +135,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.abiHash != GLOBALSTATE.headersAbiCompiled;
const auto COMPILEDOUTDATED = HLVER.hash != GLOBALSTATE.headersHashCompiled;
bool ret1 = g_pPluginManager->updatePlugins(!headersValid || force || COMPILEDOUTDATED);
@ -151,16 +149,15 @@ int main(int argc, char** argv, char** envp) {
if (ret2 != LOADSTATE_OK)
return 1;
} else {
} else if (notify)
g_pPluginManager->notify(ICON_ERROR, 0, 10000, "[hyprpm] Couldn't update headers");
}
} else if (command[0] == "enable") {
if (command.size() < 2) {
std::println(stderr, "{}", failureString("Not enough args for enable."));
return 1;
}
if (!g_pPluginManager->enablePlugin(SPluginRepoIdentifier::fromString(command[1]))) {
if (!g_pPluginManager->enablePlugin(command[1])) {
std::println(stderr, "{}", failureString("Couldn't enable plugin (missing?)"));
return 1;
}
@ -181,7 +178,7 @@ int main(int argc, char** argv, char** envp) {
return 1;
}
if (!g_pPluginManager->disablePlugin(SPluginRepoIdentifier::fromString(command[1]))) {
if (!g_pPluginManager->disablePlugin(command[1])) {
std::println(stderr, "{}", failureString("Couldn't disable plugin (missing?)"));
return 1;
}
@ -197,17 +194,19 @@ int main(int argc, char** argv, char** envp) {
auto ret = g_pPluginManager->ensurePluginsLoadState(force);
if (ret != LOADSTATE_OK) {
switch (ret) {
case LOADSTATE_FAIL:
case LOADSTATE_PARTIAL_FAIL: g_pPluginManager->notify(ICON_ERROR, 0, 10000, "[hyprpm] Failed to load plugins"); break;
case LOADSTATE_HEADERS_OUTDATED:
g_pPluginManager->notify(ICON_ERROR, 0, 10000, "[hyprpm] Failed to load plugins: Outdated headers. Please run hyprpm update manually.");
break;
default: break;
if (notify) {
switch (ret) {
case LOADSTATE_FAIL:
case LOADSTATE_PARTIAL_FAIL: g_pPluginManager->notify(ICON_ERROR, 0, 10000, "[hyprpm] Failed to load plugins"); break;
case LOADSTATE_HEADERS_OUTDATED:
g_pPluginManager->notify(ICON_ERROR, 0, 10000, "[hyprpm] Failed to load plugins: Outdated headers. Please run hyprpm update manually.");
break;
default: break;
}
}
return 1;
} else if (notify) {
} else if (notify && !notifyFail) {
g_pPluginManager->notify(ICON_OK, 0, 4000, "[hyprpm] Loaded plugins");
}
} else if (command[0] == "purge-cache") {

32
hyprpm/src/meson.build Normal file
View file

@ -0,0 +1,32 @@
globber = run_command('sh', '-c', 'find . -name "*.cpp" | sort', check: true)
src = globber.stdout().strip().split('\n')
executable(
'hyprpm',
src,
dependencies: [
dependency('hyprutils', version: '>= 0.1.1'),
dependency('threads'),
dependency('tomlplusplus'),
dependency('glaze', method: 'cmake'),
],
install: true,
)
install_data(
'../hyprpm.bash',
install_dir: join_paths(get_option('datadir'), 'bash-completion/completions'),
install_tag: 'runtime',
rename: 'hyprpm',
)
install_data(
'../hyprpm.fish',
install_dir: join_paths(get_option('datadir'), 'fish/vendor_completions.d'),
install_tag: 'runtime',
)
install_data(
'../hyprpm.zsh',
install_dir: join_paths(get_option('datadir'), 'zsh/site-functions'),
install_tag: 'runtime',
rename: '_hyprpm',
)

View file

@ -9,10 +9,8 @@
#include <algorithm>
#include <sstream>
#include <hyprutils/memory/Casts.hpp>
#include "../helpers/Colors.hpp"
using namespace Hyprutils::Memory;
#include "../helpers/Colors.hpp"
static winsize getTerminalSize() {
winsize w{};
@ -46,7 +44,7 @@ void CProgressBar::print() {
percentDone = m_fPercentage;
else {
// check for divide-by-zero
percentDone = m_iMaxSteps > 0 ? sc<float>(m_iSteps) / m_iMaxSteps : 0.0f;
percentDone = m_iMaxSteps > 0 ? static_cast<float>(m_iSteps) / m_iMaxSteps : 0.0f;
}
// clamp to ensure no overflows (sanity check)
percentDone = std::clamp(percentDone, 0.0f, 1.0f);
@ -56,7 +54,7 @@ void CProgressBar::print() {
std::ostringstream oss;
oss << ' ' << Colors::GREEN;
size_t filled = std::floor(percentDone * BARWIDTH);
size_t filled = static_cast<size_t>(std::floor(percentDone * BARWIDTH));
size_t i = 0;
for (; i < filled; ++i)
@ -71,7 +69,7 @@ void CProgressBar::print() {
oss << Colors::RESET;
if (m_fPercentage >= 0.0f)
oss << " " << std::format("{}%", sc<int>(percentDone * 100.0)) << ' ';
oss << " " << std::format("{}%", static_cast<int>(percentDone * 100.0)) << ' ';
else
oss << " " << std::format("{} / {}", m_iSteps, m_iMaxSteps) << ' ';

View file

@ -5,7 +5,6 @@ project(hyprtester DESCRIPTION "Hyprland test suite")
include(GNUInstallDirs)
set(CMAKE_CXX_STANDARD 26)
set(CMAKE_EXPORT_COMPILE_COMMANDS TRUE)
find_package(PkgConfig REQUIRED)
@ -29,74 +28,3 @@ install(FILES ${CMAKE_CURRENT_SOURCE_DIR}/test.conf
install(FILES ${CMAKE_CURRENT_SOURCE_DIR}/plugin/hyprtestplugin.so
DESTINATION ${CMAKE_INSTALL_PREFIX}/lib)
file(WRITE ${CMAKE_CURRENT_SOURCE_DIR}/src/tests/clients/build.hpp
"#include <string>\n"
"static const std::string binaryDir = \"${CMAKE_CURRENT_BINARY_DIR}\";"
)
######## wayland protocols testing stuff
if(CMAKE_BUILD_TYPE MATCHES Debug OR CMAKE_BUILD_TYPE MATCHES DEBUG)
set(CMAKE_EXECUTABLE_ENABLE_EXPORTS TRUE)
endif()
find_package(hyprwayland-scanner 0.4.0 REQUIRED)
pkg_check_modules(
protocols_deps
REQUIRED
IMPORTED_TARGET
hyprutils>=0.8.0
wayland-client
wayland-protocols
)
pkg_get_variable(WAYLAND_PROTOCOLS_DIR wayland-protocols pkgdatadir)
message(STATUS "Found wayland-protocols at ${WAYLAND_PROTOCOLS_DIR}")
pkg_get_variable(WAYLAND_SCANNER_PKGDATA_DIR wayland-scanner pkgdatadir)
message(STATUS "Found wayland-scanner pkgdatadir at ${WAYLAND_SCANNER_PKGDATA_DIR}")
# gen core wayland stuff
add_custom_command(
OUTPUT ${CMAKE_CURRENT_SOURCE_DIR}/protocols/wayland.cpp
${CMAKE_CURRENT_SOURCE_DIR}/protocols/wayland.hpp
COMMAND hyprwayland-scanner --wayland-enums --client
${WAYLAND_SCANNER_PKGDATA_DIR}/wayland.xml ${CMAKE_CURRENT_SOURCE_DIR}/protocols/
WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR})
function(protocolNew protoPath protoName external)
if(external)
set(path ${CMAKE_CURRENT_SOURCE_DIR}/${protoPath})
else()
set(path ${WAYLAND_PROTOCOLS_DIR}/${protoPath})
endif()
add_custom_command(
OUTPUT ${CMAKE_CURRENT_SOURCE_DIR}/protocols/${protoName}.cpp
${CMAKE_CURRENT_SOURCE_DIR}/protocols/${protoName}.hpp
COMMAND hyprwayland-scanner --client ${path}/${protoName}.xml
${CMAKE_CURRENT_SOURCE_DIR}/protocols/
WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR})
endfunction()
function(clientNew sourceName)
cmake_parse_arguments(PARSE_ARGV 1 ARG "" "" "PROTOS")
add_executable(${sourceName} clients/${sourceName}.cpp)
target_include_directories(${sourceName} BEFORE PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}/protocols")
target_link_libraries(${sourceName} PUBLIC PkgConfig::protocols_deps)
target_sources(${sourceName} PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/protocols/wayland.cpp ${CMAKE_CURRENT_SOURCE_DIR}/protocols/wayland.hpp)
foreach(protoName IN LISTS ARG_PROTOS)
target_sources(${sourceName} PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/protocols/${protoName}.cpp
${CMAKE_CURRENT_SOURCE_DIR}/protocols/${protoName}.hpp)
endforeach()
endfunction()
protocolnew("staging/pointer-warp" "pointer-warp-v1" false)
protocolnew("stable/xdg-shell" "xdg-shell" false)
clientNew("pointer-warp" PROTOS "pointer-warp-v1" "xdg-shell")
clientNew("pointer-scroll" PROTOS "xdg-shell")
clientNew("child-window" PROTOS "xdg-shell")

View file

@ -1,335 +0,0 @@
#include <print>
#include <poll.h>
#include <sys/mman.h>
#include <fcntl.h>
#include <stdio.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

@ -1,318 +0,0 @@
#include <cstring>
#include <sys/poll.h>
#include <sys/mman.h>
#include <fcntl.h>
#include <print>
#include <format>
#include <string>
#include <fstream>
#include <wayland-client.h>
#include <wayland.hpp>
#include <xdg-shell.hpp>
#include <pointer-warp-v1.hpp>
#include <hyprutils/memory/SharedPtr.hpp>
#include <hyprutils/math/Vector2D.hpp>
#include <hyprutils/os/FileDescriptor.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;
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;
// last delta
float lastScrollDelta = -1.F;
bool writeDelta = false;
};
static std::ofstream logfile;
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);
logfile << text << std::endl;
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...));
logfile << text << std::endl;
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-scroll";
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("pointer-scroll test client");
state.xdgToplevel->sendSetAppId("pointer-scroll");
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->setAxis([&](CCWlPointer* p, uint32_t time, wl_pointer_axis axis, wl_fixed_t delta) {
debugLog("axis: ax {} delta {}", (int)axis, wl_fixed_to_double(delta));
if (state.writeDelta) {
clientLog("{:.2f}", wl_fixed_to_double(delta));
state.writeDelta = false;
state.lastScrollDelta = -1;
return;
}
state.lastScrollDelta = wl_fixed_to_double(delta);
state.writeDelta = true;
});
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;
}
// return last delta after axis
static void parseRequest(SWlState& state, std::string req) {
if (!state.writeDelta) {
state.writeDelta = true;
return;
}
clientLog("{:.2f}", state.lastScrollDelta);
state.writeDelta = false;
state.lastScrollDelta = -1;
}
int main(int argc, char** argv) {
logfile.open("pointer-scroll.txt", std::ios::trunc);
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);
logfile.flush();
logfile.close();
return 0;
}

View file

@ -1,317 +0,0 @@
#include <cstring>
#include <sys/poll.h>
#include <sys/mman.h>
#include <fcntl.h>
#include <print>
#include <format>
#include <string>
#include <wayland-client.h>
#include <wayland.hpp>
#include <xdg-shell.hpp>
#include <pointer-warp-v1.hpp>
#include <hyprutils/memory/SharedPtr.hpp>
#include <hyprutils/math/Vector2D.hpp>
#include <hyprutils/os/FileDescriptor.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<CCWpPointerWarpV1> pointerWarp;
// 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;
};
static bool debug, started, shouldExit;
template <typename... Args>
//NOLINTNEXTLINE
static void clientLog(std::format_string<Args...> fmt, Args&&... args) {
std::println("{}", std::vformat(fmt.get(), std::make_format_args(args...)));
std::fflush(stdout);
}
template <typename... Args>
//NOLINTNEXTLINE
static void debugLog(std::format_string<Args...> fmt, Args&&... args) {
if (!debug)
return;
std::println("{}", std::vformat(fmt.get(), std::make_format_args(args...)));
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 == "wp_pointer_warp_v1") {
debugLog(" > binding to global: {} (version {}) with id {}", name, version, id);
state.pointerWarp = makeShared<CCWpPointerWarpV1>((wl_proxy*)wl_registry_bind((wl_registry*)state.registry->resource(), id, &wp_pointer_warp_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.pointerWarp) {
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) < 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("pointer-warp test client");
state.xdgToplevel->sendSetAppId("pointer-warp");
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;
}
// format is like below
// "warp 20 20\n" would ask to warp cursor to x=20,y=20 in surface local coords
static void parseRequest(SWlState& state, std::string req) {
if (req.contains("exit")) {
shouldExit = true;
return;
}
if (!req.starts_with("warp "))
return;
auto it = req.find_first_of('\n');
if (it == std::string::npos)
return;
req = req.substr(0, it);
it = req.find_first_of(' ');
if (it == std::string::npos)
return;
req = req.substr(it + 1);
it = req.find_first_of(' ');
int x = std::stoi(req.substr(0, it));
int y = std::stoi(req.substr(it + 1));
state.pointerWarp->sendWarpPointer(state.surf->resource(), state.pointer->resource(), wl_fixed_from_int(x), wl_fixed_from_int(y), state.enterSerial);
// sync the request then reply
wl_display_roundtrip(state.display);
clientLog("parsed request to move to x:{}, y:{}", x, y);
}
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

@ -1,4 +1,4 @@
CXXFLAGS = -shared -fPIC -g -std=c++2b -Wno-c++11-narrowing
CXXFLAGS = -shared -fPIC --no-gnu-unique -g -std=c++2b -Wno-c++11-narrowing
INCLUDES = `pkg-config --cflags pixman-1 libdrm pangocairo libinput libudev wayland-server xkbcommon`
LIBS = `pkg-config --libs pangocairo`

View file

@ -8,20 +8,9 @@
#include <src/config/ConfigDescriptions.hpp>
#include <src/layout/IHyprLayout.hpp>
#include <src/managers/LayoutManager.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/windowRule/WindowRuleApplicator.hpp>
#include <src/Compositor.hpp>
#include <src/desktop/state/FocusState.hpp>
#undef private
#include <hyprutils/utils/ScopeGuard.hpp>
#include <hyprutils/string/VarList.hpp>
using namespace Hyprutils::Utils;
using namespace Hyprutils::String;
#include "globals.hpp"
// Do NOT change this function.
@ -46,7 +35,7 @@ static SDispatchResult test(std::string in) {
// Trigger a snap move event for the active window
static SDispatchResult snapMove(std::string in) {
const auto PLASTWINDOW = Desktop::focusState()->window();
const auto PLASTWINDOW = g_pCompositor->m_lastWindow.lock();
if (!PLASTWINDOW->m_isFloating)
return {.success = false, .error = "Window must be floating"};
@ -59,248 +48,15 @@ static SDispatchResult snapMove(std::string in) {
return {};
}
class CTestKeyboard : public IKeyboard {
public:
static SP<CTestKeyboard> create(bool isVirtual) {
auto keeb = SP<CTestKeyboard>(new CTestKeyboard());
keeb->m_self = keeb;
keeb->m_isVirtual = isVirtual;
keeb->m_shareStates = !isVirtual;
return keeb;
}
virtual bool isVirtual() {
return m_isVirtual;
}
virtual SP<Aquamarine::IKeyboard> aq() {
return nullptr;
}
void sendKey(uint32_t key, bool pressed) {
auto event = IKeyboard::SKeyEvent{
.timeMs = sc<uint32_t>(Time::millis(Time::steadyNow())),
.keycode = key,
.state = pressed ? WL_KEYBOARD_KEY_STATE_PRESSED : WL_KEYBOARD_KEY_STATE_RELEASED,
};
updatePressed(event.keycode, pressed);
m_keyboardEvents.key.emit(event);
}
void destroy() {
m_events.destroy.emit();
}
private:
bool m_isVirtual = false;
};
class CTestMouse : public IPointer {
public:
static SP<CTestMouse> create(bool isVirtual) {
auto maus = SP<CTestMouse>(new CTestMouse());
maus->m_self = maus;
maus->m_isVirtual = isVirtual;
maus->m_deviceName = "test-mouse";
maus->m_hlName = "test-mouse";
return maus;
}
virtual bool isVirtual() {
return m_isVirtual;
}
virtual SP<Aquamarine::IPointer> aq() {
return nullptr;
}
void destroy() {
m_events.destroy.emit();
}
private:
bool m_isVirtual = false;
};
SP<CTestMouse> g_mouse;
SP<CTestKeyboard> g_keyboard;
static SDispatchResult pressAlt(std::string in) {
g_pInputManager->m_lastMods = in == "1" ? HL_MODIFIER_ALT : 0;
return {.success = true};
}
static SDispatchResult simulateGesture(std::string in) {
CVarList data(in);
uint32_t fingers = 3;
try {
fingers = std::stoul(data[1]);
} catch (...) { return {.success = false}; }
if (data[0] == "down") {
g_pTrackpadGestures->gestureBegin(IPointer::SSwipeBeginEvent{});
g_pTrackpadGestures->gestureUpdate(IPointer::SSwipeUpdateEvent{.fingers = fingers, .delta = {0, 300}});
g_pTrackpadGestures->gestureEnd(IPointer::SSwipeEndEvent{});
} else if (data[0] == "up") {
g_pTrackpadGestures->gestureBegin(IPointer::SSwipeBeginEvent{});
g_pTrackpadGestures->gestureUpdate(IPointer::SSwipeUpdateEvent{.fingers = fingers, .delta = {0, -300}});
g_pTrackpadGestures->gestureEnd(IPointer::SSwipeEndEvent{});
} else if (data[0] == "left") {
g_pTrackpadGestures->gestureBegin(IPointer::SSwipeBeginEvent{});
g_pTrackpadGestures->gestureUpdate(IPointer::SSwipeUpdateEvent{.fingers = fingers, .delta = {-300, 0}});
g_pTrackpadGestures->gestureEnd(IPointer::SSwipeEndEvent{});
} else {
g_pTrackpadGestures->gestureBegin(IPointer::SSwipeBeginEvent{});
g_pTrackpadGestures->gestureUpdate(IPointer::SSwipeUpdateEvent{.fingers = fingers, .delta = {300, 0}});
g_pTrackpadGestures->gestureEnd(IPointer::SSwipeEndEvent{});
}
return {.success = true};
}
static SDispatchResult vkb(std::string in) {
auto tkb0 = CTestKeyboard::create(false);
auto tkb1 = CTestKeyboard::create(false);
auto vkb0 = CTestKeyboard::create(true);
g_pInputManager->newKeyboard(tkb0);
g_pInputManager->newKeyboard(tkb1);
g_pInputManager->newKeyboard(vkb0);
CScopeGuard x([&] {
tkb0->destroy();
tkb1->destroy();
vkb0->destroy();
});
const auto& PRESSED = g_pInputManager->getKeysFromAllKBs();
const uint32_t TESTKEY = 1;
tkb0->sendKey(TESTKEY, true);
if (!std::ranges::contains(PRESSED, TESTKEY)) {
return {
.success = false,
.error = "Expected pressed key not found",
};
}
tkb1->sendKey(TESTKEY, true);
tkb0->sendKey(TESTKEY, false);
if (!std::ranges::contains(PRESSED, TESTKEY)) {
return {
.success = false,
.error = "Expected pressed key not found (kb share state)",
};
}
vkb0->sendKey(TESTKEY, true);
tkb1->sendKey(TESTKEY, false);
if (std::ranges::contains(PRESSED, TESTKEY)) {
return {
.success = false,
.error = "Expected released key found in pressed (vkb no share state)",
};
}
return {};
}
static SDispatchResult scroll(std::string in) {
double by;
try {
by = std::stod(in);
} catch (...) { return SDispatchResult{.success = false, .error = "invalid input"}; }
Log::logger->log(Log::DEBUG, "tester: scrolling by {}", by);
g_mouse->m_pointerEvents.axis.emit(IPointer::SAxisEvent{
.delta = by,
.deltaDiscrete = 120,
.mouse = true,
});
return {};
}
static SDispatchResult keybind(std::string in) {
CVarList2 data(std::move(in));
// 0 = release, 1 = press
bool press;
// See src/devices/IKeyboard.hpp : eKeyboardModifiers for modifier bitmasks
// 0 = none, eKeyboardModifiers is shifted to start at 1
uint32_t modifier;
// keycode
uint32_t key;
try {
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;
if (modifier > 0)
modifierMask = 1 << (modifier - 1);
g_pInputManager->m_lastMods = modifierMask;
g_keyboard->sendKey(key, press);
return {};
}
static Desktop::Rule::CWindowRuleEffectContainer::storageType ruleIDX = 0;
//
static SDispatchResult addRule(std::string in) {
ruleIDX = Desktop::Rule::windowEffects()->registerEffect("plugin_rule");
if (Desktop::Rule::windowEffects()->registerEffect("plugin_rule") != ruleIDX)
return {.success = false, .error = "re-registering returned a different id?"};
return {};
}
static SDispatchResult checkRule(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))
return {.success = false, .error = "No rule"};
if (PLASTWINDOW->m_ruleApplicator->m_otherProps.props[ruleIDX]->effect != "effect")
return {.success = false, .error = "Effect isn't \"effect\""};
return {};
}
APICALL EXPORT PLUGIN_DESCRIPTION_INFO PLUGIN_INIT(HANDLE handle) {
PHANDLE = handle;
HyprlandAPI::addDispatcherV2(PHANDLE, "plugin:test:test", ::test);
HyprlandAPI::addDispatcherV2(PHANDLE, "plugin:test:snapmove", ::snapMove);
HyprlandAPI::addDispatcherV2(PHANDLE, "plugin:test:vkb", ::vkb);
HyprlandAPI::addDispatcherV2(PHANDLE, "plugin:test:alt", ::pressAlt);
HyprlandAPI::addDispatcherV2(PHANDLE, "plugin:test:gesture", ::simulateGesture);
HyprlandAPI::addDispatcherV2(PHANDLE, "plugin:test:scroll", ::scroll);
HyprlandAPI::addDispatcherV2(PHANDLE, "plugin:test:keybind", ::keybind);
HyprlandAPI::addDispatcherV2(PHANDLE, "plugin:test:add_rule", ::addRule);
HyprlandAPI::addDispatcherV2(PHANDLE, "plugin:test:check_rule", ::checkRule);
// init mouse
g_mouse = CTestMouse::create(false);
g_pInputManager->newMouse(g_mouse);
// init keyboard
g_keyboard = CTestKeyboard::create(false);
g_pInputManager->newKeyboard(g_keyboard);
return {"hyprtestplugin", "hyprtestplugin", "Vaxry", "1.0"};
}
APICALL EXPORT void PLUGIN_EXIT() {
g_mouse->destroy();
g_mouse.reset();
g_keyboard->destroy();
g_keyboard.reset();
;
}

View file

@ -1,2 +0,0 @@
*
!.gitignore

View file

@ -14,8 +14,6 @@
#include <csignal>
#include <cerrno>
#include <print>
#include <hyprutils/memory/Casts.hpp>
using namespace Hyprutils::Memory;
static int getUID() {
const auto UID = getuid();
@ -97,7 +95,7 @@ std::string getFromSocket(const std::string& cmd) {
strncpy(serverAddress.sun_path, socketPath.c_str(), sizeof(serverAddress.sun_path) - 1);
if (connect(SERVERSOCKET, rc<sockaddr*>(&serverAddress), SUN_LEN(&serverAddress)) < 0) {
if (connect(SERVERSOCKET, (sockaddr*)&serverAddress, SUN_LEN(&serverAddress)) < 0) {
std::println("Couldn't connect to {}. (3)", socketPath);
return "";
}

View file

@ -17,22 +17,16 @@
#include "shared.hpp"
#include "hyprctlCompat.hpp"
#include "tests/main/tests.hpp"
#include "tests/clients/tests.hpp"
#include "tests/plugin/plugin.hpp"
#include <filesystem>
#include <hyprutils/os/Process.hpp>
#include <hyprutils/memory/WeakPtr.hpp>
#include <hyprutils/memory/Casts.hpp>
using namespace Hyprutils::Memory;
#include <csignal>
#include <cerrno>
#include <chrono>
#include <thread>
#include <print>
#include <string_view>
#include <span>
#include "Log.hpp"
@ -93,101 +87,97 @@ static void help() {
int main(int argc, char** argv, char** envp) {
std::string configPath = "";
std::string binaryPath = "";
std::string pluginPath = std::filesystem::current_path().string();
std::string configPath = "";
std::string binaryPath = "";
std::string pluginPath = std::filesystem::current_path().string();
if (argc > 1) {
std::span<char*> args{argv + 1, sc<std::size_t>(argc - 1)};
std::vector<std::string> args{argv + 1, argv + argc};
for (auto it = args.begin(); it != args.end(); it++) {
std::string_view value = *it;
if (value == "--config" || value == "-c") {
if (std::next(it) == args.end()) {
help();
return 1;
}
configPath = *std::next(it);
try {
configPath = std::filesystem::canonical(configPath);
if (!std::filesystem::is_regular_file(configPath)) {
throw std::exception();
}
} catch (...) {
std::println(stderr, "[ ERROR ] Config file '{}' doesn't exist!", configPath);
help();
return 1;
}
it++;
continue;
} else if (value == "--binary" || value == "-b") {
if (std::next(it) == args.end()) {
help();
return 1;
}
binaryPath = *std::next(it);
try {
binaryPath = std::filesystem::canonical(binaryPath);
if (!std::filesystem::is_regular_file(binaryPath)) {
throw std::exception();
}
} catch (...) {
std::println(stderr, "[ ERROR ] Binary '{}' doesn't exist!", binaryPath);
help();
return 1;
}
it++;
continue;
} else if (value == "--plugin" || value == "-p") {
if (std::next(it) == args.end()) {
help();
return 1;
}
pluginPath = *std::next(it);
try {
pluginPath = std::filesystem::canonical(pluginPath);
if (!std::filesystem::is_regular_file(pluginPath)) {
throw std::exception();
}
} catch (...) {
std::println(stderr, "[ ERROR ] plugin '{}' doesn't exist!", pluginPath);
help();
return 1;
}
it++;
continue;
} else if (value == "--help" || value == "-h") {
help();
return 0;
} else {
std::println(stderr, "[ ERROR ] Unknown option '{}' !", *it);
for (auto it = args.begin(); it != args.end(); it++) {
if (*it == "--config" || *it == "-c") {
if (std::next(it) == args.end()) {
help();
return 1;
}
configPath = *std::next(it);
try {
configPath = std::filesystem::canonical(configPath);
if (!std::filesystem::is_regular_file(configPath)) {
throw std::exception();
}
} catch (...) {
std::println(stderr, "[ ERROR ] Config file '{}' doesn't exist!", configPath);
help();
return 1;
}
it++;
continue;
} else if (*it == "--binary" || *it == "-b") {
if (std::next(it) == args.end()) {
help();
return 1;
}
binaryPath = *std::next(it);
try {
binaryPath = std::filesystem::canonical(binaryPath);
if (!std::filesystem::is_regular_file(binaryPath)) {
throw std::exception();
}
} catch (...) {
std::println(stderr, "[ ERROR ] Binary '{}' doesn't exist!", binaryPath);
help();
return 1;
}
it++;
continue;
} else if (*it == "--plugin" || *it == "-p") {
if (std::next(it) == args.end()) {
help();
return 1;
}
pluginPath = *std::next(it);
try {
pluginPath = std::filesystem::canonical(pluginPath);
if (!std::filesystem::is_regular_file(pluginPath)) {
throw std::exception();
}
} catch (...) {
std::println(stderr, "[ ERROR ] plugin '{}' doesn't exist!", pluginPath);
help();
return 1;
}
it++;
continue;
} else if (*it == "--help" || *it == "-h") {
help();
return 0;
} else {
std::println(stderr, "[ ERROR ] Unknown option '{}' !", *it);
help();
return 1;
}
}
@ -228,24 +218,13 @@ int main(int argc, char** argv, char** envp) {
NLog::log("{}Loaded plugin", Colors::YELLOW);
NLog::log("{}Running main tests", Colors::YELLOW);
for (const auto& fn : testFns) {
EXPECT(fn(), true);
}
NLog::log("{}Running protocol client tests", Colors::YELLOW);
for (const auto& fn : clientTestFns) {
EXPECT(fn(), true);
}
NLog::log("{}running plugin test", Colors::YELLOW);
EXPECT(testPlugin(), true);
NLog::log("{}running vkb test from plugin", Colors::YELLOW);
EXPECT(testVkb(), true);
// kill hyprland
NLog::log("{}dispatching exit", Colors::YELLOW);
getFromSocket("/dispatch exit");

View file

@ -18,17 +18,6 @@ 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__); \
@ -56,7 +45,7 @@ namespace Colors {
#define EXPECT_CONTAINS(haystack, needle) \
if (const auto EXPECTED = needle; !std::string{haystack}.contains(EXPECTED)) { \
NLog::log("{}Failed: {}{} should contain {} but doesn't. Source: {}@{}. Haystack is:\n{}", Colors::RED, Colors::RESET, #haystack, #needle, __FILE__, __LINE__, \
NLog::log("{}Failed: {}{} should contain {} but doesn't. Source: {}@{}. Haystack is:\n{}", Colors::RED, Colors::RESET, #haystack, EXPECTED, __FILE__, __LINE__, \
std::string{haystack}); \
ret = 1; \
TESTS_FAILED++; \

View file

@ -1 +0,0 @@
build.hpp

View file

@ -1,123 +0,0 @@
#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>
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);
return !ret;
}
REGISTER_CLIENT_TEST_FN(test);

View file

@ -1,145 +0,0 @@
#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>
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) {
client.proc = makeShared<CProcess>(binaryDir + "/pointer-scroll", 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]);
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))
return false;
client.readBuf.fill(0);
if (read(client.readFd.get(), client.readBuf.data(), client.readBuf.size() - 1) == -1)
return false;
std::string ret = std::string{client.readBuf.data()};
if (ret.find("started") == std::string::npos) {
NLog::log("{}Failed to start pointer-scroll client, read {}", Colors::RED, ret);
return false;
}
// wait for window to appear
std::this_thread::sleep_for(std::chrono::milliseconds(5000));
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);
return false;
}
if (getFromSocket(std::format("/dispatch focuswindow pid:{}", client.proc->pid())) != "ok") {
NLog::log("{}Failed to focus pointer-scroll client", Colors::RED, ret);
return false;
}
NLog::log("{}Started pointer-scroll 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 int getLastDelta(SClient& client) {
std::string cmd = "hypr";
if ((size_t)write(client.writeFd.get(), cmd.c_str(), cmd.length()) != cmd.length())
return false;
if (poll(&client.fds, 1, 1500) != 1 || !(client.fds.revents & POLLIN))
return false;
ssize_t bytesRead = read(client.fds.fd, client.readBuf.data(), 1023);
if (bytesRead == -1)
return false;
client.readBuf[bytesRead] = 0;
std::string received = std::string{client.readBuf.data()};
received.pop_back();
try {
return std::stoi(received);
} catch (...) { return -1; }
}
static bool sendScroll(int delta) {
return getFromSocket(std::format("/dispatch plugin:test:scroll {}", delta)) == "ok";
}
static bool test() {
SClient client;
if (!startClient(client))
return false;
EXPECT(getFromSocket("/keyword input:emulate_discrete_scroll 0"), "ok");
EXPECT(sendScroll(10), true);
EXPECT(getLastDelta(client), 10);
EXPECT(getFromSocket("/keyword input:scroll_factor 2"), "ok");
EXPECT(sendScroll(10), true);
EXPECT(getLastDelta(client), 20);
EXPECT(getFromSocket("r/keyword device[test-mouse-1]:scroll_factor 3"), "ok");
EXPECT(sendScroll(10), true);
EXPECT(getLastDelta(client), 30);
EXPECT(getFromSocket("r/dispatch setprop active scroll_mouse 4"), "ok");
EXPECT(sendScroll(10), true);
EXPECT(getLastDelta(client), 40);
stopClient(client);
NLog::log("{}Reloading the config", Colors::YELLOW);
OK(getFromSocket("/reload"));
return !ret;
}
REGISTER_CLIENT_TEST_FN(test);

View file

@ -1,183 +0,0 @@
#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>
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) {
client.proc = makeShared<CProcess>(binaryDir + "/pointer-warp", 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]);
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))
return false;
client.readBuf.fill(0);
if (read(client.readFd.get(), client.readBuf.data(), client.readBuf.size() - 1) == -1)
return false;
std::string ret = std::string{client.readBuf.data()};
if (ret.find("started") == std::string::npos) {
NLog::log("{}Failed to start pointer-warp client, read {}", Colors::RED, ret);
return false;
}
// wait for window to appear
std::this_thread::sleep_for(std::chrono::milliseconds(5000));
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);
return false;
}
if (getFromSocket(std::format("/dispatch focuswindow pid:{}", client.proc->pid())) != "ok") {
NLog::log("{}Failed to focus pointer-warp client", Colors::RED, ret);
return false;
}
NLog::log("{}Started pointer-warp 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();
}
// format is like below
// "warp 20 20\n" would ask to warp cursor to x=20,y=20 in surface local coords
static bool sendWarp(SClient& client, int x, int y) {
std::string cmd = std::format("warp {} {}\n", x, y);
if ((size_t)write(client.writeFd.get(), cmd.c_str(), cmd.length()) != cmd.length())
return false;
if (poll(&client.fds, 1, 1500) != 1 || !(client.fds.revents & POLLIN))
return false;
ssize_t bytesRead = read(client.fds.fd, client.readBuf.data(), 1023);
if (bytesRead == -1)
return false;
client.readBuf[bytesRead] = 0;
std::string recieved = std::string{client.readBuf.data()};
recieved.pop_back();
return true;
}
static bool isCursorPos(int x, int y) {
// TODO: add a better way to do this using test plugin?
std::string res = getFromSocket("/cursorpos");
if (res == "error") {
NLog::log("{}Cursorpos err'd: {}", Colors::RED, res);
return false;
}
auto it = res.find_first_of(' ');
if (res.at(it - 1) != ',') {
NLog::log("{}Cursorpos err'd: {}", Colors::RED, res);
return false;
}
int cursorX = std::stoi(res.substr(0, it - 1));
int cursorY = std::stoi(res.substr(it + 1));
// somehow this is always gives 1 less than surfbox->pos()??
res = getFromSocket("/activewindow");
it = res.find("at: ") + 4;
res = res.substr(it, res.find_first_of('\n', it) - it);
it = res.find_first_of(',');
int clientX = cursorX - std::stoi(res.substr(0, it)) + 1;
int clientY = cursorY - std::stoi(res.substr(it + 1)) + 1;
return clientX == x && clientY == y;
}
static bool test() {
SClient client;
if (!startClient(client))
return false;
EXPECT(sendWarp(client, 100, 100), true);
EXPECT(isCursorPos(100, 100), true);
EXPECT(sendWarp(client, 0, 0), true);
EXPECT(isCursorPos(0, 0), true);
EXPECT(sendWarp(client, 200, 200), true);
EXPECT(isCursorPos(200, 200), true);
EXPECT(sendWarp(client, 100, -100), true);
EXPECT(isCursorPos(200, 200), true);
EXPECT(sendWarp(client, 234, 345), true);
EXPECT(isCursorPos(234, 345), true);
EXPECT(sendWarp(client, -1, -1), true);
EXPECT(isCursorPos(234, 345), true);
EXPECT(sendWarp(client, 1, -1), true);
EXPECT(isCursorPos(234, 345), true);
EXPECT(sendWarp(client, 13, 37), true);
EXPECT(isCursorPos(13, 37), true);
EXPECT(sendWarp(client, -100, 100), true);
EXPECT(isCursorPos(13, 37), true);
EXPECT(sendWarp(client, -1, 1), true);
EXPECT(isCursorPos(13, 37), true);
stopClient(client);
NLog::log("{}Reloading the config", Colors::YELLOW);
OK(getFromSocket("/reload"));
return !ret;
}
REGISTER_CLIENT_TEST_FN(test);

View file

@ -1,12 +0,0 @@
#pragma once
#include <vector>
#include <functional>
inline std::vector<std::function<bool()>> clientTestFns;
#define REGISTER_CLIENT_TEST_FN(fn) \
static auto _register_fn = [] { \
clientTestFns.emplace_back(fn); \
return 1; \
}();

View file

@ -1,22 +0,0 @@
#include "../../Log.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 test() {
NLog::log("{}Testing animations", Colors::GREEN);
auto str = getFromSocket("/animations");
NLog::log("{}Testing bezier curve output from `hyprctl animations`", Colors::YELLOW);
{EXPECT_CONTAINS(str, std::format("beziers:\n\n\tname: quick\n\t\tX0: 0.15\n\t\tY0: 0.00\n\t\tX1: 0.10\n\t\tY1: 1.00"))};
return !ret;
}
REGISTER_TEST_FN(test)

View file

@ -1,27 +0,0 @@
#include "tests.hpp"
#include "../../shared.hpp"
#include "../../hyprctlCompat.hpp"
#include "../shared.hpp"
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(getFromSocket("/keyword monitor HEADLESS-2,1920x1080x60.00000,0x0,1.0,bitdepth,10,cm,wide"), "ok")
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)");
return !ret;
}
REGISTER_TEST_FN(test)

View file

@ -1,54 +0,0 @@
#include "../shared.hpp"
#include "../../shared.hpp"
#include "../../hyprctlCompat.hpp"
#include "tests.hpp"
static int ret = 0;
static void testFloatClamp() {
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("/keyword dwindle:force_split 2"));
OK(getFromSocket("/keyword monitor HEADLESS-2, addreserved, 0, 20, 0, 20"));
OK(getFromSocket("/dispatch focuswindow class:c"));
OK(getFromSocket("/dispatch setfloating class:c"));
OK(getFromSocket("/dispatch resizewindowpixel exact 1200 900,class:c"));
OK(getFromSocket("/dispatch settiled class:c"));
OK(getFromSocket("/dispatch setfloating class:c"));
{
auto str = getFromSocket("/clients");
EXPECT_CONTAINS(str, "at: 698,158");
EXPECT_CONTAINS(str, "size: 1200,900");
}
OK(getFromSocket("/keyword dwindle:force_split 0"));
// clean up
NLog::log("{}Killing all windows", Colors::YELLOW);
Tests::killAllWindows();
}
static bool test() {
NLog::log("{}Testing Dwindle layout", Colors::GREEN);
// test
NLog::log("{}Testing float clamp", Colors::GREEN);
testFloatClamp();
// clean up
NLog::log("Cleaning up", Colors::YELLOW);
getFromSocket("/dispatch workspace 1");
OK(getFromSocket("/reload"));
return !ret;
}
REGISTER_TEST_FN(test);

View file

@ -1,54 +0,0 @@
#include "tests.hpp"
#include "../../shared.hpp"
#include "../../hyprctlCompat.hpp"
#include <chrono>
#include <thread>
#include <hyprutils/os/Process.hpp>
#include <hyprutils/memory/WeakPtr.hpp>
#include "../shared.hpp"
static int ret = 0;
using namespace Hyprutils::OS;
using namespace Hyprutils::Memory;
#define UP CUniquePointer
#define SP CSharedPointer
static bool test() {
NLog::log("{}Testing 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"));
// 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;
}
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;
}
REGISTER_TEST_FN(test)

View file

@ -1,203 +0,0 @@
#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 waitForWindowCount(int expectedWindowCnt, std::string_view expectation, int waitMillis = 100, int maxWaitCnt = 50) {
int counter = 0;
while (Tests::windowCount() != expectedWindowCnt) {
counter++;
std::this_thread::sleep_for(std::chrono::milliseconds(waitMillis));
if (counter > maxWaitCnt) {
NLog::log("{}Unmet expectation: {}", Colors::RED, expectation);
return false;
}
}
return true;
}
static bool test() {
NLog::log("{}Testing gestures", Colors::GREEN);
EXPECT(Tests::windowCount(), 0);
// test on workspace "window"
NLog::log("{}Switching to workspace 1", Colors::YELLOW);
getFromSocket("/dispatch workspace 1"); // no OK: we might be on 1 already
Tests::spawnKitty();
EXPECT(Tests::windowCount(), 1);
// Give the shell a moment to initialize
std::this_thread::sleep_for(std::chrono::milliseconds(500));
OK(getFromSocket("/dispatch plugin:test:gesture up,5"));
OK(getFromSocket("/dispatch plugin:test:gesture down,5"));
OK(getFromSocket("/dispatch plugin:test:gesture left,5"));
OK(getFromSocket("/dispatch plugin:test:gesture right,5"));
OK(getFromSocket("/dispatch plugin:test:gesture right,4"));
EXPECT(waitForWindowCount(0, "Gesture sent paste exit + enter to kitty"), true);
EXPECT(Tests::windowCount(), 0);
OK(getFromSocket("/dispatch plugin:test:gesture left,3"));
EXPECT(waitForWindowCount(1, "Gesture spawned kitty"), true);
EXPECT(Tests::windowCount(), 1);
OK(getFromSocket("/dispatch plugin:test:gesture right,3"));
{
auto str = getFromSocket("/clients");
EXPECT_CONTAINS(str, "floating: 1");
}
OK(getFromSocket("/dispatch plugin:test:gesture down,3"));
{
auto str = getFromSocket("/clients");
EXPECT_CONTAINS(str, "fullscreen: 2");
}
OK(getFromSocket("/dispatch plugin:test:gesture down,3"));
{
auto str = getFromSocket("/clients");
EXPECT_CONTAINS(str, "fullscreen: 0");
}
OK(getFromSocket("/dispatch plugin:test:alt 1"));
OK(getFromSocket("/dispatch plugin:test:gesture left,3"));
{
auto str = getFromSocket("/workspaces");
EXPECT_CONTAINS(str, "ID 2 (2)");
}
OK(getFromSocket("/dispatch plugin:test:gesture right,3"));
{
auto str = getFromSocket("/workspaces");
EXPECT_NOT_CONTAINS(str, "ID 2 (2)");
}
// check for crashes
OK(getFromSocket("/dispatch plugin:test:gesture right,3"));
{
auto str = getFromSocket("/workspaces");
EXPECT_NOT_CONTAINS(str, "ID 2 (2)");
}
OK(getFromSocket("/keyword gestures:workspace_swipe_invert 0"));
OK(getFromSocket("/dispatch plugin:test:gesture right,3"));
{
auto str = getFromSocket("/workspaces");
EXPECT_CONTAINS(str, "ID 2 (2)");
}
OK(getFromSocket("/dispatch plugin:test:gesture left,3"));
{
auto str = getFromSocket("/workspaces");
EXPECT_NOT_CONTAINS(str, "ID 2 (2)");
}
OK(getFromSocket("/keyword gestures:workspace_swipe_invert 1"));
OK(getFromSocket("/keyword gestures:workspace_swipe_create_new 0"));
OK(getFromSocket("/dispatch plugin:test:gesture left,3"));
{
auto str = getFromSocket("/workspaces");
EXPECT_NOT_CONTAINS(str, "ID 2 (2)");
EXPECT_CONTAINS(str, "ID 1 (1)");
}
OK(getFromSocket("/dispatch plugin:test:gesture down,3"));
{
auto str = getFromSocket("/clients");
EXPECT_CONTAINS(str, "floating: 0");
}
OK(getFromSocket("/dispatch plugin:test:alt 0"));
OK(getFromSocket("/dispatch plugin:test:gesture up,3"));
EXPECT(waitForWindowCount(0, "Gesture closed kitty"), true);
EXPECT(Tests::windowCount(), 0);
// This test ensures that `movecursortocorner`, which expects
// a single-character direction argument, is parsed correctly.
Tests::spawnKitty();
OK(getFromSocket("/dispatch movecursortocorner 0"));
const std::string cursorPos1 = getFromSocket("/cursorpos");
OK(getFromSocket("/dispatch plugin:test:gesture left,4"));
const std::string cursorPos2 = getFromSocket("/cursorpos");
// The cursor should have moved because of the gesture
EXPECT(cursorPos1 != cursorPos2, true);
// Test that `workspace previous` works correctly after a workspace gesture.
{
OK(getFromSocket("/keyword gestures:workspace_swipe_invert 0"));
OK(getFromSocket("/keyword gestures:workspace_swipe_create_new 1"));
OK(getFromSocket("/dispatch workspace 3"));
// 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();
NLog::log("{}Expecting 0 windows", Colors::YELLOW);
EXPECT(Tests::windowCount(), 0);
// reload cfg
OK(getFromSocket("/reload"));
return !ret;
}
REGISTER_TEST_FN(test)

View file

@ -1,184 +0,0 @@
#include "tests.hpp"
#include "../../shared.hpp"
#include "../../hyprctlCompat.hpp"
#include <cstdint>
#include <print>
#include <string>
#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 std::string getCommandStdOut(std::string command) {
CProcess process("bash", {"-c", command});
process.addEnv("HYPRLAND_INSTANCE_SIGNATURE", HIS);
process.runSync();
const std::string& stdOut = process.stdOut();
// Remove trailing new line
return stdOut.substr(0, stdOut.length() - 1);
}
static bool testDevicesActiveLayoutIndex() {
NLog::log("{}Testing hyprctl devices active_layout_index", Colors::GREEN);
// configure layouts
getFromSocket("/keyword input:kb_layout us,pl,ua");
for (uint8_t i = 0; i < 3; i++) {
// set layout
getFromSocket("/switchxkblayout all " + std::to_string(i));
std::string devicesJson = getFromSocket("j/devices");
std::string expected = R"("active_layout_index": )" + std::to_string(i);
// check layout index
EXPECT_CONTAINS(devicesJson, expected);
}
return true;
}
static bool testGetprop() {
NLog::log("{}Testing hyprctl getprop", Colors::GREEN);
if (!Tests::spawnKitty()) {
NLog::log("{}Error: kitty did not spawn", Colors::RED);
return false;
}
// animation
EXPECT(getCommandStdOut("hyprctl getprop class:kitty animation"), "(unset)");
EXPECT(getCommandStdOut("hyprctl getprop class:kitty animation -j"), R"({"animation": ""})");
getFromSocket("/dispatch setprop class:kitty animation teststyle");
EXPECT(getCommandStdOut("hyprctl getprop class:kitty animation"), "teststyle");
EXPECT(getCommandStdOut("hyprctl getprop class:kitty animation -j"), R"({"animation": "teststyle"})");
// max_size
EXPECT(getCommandStdOut("hyprctl getprop class:kitty max_size"), "inf inf");
EXPECT(getCommandStdOut("hyprctl getprop class:kitty max_size -j"), R"({"max_size": [null,null]})");
getFromSocket("/dispatch setprop class:kitty max_size 200 150");
EXPECT(getCommandStdOut("hyprctl getprop class:kitty max_size"), "200 150");
EXPECT(getCommandStdOut("hyprctl getprop class:kitty max_size -j"), R"({"max_size": [200,150]})");
// min_size
EXPECT(getCommandStdOut("hyprctl getprop class:kitty min_size"), "20 20");
EXPECT(getCommandStdOut("hyprctl getprop class:kitty min_size -j"), R"({"min_size": [20,20]})");
getFromSocket("/dispatch setprop class:kitty min_size 100 50");
EXPECT(getCommandStdOut("hyprctl getprop class:kitty min_size"), "100 50");
EXPECT(getCommandStdOut("hyprctl getprop class:kitty min_size -j"), R"({"min_size": [100,50]})");
// opacity
EXPECT(getCommandStdOut("hyprctl getprop class:kitty opacity"), "1");
EXPECT(getCommandStdOut("hyprctl getprop class:kitty opacity -j"), R"({"opacity": 1})");
getFromSocket("/dispatch setprop class:kitty opacity 0.3");
EXPECT(getCommandStdOut("hyprctl getprop class:kitty opacity"), "0.3");
EXPECT(getCommandStdOut("hyprctl getprop class:kitty opacity -j"), R"({"opacity": 0.3})");
// opacity_inactive
EXPECT(getCommandStdOut("hyprctl getprop class:kitty opacity_inactive"), "1");
EXPECT(getCommandStdOut("hyprctl getprop class:kitty opacity_inactive -j"), R"({"opacity_inactive": 1})");
getFromSocket("/dispatch setprop class:kitty opacity_inactive 0.5");
EXPECT(getCommandStdOut("hyprctl getprop class:kitty opacity_inactive"), "0.5");
EXPECT(getCommandStdOut("hyprctl getprop class:kitty opacity_inactive -j"), R"({"opacity_inactive": 0.5})");
// opacity_fullscreen
EXPECT(getCommandStdOut("hyprctl getprop class:kitty opacity_fullscreen"), "1");
EXPECT(getCommandStdOut("hyprctl getprop class:kitty opacity_fullscreen -j"), R"({"opacity_fullscreen": 1})");
getFromSocket("/dispatch setprop class:kitty opacity_fullscreen 0.75");
EXPECT(getCommandStdOut("hyprctl getprop class:kitty opacity_fullscreen"), "0.75");
EXPECT(getCommandStdOut("hyprctl getprop class:kitty opacity_fullscreen -j"), R"({"opacity_fullscreen": 0.75})");
// opacity_override
EXPECT(getCommandStdOut("hyprctl getprop class:kitty opacity_override"), "false");
EXPECT(getCommandStdOut("hyprctl getprop class:kitty opacity_override -j"), R"({"opacity_override": false})");
getFromSocket("/dispatch setprop class:kitty opacity_override true");
EXPECT(getCommandStdOut("hyprctl getprop class:kitty opacity_override"), "true");
EXPECT(getCommandStdOut("hyprctl getprop class:kitty opacity_override -j"), R"({"opacity_override": true})");
// opacity_inactive_override
EXPECT(getCommandStdOut("hyprctl getprop class:kitty opacity_inactive_override"), "false");
EXPECT(getCommandStdOut("hyprctl getprop class:kitty opacity_inactive_override -j"), R"({"opacity_inactive_override": false})");
getFromSocket("/dispatch setprop class:kitty opacity_inactive_override true");
EXPECT(getCommandStdOut("hyprctl getprop class:kitty opacity_inactive_override"), "true");
EXPECT(getCommandStdOut("hyprctl getprop class:kitty opacity_inactive_override -j"), R"({"opacity_inactive_override": true})");
// opacity_fullscreen_override
EXPECT(getCommandStdOut("hyprctl getprop class:kitty opacity_fullscreen_override"), "false");
EXPECT(getCommandStdOut("hyprctl getprop class:kitty opacity_fullscreen_override -j"), R"({"opacity_fullscreen_override": false})");
getFromSocket("/dispatch setprop class:kitty opacity_fullscreen_override true");
EXPECT(getCommandStdOut("hyprctl getprop class:kitty opacity_fullscreen_override"), "true");
EXPECT(getCommandStdOut("hyprctl getprop class:kitty opacity_fullscreen_override -j"), R"({"opacity_fullscreen_override": true})");
// active_border_color
EXPECT(getCommandStdOut("hyprctl getprop class:kitty active_border_color"), "ee33ccff ee00ff99 45deg");
EXPECT(getCommandStdOut("hyprctl getprop class:kitty active_border_color -j"), R"({"active_border_color": "ee33ccff ee00ff99 45deg"})");
getFromSocket("/dispatch setprop class:kitty active_border_color rgb(abcdef)");
EXPECT(getCommandStdOut("hyprctl getprop class:kitty active_border_color"), "ffabcdef 0deg");
EXPECT(getCommandStdOut("hyprctl getprop class:kitty active_border_color -j"), R"({"active_border_color": "ffabcdef 0deg"})");
// bool window properties
EXPECT(getCommandStdOut("hyprctl getprop class:kitty allows_input"), "false");
EXPECT(getCommandStdOut("hyprctl getprop class:kitty allows_input -j"), R"({"allows_input": false})");
getFromSocket("/dispatch setprop class:kitty allows_input true");
EXPECT(getCommandStdOut("hyprctl getprop class:kitty allows_input"), "true");
EXPECT(getCommandStdOut("hyprctl getprop class:kitty allows_input -j"), R"({"allows_input": true})");
// int window properties
EXPECT(getCommandStdOut("hyprctl getprop class:kitty rounding"), "10");
EXPECT(getCommandStdOut("hyprctl getprop class:kitty rounding -j"), R"({"rounding": 10})");
getFromSocket("/dispatch setprop class:kitty rounding 4");
EXPECT(getCommandStdOut("hyprctl getprop class:kitty rounding"), "4");
EXPECT(getCommandStdOut("hyprctl getprop class:kitty rounding -j"), R"({"rounding": 4})");
// float window properties
EXPECT(getCommandStdOut("hyprctl getprop class:kitty rounding_power"), "2");
EXPECT(getCommandStdOut("hyprctl getprop class:kitty rounding_power -j"), R"({"rounding_power": 2})");
getFromSocket("/dispatch setprop class:kitty rounding_power 1.25");
EXPECT(getCommandStdOut("hyprctl getprop class:kitty rounding_power"), "1.25");
EXPECT(getCommandStdOut("hyprctl getprop class:kitty rounding_power -j"), R"({"rounding_power": 1.25})");
// errors
EXPECT(getCommandStdOut("hyprctl getprop"), "not enough args");
EXPECT(getCommandStdOut("hyprctl getprop class:kitty"), "not enough args");
EXPECT(getCommandStdOut("hyprctl getprop class:nonexistantclass animation"), "window not found");
EXPECT(getCommandStdOut("hyprctl getprop class:kitty nonexistantprop"), "prop not found");
// kill all
NLog::log("{}Killing all windows", Colors::YELLOW);
Tests::killAllWindows();
NLog::log("{}Expecting 0 windows", Colors::YELLOW);
EXPECT(Tests::windowCount(), 0);
return true;
}
static bool test() {
NLog::log("{}Testing hyprctl", Colors::GREEN);
{
NLog::log("{}Testing hyprctl descriptions for any json errors", Colors::GREEN);
CProcess jqProc("bash", {"-c", "hyprctl descriptions | jq"});
jqProc.addEnv("HYPRLAND_INSTANCE_SIGNATURE", HIS);
jqProc.runSync();
EXPECT(jqProc.exitCode(), 0);
}
testGetprop();
testDevicesActiveLayoutIndex();
getFromSocket("/reload");
return !ret;
}
REGISTER_TEST_FN(test);

View file

@ -1,515 +0,0 @@
#include <filesystem>
#include <linux/input-event-codes.h>
#include <thread>
#include "../../shared.hpp"
#include "../../hyprctlCompat.hpp"
#include "../shared.hpp"
#include "tests.hpp"
using namespace Hyprutils::OS;
using namespace Hyprutils::Memory;
static int ret = 0;
static std::string flagFile = "/tmp/hyprtester-keybinds.txt";
// Because i don't feel like changing someone elses code.
enum eKeyboardModifierIndex : uint8_t {
MOD_SHIFT = 1,
MOD_CAPS,
MOD_CTRL,
MOD_ALT,
MOD_MOD2,
MOD_MOD3,
MOD_META,
MOD_MOD5
};
static void clearFlag() {
std::filesystem::remove(flagFile);
}
static bool checkFlag() {
bool exists = std::filesystem::exists(flagFile);
clearFlag();
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 std::string readKittyOutput() {
std::string output = Tests::execAndGet("kitten @ --to unix:/tmp/hyprtester-kitty.sock get-text --extent all");
// chop off shell prompt
std::size_t pos = output.rfind("$");
if (pos != std::string::npos) {
pos += 1;
if (pos < output.size())
output.erase(0, pos);
}
// NLog::log("Kitty output: '{}'", output);
return output;
}
static void awaitKittyPrompt() {
// wait until we see the shell prompt, meaning it's ready for test inputs
for (int i = 0; i < 10; i++) {
std::string output = Tests::execAndGet("kitten @ --to unix:/tmp/hyprtester-kitty.sock get-text --extent all");
if (output.rfind("$") == std::string::npos) {
std::this_thread::sleep_for(std::chrono::milliseconds(200));
continue;
}
return;
}
NLog::log("{}Error: timed out waiting for kitty prompt", Colors::RED);
}
static CUniquePointer<CProcess> spawnRemoteControlKitty() {
auto kittyProc = Tests::spawnKitty("keybinds_test", {"-o", "allow_remote_control=yes", "--listen-on", "unix:/tmp/hyprtester-kitty.sock", "--config", "NONE", "/bin/sh"});
// wait a bit to ensure shell prompt is sent, we are going to read the text after it
std::this_thread::sleep_for(std::chrono::milliseconds(100));
if (kittyProc)
awaitKittyPrompt();
return kittyProc;
}
static void testBind() {
EXPECT(checkFlag(), false);
EXPECT(getFromSocket("/keyword bind SUPER,Y,exec,touch " + flagFile), "ok");
// press keybind
OK(getFromSocket("/dispatch plugin:test:keybind 1,7,29"));
// await flag
EXPECT(attemptCheckFlag(20, 50), true);
// release keybind
OK(getFromSocket("/dispatch plugin:test:keybind 0,0,29"));
EXPECT(getFromSocket("/keyword unbind SUPER,Y"), "ok");
}
static void testBindKey() {
EXPECT(checkFlag(), false);
EXPECT(getFromSocket("/keyword bind ,Y,exec,touch " + flagFile), "ok");
// press keybind
OK(getFromSocket("/dispatch plugin:test:keybind 1,0,29"));
// await flag
EXPECT(attemptCheckFlag(20, 50), true);
// release keybind
OK(getFromSocket("/dispatch plugin:test:keybind 0,0,29"));
EXPECT(getFromSocket("/keyword unbind ,Y"), "ok");
}
static void testLongPress() {
EXPECT(checkFlag(), false);
EXPECT(getFromSocket("/keyword bindo SUPER,Y,exec,touch " + flagFile), "ok");
EXPECT(getFromSocket("/keyword input:repeat_delay 100"), "ok");
// press keybind
OK(getFromSocket("/dispatch plugin:test:keybind 1,7,29"));
// check no flag on short press
std::this_thread::sleep_for(std::chrono::milliseconds(50));
EXPECT(checkFlag(), false);
// await repeat delay
std::this_thread::sleep_for(std::chrono::milliseconds(200));
EXPECT(checkFlag(), true);
// release keybind
OK(getFromSocket("/dispatch plugin:test:keybind 0,0,29"));
EXPECT(getFromSocket("/keyword unbind SUPER,Y"), "ok");
}
static void testKeyLongPress() {
EXPECT(checkFlag(), false);
EXPECT(getFromSocket("/keyword bindo ,Y,exec,touch " + flagFile), "ok");
EXPECT(getFromSocket("/keyword input:repeat_delay 100"), "ok");
// press keybind
OK(getFromSocket("/dispatch plugin:test:keybind 1,0,29"));
// check no flag on short press
std::this_thread::sleep_for(std::chrono::milliseconds(50));
EXPECT(checkFlag(), false);
// await repeat delay
std::this_thread::sleep_for(std::chrono::milliseconds(200));
EXPECT(checkFlag(), true);
// release keybind
OK(getFromSocket("/dispatch plugin:test:keybind 0,0,29"));
EXPECT(getFromSocket("/keyword unbind ,Y"), "ok");
}
static void testLongPressRelease() {
EXPECT(checkFlag(), false);
EXPECT(getFromSocket("/keyword bindo SUPER,Y,exec,touch " + flagFile), "ok");
EXPECT(getFromSocket("/keyword input:repeat_delay 100"), "ok");
// press keybind
OK(getFromSocket("/dispatch plugin:test:keybind 1,7,29"));
// check no flag on short press
std::this_thread::sleep_for(std::chrono::milliseconds(50));
EXPECT(checkFlag(), false);
// release keybind
OK(getFromSocket("/dispatch plugin:test:keybind 0,0,29"));
// await repeat delay
std::this_thread::sleep_for(std::chrono::milliseconds(200));
EXPECT(checkFlag(), false);
EXPECT(getFromSocket("/keyword unbind SUPER,Y"), "ok");
}
static void testLongPressOnlyKeyRelease() {
EXPECT(checkFlag(), false);
EXPECT(getFromSocket("/keyword bindo SUPER,Y,exec,touch " + flagFile), "ok");
EXPECT(getFromSocket("/keyword input:repeat_delay 100"), "ok");
// press keybind
OK(getFromSocket("/dispatch plugin:test:keybind 1,7,29"));
// check no flag on short press
std::this_thread::sleep_for(std::chrono::milliseconds(50));
EXPECT(checkFlag(), false);
// release key, keep modifier
OK(getFromSocket("/dispatch plugin:test:keybind 0,7,29"));
// await repeat delay
std::this_thread::sleep_for(std::chrono::milliseconds(200));
EXPECT(checkFlag(), false);
OK(getFromSocket("/dispatch plugin:test:keybind 0,0,29"));
EXPECT(getFromSocket("/keyword unbind SUPER,Y"), "ok");
}
static void testRepeat() {
EXPECT(checkFlag(), false);
EXPECT(getFromSocket("/keyword binde SUPER,Y,exec,touch " + flagFile), "ok");
EXPECT(getFromSocket("/keyword input:repeat_delay 100"), "ok");
// press keybind
OK(getFromSocket("/dispatch plugin:test:keybind 1,7,29"));
// await flag
std::this_thread::sleep_for(std::chrono::milliseconds(200));
EXPECT(checkFlag(), true);
// await repeat delay
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(200));
EXPECT(checkFlag(), true);
// release keybind
OK(getFromSocket("/dispatch plugin:test:keybind 0,0,29"));
EXPECT(getFromSocket("/keyword unbind SUPER,Y"), "ok");
}
static void testKeyRepeat() {
EXPECT(checkFlag(), false);
EXPECT(getFromSocket("/keyword binde ,Y,exec,touch " + flagFile), "ok");
EXPECT(getFromSocket("/keyword input:repeat_delay 100"), "ok");
// 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);
// await repeat delay
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(200));
EXPECT(checkFlag(), true);
// release keybind
OK(getFromSocket("/dispatch plugin:test:keybind 0,0,29"));
EXPECT(getFromSocket("/keyword unbind ,Y"), "ok");
}
static void testRepeatRelease() {
EXPECT(checkFlag(), false);
EXPECT(getFromSocket("/keyword binde SUPER,Y,exec,touch " + flagFile), "ok");
EXPECT(getFromSocket("/keyword input:repeat_delay 100"), "ok");
// 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);
// release keybind
OK(getFromSocket("/dispatch plugin:test:keybind 0,0,29"));
// await repeat delay
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(200));
EXPECT(checkFlag(), false);
EXPECT(getFromSocket("/keyword unbind SUPER,Y"), "ok");
}
static void testRepeatOnlyKeyRelease() {
EXPECT(checkFlag(), false);
EXPECT(getFromSocket("/keyword binde SUPER,Y,exec,touch " + flagFile), "ok");
EXPECT(getFromSocket("/keyword input:repeat_delay 100"), "ok");
// press keybind
OK(getFromSocket("/dispatch plugin:test:keybind 1,7,29"));
// await flag
std::this_thread::sleep_for(std::chrono::milliseconds(200));
EXPECT(checkFlag(), true);
// release key, keep modifier
OK(getFromSocket("/dispatch plugin:test:keybind 0,7,29"));
// await repeat delay
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(200));
EXPECT(checkFlag(), false);
OK(getFromSocket("/dispatch plugin:test:keybind 0,0,29"));
EXPECT(getFromSocket("/keyword unbind SUPER,Y"), "ok");
}
static void testShortcutBind() {
auto kittyProc = spawnRemoteControlKitty();
if (!kittyProc) {
NLog::log("{}Error: kitty did not spawn", Colors::RED);
ret = 1;
return;
}
EXPECT(getFromSocket("/dispatch focuswindow class:keybinds_test"), "ok");
EXPECT(getFromSocket("/keyword bind SUPER,Y,sendshortcut,,q,"), "ok");
// press keybind
OK(getFromSocket("/dispatch plugin:test:keybind 1,7,29"));
// release keybind
std::this_thread::sleep_for(std::chrono::milliseconds(50));
OK(getFromSocket("/dispatch plugin:test:keybind 0,0,29"));
std::this_thread::sleep_for(std::chrono::milliseconds(50));
const std::string output = readKittyOutput();
EXPECT_COUNT_STRING(output, "y", 0);
EXPECT_COUNT_STRING(output, "q", 1);
EXPECT(getFromSocket("/keyword unbind SUPER,Y"), "ok");
Tests::killAllWindows();
}
static void testShortcutBindKey() {
auto kittyProc = spawnRemoteControlKitty();
if (!kittyProc) {
NLog::log("{}Error: kitty did not spawn", Colors::RED);
ret = 1;
return;
}
EXPECT(getFromSocket("/dispatch focuswindow class:keybinds_test"), "ok");
EXPECT(getFromSocket("/keyword bind ,Y,sendshortcut,,q,"), "ok");
// press keybind
OK(getFromSocket("/dispatch plugin:test:keybind 1,0,29"));
// release keybind
std::this_thread::sleep_for(std::chrono::milliseconds(50));
OK(getFromSocket("/dispatch plugin:test:keybind 0,0,29"));
std::this_thread::sleep_for(std::chrono::milliseconds(50));
const std::string output = readKittyOutput();
EXPECT_COUNT_STRING(output, "y", 0);
// disabled: doesn't work in CI
// EXPECT_COUNT_STRING(output, "q", 1);
EXPECT(getFromSocket("/keyword unbind ,Y"), "ok");
Tests::killAllWindows();
}
static void testShortcutLongPress() {
auto kittyProc = spawnRemoteControlKitty();
if (!kittyProc) {
NLog::log("{}Error: kitty did not spawn", Colors::RED);
ret = 1;
return;
}
EXPECT(getFromSocket("/dispatch focuswindow class:keybinds_test"), "ok");
EXPECT(getFromSocket("/keyword bindo SUPER,Y,sendshortcut,,q,"), "ok");
EXPECT(getFromSocket("/keyword input:repeat_delay 100"), "ok");
EXPECT(getFromSocket("/keyword input:repeat_rate 10"), "ok");
// press keybind
OK(getFromSocket("/dispatch plugin:test:keybind 1,7,29"));
// await repeat delay
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(200));
const std::string output = readKittyOutput();
int yCount = Tests::countOccurrences(output, "y");
// sometimes 1, sometimes 2, not sure why
// keybind press sends 1 y immediately
// then repeat triggers, sending 1 y
// final release stop repeats, and shouldn't send any more
EXPECT(true, yCount == 1 || yCount == 2);
EXPECT_COUNT_STRING(output, "q", 1);
EXPECT(getFromSocket("/keyword unbind SUPER,Y"), "ok");
Tests::killAllWindows();
}
static void testShortcutLongPressKeyRelease() {
auto kittyProc = spawnRemoteControlKitty();
if (!kittyProc) {
NLog::log("{}Error: kitty did not spawn", Colors::RED);
ret = 1;
return;
}
EXPECT(getFromSocket("/dispatch focuswindow class:keybinds_test"), "ok");
EXPECT(getFromSocket("/keyword bindo SUPER,Y,sendshortcut,,q,"), "ok");
EXPECT(getFromSocket("/keyword input:repeat_delay 100"), "ok");
EXPECT(getFromSocket("/keyword input:repeat_rate 10"), "ok");
// press keybind
OK(getFromSocket("/dispatch plugin:test:keybind 1,7,29"));
std::this_thread::sleep_for(std::chrono::milliseconds(10));
// release key, keep modifier
OK(getFromSocket("/dispatch plugin:test:keybind 0,7,29"));
// await repeat delay
std::this_thread::sleep_for(std::chrono::milliseconds(200));
const std::string output = readKittyOutput();
// disabled: doesn't work on CI
// EXPECT_COUNT_STRING(output, "y", 1);
EXPECT_COUNT_STRING(output, "q", 0);
OK(getFromSocket("/dispatch plugin:test:keybind 0,0,29"));
EXPECT(getFromSocket("/keyword unbind SUPER,Y"), "ok");
Tests::killAllWindows();
}
static void testShortcutRepeat() {
auto kittyProc = spawnRemoteControlKitty();
if (!kittyProc) {
NLog::log("{}Error: kitty did not spawn", Colors::RED);
ret = 1;
return;
}
EXPECT(getFromSocket("/dispatch focuswindow class:keybinds_test"), "ok");
EXPECT(getFromSocket("/keyword binde SUPER,Y,sendshortcut,,q,"), "ok");
EXPECT(getFromSocket("/keyword input:repeat_rate 5"), "ok");
EXPECT(getFromSocket("/keyword input:repeat_delay 200"), "ok");
// press keybind
OK(getFromSocket("/dispatch plugin:test:keybind 1,7,29"));
// await repeat
std::this_thread::sleep_for(std::chrono::milliseconds(210));
// release keybind
OK(getFromSocket("/dispatch plugin:test:keybind 0,0,29"));
std::this_thread::sleep_for(std::chrono::milliseconds(450));
const std::string output = readKittyOutput();
EXPECT_COUNT_STRING(output, "y", 0);
int qCount = Tests::countOccurrences(output, "q");
// sometimes 2, sometimes 3, not sure why
// keybind press sends 1 q immediately
// then repeat triggers, sending 1 q
// final release stop repeats, and shouldn't send any more
EXPECT(true, qCount == 2 || qCount == 3);
EXPECT(getFromSocket("/keyword unbind SUPER,Y"), "ok");
Tests::killAllWindows();
}
static void testShortcutRepeatKeyRelease() {
auto kittyProc = spawnRemoteControlKitty();
if (!kittyProc) {
NLog::log("{}Error: kitty did not spawn", Colors::RED);
ret = 1;
return;
}
EXPECT(getFromSocket("/dispatch focuswindow class:keybinds_test"), "ok");
EXPECT(getFromSocket("/keyword binde SUPER,Y,sendshortcut,,q,"), "ok");
EXPECT(getFromSocket("/keyword input:repeat_rate 5"), "ok");
EXPECT(getFromSocket("/keyword input:repeat_delay 200"), "ok");
// press keybind
OK(getFromSocket("/dispatch plugin:test:keybind 1,7,29"));
std::this_thread::sleep_for(std::chrono::milliseconds(210));
// release key, keep modifier
OK(getFromSocket("/dispatch plugin:test:keybind 0,7,29"));
// if repeat was still active, we'd get 2 more q's here
std::this_thread::sleep_for(std::chrono::milliseconds(450));
// release modifier
const std::string output = readKittyOutput();
EXPECT_COUNT_STRING(output, "y", 0);
int qCount = Tests::countOccurrences(output, "q");
// sometimes 2, sometimes 3, not sure why
// keybind press sends 1 q immediately
// then repeat triggers, sending 1 q
// final release stop repeats, and shouldn't send any more
EXPECT(true, qCount == 2 || qCount == 3);
OK(getFromSocket("/dispatch plugin:test:keybind 0,0,29"));
EXPECT(getFromSocket("/keyword unbind SUPER,Y"), "ok");
Tests::killAllWindows();
}
static void testSubmap() {
const auto press = [](const uint32_t key, const uint32_t mod = 0) {
// +8 because udev -> XKB keycode.
getFromSocket("/dispatch plugin:test:keybind 1," + std::to_string(mod) + "," + std::to_string(key + 8));
getFromSocket("/dispatch plugin:test:keybind 0," + std::to_string(mod) + "," + std::to_string(key + 8));
};
NLog::log("{}Testing submaps", Colors::GREEN);
// submap 1 no resets
press(KEY_U, MOD_META);
EXPECT_CONTAINS(getFromSocket("/submap"), "submap1");
press(KEY_O);
Tests::waitUntilWindowsN(1);
EXPECT_CONTAINS(getFromSocket("/submap"), "submap1");
// submap 2 resets to submap 1
press(KEY_U);
EXPECT_CONTAINS(getFromSocket("/submap"), "submap2");
press(KEY_O);
Tests::waitUntilWindowsN(2);
EXPECT_CONTAINS(getFromSocket("/submap"), "submap1");
// submap 3 resets to default
press(KEY_I);
EXPECT_CONTAINS(getFromSocket("/submap"), "submap3");
press(KEY_O);
Tests::waitUntilWindowsN(3);
EXPECT_CONTAINS(getFromSocket("/submap"), "default");
// submap 1 reset via keybind
press(KEY_U, MOD_META);
EXPECT_CONTAINS(getFromSocket("/submap"), "submap1");
press(KEY_P);
EXPECT_CONTAINS(getFromSocket("/submap"), "default");
Tests::killAllWindows();
}
static void testSubmapUniversal() {
NLog::log("{}Testing submap universal", Colors::GREEN);
EXPECT(checkFlag(), false);
EXPECT(getFromSocket("/keyword bindu SUPER,Y,exec,touch " + flagFile), "ok");
EXPECT_CONTAINS(getFromSocket("/submap"), "default");
// keybind works on default submap
OK(getFromSocket("/dispatch plugin:test:keybind 1,7,29"));
OK(getFromSocket("/dispatch plugin:test:keybind 0,7,29"));
EXPECT(attemptCheckFlag(30, 5), true);
// keybind works on submap1
getFromSocket("/dispatch plugin:test:keybind 1,7,30");
getFromSocket("/dispatch plugin:test:keybind 0,7,30");
EXPECT_CONTAINS(getFromSocket("/submap"), "submap1");
OK(getFromSocket("/dispatch plugin:test:keybind 1,7,29"));
OK(getFromSocket("/dispatch plugin:test:keybind 0,7,29"));
EXPECT(attemptCheckFlag(30, 5), true);
// reset to default submap
getFromSocket("/dispatch plugin:test:keybind 1,0,33");
getFromSocket("/dispatch plugin:test:keybind 0,0,33");
EXPECT_CONTAINS(getFromSocket("/submap"), "default");
EXPECT(getFromSocket("/keyword unbind SUPER,Y"), "ok");
}
static bool test() {
NLog::log("{}Testing keybinds", Colors::GREEN);
clearFlag();
testBind();
testBindKey();
testLongPress();
testKeyLongPress();
testLongPressRelease();
testLongPressOnlyKeyRelease();
testRepeat();
testKeyRepeat();
testRepeatRelease();
testRepeatOnlyKeyRelease();
testShortcutBind();
testShortcutBindKey();
testShortcutLongPress();
testShortcutLongPressKeyRelease();
testShortcutRepeat();
testShortcutRepeatKeyRelease();
testSubmap();
testSubmapUniversal();
clearFlag();
return !ret;
}
REGISTER_TEST_FN(test)

View file

@ -1,71 +0,0 @@
#include "../shared.hpp"
#include "../../shared.hpp"
#include "../../hyprctlCompat.hpp"
#include "tests.hpp"
static int ret = 0;
static void focusMasterPrevious() {
// setup
NLog::log("{}Spawning 1 master and 3 slave windows", Colors::YELLOW);
// order of windows set according to new_status = master (set in test.conf)
for (auto const& win : {"slave1", "slave2", "slave3", "master"}) {
if (!Tests::spawnKitty(win)) {
NLog::log("{}Failed to spawn kitty with win class `{}`", Colors::RED, win);
++TESTS_FAILED;
ret = 1;
return;
}
}
NLog::log("{}Ensuring focus is on master before testing", Colors::YELLOW);
OK(getFromSocket("/dispatch layoutmsg focusmaster master"));
EXPECT_CONTAINS(getFromSocket("/activewindow"), "class: master");
// test
NLog::log("{}Testing fallback to focusmaster auto", Colors::YELLOW);
OK(getFromSocket("/dispatch layoutmsg focusmaster previous"));
EXPECT_CONTAINS(getFromSocket("/activewindow"), "class: slave1");
NLog::log("{}Testing focusing from slave to master", Colors::YELLOW);
OK(getFromSocket("/dispatch layoutmsg cyclenext noloop"));
EXPECT_CONTAINS(getFromSocket("/activewindow"), "class: slave2");
OK(getFromSocket("/dispatch layoutmsg focusmaster previous"));
EXPECT_CONTAINS(getFromSocket("/activewindow"), "class: master");
NLog::log("{}Testing focusing on previous window", Colors::YELLOW);
OK(getFromSocket("/dispatch layoutmsg focusmaster previous"));
EXPECT_CONTAINS(getFromSocket("/activewindow"), "class: slave2");
NLog::log("{}Testing focusing back to master", Colors::YELLOW);
OK(getFromSocket("/dispatch layoutmsg focusmaster previous"));
EXPECT_CONTAINS(getFromSocket("/activewindow"), "class: master");
// clean up
NLog::log("{}Killing all windows", Colors::YELLOW);
Tests::killAllWindows();
}
static bool test() {
NLog::log("{}Testing Master layout", Colors::GREEN);
// setup
OK(getFromSocket("/dispatch workspace name:master"));
OK(getFromSocket("/keyword general:layout master"));
// test
NLog::log("{}Testing `focusmaster previous` layoutmsg", Colors::GREEN);
focusMasterPrevious();
// clean up
NLog::log("Cleaning up", Colors::YELLOW);
OK(getFromSocket("/dispatch workspace 1"));
OK(getFromSocket("/reload"));
return !ret;
}
REGISTER_TEST_FN(test);

View file

@ -53,7 +53,7 @@ static bool test() {
NLog::log("{}Testing new_window_takes_over_fullscreen", Colors::YELLOW);
OK(getFromSocket("/keyword misc:on_focus_under_fullscreen 0"));
OK(getFromSocket("/keyword misc:new_window_takes_over_fullscreen 0"));
Tests::spawnKitty("kitty_A");
@ -73,16 +73,7 @@ static bool test() {
EXPECT_CONTAINS(str, "kitty_A");
}
OK(getFromSocket("/dispatch focuswindow class:kitty_B"));
{
// should be ignored as per focus_under_fullscreen 0
auto str = getFromSocket("/activewindow");
EXPECT_CONTAINS(str, "fullscreen: 2");
EXPECT_CONTAINS(str, "kitty_A");
}
OK(getFromSocket("/keyword misc:on_focus_under_fullscreen 1"));
OK(getFromSocket("/keyword misc:new_window_takes_over_fullscreen 1"));
Tests::spawnKitty("kitty_C");
@ -92,7 +83,7 @@ static bool test() {
EXPECT_CONTAINS(str, "kitty_C");
}
OK(getFromSocket("/keyword misc:on_focus_under_fullscreen 2"));
OK(getFromSocket("/keyword misc:new_window_takes_over_fullscreen 2"));
Tests::spawnKitty("kitty_D");
@ -102,7 +93,7 @@ static bool test() {
EXPECT_CONTAINS(str, "kitty_D");
}
OK(getFromSocket("/keyword misc:on_focus_under_fullscreen 0"));
OK(getFromSocket("/keyword misc:new_window_takes_over_fullscreen 0"));
Tests::killAllWindows();
@ -140,74 +131,6 @@ static bool test() {
EXPECT_CONTAINS(str, "fullscreen: 2");
}
Tests::killAllWindows();
NLog::log("{}Testing fullscreen and fullscreenstate dispatcher", Colors::YELLOW);
Tests::spawnKitty("kitty_A");
Tests::spawnKitty("kitty_B");
OK(getFromSocket("/dispatch focuswindow class:kitty_A"));
OK(getFromSocket("/dispatch fullscreen 0 set"));
{
auto str = getFromSocket("/activewindow");
EXPECT_CONTAINS(str, "fullscreen: 2");
}
OK(getFromSocket("/dispatch fullscreen 0 unset"));
{
auto str = getFromSocket("/activewindow");
EXPECT_CONTAINS(str, "fullscreen: 0");
}
OK(getFromSocket("/dispatch fullscreen 1 toggle"));
{
auto str = getFromSocket("/activewindow");
EXPECT_CONTAINS(str, "fullscreen: 1");
}
OK(getFromSocket("/dispatch fullscreen 1 toggle"));
{
auto str = getFromSocket("/activewindow");
EXPECT_CONTAINS(str, "fullscreen: 0");
}
OK(getFromSocket("/dispatch fullscreenstate 2 2 set"));
{
auto str = getFromSocket("/activewindow");
EXPECT_CONTAINS(str, "fullscreen: 2");
}
OK(getFromSocket("/dispatch fullscreenstate 2 2 set"));
{
auto str = getFromSocket("/activewindow");
EXPECT_CONTAINS(str, "fullscreen: 2");
}
OK(getFromSocket("/dispatch fullscreenstate 2 2 toggle"));
{
auto str = getFromSocket("/activewindow");
EXPECT_CONTAINS(str, "fullscreen: 0");
}
OK(getFromSocket("/dispatch fullscreenstate 2 2 toggle"));
{
auto str = getFromSocket("/activewindow");
EXPECT_CONTAINS(str, "fullscreen: 2");
}
// Ensure that the process autostarted in the config does not
// become a zombie even if it terminates very quickly.
EXPECT(Tests::execAndGet("pgrep -f 'sleep 0'").empty(), true);
// kill all
NLog::log("{}Killing all windows", Colors::YELLOW);
Tests::killAllWindows();

View file

@ -1,85 +0,0 @@
#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 persistent workspaces", Colors::GREEN);
EXPECT(Tests::windowCount(), 0);
// test on workspace "window"
NLog::log("{}Switching to workspace 1", Colors::YELLOW);
getFromSocket("/dispatch workspace 1"); // no OK: we might be on 1 already
OK(getFromSocket("/keyword workspace 5, monitor:HEADLESS-2, persistent:1"));
OK(getFromSocket("/keyword workspace 6, monitor:HEADLESS-PERSISTENT-TEST, persistent:1"));
OK(getFromSocket("/keyword workspace name:PERSIST, monitor:HEADLESS-PERSISTENT-TEST, persistent:1"));
OK(getFromSocket("/keyword workspace name:PERSIST-2, monitor:HEADLESS-PERSISTENT-TEST, persistent:1"));
{
auto str = getFromSocket("/workspaces");
EXPECT_CONTAINS(str, "ID 5 (5)");
EXPECT_COUNT_STRING(str, "workspace ID ", 2);
}
OK(getFromSocket("/output create headless HEADLESS-PERSISTENT-TEST"));
{
auto str = getFromSocket("/monitors");
EXPECT_CONTAINS(str, "HEADLESS-PERSISTENT-TEST");
}
OK(getFromSocket("/dispatch focusmonitor HEADLESS-PERSISTENT-TEST"));
{
auto str = getFromSocket("/workspaces");
EXPECT_CONTAINS(str, "ID 2 (2)"); // this should be automatically generated by hl
EXPECT_CONTAINS(str, "ID 5 (5)");
EXPECT_CONTAINS(str, "ID 6 (6)");
EXPECT_CONTAINS(str, "(PERSIST) on monitor");
EXPECT_CONTAINS(str, "(PERSIST-2) on monitor");
EXPECT_COUNT_STRING(str, "workspace ID ", 6);
}
OK(getFromSocket("/reload"));
{
auto str = getFromSocket("/workspaces");
EXPECT_NOT_CONTAINS(str, "ID 5 (5)");
EXPECT_NOT_CONTAINS(str, "ID 6 (6)");
EXPECT_NOT_CONTAINS(str, "(PERSIST) on monitor");
EXPECT_COUNT_STRING(str, "workspace ID ", 2);
}
OK(getFromSocket("/output remove HEADLESS-PERSISTENT-TEST"));
// kill all
NLog::log("{}Killing all windows", Colors::YELLOW);
Tests::killAllWindows();
NLog::log("{}Expecting 0 windows", Colors::YELLOW);
EXPECT(Tests::windowCount(), 0);
// reload cfg
OK(getFromSocket("/reload"));
return !ret;
}
REGISTER_TEST_FN(test)

View file

@ -45,68 +45,69 @@ static void expectSnapMove(const Vector2D FROM, const Vector2D* TO) {
EXPECT_CONTAINS(getFromSocket("/activewindow"), std::format("at: {},{}", B.x, B.y));
}
static void testWindowSnap(const bool RESPECTGAPS) {
const int BORDERSIZE = 2;
const int WINDOWSIZE = 100;
static void testSnap(const bool OVERLAP, const bool RESPECT) {
const double BORDERSIZE = 2;
const double WINDOWSIZE = 100;
const int OTHER = 500;
const int WINDOWGAP = 8;
const int GAPSIN = 5;
const int GAP = (RESPECTGAPS ? 2 * GAPSIN : 0) + (2 * BORDERSIZE);
const int END = GAP + WINDOWSIZE;
// test window snapping
{
const double OTHER = 500;
const double WINDOWGAP = 8;
const double GAPSIN = 5;
const double GAP = (RESPECT ? GAPSIN : 0) + BORDERSIZE + (OVERLAP ? 0 : BORDERSIZE);
const double END = GAP + WINDOWSIZE;
int x;
Vector2D predict;
double x;
Vector2D predict;
x = WINDOWGAP + END;
expectSnapMove({OTHER + x, OTHER}, nullptr);
expectSnapMove({OTHER - x, OTHER}, nullptr);
expectSnapMove({OTHER, OTHER + x}, nullptr);
expectSnapMove({OTHER, OTHER - x}, nullptr);
x -= 1;
expectSnapMove({OTHER + x, OTHER}, &(predict = {OTHER + END, OTHER}));
expectSnapMove({OTHER - x, OTHER}, &(predict = {OTHER - END, OTHER}));
expectSnapMove({OTHER, OTHER + x}, &(predict = {OTHER, OTHER + END}));
expectSnapMove({OTHER, OTHER - x}, &(predict = {OTHER, OTHER - END}));
}
x = WINDOWGAP + END;
expectSnapMove({OTHER + x, OTHER}, nullptr);
expectSnapMove({OTHER - x, OTHER}, nullptr);
expectSnapMove({OTHER, OTHER + x}, nullptr);
expectSnapMove({OTHER, OTHER - x}, nullptr);
x -= 1;
expectSnapMove({OTHER + x, OTHER}, &(predict = {OTHER + END, OTHER}));
expectSnapMove({OTHER - x, OTHER}, &(predict = {OTHER - END, OTHER}));
expectSnapMove({OTHER, OTHER + x}, &(predict = {OTHER, OTHER + END}));
expectSnapMove({OTHER, OTHER - x}, &(predict = {OTHER, OTHER - END}));
}
static void testMonitorSnap(const bool RESPECTGAPS, const bool OVERLAP) {
const int BORDERSIZE = 2;
const int WINDOWSIZE = 100;
// test monitor snapping
{
const double MONITORGAP = 10;
const double GAPSOUT = 20;
const double RESP = (RESPECT ? GAPSOUT : 0);
const double GAP = RESP + (OVERLAP ? 0 : BORDERSIZE);
const double END = GAP + WINDOWSIZE;
const int MONITORGAP = 10;
const int GAPSOUT = 20;
const int RESP = (RESPECTGAPS ? GAPSOUT : 0);
const int GAP = RESP + (OVERLAP ? 0 : BORDERSIZE);
const int END = GAP + WINDOWSIZE;
double x;
Vector2D predict;
int x;
Vector2D predict;
x = MONITORGAP + GAP;
expectSnapMove({x, x}, nullptr);
x -= 1;
expectSnapMove({x, x}, &(predict = {GAP, GAP}));
x = MONITORGAP + GAP;
expectSnapMove({x, x}, nullptr);
x -= 1;
expectSnapMove({x, x}, &(predict = {GAP, GAP}));
x = MONITORGAP + END;
expectSnapMove({1920 - x, 1080 - x}, nullptr);
x -= 1;
expectSnapMove({1920 - x, 1080 - x}, &(predict = {1920 - END, 1080 - END}));
x = MONITORGAP + END;
expectSnapMove({1920 - x, 1080 - x}, nullptr);
x -= 1;
expectSnapMove({1920 - x, 1080 - x}, &(predict = {1920 - END, 1080 - END}));
// test reserved area
const double RESERVED = 200;
const double RGAP = RESERVED + RESP + BORDERSIZE;
const double REND = RGAP + WINDOWSIZE;
// test reserved area
const int RESERVED = 200;
const int RGAP = RESERVED + RESP + BORDERSIZE;
const int REND = RGAP + WINDOWSIZE;
x = MONITORGAP + RGAP;
expectSnapMove({x, x}, nullptr);
x -= 1;
expectSnapMove({x, x}, &(predict = {RGAP, RGAP}));
x = MONITORGAP + RGAP;
expectSnapMove({x, x}, nullptr);
x -= 1;
expectSnapMove({x, x}, &(predict = {RGAP, RGAP}));
x = MONITORGAP + REND;
expectSnapMove({1920 - x, 1080 - x}, nullptr);
x -= 1;
expectSnapMove({1920 - x, 1080 - x}, &(predict = {1920 - REND, 1080 - REND}));
x = MONITORGAP + REND;
expectSnapMove({1920 - x, 1080 - x}, nullptr);
x -= 1;
expectSnapMove({1920 - x, 1080 - x}, &(predict = {1920 - REND, 1080 - REND}));
}
}
static bool test() {
@ -142,22 +143,20 @@ static bool test() {
EXPECT(Tests::windowCount(), 2);
NLog::log("");
testWindowSnap(false);
testMonitorSnap(false, false);
NLog::log("\n{}Turning on respect_gaps", Colors::YELLOW);
OK(getFromSocket("/keyword general:snap:respect_gaps true"));
testWindowSnap(true);
testMonitorSnap(true, false);
testSnap(false, false);
NLog::log("\n{}Turning on border_overlap", Colors::YELLOW);
OK(getFromSocket("/keyword general:snap:respect_gaps false"));
OK(getFromSocket("/keyword general:snap:border_overlap true"));
testMonitorSnap(false, true);
testSnap(true, false);
NLog::log("\n{}Turning on respect_gaps", Colors::YELLOW);
OK(getFromSocket("/keyword general:snap:border_overlap false"));
OK(getFromSocket("/keyword general:snap:respect_gaps true"));
testSnap(false, true);
NLog::log("\n{}Turning on both border_overlap and respect_gaps", Colors::YELLOW);
OK(getFromSocket("/keyword general:snap:respect_gaps true"));
testMonitorSnap(true, true);
OK(getFromSocket("/keyword general:snap:border_overlap true"));
testSnap(true, true);
// kill all
NLog::log("\n{}Killing all windows", Colors::YELLOW);
@ -168,7 +167,6 @@ static bool test() {
NLog::log("{}Reloading the config", Colors::YELLOW);
OK(getFromSocket("/reload"));
OK(getFromSocket("/dispatch workspace 1"));
return !ret;
}

View file

@ -1,76 +0,0 @@
#include "tests.hpp"
#include "../../shared.hpp"
#include "../../hyprctlCompat.hpp"
#include <thread>
#include <chrono>
#include <hyprutils/os/Process.hpp>
#include <hyprutils/memory/WeakPtr.hpp>
#include "../shared.hpp"
static int ret = 0;
using namespace Hyprutils::OS;
using namespace Hyprutils::Memory;
#define UP CUniquePointer
#define SP CSharedPointer
static bool test() {
NLog::log("{}Testing solitary clients", Colors::GREEN);
OK(getFromSocket("/keyword general:allow_tearing false"));
OK(getFromSocket("/keyword render:direct_scanout 0"));
OK(getFromSocket("/keyword cursor:no_hardware_cursors 1"));
NLog::log("{}Expecting blocked solitary/DS/tearing", Colors::YELLOW);
{
auto str = getFromSocket("/monitors");
EXPECT_CONTAINS(str, "solitary: 0\n");
EXPECT_CONTAINS(str, "solitaryBlockedBy: windowed mode,missing candidate");
EXPECT_CONTAINS(str, "activelyTearing: false");
EXPECT_CONTAINS(str, "tearingBlockedBy: next frame is not torn,user settings,not supported by monitor,missing candidate");
EXPECT_CONTAINS(str, "directScanoutTo: 0\n");
EXPECT_CONTAINS(str, "directScanoutBlockedBy: user settings,software renders/cursors,missing candidate");
}
// FIXME: need a reliable client with solitary opaque surface in fullscreen. kitty doesn't work all the time
// NLog::log("{}Spawning kittyProcA", Colors::YELLOW);
// auto kittyProcA = Tests::spawnKitty();
// if (!kittyProcA) {
// NLog::log("{}Error: kitty did not spawn", Colors::RED);
// return false;
// }
// OK(getFromSocket("/keyword general:allow_tearing true"));
// OK(getFromSocket("/keyword render:direct_scanout 1"));
// NLog::log("{}", getFromSocket("/clients"));
// OK(getFromSocket("/dispatch fullscreen"));
// NLog::log("{}", getFromSocket("/clients"));
// std::this_thread::sleep_for(std::chrono::milliseconds(100));
// NLog::log("{}Expecting kitty to almost pass for solitary/DS/tearing", Colors::YELLOW);
// {
// auto str = getFromSocket("/monitors");
// EXPECT_NOT_CONTAINS(str, "solitary: 0\n");
// EXPECT_CONTAINS(str, "solitaryBlockedBy: null");
// EXPECT_CONTAINS(str, "activelyTearing: false");
// EXPECT_CONTAINS(str, "tearingBlockedBy: next frame is not torn,not supported by monitor,window settings");
// }
// OK(getFromSocket("/dispatch setprop active immediate 1"));
// NLog::log("{}Expecting kitty to almost pass for tearing", Colors::YELLOW);
// {
// auto str = getFromSocket("/monitors");
// EXPECT_CONTAINS(str, "tearingBlockedBy: next frame is not torn,not supported by monitor\n");
// }
// // kill all
// NLog::log("{}Killing all windows", Colors::YELLOW);
// Tests::killAllWindows();
NLog::log("{}Reloading the config", Colors::YELLOW);
OK(getFromSocket("/reload"));
return !ret;
}
REGISTER_TEST_FN(test)

View file

@ -1,51 +0,0 @@
#include "../../shared.hpp"
#include "../../hyprctlCompat.hpp"
#include "../shared.hpp"
#include "tests.hpp"
static int ret = 0;
static bool testTags() {
NLog::log("{}Testing tags", Colors::GREEN);
EXPECT(Tests::windowCount(), 0);
NLog::log("{}Spawning kittyProcA&B on ws 1", Colors::YELLOW);
auto kittyProcA = Tests::spawnKitty("tagged");
auto kittyProcB = Tests::spawnKitty("untagged");
if (!kittyProcA || !kittyProcB) {
NLog::log("{}Error: kitty did not spawn", Colors::RED);
return false;
}
NLog::log("{}Testing testTag tags", Colors::YELLOW);
OK(getFromSocket("/keyword windowrule[tag-test-1]:tag +testTag"));
OK(getFromSocket("/keyword windowrule[tag-test-1]:match:class tagged"));
OK(getFromSocket("/keyword windowrule[tag-test-2]:match:tag negative:testTag"));
OK(getFromSocket("/keyword windowrule[tag-test-2]:no_shadow true"));
OK(getFromSocket("/keyword windowrule[tag-test-3]:match:tag testTag"));
OK(getFromSocket("/keyword windowrule[tag-test-3]:no_dim true"));
EXPECT(Tests::windowCount(), 2);
OK(getFromSocket("/dispatch focuswindow class:tagged"));
NLog::log("{}Testing tagged window for no_dim 0 & no_shadow", Colors::YELLOW);
EXPECT_CONTAINS(getFromSocket("/activewindow"), "testTag");
EXPECT_CONTAINS(getFromSocket("/getprop activewindow no_dim"), "true");
EXPECT_CONTAINS(getFromSocket("/getprop activewindow no_shadow"), "false");
NLog::log("{}Testing untagged window for no_dim & no_shadow", Colors::YELLOW);
OK(getFromSocket("/dispatch focuswindow class:untagged"));
EXPECT_NOT_CONTAINS(getFromSocket("/activewindow"), "testTag");
EXPECT_CONTAINS(getFromSocket("/getprop activewindow no_shadow"), "true");
EXPECT_CONTAINS(getFromSocket("/getprop activewindow no_dim"), "false");
Tests::killAllWindows();
EXPECT(Tests::windowCount(), 0);
OK(getFromSocket("/reload"));
return ret == 0;
}
REGISTER_TEST_FN(testTags)

View file

@ -1,377 +1,22 @@
#include <cmath>
#include <chrono>
#include <cstdlib>
#include <cstring>
#include <filesystem>
#include <thread>
#include <hyprutils/os/Process.hpp>
#include <hyprutils/memory/WeakPtr.hpp>
#include <hyprutils/string/VarList2.hpp>
#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"
#include "tests.hpp"
static int ret = 0;
static int ret = 0;
static bool spawnKitty(const std::string& class_, const std::vector<std::string>& args = {}) {
NLog::log("{}Spawning {}", Colors::YELLOW, class_);
if (!Tests::spawnKitty(class_, args)) {
NLog::log("{}Error: {} did not spawn", Colors::RED, class_);
return false;
}
return true;
}
using namespace Hyprutils::OS;
using namespace Hyprutils::Memory;
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";
}
auto pos2 = winInfo.find('\n', pos);
return winInfo.substr(pos, pos2 - pos);
}
static std::string getWindowAddress(const std::string& winInfo) {
auto pos = winInfo.find("Window ");
auto pos2 = winInfo.find(" -> ");
if (pos == std::string::npos || pos2 == std::string::npos) {
NLog::log("{}Wrong window info", Colors::RED);
ret = 1;
return "Wrong window info";
}
return winInfo.substr(pos + 7, pos2 - pos - 7);
}
static void testSwapWindow() {
NLog::log("{}Testing swapwindow", Colors::GREEN);
// test on workspace "swapwindow"
NLog::log("{}Switching to workspace \"swapwindow\"", Colors::YELLOW);
getFromSocket("/dispatch workspace name:swapwindow");
if (!Tests::spawnKitty("kitty_A")) {
ret = 1;
return;
}
if (!Tests::spawnKitty("kitty_B")) {
ret = 1;
return;
}
NLog::log("{}Expecting 2 windows", Colors::YELLOW);
EXPECT(Tests::windowCount(), 2);
// 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);
OK(getFromSocket("/dispatch swapwindow l"));
OK(getFromSocket("/dispatch focuswindow class:kitty_B"));
EXPECT_CONTAINS(getFromSocket("/activewindow"), std::format("{}", pos));
}
// Test swapwindow by class
{
getFromSocket("/dispatch focuswindow class:kitty_A");
auto pos = getWindowAttribute(getFromSocket("/activewindow"), "at:");
NLog::log("{}Testing kitty_A {}, swapwindow with class:kitty_B", Colors::YELLOW, pos);
OK(getFromSocket("/dispatch swapwindow class:kitty_B"));
OK(getFromSocket("/dispatch focuswindow class:kitty_B"));
EXPECT_CONTAINS(getFromSocket("/activewindow"), std::format("{}", pos));
}
// Test swapwindow by address
{
getFromSocket("/dispatch focuswindow class:kitty_B");
auto addr = getWindowAddress(getFromSocket("/activewindow"));
getFromSocket("/dispatch focuswindow class:kitty_A");
auto pos = 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)));
OK(getFromSocket(std::format("/dispatch focuswindow address:0x{}", addr)));
EXPECT_CONTAINS(getFromSocket("/activewindow"), std::format("{}", pos));
}
NLog::log("{}Testing swapwindow with fullscreen. Expecting to fail", Colors::YELLOW);
{
OK(getFromSocket("/dispatch fullscreen"));
auto str = getFromSocket("/dispatch swapwindow l");
EXPECT_CONTAINS(str, "Can't swap fullscreen window");
OK(getFromSocket("/dispatch fullscreen"));
}
NLog::log("{}Testing swapwindow with different workspace", Colors::YELLOW);
{
getFromSocket("/dispatch focuswindow class:kitty_B");
auto addr = getWindowAddress(getFromSocket("/activewindow"));
auto ws = getWindowAttribute(getFromSocket("/activewindow"), "workspace:");
NLog::log("{}Sending address:0x{}(kitty_B) to workspace \"swapwindow2\"", Colors::YELLOW, addr);
OK(getFromSocket("/dispatch movetoworkspacesilent name:swapwindow2"));
OK(getFromSocket(std::format("/dispatch swapwindow address:0x{}", addr)));
getFromSocket("/dispatch focuswindow class:kitty_B");
EXPECT_CONTAINS(getFromSocket("/activewindow"), std::format("{}", ws));
}
// kill all
NLog::log("{}Killing all windows", Colors::YELLOW);
Tests::killAllWindows();
NLog::log("{}Expecting 0 windows", Colors::YELLOW);
EXPECT(Tests::windowCount(), 0);
}
static void testGroupRules() {
NLog::log("{}Testing group window rules", Colors::YELLOW);
OK(getFromSocket("/keyword general:border_size 8"));
OK(getFromSocket("/keyword workspace w[tv1], bordersize:0"));
OK(getFromSocket("/keyword workspace f[1], bordersize:0"));
OK(getFromSocket("/keyword windowrule match:workspace w[tv1], border_size 0"));
OK(getFromSocket("/keyword windowrule match:workspace f[1], border_size 0"));
if (!Tests::spawnKitty("kitty_A")) {
ret = 1;
return;
}
{
auto str = getFromSocket("/getprop active border_size");
EXPECT_CONTAINS(str, "0");
}
if (!Tests::spawnKitty("kitty_B")) {
ret = 1;
return;
}
{
auto str = getFromSocket("/getprop active border_size");
EXPECT_CONTAINS(str, "8");
}
OK(getFromSocket("/dispatch focuswindow class:kitty_A"));
OK(getFromSocket("/dispatch togglegroup"));
OK(getFromSocket("/dispatch focuswindow class:kitty_B"));
OK(getFromSocket("/dispatch moveintogroup l"));
{
auto str = getFromSocket("/getprop active border_size");
EXPECT_CONTAINS(str, "0");
}
OK(getFromSocket("/dispatch changegroupactive f"));
{
auto str = getFromSocket("/getprop active border_size");
EXPECT_CONTAINS(str, "0");
}
if (!Tests::spawnKitty("kitty_C")) {
ret = 1;
return;
}
OK(getFromSocket("/dispatch moveoutofgroup r"));
{
auto str = getFromSocket("/getprop active border_size");
EXPECT_CONTAINS(str, "8");
}
OK(getFromSocket("/reload"));
Tests::killAllWindows();
}
static bool isActiveWindow(const std::string& class_, char fullscreen, bool log = true) {
std::string activeWin = getFromSocket("/activewindow");
auto winClass = getWindowAttribute(activeWin, "class:");
auto winFullscreen = 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, 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 true;
}
/// Tests behavior of a window being focused when on that window's workspace
/// another fullscreen window exists.
static bool testWindowFocusOnFullscreenConflict() {
if (!spawnKitty("kitty_A"))
return false;
if (!spawnKitty("kitty_B"))
return false;
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"));
OK(getFromSocket("/dispatch focuswindow class:kitty_A"));
OK(getFromSocket("/dispatch fullscreen 0 set"));
EXPECT(isActiveWindow("kitty_A", '2'), true);
// Dispatch-focus the same window
OK(getFromSocket("/dispatch focuswindow class:kitty_A"));
EXPECT(isActiveWindow("kitty_A", '2'), true);
// Dispatch-focus a different window
OK(getFromSocket("/dispatch focuswindow class:kitty_B"));
EXPECT(isActiveWindow("kitty_B", '0'), true);
// Make a window that will request focus
const std::string removeToActivate = spawnKittyActivating();
if (removeToActivate.empty())
return false;
OK(getFromSocket("/dispatch focuswindow class:kitty_A"));
OK(getFromSocket("/dispatch fullscreen 0 set"));
EXPECT(isActiveWindow("kitty_A", '2'), true);
std::filesystem::remove(removeToActivate);
EXPECT(waitForActiveWindow("kitty_activating", '0'), true);
OK(getFromSocket("/dispatch forcekillactive"));
Tests::waitUntilWindowsN(2);
}
// Take over on conflict
{
OK(getFromSocket("/keyword misc:on_focus_under_fullscreen 1"));
OK(getFromSocket("/dispatch focuswindow class:kitty_A"));
OK(getFromSocket("/dispatch fullscreen 0 set"));
EXPECT(isActiveWindow("kitty_A", '2'), true);
// Dispatch-focus the same window
OK(getFromSocket("/dispatch focuswindow class:kitty_A"));
EXPECT(isActiveWindow("kitty_A", '2'), true);
// Dispatch-focus a different window
OK(getFromSocket("/dispatch focuswindow class:kitty_B"));
EXPECT(isActiveWindow("kitty_B", '2'), true);
OK(getFromSocket("/dispatch fullscreenstate 0 0"));
// Make a window that will request focus
const std::string removeToActivate = spawnKittyActivating();
if (removeToActivate.empty())
return false;
OK(getFromSocket("/dispatch focuswindow class:kitty_A"));
OK(getFromSocket("/dispatch fullscreen 0 set"));
EXPECT(isActiveWindow("kitty_A", '2'), true);
std::filesystem::remove(removeToActivate);
EXPECT(waitForActiveWindow("kitty_activating", '2'), true);
OK(getFromSocket("/dispatch forcekillactive"));
Tests::waitUntilWindowsN(2);
}
// Keep the old focus on conflict
{
OK(getFromSocket("/keyword misc:on_focus_under_fullscreen 0"));
OK(getFromSocket("/dispatch focuswindow class:kitty_A"));
OK(getFromSocket("/dispatch fullscreen 0 set"));
EXPECT(isActiveWindow("kitty_A", '2'), true);
// Dispatch-focus the same window
OK(getFromSocket("/dispatch focuswindow class:kitty_A"));
EXPECT(isActiveWindow("kitty_A", '2'), true);
// Make a window that will request focus - the setting is treated normally
const std::string removeToActivate = spawnKittyActivating();
if (removeToActivate.empty())
return false;
OK(getFromSocket("/dispatch focuswindow class:kitty_A"));
OK(getFromSocket("/dispatch fullscreen 0 set"));
EXPECT(isActiveWindow("kitty_A", '2'), true);
std::filesystem::remove(removeToActivate);
EXPECT(waitForActiveWindow("kitty_A", '2'), true);
}
NLog::log("{}Reloading config", Colors::YELLOW);
OK(getFromSocket("/reload"));
NLog::log("{}Killing all windows", Colors::YELLOW);
Tests::killAllWindows();
NLog::log("{}Expecting 0 windows", Colors::YELLOW);
EXPECT(Tests::windowCount(), 0);
return true;
}
static void testMaximizeSize() {
NLog::log("{}Testing maximize size", Colors::GREEN);
EXPECT(spawnKitty("kitty_A"), true);
// 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);
}
#define UP CUniquePointer
#define SP CSharedPointer
static bool test() {
NLog::log("{}Testing windows", Colors::GREEN);
@ -380,11 +25,19 @@ static bool test() {
NLog::log("{}Switching to workspace `window`", Colors::YELLOW);
getFromSocket("/dispatch workspace name:window");
if (!spawnKitty("kitty_A"))
NLog::log("{}Spawning kittyProcA", Colors::YELLOW);
auto kittyProcA = Tests::spawnKitty();
if (!kittyProcA) {
NLog::log("{}Error: kitty did not spawn", Colors::RED);
return false;
}
NLog::log("{}Expecting 1 window", Colors::YELLOW);
EXPECT(Tests::windowCount(), 1);
// check kitty properties. One kitty should take the entire screen, as this is smart gaps
NLog::log("{}Expecting kitty_A to take up the whole screen", Colors::YELLOW);
NLog::log("{}Expecting kitty to take up the whole screen", Colors::YELLOW);
{
auto str = getFromSocket("/clients");
EXPECT(str.contains("at: 0,0"), true);
@ -392,95 +45,31 @@ static bool test() {
EXPECT(str.contains("fullscreen: 0"), true);
}
NLog::log("{}Testing window split ratios", Colors::YELLOW);
{
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 size: {},{}", Colors::YELLOW, WIDTH1, HEIGHT);
EXPECT_CONTAINS(getFromSocket("/activewindow"), std::format("size: {},{}", WIDTH1, HEIGHT));
OK(getFromSocket("/dispatch killwindow activewindow"));
Tests::waitUntilWindowsN(1);
NLog::log("{}Inverting the split ratio", Colors::YELLOW);
OK(getFromSocket("/keyword dwindle:default_split_ratio 0.75"));
if (!spawnKitty("kitty_B"))
return false;
try {
NLog::log("{}Expecting kitty_B size: {},{}", Colors::YELLOW, WIDTH2, 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"));
NLog::log("{}Spawning kittyProcB", Colors::YELLOW);
auto kittyProcB = Tests::spawnKitty();
if (!kittyProcB) {
NLog::log("{}Error: kitty did not spawn", Colors::RED);
return false;
}
NLog::log("{}Expecting 2 windows", Colors::YELLOW);
EXPECT(Tests::windowCount(), 2);
// open xeyes
NLog::log("{}Spawning xeyes", Colors::YELLOW);
getFromSocket("/dispatch exec xeyes");
NLog::log("{}Keep checking if xeyes spawned", Colors::YELLOW);
Tests::waitUntilWindowsN(3);
int counter = 0;
while (Tests::windowCount() != 3) {
counter++;
std::this_thread::sleep_for(std::chrono::milliseconds(100));
if (counter > 50) {
EXPECT(Tests::windowCount(), 3);
return !ret;
}
}
NLog::log("{}Expecting 3 windows", Colors::YELLOW);
EXPECT(Tests::windowCount(), 3);
@ -503,257 +92,6 @@ static bool test() {
NLog::log("{}Expecting 0 windows", Colors::YELLOW);
EXPECT(Tests::windowCount(), 0);
testSwapWindow();
getFromSocket("/dispatch workspace 1");
if (!testWindowFocusOnFullscreenConflict()) {
ret = 1;
return false;
}
NLog::log("{}Testing spawning a floating window over a fullscreen window", Colors::YELLOW);
{
if (!spawnKitty("kitty_A"))
return false;
OK(getFromSocket("/dispatch fullscreen 0 set"));
EXPECT(Tests::windowCount(), 1);
OK(getFromSocket("/dispatch exec [float] kitty"));
Tests::waitUntilWindowsN(2);
OK(getFromSocket("/dispatch focuswindow class:^kitty$"));
const auto focused1 = getFromSocket("/activewindow");
EXPECT_CONTAINS(focused1, "class: kitty\n");
OK(getFromSocket("/dispatch killwindow activewindow"));
Tests::waitUntilWindowsN(1);
// The old window should be focused again
const auto focused2 = getFromSocket("/activewindow");
EXPECT_CONTAINS(focused2, "class: kitty_A\n");
NLog::log("{}Killing all windows", Colors::YELLOW);
Tests::killAllWindows();
}
NLog::log("{}Testing minsize/maxsize rules for tiled windows", Colors::YELLOW);
{
// Enable the config for testing, test max/minsize for tiled windows and centering
OK(getFromSocket("/keyword misc:size_limits_tiled 1"));
OK(getFromSocket("/keyword windowrule[kitty-max-rule]:match:class kitty_maxsize"));
OK(getFromSocket("/keyword windowrule[kitty-max-rule]:max_size 1500 500"));
OK(getFromSocket("r/keyword windowrule[kitty-max-rule]:min_size 1200 500"));
if (!spawnKitty("kitty_maxsize"))
return false;
auto dwindle = getFromSocket("/activewindow");
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");
Tests::killAllWindows();
EXPECT(Tests::windowCount(), 0);
OK(getFromSocket("/keyword general:layout master"));
if (!spawnKitty("kitty_maxsize"))
return false;
auto master = getFromSocket("/activewindow");
EXPECT_CONTAINS(master, "size: 1500,500");
EXPECT_CONTAINS(master, "at: 210,290");
if (!spawnKitty("kitty_maxsize"))
return false;
OK(getFromSocket("/dispatch focuswindow class:kitty_maxsize"));
EXPECT_CONTAINS(getFromSocket("/activewindow"), "size: 1200,500")
NLog::log("{}Reloading config", Colors::YELLOW);
OK(getFromSocket("/reload"));
Tests::killAllWindows();
EXPECT(Tests::windowCount(), 0);
}
NLog::log("{}Testing window rules", Colors::YELLOW);
if (!spawnKitty("wr_kitty"))
return false;
{
auto str = getFromSocket("/activewindow");
const int SIZE = 200;
EXPECT_CONTAINS(str, "floating: 1");
EXPECT_CONTAINS(str, std::format("size: {},{}", SIZE, SIZE));
EXPECT_NOT_CONTAINS(str, "pinned: 1");
}
OK(getFromSocket("/keyword windowrule[wr-kitty-stuff]:opacity 0.5 0.5 override"));
{
auto str = getFromSocket("/getprop active opacity");
EXPECT_CONTAINS(str, "0.5");
}
OK(getFromSocket("/keyword windowrule[special-magic-kitty]:match:class magic_kitty"));
OK(getFromSocket("/keyword windowrule[special-magic-kitty]:workspace special:magic"));
if (!spawnKitty("magic_kitty"))
return false;
{
auto str = getFromSocket("/activewindow");
EXPECT_CONTAINS(str, "special:magic");
EXPECT_NOT_CONTAINS(str, "workspace: 9");
}
if (auto str = getFromSocket("/monitors"); str.contains("magic)")) {
OK(getFromSocket("/dispatch togglespecialworkspace magic"));
}
Tests::killAllWindows();
if (!spawnKitty("tag_kitty"))
return false;
{
auto str = getFromSocket("/activewindow");
EXPECT_CONTAINS(str, "floating: 1");
}
OK(getFromSocket("/reload"));
Tests::killAllWindows();
// test rules that overlap effects but don't overlap props
OK(getFromSocket("/keyword windowrule match:class overlap_kitty, border_size 0"));
OK(getFromSocket("/keyword windowrule match:fullscreen false, border_size 10"));
if (!spawnKitty("overlap_kitty"))
return false;
{
auto str = getFromSocket("/getprop active border_size");
EXPECT_CONTAINS(str, "10");
}
OK(getFromSocket("/reload"));
Tests::killAllWindows();
// test persistent_size between floating window launches
OK(getFromSocket("/keyword windowrule match:class persistent_size_kitty, persistent_size true, float true"));
if (!spawnKitty("persistent_size_kitty"))
return false;
OK(getFromSocket("/dispatch resizeactive exact 600 400"))
{
auto str = getFromSocket("/activewindow");
EXPECT_CONTAINS(str, "size: 600,400");
EXPECT_CONTAINS(str, "floating: 1");
}
Tests::killAllWindows();
if (!spawnKitty("persistent_size_kitty"))
return false;
{
auto str = getFromSocket("/activewindow");
EXPECT_CONTAINS(str, "size: 600,400");
EXPECT_CONTAINS(str, "floating: 1");
}
OK(getFromSocket("/reload"));
Tests::killAllWindows();
OK(getFromSocket("/keyword general:border_size 0"));
OK(getFromSocket("/keyword windowrule match:float true, border_size 10"));
if (!spawnKitty("border_kitty"))
return false;
{
auto str = getFromSocket("/getprop active border_size");
EXPECT_CONTAINS(str, "0");
}
OK(getFromSocket("/dispatch togglefloating"));
{
auto str = getFromSocket("/getprop active border_size");
EXPECT_CONTAINS(str, "10");
}
OK(getFromSocket("/dispatch togglefloating"));
{
auto str = getFromSocket("/getprop active border_size");
EXPECT_CONTAINS(str, "0");
}
OK(getFromSocket("/reload"));
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"));
if (!spawnKitty("expr_kitty"))
return false;
{
auto str = getFromSocket("/activewindow");
EXPECT_CONTAINS(str, "floating: 1");
EXPECT_CONTAINS(str, "at: 212,540");
EXPECT_CONTAINS(str, "size: 960,540");
}
OK(getFromSocket("/reload"));
Tests::killAllWindows();
OK(getFromSocket("/dispatch plugin:test:add_rule"));
OK(getFromSocket("/reload"));
OK(getFromSocket("/keyword windowrule match:class plugin_kitty, plugin_rule effect"));
if (!spawnKitty("plugin_kitty"))
return false;
OK(getFromSocket("/dispatch plugin:test:check_rule"));
OK(getFromSocket("/reload"));
Tests::killAllWindows();
OK(getFromSocket("/dispatch plugin:test:add_rule"));
OK(getFromSocket("/reload"));
OK(getFromSocket("/keyword windowrule[test-plugin-rule]:match:class plugin_kitty"));
OK(getFromSocket("/keyword windowrule[test-plugin-rule]:plugin_rule effect"));
if (!spawnKitty("plugin_kitty"))
return false;
OK(getFromSocket("/dispatch plugin:test:check_rule"));
OK(getFromSocket("/reload"));
Tests::killAllWindows();
testGroupRules();
testMaximizeSize();
NLog::log("{}Reloading config", Colors::YELLOW);
OK(getFromSocket("/reload"));
NLog::log("{}Killing all windows", Colors::YELLOW);
Tests::killAllWindows();
NLog::log("{}Expecting 0 windows", Colors::YELLOW);
EXPECT(Tests::windowCount(), 0);
return !ret;
}

View file

@ -6,7 +6,6 @@
#include <chrono>
#include <hyprutils/os/Process.hpp>
#include <hyprutils/memory/WeakPtr.hpp>
#include <hyprutils/utils/ScopeGuard.hpp>
#include <csignal>
#include <cerrno>
#include "../shared.hpp"
@ -15,117 +14,16 @@ static int ret = 0;
using namespace Hyprutils::OS;
using namespace Hyprutils::Memory;
using namespace Hyprutils::Utils;
#define UP CUniquePointer
#define SP CSharedPointer
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 bool test() {
NLog::log("{}Testing workspaces", Colors::GREEN);
EXPECT(Tests::windowCount(), 0);
// test on workspace "window"
NLog::log("{}Switching to workspace 1", Colors::YELLOW);
getFromSocket("/dispatch workspace 1");
NLog::log("{}Checking persistent no-mon", Colors::YELLOW);
OK(getFromSocket("r/keyword workspace 966,persistent:1"));
{
auto str = getFromSocket("/workspaces");
EXPECT_CONTAINS(str, "workspace ID 966 (966)");
}
OK(getFromSocket("/reload"));
OK(getFromSocket("/dispatch workspace 1"));
NLog::log("{}Spawning kittyProc on ws 1", Colors::YELLOW);
auto kittyProcA = Tests::spawnKitty();
@ -167,11 +65,6 @@ static bool test() {
NLog::log("{}Switching to workspace 1", Colors::YELLOW);
OK(getFromSocket("/dispatch workspace 1"));
{
auto str = getFromSocket("/workspaces");
EXPECT_NOT_CONTAINS(str, "workspace ID 2 (2)");
}
NLog::log("{}Switching to workspace m+1", Colors::YELLOW);
OK(getFromSocket("/dispatch workspace m+1"));
@ -317,7 +210,7 @@ static bool test() {
EXPECT_CONTAINS(str, "special:HELLO");
}
// no OK: will err (it shouldn't prolly but oh well)
// no OK: will err (it shouldnt prolly but oh well)
getFromSocket("/dispatch workspace 3");
{
@ -449,8 +342,6 @@ static bool test() {
NLog::log("{}Killing all windows", Colors::YELLOW);
Tests::killAllWindows();
testAsymmetricGaps();
NLog::log("{}Expecting 0 windows", Colors::YELLOW);
EXPECT(Tests::windowCount(), 0);

View file

@ -19,13 +19,3 @@ bool testPlugin() {
}
return true;
}
bool testVkb() {
const auto RESPONSE = getFromSocket("/dispatch plugin:test:vkb");
if (RESPONSE != "ok") {
NLog::log("{}Vkb tests failed, tests returned:\n{}{}", Colors::RED, Colors::RESET, RESPONSE);
return false;
}
return true;
}

View file

@ -1,4 +1,3 @@
#pragma once
bool testPlugin();
bool testVkb();
bool testPlugin();

View file

@ -9,15 +9,10 @@
using namespace Hyprutils::OS;
using namespace Hyprutils::Memory;
CUniquePointer<CProcess> Tests::spawnKitty(const std::string& class_, const std::vector<std::string> args) {
CUniquePointer<CProcess> Tests::spawnKitty(const std::string& class_) {
const auto COUNT_BEFORE = windowCount();
std::vector<std::string> programArgs = args;
if (!class_.empty()) {
programArgs.insert(programArgs.begin(), "--class");
programArgs.insert(programArgs.begin() + 1, class_);
}
CUniquePointer<CProcess> kitty = makeUnique<CProcess>("kitty", programArgs);
CUniquePointer<CProcess> kitty = makeUnique<CProcess>("kitty", class_.empty() ? std::vector<std::string>{} : std::vector<std::string>{"--class", class_});
kitty->addEnv("WAYLAND_DISPLAY", WLDISPLAY);
kitty->runAsync();
@ -54,7 +49,7 @@ int Tests::countOccurrences(const std::string& in, const std::string& what) {
auto pos = in.find(what);
while (pos != std::string::npos) {
cnt++;
pos = in.find(what, pos + what.length());
pos = in.find(what, pos + what.length() - 1);
}
return cnt;
@ -95,13 +90,3 @@ void Tests::waitUntilWindowsN(int n) {
}
}
}
std::string Tests::execAndGet(const std::string& cmd) {
CProcess proc("/bin/sh", {"-c", cmd});
if (!proc.runSync()) {
return "error";
}
return proc.stdOut();
}

View file

@ -8,11 +8,10 @@
//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> spawnKitty(const std::string& class_ = "");
bool processAlive(pid_t pid);
int windowCount();
int countOccurrences(const std::string& in, const std::string& what);
bool killAllWindows();
void waitUntilWindowsN(int n);
std::string execAndGet(const std::string& cmd);
};

View file

@ -22,7 +22,6 @@ monitor=HEADLESS-3,1920x1080@60,auto-right,1
monitor=HEADLESS-4,1920x1080@60,auto-right,1
monitor=HEADLESS-5,1920x1080@60,auto-right,1
monitor=HEADLESS-6,1920x1080@60,auto-right,1
monitor=HEADLESS-PERSISTENT-TEST,1920x1080@60,auto-right,1
monitor=,disabled
@ -46,7 +45,6 @@ $menu = wofi --show drun
# Autostart necessary processes (like notifications daemons, status bars, etc.)
# Or execute your favorite apps at launch like this:
exec-once = sleep 0 # Terminates very quickly
# exec-once = $terminal
# exec-once = nm-applet &
# exec-once = waybar & hyprpaper & firefox
@ -152,11 +150,6 @@ animations {
animation = workspacesOut, 1, 1.94, almostLinear, fade
}
device {
name = test-mouse-1
enabled = true
}
# Ref https://wiki.hyprland.org/Configuring/Workspace-Rules/
# "Smart gaps" / "No gaps when only"
# uncomment all if you wish to use that.
@ -171,7 +164,6 @@ device {
dwindle {
pseudotile = true # Master switch for pseudotiling. Enabling is bound to mainMod + P in the keybinds section below
preserve_split = true # You probably want this
split_bias = 1
}
# See https://wiki.hyprland.org/Configuring/Master-Layout/ for more
@ -209,7 +201,7 @@ input {
# https://wiki.hyprland.org/Configuring/Variables/#gestures
gestures {
workspace_swipe = false
}
# Example per-device config
@ -219,10 +211,6 @@ device {
sensitivity = -0.5
}
debug {
disable_logs = false
}
###################
### KEYBINDINGS ###
@ -297,104 +285,29 @@ bindl = , XF86AudioPause, exec, playerctl play-pause
bindl = , XF86AudioPlay, exec, playerctl play-pause
bindl = , XF86AudioPrev, exec, playerctl previous
bind = $mainMod, u, submap, submap1
submap = submap1
bind = , u, submap, submap2
bind = , i, submap, submap3
bind = , o, exec, $terminal
bind = , p, submap, reset
submap = submap2, submap1
bind = , o, exec, $terminal
submap = submap3, reset
bind = , o, exec, $terminal
submap = reset
##############################
### WINDOWS AND WORKSPACES ###
##############################
windowrule {
# Ignore maximize requests from apps. You'll probably like this.
name = suppress-maximize-events
match:class = .*
# See https://wiki.hyprland.org/Configuring/Window-Rules/ for more
# See https://wiki.hyprland.org/Configuring/Workspace-Rules/ for workspace rules
suppress_event = maximize
}
# Example windowrule v1
# windowrule = float, ^(kitty)$
windowrule {
# Fix some dragging issues with XWayland
name = fix-xwayland-drags
match:class = ^$
match:title = ^$
match:xwayland = true
match:float = true
match:fullscreen = false
match:pin = false
# Example windowrule v2
# windowrulev2 = float,class:^(kitty)$,title:^(kitty)$
no_focus = true
}
# Ignore maximize requests from apps. You'll probably like this.
windowrulev2 = suppressevent maximize, class:.*
# Fix some dragging issues with XWayland
windowrulev2 = nofocus,class:^$,title:^$,xwayland:1,floating:1,fullscreen:0,pinned:0
# Workspace "windows" is a smart gaps one
workspace = n[s:window] w[tv1], gapsout:0, gapsin:0
workspace = n[s:window] f[1], gapsout:0, gapsin:0
windowrule {
name = smart-gaps-1
match:float = false
match:workspace = n[s:window] w[tv1]
border_size = 0
rounding = 0
}
windowrule {
name = smart-gaps-2
match:float = false
match:workspace = n[s:window] f[1]
border_size = 0
rounding = 0
}
windowrule {
name = wr-kitty-stuff
match:class = wr_kitty
float = true
size = 200 200
pin = false
}
windowrule {
name = tagged-kitty-floats
match:tag = tag_kitty
float = true
}
windowrule {
name = static-kitty-tag
match:class = tag_kitty
tag = +tag_kitty
}
gesture = 3, left, dispatcher, exec, kitty
gesture = 3, right, float
gesture = 3, up, close
gesture = 3, down, fullscreen
gesture = 3, down, mod:ALT, float
gesture = 3, horizontal, mod:ALT, workspace
gesture = 5, up, dispatcher, sendshortcut, , e, activewindow
gesture = 5, down, dispatcher, sendshortcut, , x, activewindow
gesture = 5, left, dispatcher, sendshortcut, , i, activewindow
gesture = 5, right, dispatcher, sendshortcut, , t, activewindow
gesture = 4, right, dispatcher, sendshortcut, , return, activewindow
gesture = 4, left, dispatcher, movecursortocorner, 1
windowrulev2 = bordersize 0, floating:0, onworkspace:n[s:window] w[tv1]
windowrulev2 = rounding 0, floating:0, onworkspace:n[s:window] w[tv1]
windowrulev2 = bordersize 0, floating:0, onworkspace:n[s:window] f[1]
windowrulev2 = rounding 0, floating:0, onworkspace:n[s:window] f[1]

125
meson.build Normal file
View file

@ -0,0 +1,125 @@
project(
'Hyprland',
'cpp',
'c',
version: run_command('cat', join_paths(meson.project_source_root(), 'VERSION'), check: true).stdout().strip(),
default_options: [
'warning_level=2',
'default_library=static',
'optimization=3',
'buildtype=release',
'debug=false',
'cpp_std=c++26',
],
)
datarootdir = '-DDATAROOTDIR="' + get_option('prefix') / get_option('datadir') + '"'
add_project_arguments(
[
'-Wno-unused-parameter',
'-Wno-unused-value',
'-Wno-missing-field-initializers',
'-Wno-narrowing',
'-Wno-pointer-arith', datarootdir,
'-DHYPRLAND_VERSION="' + meson.project_version() + '"',
],
language: 'cpp',
)
cpp_compiler = meson.get_compiler('cpp')
if cpp_compiler.check_header('execinfo.h')
add_project_arguments('-DHAS_EXECINFO', language: 'cpp')
endif
aquamarine = dependency('aquamarine', version: '>=0.9.0')
hyprcursor = dependency('hyprcursor', version: '>=0.1.7')
hyprgraphics = dependency('hyprgraphics', version: '>= 0.1.3')
hyprlang = dependency('hyprlang', version: '>= 0.3.2')
hyprutils = dependency('hyprutils', version: '>= 0.8.1')
aquamarine_version_list = aquamarine.version().split('.')
add_project_arguments(['-DAQUAMARINE_VERSION="@0@"'.format(aquamarine.version())], language: 'cpp')
add_project_arguments(['-DAQUAMARINE_VERSION_MAJOR=@0@'.format(aquamarine_version_list.get(0))], language: 'cpp')
add_project_arguments(['-DAQUAMARINE_VERSION_MINOR=@0@'.format(aquamarine_version_list.get(1))], language: 'cpp')
add_project_arguments(['-DAQUAMARINE_VERSION_PATCH=@0@'.format(aquamarine_version_list.get(2))], language: 'cpp')
add_project_arguments(['-DHYPRCURSOR_VERSION="@0@"'.format(hyprcursor.version())], language: 'cpp')
add_project_arguments(['-DHYPRGRAPHICS_VERSION="@0@"'.format(hyprgraphics.version())], language: 'cpp')
add_project_arguments(['-DHYPRLANG_VERSION="@0@"'.format(hyprlang.version())], language: 'cpp')
add_project_arguments(['-DHYPRUTILS_VERSION="@0@"'.format(hyprutils.version())], language: 'cpp')
xcb_dep = dependency('xcb', required: get_option('xwayland'))
xcb_composite_dep = dependency('xcb-composite', required: get_option('xwayland'))
xcb_errors_dep = dependency('xcb-errors', required: get_option('xwayland'))
xcb_icccm_dep = dependency('xcb-icccm', required: get_option('xwayland'))
xcb_render_dep = dependency('xcb-render', required: get_option('xwayland'))
xcb_res_dep = dependency('xcb-res', required: get_option('xwayland'))
xcb_xfixes_dep = dependency('xcb-xfixes', required: get_option('xwayland'))
gio_dep = dependency('gio-2.0', required: true)
if not xcb_dep.found()
add_project_arguments('-DNO_XWAYLAND', language: 'cpp')
endif
backtrace_dep = cpp_compiler.find_library('execinfo', required: false)
epoll_dep = dependency('epoll-shim', required: false) # timerfd on BSDs
inotify_dep = dependency('libinotify', required: false) # inotify on BSDs
re2 = dependency('re2', required: true)
# Handle options
systemd_option = get_option('systemd')
systemd = dependency('systemd', required: systemd_option)
systemd_option.enable_auto_if(systemd.found())
if (systemd_option.enabled())
message('Enabling systemd integration')
add_project_arguments('-DUSES_SYSTEMD', language: 'cpp')
subdir('systemd')
endif
if get_option('buildtype') == 'debug'
add_project_arguments('-DHYPRLAND_DEBUG', language: 'cpp')
endif
# Generate hyprland version and populate version.h
run_command('sh', '-c', 'scripts/generateVersion.sh', check: true)
# Make shader files includable
run_command('sh', '-c', 'scripts/generateShaderIncludes.sh', check: true)
# Install headers
globber = run_command('find', 'src', '-name', '*.h*', '-o', '-name', '*.inc', check: true)
headers = globber.stdout().strip().split('\n')
foreach file : headers
install_headers(file, subdir: 'hyprland', preserve_path: true)
endforeach
tracy = dependency('tracy', static: true, required: get_option('tracy_enable'))
if get_option('tracy_enable') and get_option('buildtype') != 'debugoptimized'
warning('Profiling builds should set -- buildtype = debugoptimized')
endif
subdir('protocols')
subdir('src')
subdir('hyprctl')
subdir('assets')
subdir('example')
subdir('docs')
if get_option('hyprpm').enabled()
subdir('hyprpm/src')
endif
# Generate hyprland.pc
pkg_install_dir = join_paths(get_option('datadir'), 'pkgconfig')
import('pkgconfig').generate(
name: 'Hyprland',
filebase: 'hyprland',
url: 'https://github.com/hyprwm/Hyprland',
description: 'Hyprland header files',
install_dir: pkg_install_dir,
subdirs: ['', 'hyprland/protocols', 'hyprland'],
)

5
meson_options.txt Normal file
View file

@ -0,0 +1,5 @@
option('xwayland', type: 'feature', value: 'auto', description: 'Enable support for X11 applications')
option('systemd', type: 'feature', value: 'auto', description: 'Enable systemd integration')
option('uwsm', type: 'feature', value: 'enabled', description: 'Enable uwsm integration (only if systemd is enabled)')
option('hyprpm', type: 'feature', value: 'enabled', description: 'Enable hyprpm')
option('tracy_enable', type: 'boolean', value: false , description: 'Enable profiling')

View file

@ -6,21 +6,21 @@
pkgconf,
makeWrapper,
cmake,
meson,
ninja,
aquamarine,
binutils,
cairo,
epoll-shim,
git,
glaze-hyprland,
gtest,
glaze,
hyprcursor,
hyprgraphics,
hyprland-protocols,
hyprland-guiutils,
hyprland-qtutils,
hyprlang,
hyprutils,
hyprwayland-scanner,
hyprwire,
libGL,
libdrm,
libexecinfo,
@ -28,7 +28,6 @@
libxkbcommon,
libuuid,
libgbm,
muparser,
pango,
pciutils,
re2,
@ -41,7 +40,6 @@
xorg,
xwayland,
debug ? false,
withTests ? false,
enableXWayland ? true,
withSystemd ? lib.meta.availableOn stdenv.hostPlatform systemd,
wrapRuntimeDeps ? true,
@ -54,13 +52,12 @@
nvidiaPatches ? false,
hidpiXWayland ? false,
legacyRenderer ? false,
withHyprtester ? false,
}: 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.strings) makeBinPath optionalString mesonBool mesonEnable trim;
fs = lib.fileset;
adapters = flatten [
@ -74,10 +71,9 @@ in
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;
inherit version;
src = fs.toSource {
root = ../.;
@ -85,23 +81,22 @@ in
fs.intersection
# allows non-flake builds to only include files tracked by git
(fs.gitTracked ../.)
(fs.unions (flatten [
(fs.unions [
../assets/hyprland-portals.conf
../assets/install
../hyprctl
../hyprland.pc.in
../LICENSE
../meson_options.txt
../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 "sh") ../scripts)
(fs.fileFilter (file: file.name == "CMakeLists.txt") ../.)
(optional withTests [../tests ../hyprtester])
]));
(fs.fileFilter (file: file.name == "meson.build") ../.)
]);
};
postPatch = ''
@ -112,13 +107,11 @@ in
sed -i "s#@PREFIX@/##g" hyprland.pc.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")}";
};
COMMITS = revCount;
DATE = date;
DIRTY = optionalString (commit == "") "dirty";
HASH = commit;
TAG = "v${trim (readFile "${finalAttrs.src}/VERSION")}";
depsBuildBuild = [
pkg-config
@ -126,9 +119,10 @@ in
nativeBuildInputs = [
hyprwayland-scanner
hyprwire
makeWrapper
cmake
meson
ninja
cmake # needed for glaze
pkg-config
];
@ -143,21 +137,18 @@ in
aquamarine
cairo
git
glaze-hyprland
gtest
glaze
hyprcursor
hyprgraphics
hyprland-protocols
hyprlang
hyprutils
hyprwire
libdrm
libGL
libinput
libuuid
libxkbcommon
libgbm
muparser
pango
pciutils
re2
@ -183,49 +174,34 @@ in
strictDeps = true;
cmakeBuildType =
mesonBuildType =
if debug
then "Debug"
else "RelWithDebInfo";
then "debug"
else "release";
# we want as much debug info as possible
dontStrip = debug;
cmakeFlags = mapAttrsToList cmakeBool {
"BUILT_WITH_NIX" = true;
"NO_XWAYLAND" = !enableXWayland;
"LEGACY_RENDERER" = legacyRenderer;
"NO_SYSTEMD" = !withSystemd;
"CMAKE_DISABLE_PRECOMPILE_HEADERS" = true;
"NO_UWSM" = true;
"NO_HYPRPM" = true;
"TRACY_ENABLE" = false;
"WITH_TESTS" = withTests;
};
preConfigure = ''
substituteInPlace hyprtester/CMakeLists.txt --replace-fail \
"\''${CMAKE_CURRENT_BINARY_DIR}" \
"${placeholder "out"}/bin"
'';
mesonFlags = flatten [
(mapAttrsToList mesonEnable {
"xwayland" = enableXWayland;
"systemd" = withSystemd;
"uwsm" = false;
"hyprpm" = false;
})
(mapAttrsToList mesonBool {
"b_pch" = false;
"tracy_enable" = false;
})
];
postInstall = ''
${optionalString wrapRuntimeDeps ''
wrapProgram $out/bin/Hyprland \
--suffix PATH : ${makeBinPath [
binutils
hyprland-guiutils
hyprland-qtutils
pciutils
pkgconf
]}
''}
${optionalString withTests ''
install hyprtester/pointer-warp -t $out/bin
install hyprtester/pointer-scroll -t $out/bin
install hyprland_gtests -t $out/bin
install hyprtester/child-window -t $out/bin
''}
'';
passthru.providedSessions = ["hyprland"];

60
nix/hyprtester.nix Normal file
View file

@ -0,0 +1,60 @@
{
lib,
stdenv,
stdenvAdapters,
cmake,
pkg-config,
hyprland,
hyprwayland-scanner,
version ? "git",
}: let
inherit (lib.lists) flatten foldl';
inherit (lib.sources) cleanSourceWith cleanSource;
inherit (lib.strings) hasSuffix cmakeBool;
adapters = flatten [
stdenvAdapters.useMoldLinker
stdenvAdapters.keepDebugInfo
];
customStdenv = foldl' (acc: adapter: adapter acc) stdenv adapters;
in
customStdenv.mkDerivation (finalAttrs: {
pname = "hyprtester";
inherit version;
src = cleanSourceWith {
filter = name: _type: let
baseName = baseNameOf (toString name);
in
! (hasSuffix ".nix" baseName);
src = cleanSource ../.;
};
nativeBuildInputs = [
cmake
pkg-config
hyprwayland-scanner
];
buildInputs = hyprland.buildInputs;
preConfigure = ''
cmake -S . -B .
cmake --build . --target generate-protocol-headers -j`nproc 2>/dev/null || getconf NPROCESSORS_CONF`
cd hyprtester
'';
cmakeBuildType = "Debug";
cmakeFlags = [(cmakeBool "TESTS" true)];
meta = {
homepage = "https://github.com/hyprwm/Hyprland";
description = "Hyprland testing framework";
license = lib.licenses.bsd3;
platforms = hyprland.meta.platforms;
mainProgram = "hyprtester";
};
})

View file

@ -82,7 +82,7 @@ lib: let
:::
*/
toHyprlang = {
topCommandsPrefixes ? ["$" "bezier"],
topCommandsPrefixes ? ["$"],
bottomCommandsPrefixes ? [],
}: attrs: let
toHyprlang' = attrs: let

View file

@ -24,13 +24,11 @@ in {
inputs.hyprcursor.overlays.default
inputs.hyprgraphics.overlays.default
inputs.hyprland-protocols.overlays.default
inputs.hyprland-guiutils.overlays.default
inputs.hyprland-qtutils.overlays.default
inputs.hyprlang.overlays.default
inputs.hyprutils.overlays.default
inputs.hyprwayland-scanner.overlays.default
inputs.hyprwire.overlays.default
self.overlays.udis86
self.overlays.glaze
# Hyprland packages themselves
(final: _prev: let
@ -45,14 +43,9 @@ in {
};
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;
hyprtester = final.callPackage ./hyprtester.nix {
inherit version;
};
# deprecated packages
hyprland-legacy-renderer =
@ -111,13 +104,4 @@ in {
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,16 +1,16 @@
inputs: pkgs: let
flake = inputs.self.packages.${pkgs.stdenv.hostPlatform.system};
hyprland = flake.hyprland-with-tests;
hyprland = flake.hyprland;
in {
tests = pkgs.testers.runNixOSTest {
name = "hyprland-tests";
nodes.machine = {pkgs, ...}: {
environment.systemPackages = with pkgs; [
flake.hyprtester
# Programs needed for tests
jq
kitty
wl-clipboard
xorg.xeyes
];
@ -22,13 +22,8 @@ in {
"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;
@ -36,7 +31,7 @@ in {
};
# Test configuration
environment.etc."test.conf".source = "${hyprland}/share/hypr/test.conf";
environment.etc."test.conf".source = "${flake.hyprtester}/share/hypr/test.conf";
# Disable portals
xdg.portal.enable = pkgs.lib.mkForce false;
@ -68,21 +63,14 @@ in {
# 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')
# Run hyprtester testing framework/suite
print("Running hyprtester")
exit_status, _out = machine.execute("su - alice -c 'hyprtester -b ${hyprland}/bin/Hyprland -c /etc/test.conf -p ${hyprland}/lib/hyprtestplugin.so 2>&1 | tee /tmp/testerlog; exit ''${PIPESTATUS[0]}'")
exit_status, _out = machine.execute("su - alice -c 'hyprtester -b ${hyprland}/bin/Hyprland -c /etc/test.conf -p ${flake.hyprtester}/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/gtestslog")
machine.copy_from_vm("/tmp/testerlog")
machine.copy_from_vm("/tmp/hyprlog")
machine.copy_from_vm("/tmp/exit_status")

View file

@ -1,25 +1,16 @@
#!/usr/bin/env -S nix shell nixpkgs#jq -c bash
# Update inputs when the Mesa or QT version is outdated. We don't want
# Update inputs when the Mesa version is outdated. We don't want
# incompatibilities between the user's system and Hyprland.
# get the current Nixpkgs revision
REV=$(jq <flake.lock '.nodes.nixpkgs.locked.rev' -r)
# check versions for current and remote nixpkgs' mesa
CRT_VER=$(nix eval --raw github:nixos/nixpkgs/"$REV"#mesa.version)
NEW_VER=$(nix eval --raw github:nixos/nixpkgs/nixos-unstable#mesa.version)
get_ver() {
nix eval --raw "github:nixos/nixpkgs/$1#$2"
}
# check versions for current and remote nixpkgs'
MESA_OLD=$(get_ver "$REV" mesa.version)
MESA_NEW=$(get_ver nixos-unstable mesa.version)
QT_OLD=$(get_ver "$REV" kdePackages.qtbase.version)
QT_NEW=$(get_ver nixos-unstable kdePackages.qtbase.version)
if [ "$MESA_OLD" != "$MESA_NEW" ] || [ "$QT_OLD" != "$QT_NEW" ]; then
echo "Updating flake inputs..."
echo "Mesa: $MESA_OLD -> $MESA_NEW"
echo "Qt: $QT_OLD -> $QT_NEW"
if [ "$CRT_VER" != "$NEW_VER" ]; then
echo "Updating Mesa $CRT_VER -> $NEW_VER and flake inputs"
# update inputs to latest versions
nix flake update

115
protocols/meson.build Normal file
View file

@ -0,0 +1,115 @@
wayland_protos = dependency(
'wayland-protocols',
version: '>=1.43',
fallback: 'wayland-protocols',
default_options: ['tests=false'],
)
hyprland_protos = dependency(
'hyprland-protocols',
version: '>=0.6.4',
fallback: 'hyprland-protocols',
)
wayland_protocol_dir = wayland_protos.get_variable('pkgdatadir')
hyprland_protocol_dir = hyprland_protos.get_variable('pkgdatadir')
hyprwayland_scanner_dep = dependency('hyprwayland-scanner', version: '>=0.3.10', native: true)
hyprwayland_scanner = find_program(
hyprwayland_scanner_dep.get_variable('hyprwayland_scanner'),
native: true,
)
protocols = [
'wlr-gamma-control-unstable-v1.xml',
'wlr-foreign-toplevel-management-unstable-v1.xml',
'wlr-output-power-management-unstable-v1.xml',
'input-method-unstable-v2.xml',
'virtual-keyboard-unstable-v1.xml',
'wlr-virtual-pointer-unstable-v1.xml',
'wlr-output-management-unstable-v1.xml',
'kde-server-decoration.xml',
'wlr-layer-shell-unstable-v1.xml',
'wayland-drm.xml',
'wlr-data-control-unstable-v1.xml',
'wlr-screencopy-unstable-v1.xml',
'xx-color-management-v4.xml',
'frog-color-management-v1.xml',
hyprland_protocol_dir / 'protocols/hyprland-global-shortcuts-v1.xml',
hyprland_protocol_dir / 'protocols/hyprland-toplevel-export-v1.xml',
hyprland_protocol_dir / 'protocols/hyprland-toplevel-mapping-v1.xml',
hyprland_protocol_dir / 'protocols/hyprland-focus-grab-v1.xml',
hyprland_protocol_dir / 'protocols/hyprland-ctm-control-v1.xml',
hyprland_protocol_dir / 'protocols/hyprland-surface-v1.xml',
hyprland_protocol_dir / 'protocols/hyprland-lock-notify-v1.xml',
wayland_protocol_dir / 'staging/tearing-control/tearing-control-v1.xml',
wayland_protocol_dir / 'staging/fractional-scale/fractional-scale-v1.xml',
wayland_protocol_dir / 'unstable/xdg-output/xdg-output-unstable-v1.xml',
wayland_protocol_dir / 'staging/cursor-shape/cursor-shape-v1.xml',
wayland_protocol_dir / 'unstable/idle-inhibit/idle-inhibit-unstable-v1.xml',
wayland_protocol_dir / 'unstable/relative-pointer/relative-pointer-unstable-v1.xml',
wayland_protocol_dir / 'unstable/xdg-decoration/xdg-decoration-unstable-v1.xml',
wayland_protocol_dir / 'staging/alpha-modifier/alpha-modifier-v1.xml',
wayland_protocol_dir / 'staging/ext-foreign-toplevel-list/ext-foreign-toplevel-list-v1.xml',
wayland_protocol_dir / 'unstable/pointer-gestures/pointer-gestures-unstable-v1.xml',
wayland_protocol_dir / 'unstable/keyboard-shortcuts-inhibit/keyboard-shortcuts-inhibit-unstable-v1.xml',
wayland_protocol_dir / 'unstable/text-input/text-input-unstable-v3.xml',
wayland_protocol_dir / 'unstable/text-input/text-input-unstable-v1.xml',
wayland_protocol_dir / 'unstable/pointer-constraints/pointer-constraints-unstable-v1.xml',
wayland_protocol_dir / 'staging/xdg-activation/xdg-activation-v1.xml',
wayland_protocol_dir / 'staging/ext-idle-notify/ext-idle-notify-v1.xml',
wayland_protocol_dir / 'staging/ext-session-lock/ext-session-lock-v1.xml',
wayland_protocol_dir / 'stable/tablet/tablet-v2.xml',
wayland_protocol_dir / 'stable/presentation-time/presentation-time.xml',
wayland_protocol_dir / 'stable/xdg-shell/xdg-shell.xml',
wayland_protocol_dir / 'unstable/primary-selection/primary-selection-unstable-v1.xml',
wayland_protocol_dir / 'staging/xwayland-shell/xwayland-shell-v1.xml',
wayland_protocol_dir / 'stable/viewporter/viewporter.xml',
wayland_protocol_dir / 'stable/linux-dmabuf/linux-dmabuf-v1.xml',
wayland_protocol_dir / 'staging/drm-lease/drm-lease-v1.xml',
wayland_protocol_dir / 'staging/linux-drm-syncobj/linux-drm-syncobj-v1.xml',
wayland_protocol_dir / 'staging/xdg-dialog/xdg-dialog-v1.xml',
wayland_protocol_dir / 'staging/single-pixel-buffer/single-pixel-buffer-v1.xml',
wayland_protocol_dir / 'staging/security-context/security-context-v1.xml',
wayland_protocol_dir / 'staging/content-type/content-type-v1.xml',
wayland_protocol_dir / 'staging/color-management/color-management-v1.xml',
wayland_protocol_dir / 'staging/xdg-toplevel-tag/xdg-toplevel-tag-v1.xml',
wayland_protocol_dir / 'staging/xdg-system-bell/xdg-system-bell-v1.xml',
wayland_protocol_dir / 'staging/ext-workspace/ext-workspace-v1.xml',
]
wl_protocols = []
foreach protocol : protocols
wl_protocols += custom_target(
protocol.underscorify(),
input: protocol,
install: true,
install_dir: [false, join_paths(get_option('includedir'), 'hyprland/protocols')],
output: ['@BASENAME@.cpp', '@BASENAME@.hpp'],
command: [hyprwayland_scanner, '@INPUT@', '@OUTDIR@'],
)
endforeach
# wayland.xml generation
wayland_scanner = dependency('wayland-scanner', native: true)
wayland_scanner_datadir = wayland_scanner.get_variable('pkgdatadir')
wayland_xml = wayland_scanner_datadir / 'wayland.xml'
wayland_protocol = custom_target(
wayland_xml.underscorify(),
input: wayland_xml,
install: true,
install_dir: [false, join_paths(get_option('includedir'), 'hyprland/protocols')],
output: ['@BASENAME@.cpp', '@BASENAME@.hpp'],
command: [hyprwayland_scanner, '--wayland-enums', '@INPUT@', '@OUTDIR@'],
)
lib_server_protos = static_library(
'server_protos',
wl_protocols + wayland_protocol,
)
server_protos = declare_dependency(
link_with: lib_server_protos,
sources: wl_protocols + wayland_protocol,
)

27
scripts/generateVersion.sh Executable file
View file

@ -0,0 +1,27 @@
#!/bin/sh
# if the git directory doesn't exist, don't gather data to avoid overwriting, unless
# the version file is missing altogether (otherwise compiling will fail)
if [ ! -d ./.git ]; then
if [ -f ./src/version.h ]; then
exit 0
fi
fi
cp -fr ./src/version.h.in ./src/version.h
HASH=${HASH-$(git rev-parse HEAD)}
BRANCH=${BRANCH-$(git branch --show-current)}
MESSAGE=${MESSAGE-$(git show | head -n 5 | tail -n 1 | sed -e 's/#//g' -e 's/\"//g')}
DATE=${DATE-$(git show --no-patch --format=%cd --date=local)}
DIRTY=${DIRTY-$(git diff-index --quiet HEAD -- || echo dirty)}
TAG=${TAG-$(git describe --tags)}
COMMITS=${COMMITS-$(git rev-list --count HEAD)}
sed -i -e "s#@HASH@#${HASH}#" ./src/version.h
sed -i -e "s#@BRANCH@#${BRANCH}#" ./src/version.h
sed -i -e "s#@MESSAGE@#${MESSAGE}#" ./src/version.h
sed -i -e "s#@DATE@#${DATE}#" ./src/version.h
sed -i -e "s#@DIRTY@#${DIRTY}#" ./src/version.h
sed -i -e "s#@TAG@#${TAG}#" ./src/version.h
sed -i -e "s#@COMMITS@#${COMMITS}#" ./src/version.h

File diff suppressed because it is too large Load diff

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