Compare commits

..

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

609 changed files with 26022 additions and 48966 deletions

View file

@ -1,111 +1,4 @@
WarningsAsErrors: > 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,
HeaderFilterRegex: '.*\.hpp' HeaderFilterRegex: '.*\.hpp'
FormatStyle: file FormatStyle: file
Checks: > Checks: >

View file

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

4
.github/labeler.yml vendored
View file

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

View file

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

View file

@ -21,19 +21,22 @@ jobs:
- name: Build Hyprland - name: Build Hyprland
run: | run: |
CFLAGS=-Werror CXXFLAGS=-Werror make nopch CFLAGS=-Werror CXXFLAGS=-Werror make all
- name: Compress and package artifacts - name: Compress and package artifacts
run: | run: |
mkdir x86_64-pc-linux-gnu mkdir x86_64-pc-linux-gnu
mkdir hyprland mkdir hyprland
mkdir hyprland/example
mkdir hyprland/assets
cp ./LICENSE hyprland/ cp ./LICENSE hyprland/
cp build/Hyprland hyprland/ cp build/Hyprland hyprland/
cp build/hyprctl/hyprctl hyprland/ cp build/hyprctl/hyprctl hyprland/
cp build/hyprpm/hyprpm hyprland/ cp build/hyprpm/hyprpm hyprland/
cp build/Hyprland hyprland/
cp -r example/ hyprland/ cp -r example/ hyprland/
cp -r assets/ hyprland/ cp -r assets/ hyprland/
tar -cvJf Hyprland.tar.xz hyprland tar -cvf Hyprland.tar.xz hyprland
- name: Release - name: Release
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v4
@ -41,43 +44,86 @@ jobs:
name: Build archive name: Build archive
path: Hyprland.tar.xz path: Hyprland.tar.xz
clang-format: meson:
permissions: read-all
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork
name: "Code Style" name: "Build Hyprland with Meson (Arch)"
runs-on: ubuntu-latest runs-on: ubuntu-latest
container: container:
image: archlinux image: archlinux
steps: steps:
- name: Checkout repository - name: Checkout repository actions
uses: actions/checkout@v4 uses: actions/checkout@v4
with:
sparse-checkout: .github/actions
# - name: clang-format check - name: Setup base
# uses: jidicula/clang-format-action@v4.16.0 uses: ./.github/actions/setup_base
# with:
# exclude-regex: ^subprojects$
- name: Install clang-format - name: Configure
run: | run: meson setup build -Ddefault_library=static
pacman --noconfirm --noprogressbar -Syyu
pacman --noconfirm --noprogressbar -Sy clang - 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 (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: clang-format check - name: clang-format check
run: .github/workflows/clang-format-check.sh "." "llvm" "^subprojects$" "" run: ninja -C build clang-format-check
- name: Save PR head commit SHA
if: failure() && github.event_name == 'pull_request'
shell: bash
run: |
SHA="${{ github.event.pull_request.head.sha }}"
echo "SHA=$SHA" >> $GITHUB_ENV
- name: Save latest commit SHA if not PR
if: failure() && github.event_name != 'pull_request'
shell: bash
run: echo "SHA=${{ github.sha }}" >> $GITHUB_ENV
- name: Report failure in job summary
if: failure()
run: |
DEEPLINK="${{ github.server_url }}/${{ github.repository }}/commit/${{ env.SHA }}"
echo -e "Format check failed on commit [${GITHUB_SHA:0:8}]($DEEPLINK) with files:\n$(<$GITHUB_WORKSPACE/failing-files.txt)" >> $GITHUB_STEP_SUMMARY

View file

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

View file

@ -4,23 +4,43 @@ jobs:
clang-format: clang-format:
permissions: write-all permissions: write-all
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork
name: "Code Style" name: "Code Style (Arch)"
runs-on: ubuntu-latest runs-on: ubuntu-latest
container:
image: archlinux
steps: steps:
- name: Checkout repository - name: Checkout repository actions
uses: actions/checkout@v4 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 - name: clang-format check
uses: jidicula/clang-format-action@v4.16.0 run: ninja -C build clang-format-check
with:
exclude-regex: ^subprojects$
- 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' }} if: ${{ failure() && github.event_name == 'pull_request' }}
run: | 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' }} if: ${{ failure() && github.event_name == 'pull_request' }}
uses: mshick/add-pr-comment@v2 uses: mshick/add-pr-comment@v2
with: 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 uses: ./.github/workflows/nix.yml
secrets: inherit secrets: inherit
with: 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: xdph:
if: (github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork) if: (github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork)
@ -21,9 +21,10 @@ jobs:
uses: ./.github/workflows/nix.yml uses: ./.github/workflows/nix.yml
secrets: inherit secrets: inherit
with: 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: test:
if: (github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork) if: (github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork)
needs: hyprland
uses: ./.github/workflows/nix-test.yml uses: ./.github/workflows/nix-test.yml
secrets: inherit secrets: inherit

View file

@ -20,13 +20,25 @@ jobs:
- name: Restore and save Nix store - name: Restore and save Nix store
uses: nix-community/cache-nix-action@v6 uses: nix-community/cache-nix-action@v6
with: with:
# restore and save a cache using this key (per job) # restore and save a cache using this key
primary-key: nix-${{ runner.os }}-${{ github.job }} primary-key: nix-${{ runner.os }}
# if there's no cache hit, restore a cache by this prefix # 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 # collect garbage until the Nix store size (in bytes) is at most this number
# before trying to save a new cache # before trying to save a new cache
# 1G = 1073741824
gc-max-store-size-linux: 5G 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 - uses: cachix/cachix-action@v15
with: with:
@ -34,7 +46,7 @@ jobs:
authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}" authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}"
- name: Run test VM - 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 - name: Check exit status
run: grep 0 result/exit_status run: grep 0 result/exit_status

View file

@ -27,13 +27,25 @@ jobs:
- name: Restore and save Nix store - name: Restore and save Nix store
uses: nix-community/cache-nix-action@v6 uses: nix-community/cache-nix-action@v6
with: with:
# restore and save a cache using this key (per job) # restore and save a cache using this key
primary-key: nix-${{ runner.os }}-${{ github.job }} primary-key: nix-${{ runner.os }}-${{ hashFiles('**/*.nix', '**/flake.lock') }}
# if there's no cache hit, restore a cache by this prefix # 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 # collect garbage until the Nix store size (in bytes) is at most this number
# before trying to save a new cache # 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 - name: Update inputs
run: nix/update-inputs.sh run: nix/update-inputs.sh

View file

@ -25,13 +25,25 @@ jobs:
- name: Restore and save Nix store - name: Restore and save Nix store
uses: nix-community/cache-nix-action@v6 uses: nix-community/cache-nix-action@v6
with: with:
# restore and save a cache using this key (per job) # restore and save a cache using this key
primary-key: nix-${{ runner.os }}-${{ github.job }} primary-key: nix-${{ runner.os }}
# if there's no cache hit, restore a cache by this prefix # 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 # collect garbage until the Nix store size (in bytes) is at most this number
# before trying to save a new cache # before trying to save a new cache
# 1G = 1073741824
gc-max-store-size-linux: 5G 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 - uses: cachix/cachix-action@v15
with: with:

View file

@ -9,36 +9,17 @@ jobs:
source-tarball: source-tarball:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout repository - name: Checkout Hyprland
uses: actions/checkout@v5 id: checkout
uses: actions/checkout@v4
with: with:
fetch-depth: 0
submodules: recursive submodules: recursive
- name: Populate git info in version.h.in - name: Generate version
id: genversion
run: | run: |
git fetch --tags --unshallow || true git fetch --unshallow || echo "failed unshallowing"
bash -c scripts/generateVersion.sh
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
- name: Create tarball with submodules - name: Create tarball with submodules
id: tar 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

3
.gitignore vendored
View file

@ -32,8 +32,6 @@ src/render/shaders/*.inc
src/render/shaders/Shaders.hpp src/render/shaders/Shaders.hpp
hyprctl/hyprctl hyprctl/hyprctl
hyprctl/hw-protocols/*.c*
hyprctl/hw-protocols/*.h*
gmon.out gmon.out
*.out *.out
@ -44,7 +42,6 @@ PKGBUILD
src/version.h src/version.h
hyprpm/Makefile hyprpm/Makefile
hyprctl/Makefile hyprctl/Makefile
example/hyprland.desktop
**/.#*.* **/.#*.*
**/#*.*# **/#*.*#

View file

@ -17,21 +17,18 @@ set(HYPRLAND_VERSION ${VER})
set(PREFIX ${CMAKE_INSTALL_PREFIX}) set(PREFIX ${CMAKE_INSTALL_PREFIX})
set(INCLUDEDIR ${CMAKE_INSTALL_INCLUDEDIR}) set(INCLUDEDIR ${CMAKE_INSTALL_INCLUDEDIR})
set(BINDIR ${CMAKE_INSTALL_BINDIR}) set(BINDIR ${CMAKE_INSTALL_BINDIR})
configure_file(hyprland.pc.in hyprland.pc @ONLY)
set(CMAKE_MESSAGE_LOG_LEVEL "STATUS") set(CMAKE_MESSAGE_LOG_LEVEL "STATUS")
message(STATUS "Gathering git info") 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 # Make shader files includable
execute_process(COMMAND ./scripts/generateShaderIncludes.sh execute_process(COMMAND ./scripts/generateShaderIncludes.sh
WORKING_DIRECTORY ${CMAKE_SOURCE_DIR} WORKING_DIRECTORY ${CMAKE_SOURCE_DIR})
RESULT_VARIABLE HYPR_SHADER_GEN_RESULT)
if(NOT HYPR_SHADER_GEN_RESULT EQUAL 0)
message(
FATAL_ERROR
"Failed to generate shader includes (scripts/generateShaderIncludes.sh), exit code: ${HYPR_SHADER_GEN_RESULT}"
)
endif()
find_package(PkgConfig REQUIRED) find_package(PkgConfig REQUIRED)
@ -39,24 +36,12 @@ find_package(PkgConfig REQUIRED)
# provide a .pc file and won't be detected this way # provide a .pc file and won't be detected this way
pkg_check_modules(udis_dep IMPORTED_TARGET udis86>=1.7.2) 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) 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") add_subdirectory("subprojects/udis86")
include_directories("subprojects/udis86") include_directories("subprojects/udis86")
message(STATUS "udis86 dependency not found, falling back to subproject") message(STATUS "udis86 dependency not found, falling back to subproject")
endif() endif()
endif()
find_library(librt rt)
if("${librt}" MATCHES "librt-NOTFOUND")
unset(LIBRT)
else()
set(LIBRT rt)
endif()
if(CMAKE_BUILD_TYPE) if(CMAKE_BUILD_TYPE)
string(TOLOWER ${CMAKE_BUILD_TYPE} BUILDTYPE_LOWER) string(TOLOWER ${CMAKE_BUILD_TYPE} BUILDTYPE_LOWER)
@ -86,11 +71,9 @@ message(
if(CMAKE_BUILD_TYPE MATCHES Debug OR CMAKE_BUILD_TYPE MATCHES DEBUG) if(CMAKE_BUILD_TYPE MATCHES Debug OR CMAKE_BUILD_TYPE MATCHES DEBUG)
message(STATUS "Configuring Hyprland in Debug with CMake") message(STATUS "Configuring Hyprland in Debug with CMake")
add_compile_definitions(HYPRLAND_DEBUG) add_compile_definitions(HYPRLAND_DEBUG)
set(BUILD_TESTING ON)
else() else()
add_compile_options(-O3) add_compile_options(-O3)
message(STATUS "Configuring Hyprland in Release with CMake") message(STATUS "Configuring Hyprland in Release with CMake")
set(BUILD_TESTING OFF)
endif() endif()
add_compile_definitions(HYPRLAND_VERSION="${HYPRLAND_VERSION}") add_compile_definitions(HYPRLAND_VERSION="${HYPRLAND_VERSION}")
@ -102,20 +85,15 @@ set(CXX_STANDARD_REQUIRED ON)
add_compile_options( add_compile_options(
-Wall -Wall
-Wextra -Wextra
-Wpedantic
-Wno-unused-parameter -Wno-unused-parameter
-Wno-unused-value -Wno-unused-value
-Wno-missing-field-initializers -Wno-missing-field-initializers
-Wno-gnu-zero-variadic-macro-arguments
-Wno-narrowing -Wno-narrowing
-Wno-pointer-arith -Wno-pointer-arith
-Wno-clobbered -Wno-clobbered
-frtti -Wpedantic
-fmacro-prefix-map=${CMAKE_SOURCE_DIR}/=) -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_EXECUTABLE_ENABLE_EXPORTS TRUE)
set(CMAKE_EXPORT_COMPILE_COMMANDS TRUE) set(CMAKE_EXPORT_COMPILE_COMMANDS TRUE)
@ -125,156 +103,49 @@ find_package(Threads REQUIRED)
set(GLES_VERSION "GLES3") set(GLES_VERSION "GLES3")
find_package(OpenGL REQUIRED COMPONENTS ${GLES_VERSION}) find_package(OpenGL REQUIRED COMPONENTS ${GLES_VERSION})
find_package(glslang CONFIG REQUIRED)
set(AQUAMARINE_MINIMUM_VERSION 0.9.3) pkg_check_modules(aquamarine_dep REQUIRED IMPORTED_TARGET aquamarine>=0.9.0)
set(HYPRLANG_MINIMUM_VERSION 0.6.7) pkg_check_modules(hyprlang_dep REQUIRED IMPORTED_TARGET hyprlang>=0.3.2)
set(HYPRCURSOR_MINIMUM_VERSION 0.1.7) pkg_check_modules(hyprcursor_dep REQUIRED IMPORTED_TARGET hyprcursor>=0.1.7)
set(HYPRUTILS_MINIMUM_VERSION 0.11.0) pkg_check_modules(hyprutils_dep REQUIRED IMPORTED_TARGET hyprutils>=0.8.1)
set(HYPRGRAPHICS_MINIMUM_VERSION 0.1.6) pkg_check_modules(hyprgraphics_dep REQUIRED IMPORTED_TARGET hyprgraphics>=0.1.3)
pkg_check_modules(aquamarine_dep REQUIRED IMPORTED_TARGET aquamarine>=${AQUAMARINE_MINIMUM_VERSION})
pkg_check_modules(hyprlang_dep REQUIRED IMPORTED_TARGET hyprlang>=${HYPRLANG_MINIMUM_VERSION})
pkg_check_modules(hyprcursor_dep REQUIRED IMPORTED_TARGET hyprcursor>=${HYPRCURSOR_MINIMUM_VERSION})
pkg_check_modules(hyprutils_dep REQUIRED IMPORTED_TARGET hyprutils>=${HYPRUTILS_MINIMUM_VERSION})
pkg_check_modules(hyprgraphics_dep REQUIRED IMPORTED_TARGET hyprgraphics>=${HYPRGRAPHICS_MINIMUM_VERSION})
string(REPLACE "." ";" AQ_VERSION_LIST ${aquamarine_dep_VERSION}) string(REPLACE "." ";" AQ_VERSION_LIST ${aquamarine_dep_VERSION})
list(GET AQ_VERSION_LIST 0 AQ_VERSION_MAJOR) list(GET AQ_VERSION_LIST 0 AQ_VERSION_MAJOR)
list(GET AQ_VERSION_LIST 1 AQ_VERSION_MINOR) list(GET AQ_VERSION_LIST 1 AQ_VERSION_MINOR)
list(GET AQ_VERSION_LIST 2 AQ_VERSION_PATCH) list(GET AQ_VERSION_LIST 2 AQ_VERSION_PATCH)
set(AQUAMARINE_VERSION "${aquamarine_dep_VERSION}") add_compile_definitions(AQUAMARINE_VERSION="${aquamarine_dep_VERSION}")
set(AQUAMARINE_VERSION_MAJOR "${AQ_VERSION_MAJOR}") add_compile_definitions(AQUAMARINE_VERSION_MAJOR=${AQ_VERSION_MAJOR})
set(AQUAMARINE_VERSION_MINOR "${AQ_VERSION_MINOR}") add_compile_definitions(AQUAMARINE_VERSION_MINOR=${AQ_VERSION_MINOR})
set(AQUAMARINE_VERSION_PATCH "${AQ_VERSION_PATCH}") add_compile_definitions(AQUAMARINE_VERSION_PATCH=${AQ_VERSION_PATCH})
set(HYPRLANG_VERSION "${hyprlang_dep_VERSION}") add_compile_definitions(HYPRLANG_VERSION="${hyprlang_dep_VERSION}")
set(HYPRUTILS_VERSION "${hyprutils_dep_VERSION}") add_compile_definitions(HYPRUTILS_VERSION="${hyprutils_dep_VERSION}")
set(HYPRCURSOR_VERSION "${hyprcursor_dep_VERSION}") add_compile_definitions(HYPRCURSOR_VERSION="${hyprcursor_dep_VERSION}")
set(HYPRGRAPHICS_VERSION "${hyprgraphics_dep_VERSION}") add_compile_definitions(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.91)
set(WAYLAND_SERVER_PROTOCOLS_MINIMUM_VERSION 1.45)
set(LIBINPUT_MINIMUM_VERSION 1.28)
pkg_check_modules( pkg_check_modules(
deps deps
REQUIRED REQUIRED
IMPORTED_TARGET GLOBAL IMPORTED_TARGET
xkbcommon>=${XKBCOMMON_MINIMUM_VERSION} xkbcommon
uuid uuid
wayland-server>=${WAYLAND_SERVER_MINIMUM_VERSION} wayland-server>=1.22.90
wayland-protocols>=${WAYLAND_SERVER_PROTOCOLS_MINIMUM_VERSION} wayland-protocols>=1.43
cairo cairo
pango pango
pangocairo pangocairo
pixman-1 pixman-1
xcursor xcursor
libdrm libdrm
libinput>=${LIBINPUT_MINIMUM_VERSION} libinput>=1.28
gbm gbm
gio-2.0 gio-2.0
re2 re2)
muparser
lcms2)
find_package(hyprwayland-scanner 0.3.10 REQUIRED) find_package(hyprwayland-scanner 0.3.10 REQUIRED)
file(GLOB_RECURSE SRCFILES "src/*.cpp") 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 "") set(TRACY_CPP_FILES "")
if(USE_TRACY) if(USE_TRACY)
@ -282,12 +153,7 @@ if(USE_TRACY)
message(STATUS "Tracy enabled, TRACY_CPP_FILES: " ${TRACY_CPP_FILES}) message(STATUS "Tracy enabled, TRACY_CPP_FILES: " ${TRACY_CPP_FILES})
endif() endif()
add_library(hyprland_lib STATIC ${SRCFILES}) add_executable(Hyprland ${SRCFILES} ${TRACY_CPP_FILES})
add_executable(Hyprland src/main.cpp ${TRACY_CPP_FILES})
target_link_libraries(Hyprland hyprland_lib)
target_include_directories(hyprland_lib PUBLIC ${deps_INCLUDE_DIRS})
target_include_directories(Hyprland PUBLIC ${deps_INCLUDE_DIRS})
set(USE_GPROF OFF) set(USE_GPROF OFF)
@ -297,16 +163,8 @@ if(CMAKE_BUILD_TYPE MATCHES Debug OR CMAKE_BUILD_TYPE MATCHES DEBUG)
if(WITH_ASAN) if(WITH_ASAN)
message(STATUS "Enabling ASan") message(STATUS "Enabling ASan")
target_link_libraries(hyprland_lib PUBLIC asan) target_link_libraries(Hyprland asan)
target_compile_options(hyprland_lib PUBLIC -fsanitize=address) target_compile_options(Hyprland PUBLIC -fsanitize=address)
endif()
add_compile_options(-fno-pie -fno-builtin)
add_link_options(-no-pie -fno-builtin)
if(USE_GPROF)
add_compile_options(-pg)
add_link_options(-pg)
endif()
endif() endif()
if(USE_TRACY) if(USE_TRACY)
@ -316,9 +174,7 @@ if(USE_TRACY)
option(TRACY_ON_DEMAND "" ON) option(TRACY_ON_DEMAND "" ON)
add_subdirectory(subprojects/tracy) add_subdirectory(subprojects/tracy)
add_compile_options(-fno-omit-frame-pointer) target_link_libraries(Hyprland Tracy::TracyClient)
target_link_libraries(hyprland_lib PUBLIC Tracy::TracyClient)
if(USE_TRACY_GPU) if(USE_TRACY_GPU)
message(STATUS "Tracy GPU Profiling is turned on") message(STATUS "Tracy GPU Profiling is turned on")
@ -326,8 +182,12 @@ if(USE_TRACY)
endif() endif()
endif() endif()
if(BUILT_WITH_NIX) add_compile_options(-fno-pie -fno-builtin)
add_compile_definitions(BUILT_WITH_NIX) add_link_options(-no-pie -fno-builtin)
if(USE_GPROF)
add_compile_options(-pg)
add_link_options(-pg)
endif()
endif() endif()
check_include_file("execinfo.h" EXECINFOH) check_include_file("execinfo.h" EXECINFOH)
@ -339,19 +199,19 @@ endif()
include(CheckLibraryExists) include(CheckLibraryExists)
check_library_exists(execinfo backtrace "" HAVE_LIBEXECINFO) check_library_exists(execinfo backtrace "" HAVE_LIBEXECINFO)
if(HAVE_LIBEXECINFO) if(HAVE_LIBEXECINFO)
target_link_libraries(hyprland_lib PUBLIC execinfo) target_link_libraries(Hyprland execinfo)
endif() endif()
check_include_file("sys/timerfd.h" HAS_TIMERFD) check_include_file("sys/timerfd.h" HAS_TIMERFD)
pkg_check_modules(epoll IMPORTED_TARGET epoll-shim) pkg_check_modules(epoll IMPORTED_TARGET epoll-shim)
if(NOT HAS_TIMERFD AND epoll_FOUND) if(NOT HAS_TIMERFD AND epoll_FOUND)
target_link_libraries(hyprland_lib PUBLIC PkgConfig::epoll) target_link_libraries(Hyprland PkgConfig::epoll)
endif() endif()
check_include_file("sys/inotify.h" HAS_INOTIFY) check_include_file("sys/inotify.h" HAS_INOTIFY)
pkg_check_modules(inotify IMPORTED_TARGET libinotify) pkg_check_modules(inotify IMPORTED_TARGET libinotify)
if(NOT HAS_INOTIFY AND inotify_FOUND) if(NOT HAS_INOTIFY AND inotify_FOUND)
target_link_libraries(hyprland_lib PUBLIC PkgConfig::inotify) target_link_libraries(Hyprland PkgConfig::inotify)
endif() endif()
if(NO_XWAYLAND) if(NO_XWAYLAND)
@ -359,7 +219,10 @@ if(NO_XWAYLAND)
add_compile_definitions(NO_XWAYLAND) add_compile_definitions(NO_XWAYLAND)
else() else()
message(STATUS "XWAYLAND Enabled (NO_XWAYLAND not defined) checking deps...") message(STATUS "XWAYLAND Enabled (NO_XWAYLAND not defined) checking deps...")
set(XWAYLAND_DEPENDENCIES pkg_check_modules(
xdeps
REQUIRED
IMPORTED_TARGET
xcb xcb
xcb-render xcb-render
xcb-xfixes xcb-xfixes
@ -367,21 +230,9 @@ else()
xcb-composite xcb-composite
xcb-res xcb-res
xcb-errors) xcb-errors)
target_link_libraries(Hyprland PkgConfig::xdeps)
pkg_check_modules(
xdeps
REQUIRED
IMPORTED_TARGET
${XWAYLAND_DEPENDENCIES})
string(JOIN ", " PKGCONFIG_XWAYLAND_DEPENDENCIES ${XWAYLAND_DEPENDENCIES})
string(PREPEND PKGCONFIG_XWAYLAND_DEPENDENCIES ", ")
target_link_libraries(hyprland_lib PUBLIC PkgConfig::xdeps)
endif() endif()
configure_file(hyprland.pc.in hyprland.pc @ONLY)
if(NO_SYSTEMD) if(NO_SYSTEMD)
message(STATUS "SYSTEMD support is disabled...") message(STATUS "SYSTEMD support is disabled...")
else() else()
@ -402,42 +253,30 @@ set(CPACK_PROJECT_NAME ${PROJECT_NAME})
set(CPACK_PROJECT_VERSION ${PROJECT_VERSION}) set(CPACK_PROJECT_VERSION ${PROJECT_VERSION})
include(CPack) include(CPack)
if(CMAKE_DISABLE_PRECOMPILE_HEADERS)
message(STATUS "Not using precompiled headers")
else()
message(STATUS "Setting precompiled headers") message(STATUS "Setting precompiled headers")
target_precompile_headers(hyprland_lib PRIVATE
target_precompile_headers(Hyprland PRIVATE
$<$<COMPILE_LANGUAGE:CXX>:src/pch/pch.hpp>) $<$<COMPILE_LANGUAGE:CXX>:src/pch/pch.hpp>)
endif()
message(STATUS "Setting link libraries") message(STATUS "Setting link libraries")
target_link_libraries( target_link_libraries(
hyprland_lib Hyprland
PUBLIC rt
PkgConfig::aquamarine_dep PkgConfig::aquamarine_dep
PkgConfig::hyprlang_dep PkgConfig::hyprlang_dep
PkgConfig::hyprutils_dep PkgConfig::hyprutils_dep
PkgConfig::hyprcursor_dep PkgConfig::hyprcursor_dep
PkgConfig::hyprgraphics_dep PkgConfig::hyprgraphics_dep
PkgConfig::deps PkgConfig::deps)
)
target_link_libraries(
Hyprland
${LIBRT}
hyprland_lib)
if(udis_dep_FOUND) if(udis_dep_FOUND)
target_link_libraries(hyprland_lib PUBLIC PkgConfig::udis_dep) target_link_libraries(Hyprland PkgConfig::udis_dep)
elseif(NOT("${udis_nopc}" MATCHES "udis_nopc-NOTFOUND"))
target_link_libraries(hyprland_lib PUBLIC ${udis_nopc})
else() else()
target_link_libraries(hyprland_lib PUBLIC libudis86) target_link_libraries(Hyprland libudis86)
endif() endif()
# used by `make installheaders`, to ensure the headers are generated # used by `make installheaders`, to ensure the headers are generated
add_custom_target(generate-protocol-headers) add_custom_target(generate-protocol-headers)
set(PROTOCOL_SOURCES "")
function(protocolnew protoPath protoName external) function(protocolnew protoPath protoName external)
if(external) if(external)
@ -451,15 +290,10 @@ function(protocolnew protoPath protoName external)
COMMAND hyprwayland-scanner ${path}/${protoName}.xml COMMAND hyprwayland-scanner ${path}/${protoName}.xml
${CMAKE_SOURCE_DIR}/protocols/ ${CMAKE_SOURCE_DIR}/protocols/
WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}) WORKING_DIRECTORY ${CMAKE_SOURCE_DIR})
target_sources(hyprland_lib PRIVATE protocols/${protoName}.cpp target_sources(Hyprland PRIVATE protocols/${protoName}.cpp
protocols/${protoName}.hpp) protocols/${protoName}.hpp)
target_sources(generate-protocol-headers target_sources(generate-protocol-headers
PRIVATE ${CMAKE_SOURCE_DIR}/protocols/${protoName}.hpp) 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() endfunction()
function(protocolWayland) function(protocolWayland)
add_custom_command( add_custom_command(
@ -469,21 +303,12 @@ function(protocolWayland)
hyprwayland-scanner --wayland-enums hyprwayland-scanner --wayland-enums
${WAYLAND_SCANNER_PKGDATA_DIR}/wayland.xml ${CMAKE_SOURCE_DIR}/protocols/ ${WAYLAND_SCANNER_PKGDATA_DIR}/wayland.xml ${CMAKE_SOURCE_DIR}/protocols/
WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}) 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 target_sources(generate-protocol-headers
PRIVATE ${CMAKE_SOURCE_DIR}/protocols/wayland.hpp) 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() endfunction()
if(TARGET OpenGL::GL) target_link_libraries(Hyprland OpenGL::EGL OpenGL::GL Threads::Threads)
target_link_libraries(hyprland_lib PUBLIC OpenGL::EGL OpenGL::GL glslang::glslang glslang::glslang-default-resource-limits glslang::SPIRV Threads::Threads)
else()
target_link_libraries(hyprland_lib PUBLIC OpenGL::EGL OpenGL::GLES3 glslang::glslang glslang::glslang-default-resource-limits glslang::SPIRV Threads::Threads)
endif()
pkg_check_modules(hyprland_protocols_dep hyprland-protocols>=0.6.4) pkg_check_modules(hyprland_protocols_dep hyprland-protocols>=0.6.4)
if(hyprland_protocols_dep_FOUND) if(hyprland_protocols_dep_FOUND)
@ -511,6 +336,8 @@ protocolnew("protocols" "kde-server-decoration" true)
protocolnew("protocols" "wlr-data-control-unstable-v1" true) protocolnew("protocols" "wlr-data-control-unstable-v1" true)
protocolnew("${HYPRLAND_PROTOCOLS}/protocols" "hyprland-focus-grab-v1" true) protocolnew("${HYPRLAND_PROTOCOLS}/protocols" "hyprland-focus-grab-v1" true)
protocolnew("protocols" "wlr-layer-shell-unstable-v1" true) protocolnew("protocols" "wlr-layer-shell-unstable-v1" true)
protocolnew("protocols" "xx-color-management-v4" true)
protocolnew("protocols" "frog-color-management-v1" true)
protocolnew("protocols" "wayland-drm" true) protocolnew("protocols" "wayland-drm" true)
protocolnew("${HYPRLAND_PROTOCOLS}/protocols" "hyprland-ctm-control-v1" true) protocolnew("${HYPRLAND_PROTOCOLS}/protocols" "hyprland-ctm-control-v1" true)
protocolnew("${HYPRLAND_PROTOCOLS}/protocols" "hyprland-surface-v1" true) protocolnew("${HYPRLAND_PROTOCOLS}/protocols" "hyprland-surface-v1" true)
@ -554,18 +381,11 @@ protocolnew("staging/color-management" "color-management-v1" false)
protocolnew("staging/xdg-toplevel-tag" "xdg-toplevel-tag-v1" false) protocolnew("staging/xdg-toplevel-tag" "xdg-toplevel-tag-v1" false)
protocolnew("staging/xdg-system-bell" "xdg-system-bell-v1" false) protocolnew("staging/xdg-system-bell" "xdg-system-bell-v1" false)
protocolnew("staging/ext-workspace" "ext-workspace-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)
protocolnew("staging/ext-image-capture-source" "ext-image-capture-source-v1" false)
protocolnew("staging/ext-image-copy-capture" "ext-image-copy-capture-v1" false)
protocolwayland() protocolwayland()
# tools # tools
add_subdirectory(hyprctl) add_subdirectory(hyprctl)
add_subdirectory(start)
if(NO_HYPRPM) if(NO_HYPRPM)
message(STATUS "hyprpm is disabled") message(STATUS "hyprpm is disabled")
@ -574,6 +394,12 @@ else()
message(STATUS "hyprpm is enabled (NO_HYPRPM not defined)") message(STATUS "hyprpm is enabled (NO_HYPRPM not defined)")
endif() 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 # binary and symlink
install(TARGETS Hyprland) install(TARGETS Hyprland)
@ -584,11 +410,6 @@ install(
\"\$ENV{DESTDIR}${CMAKE_INSTALL_FULL_BINDIR}/hyprland\" \ \"\$ENV{DESTDIR}${CMAKE_INSTALL_FULL_BINDIR}/hyprland\" \
)") )")
# session file # session file
configure_file(
${CMAKE_SOURCE_DIR}/example/hyprland.desktop.in
${CMAKE_SOURCE_DIR}/example/hyprland.desktop
@ONLY
)
install(FILES ${CMAKE_SOURCE_DIR}/example/hyprland.desktop install(FILES ${CMAKE_SOURCE_DIR}/example/hyprland.desktop
DESTINATION ${CMAKE_INSTALL_DATAROOTDIR}/wayland-sessions) DESTINATION ${CMAKE_INSTALL_DATAROOTDIR}/wayland-sessions)
@ -597,6 +418,7 @@ add_compile_definitions(DATAROOTDIR="${CMAKE_INSTALL_FULL_DATAROOTDIR}")
# installable assets # installable assets
file(GLOB_RECURSE INSTALLABLE_ASSETS "assets/install/*") file(GLOB_RECURSE INSTALLABLE_ASSETS "assets/install/*")
list(FILTER INSTALLABLE_ASSETS EXCLUDE REGEX "meson.build")
install(FILES ${INSTALLABLE_ASSETS} install(FILES ${INSTALLABLE_ASSETS}
DESTINATION ${CMAKE_INSTALL_DATAROOTDIR}/hypr) DESTINATION ${CMAKE_INSTALL_DATAROOTDIR}/hypr)
@ -634,47 +456,15 @@ install(
PATTERN "*.hpp" PATTERN "*.hpp"
PATTERN "*.inc") PATTERN "*.inc")
if(BUILD_TESTING OR WITH_TESTS) if(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")
enable_testing() enable_testing()
add_custom_target(tests) 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() add_dependencies(tests hyprtester)
message(STATUS "Testing is disabled")
endif() endif()

View file

@ -1,6 +1,6 @@
BSD 3-Clause License BSD 3-Clause License
Copyright (c) 2022-2026, vaxerski Copyright (c) 2022-2024, vaxerski
All rights reserved. All rights reserved.
Redistribution and use in source and binary forms, with or without 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` cmake --build ./build --config Release --target all -j`nproc 2>/dev/null || getconf NPROCESSORS_CONF`
debug: 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` cmake --build ./build --config Debug --target all -j`nproc 2>/dev/null || getconf NPROCESSORS_CONF`
nopch: nopch:
@ -18,7 +18,6 @@ nopch:
clear: clear:
rm -rf build rm -rf build
rm -f ./protocols/*.h ./protocols/*.c ./protocols/*.cpp ./protocols/*.hpp rm -f ./protocols/*.h ./protocols/*.c ./protocols/*.cpp ./protocols/*.hpp
rm -f ./hyprctl/hw-protocols/*.cpp ./hyprctl/hw-protocols/*.hpp
all: all:
$(MAKE) clear $(MAKE) clear
@ -88,12 +87,8 @@ asan:
@echo "Wayland done" @echo "Wayland done"
patch -p1 < ./scripts/hyprlandStaticAsan.diff patch -p1 < ./scripts/hyprlandStaticAsan.diff
cmake --no-warn-unused-cli -DCMAKE_BUILD_TYPE:STRING=Debug -DWITH_ASAN:STRING=True -DUSE_TRACY:STRING=False -DUSE_TRACY_GPU:STRING=False -S . -B ./build cmake --no-warn-unused-cli -DCMAKE_BUILD_TYPE:STRING=Debug -DWITH_ASAN:STRING=True -DUSE_TRACY:STRING=False -DUSE_TRACY_GPU:STRING=False -S . -B ./build -G Ninja
cmake --build ./build --config Debug --target all cmake --build ./build --config Debug --target all
@echo "Hyprland done" @echo "Hyprland done"
ASAN_OPTIONS="detect_odr_violation=0,log_path=asan.log" HYPRLAND_NO_CRASHREPORTER=1 ./build/Hyprland -c ~/.config/hypr/hyprland.conf 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> <br>
[![Badge Workflow]][Workflow] ![Badge Workflow]
[![Badge License]][License] [![Badge License]][License]
![Badge Language] ![Badge Language]
[![Badge Pull Requests]][Pull Requests] [![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/ [Contribute]: https://wiki.hypr.land/Contributing-and-Debugging/
[Install]: https://wiki.hypr.land/Getting-Started/Installation/ [Install]: https://wiki.hypr.land/Getting-Started/Installation/
[Quick Start]: https://wiki.hypr.land/Getting-Started/Master-Tutorial/ [Quick Start]: https://wiki.hypr.land/Getting-Started/Master-Tutorial/
[Workflow]: https://github.com/hyprwm/Hyprland/actions/workflows/ci.yaml
[License]: LICENSE [License]: LICENSE

View file

@ -1 +1 @@
0.54.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 # Set programs that you use
$terminal = kitty $terminal = kitty
$fileManager = dolphin $fileManager = dolphin
$menu = hyprlauncher $menu = wofi --show drun
################# #################
@ -125,16 +125,14 @@ decoration {
animations { animations {
enabled = yes, please :) enabled = yes, please :)
# Default curves, see https://wiki.hypr.land/Configuring/Animations/#curves # Default animations, see https://wiki.hypr.land/Configuring/Animations/ for more
# NAME, X0, Y0, X1, Y1
bezier = easeOutQuint,0.23,1,0.32,1 bezier = easeOutQuint,0.23,1,0.32,1
bezier = easeInOutCubic,0.65,0.05,0.36,1 bezier = easeInOutCubic,0.65,0.05,0.36,1
bezier = linear,0,0,1,1 bezier = linear,0,0,1,1
bezier = almostLinear, 0.5, 0.5, 0.75, 1 bezier = almostLinear,0.5,0.5,0.75,1.0
bezier = quick,0.15,0,0.1,1 bezier = quick,0.15,0,0.1,1
# Default animations, see https://wiki.hypr.land/Configuring/Animations/
# NAME, ONOFF, SPEED, CURVE, [STYLE]
animation = global, 1, 10, default animation = global, 1, 10, default
animation = border, 1, 5.39, easeOutQuint animation = border, 1, 5.39, easeOutQuint
animation = windows, 1, 4.79, easeOutQuint animation = windows, 1, 4.79, easeOutQuint
@ -151,7 +149,6 @@ animations {
animation = workspaces, 1, 1.94, almostLinear, fade animation = workspaces, 1, 1.94, almostLinear, fade
animation = workspacesIn, 1, 1.21, almostLinear, fade animation = workspacesIn, 1, 1.21, almostLinear, fade
animation = workspacesOut, 1, 1.94, almostLinear, fade animation = workspacesOut, 1, 1.94, almostLinear, fade
animation = zoomFactor, 1, 7, quick
} }
# Ref https://wiki.hypr.land/Configuring/Workspace-Rules/ # Ref https://wiki.hypr.land/Configuring/Workspace-Rules/
@ -159,23 +156,10 @@ animations {
# uncomment all if you wish to use that. # uncomment all if you wish to use that.
# workspace = w[tv1], gapsout:0, gapsin:0 # workspace = w[tv1], gapsout:0, gapsin:0
# workspace = f[1], gapsout:0, gapsin:0 # workspace = f[1], gapsout:0, gapsin:0
# windowrule { # windowrule = bordersize 0, floating:0, onworkspace:w[tv1]
# name = no-gaps-wtv1 # windowrule = rounding 0, floating:0, onworkspace:w[tv1]
# match:float = false # windowrule = bordersize 0, floating:0, onworkspace:f[1]
# match:workspace = w[tv1] # windowrule = rounding 0, floating:0, onworkspace:f[1]
#
# border_size = 0
# rounding = 0
# }
#
# windowrule {
# name = no-gaps-f1
# match:float = false
# match:workspace = f[1]
#
# border_size = 0
# rounding = 0
# }
# See https://wiki.hypr.land/Configuring/Dwindle-Layout/ for more # See https://wiki.hypr.land/Configuring/Dwindle-Layout/ for more
dwindle { dwindle {
@ -216,8 +200,10 @@ input {
} }
} }
# See https://wiki.hypr.land/Configuring/Gestures # https://wiki.hypr.land/Configuring/Variables/#gestures
gesture = 3, horizontal, workspace gestures {
workspace_swipe = false
}
# Example per-device config # Example per-device config
# See https://wiki.hypr.land/Configuring/Keywords/#per-device-input-configs for more # See https://wiki.hypr.land/Configuring/Keywords/#per-device-input-configs for more
@ -237,12 +223,12 @@ $mainMod = SUPER # Sets "Windows" key as main modifier
# Example binds, see https://wiki.hypr.land/Configuring/Binds/ for more # Example binds, see https://wiki.hypr.land/Configuring/Binds/ for more
bind = $mainMod, Q, exec, $terminal bind = $mainMod, Q, exec, $terminal
bind = $mainMod, C, killactive, 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, E, exec, $fileManager
bind = $mainMod, V, togglefloating, bind = $mainMod, V, togglefloating,
bind = $mainMod, R, exec, $menu bind = $mainMod, R, exec, $menu
bind = $mainMod, P, pseudo, # dwindle bind = $mainMod, P, pseudo, # dwindle
bind = $mainMod, J, layoutmsg, togglesplit # dwindle bind = $mainMod, J, togglesplit, # dwindle
# Move focus with mainMod + arrow keys # Move focus with mainMod + arrow keys
bind = $mainMod, left, movefocus, l bind = $mainMod, left, movefocus, l
@ -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/Window-Rules/ for more
# See https://wiki.hypr.land/Configuring/Workspace-Rules/ for workspace rules # 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 apps. You'll probably like this.
# Ignore maximize requests from all apps. You'll probably like this. windowrule = suppressevent maximize, class:.*
name = suppress-maximize-events
match:class = .*
suppress_event = maximize
}
windowrule {
# Fix some dragging issues with XWayland # Fix some dragging issues with XWayland
name = fix-xwayland-drags windowrule = nofocus,class:^$,title:^$,xwayland:1,floating:1,fullscreen:0,pinned:0
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
}

View file

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

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

244
flake.lock generated
View file

@ -16,11 +16,11 @@
] ]
}, },
"locked": { "locked": {
"lastModified": 1772292445, "lastModified": 1751740947,
"narHash": "sha256-4F1Q7U313TKUDDovCC96m/Za4wZcJ3yqtu4eSrj8lk8=", "narHash": "sha256-35040CHH7P3JGmhGVfEb2oJHL/A5mI2IXumhkxrBnao=",
"owner": "hyprwm", "owner": "hyprwm",
"repo": "aquamarine", "repo": "aquamarine",
"rev": "1dbbba659c1cef0b0202ce92cadfe13bae550e8f", "rev": "dfc1db15a08c4cd234288f66e1199c653495301f",
"type": "github" "type": "github"
}, },
"original": { "original": {
@ -32,15 +32,15 @@
"flake-compat": { "flake-compat": {
"flake": false, "flake": false,
"locked": { "locked": {
"lastModified": 1767039857, "lastModified": 1696426674,
"narHash": "sha256-vNpUSpF5Nuw8xvDLj2KCwwksIbjua2LZCqhV1LNRDns=", "narHash": "sha256-kvjfFW7WAETZlt09AgDn1MrtKzP7t90Vf7vypd3OL1U=",
"owner": "NixOS", "owner": "edolstra",
"repo": "flake-compat", "repo": "flake-compat",
"rev": "5edf11c44bc78a0d334f6334cdaf7d60d732daab", "rev": "0f9255e01c2351cc7d116c072cb317785dd33b33",
"type": "github" "type": "github"
}, },
"original": { "original": {
"owner": "NixOS", "owner": "edolstra",
"repo": "flake-compat", "repo": "flake-compat",
"type": "github" "type": "github"
} }
@ -79,11 +79,11 @@
] ]
}, },
"locked": { "locked": {
"lastModified": 1753964049, "lastModified": 1749155331,
"narHash": "sha256-lIqabfBY7z/OANxHoPeIrDJrFyYy9jAM4GQLzZ2feCM=", "narHash": "sha256-XR9fsI0zwLiFWfqi/pdS/VD+YNorKb3XIykgTg4l1nA=",
"owner": "hyprwm", "owner": "hyprwm",
"repo": "hyprcursor", "repo": "hyprcursor",
"rev": "44e91d467bdad8dcf8bbd2ac7cf49972540980a5", "rev": "45fcc10b4c282746d93ec406a740c43b48b4ef80",
"type": "github" "type": "github"
}, },
"original": { "original": {
@ -105,11 +105,11 @@
] ]
}, },
"locked": { "locked": {
"lastModified": 1770511807, "lastModified": 1751808145,
"narHash": "sha256-suKmSbSk34uPOJDTg/GbPrKEJutzK08vj0VoTvAFBCA=", "narHash": "sha256-OXgL0XaKMmfX2rRQkt9SkJw+QNfv0jExlySt1D6O72g=",
"owner": "hyprwm", "owner": "hyprwm",
"repo": "hyprgraphics", "repo": "hyprgraphics",
"rev": "7c75487edd43a71b61adb01cae8326d277aab683", "rev": "b841473a0bd4a1a74a0b64f1ec2ab199035c349f",
"type": "github" "type": "github"
}, },
"original": { "original": {
@ -118,45 +118,6 @@
"type": "github" "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": 1767023960,
"narHash": "sha256-R2HgtVS1G3KSIKAQ77aOZ+Q0HituOmPgXW9nBNkpp3Q=",
"owner": "hyprwm",
"repo": "hyprland-guiutils",
"rev": "c2e906261142f5dd1ee0bfc44abba23e2754c660",
"type": "github"
},
"original": {
"owner": "hyprwm",
"repo": "hyprland-guiutils",
"type": "github"
}
},
"hyprland-protocols": { "hyprland-protocols": {
"inputs": { "inputs": {
"nixpkgs": [ "nixpkgs": [
@ -167,11 +128,11 @@
] ]
}, },
"locked": { "locked": {
"lastModified": 1765214753, "lastModified": 1749046714,
"narHash": "sha256-P9zdGXOzToJJgu5sVjv7oeOGPIIwrd9hAUAP3PsmBBs=", "narHash": "sha256-kymV5FMnddYGI+UjwIw8ceDjdeg7ToDVjbHCvUlhn14=",
"owner": "hyprwm", "owner": "hyprwm",
"repo": "hyprland-protocols", "repo": "hyprland-protocols",
"rev": "3f3860b869014c00e8b9e0528c7b4ddc335c21ab", "rev": "613878cb6f459c5e323aaafe1e6f388ac8a36330",
"type": "github" "type": "github"
}, },
"original": { "original": {
@ -180,6 +141,67 @@
"type": "github" "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": { "hyprlang": {
"inputs": { "inputs": {
"hyprutils": [ "hyprutils": [
@ -193,11 +215,11 @@
] ]
}, },
"locked": { "locked": {
"lastModified": 1771866172, "lastModified": 1750371198,
"narHash": "sha256-fYFoXhQLrm1rD8vSFKQBOEX4OGCuJdLt1amKfHd5GAw=", "narHash": "sha256-/iuJ1paQOBoSLqHflRNNGyroqfF/yvPNurxzcCT0cAE=",
"owner": "hyprwm", "owner": "hyprwm",
"repo": "hyprlang", "repo": "hyprlang",
"rev": "0b219224910e7642eb0ed49f0db5ec3d008e3e41", "rev": "cee01452bca58d6cadb3224e21e370de8bc20f0b",
"type": "github" "type": "github"
}, },
"original": { "original": {
@ -206,51 +228,6 @@
"type": "github" "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": { "hyprutils": {
"inputs": { "inputs": {
"nixpkgs": [ "nixpkgs": [
@ -261,11 +238,11 @@
] ]
}, },
"locked": { "locked": {
"lastModified": 1771271487, "lastModified": 1751888065,
"narHash": "sha256-41gEiUS0Pyw3L/ge1l8MXn61cK14VAhgWB/JV8s/oNI=", "narHash": "sha256-F2SV9WGqgtRsXIdUrl3sRe0wXlQD+kRRZcSfbepjPJY=",
"owner": "hyprwm", "owner": "hyprwm",
"repo": "hyprutils", "repo": "hyprutils",
"rev": "340a792e3b3d482c4ae5f66d27a9096bdee6d76d", "rev": "a8229739cf36d159001cfc203871917b83fdf917",
"type": "github" "type": "github"
}, },
"original": { "original": {
@ -284,11 +261,11 @@
] ]
}, },
"locked": { "locked": {
"lastModified": 1770501770, "lastModified": 1751881472,
"narHash": "sha256-NWRM6+YxTRv+bT9yvlhhJ2iLae1B1pNH3mAL5wi2rlQ=", "narHash": "sha256-meB0SnXbwIe2trD041MLKEv6R7NZ759QwBcVIhlSBfE=",
"owner": "hyprwm", "owner": "hyprwm",
"repo": "hyprwayland-scanner", "repo": "hyprwayland-scanner",
"rev": "0bd8b6cde9ec27d48aad9e5b4deefb3746909d40", "rev": "8fb426b3e5452fd9169453fd6c10f8c14ca37120",
"type": "github" "type": "github"
}, },
"original": { "original": {
@ -297,39 +274,13 @@
"type": "github" "type": "github"
} }
}, },
"hyprwire": {
"inputs": {
"hyprutils": [
"hyprutils"
],
"nixpkgs": [
"nixpkgs"
],
"systems": [
"systems"
]
},
"locked": {
"lastModified": 1771606233,
"narHash": "sha256-F3PLUqQ/TwgR70U+UeOqJnihJZ2EuunzojYC4g5xHr0=",
"owner": "hyprwm",
"repo": "hyprwire",
"rev": "06c7f1f8c4194786c8400653c4efc49dc14c0f3a",
"type": "github"
},
"original": {
"owner": "hyprwm",
"repo": "hyprwire",
"type": "github"
}
},
"nixpkgs": { "nixpkgs": {
"locked": { "locked": {
"lastModified": 1772198003, "lastModified": 1751792365,
"narHash": "sha256-I45esRSssFtJ8p/gLHUZ1OUaaTaVLluNkABkk6arQwE=", "narHash": "sha256-J1kI6oAj25IG4EdVlg2hQz8NZTBNYvIS0l4wpr9KcUo=",
"owner": "NixOS", "owner": "NixOS",
"repo": "nixpkgs", "repo": "nixpkgs",
"rev": "dd9b079222d43e1943b6ebd802f04fd959dc8e61", "rev": "1fd8bada0b6117e6c7eb54aad5813023eed37ccb",
"type": "github" "type": "github"
}, },
"original": { "original": {
@ -348,11 +299,11 @@
] ]
}, },
"locked": { "locked": {
"lastModified": 1772024342, "lastModified": 1750779888,
"narHash": "sha256-+eXlIc4/7dE6EcPs9a2DaSY3fTA9AE526hGqkNID3Wg=", "narHash": "sha256-wibppH3g/E2lxU43ZQHC5yA/7kIKLGxVEnsnVK1BtRg=",
"owner": "cachix", "owner": "cachix",
"repo": "git-hooks.nix", "repo": "git-hooks.nix",
"rev": "6e34e97ed9788b17796ee43ccdbaf871a5c2b476", "rev": "16ec914f6fb6f599ce988427d9d94efddf25fe6d",
"type": "github" "type": "github"
}, },
"original": { "original": {
@ -366,12 +317,11 @@
"aquamarine": "aquamarine", "aquamarine": "aquamarine",
"hyprcursor": "hyprcursor", "hyprcursor": "hyprcursor",
"hyprgraphics": "hyprgraphics", "hyprgraphics": "hyprgraphics",
"hyprland-guiutils": "hyprland-guiutils",
"hyprland-protocols": "hyprland-protocols", "hyprland-protocols": "hyprland-protocols",
"hyprland-qtutils": "hyprland-qtutils",
"hyprlang": "hyprlang", "hyprlang": "hyprlang",
"hyprutils": "hyprutils", "hyprutils": "hyprutils",
"hyprwayland-scanner": "hyprwayland-scanner", "hyprwayland-scanner": "hyprwayland-scanner",
"hyprwire": "hyprwire",
"nixpkgs": "nixpkgs", "nixpkgs": "nixpkgs",
"pre-commit-hooks": "pre-commit-hooks", "pre-commit-hooks": "pre-commit-hooks",
"systems": "systems", "systems": "systems",
@ -415,11 +365,11 @@
] ]
}, },
"locked": { "locked": {
"lastModified": 1761431178, "lastModified": 1751300244,
"narHash": "sha256-xzjC1CV3+wpUQKNF+GnadnkeGUCJX+vgaWIZsnz9tzI=", "narHash": "sha256-PFuv1TZVYvQhha0ac53E3YgdtmLShrN0t4T6xqHl0jE=",
"owner": "hyprwm", "owner": "hyprwm",
"repo": "xdg-desktop-portal-hyprland", "repo": "xdg-desktop-portal-hyprland",
"rev": "4b8801228ff958d028f588f0c2b911dbf32297f9", "rev": "6115f3fdcb2c1a57b4a80a69f3c797e47607b90a",
"type": "github" "type": "github"
}, },
"original": { "original": {

View file

@ -35,15 +35,11 @@
inputs.systems.follows = "systems"; inputs.systems.follows = "systems";
}; };
hyprland-guiutils = { hyprland-qtutils = {
url = "github:hyprwm/hyprland-guiutils"; url = "github:hyprwm/hyprland-qtutils";
inputs.nixpkgs.follows = "nixpkgs"; inputs.nixpkgs.follows = "nixpkgs";
inputs.systems.follows = "systems"; inputs.systems.follows = "systems";
inputs.aquamarine.follows = "aquamarine";
inputs.hyprgraphics.follows = "hyprgraphics";
inputs.hyprutils.follows = "hyprutils";
inputs.hyprlang.follows = "hyprlang"; inputs.hyprlang.follows = "hyprlang";
inputs.hyprwayland-scanner.follows = "hyprwayland-scanner";
}; };
hyprlang = { hyprlang = {
@ -65,13 +61,6 @@
inputs.systems.follows = "systems"; inputs.systems.follows = "systems";
}; };
hyprwire = {
url = "github:hyprwm/hyprwire";
inputs.nixpkgs.follows = "nixpkgs";
inputs.systems.follows = "systems";
inputs.hyprutils.follows = "hyprutils";
};
xdph = { xdph = {
url = "github:hyprwm/xdg-desktop-portal-hyprland"; url = "github:hyprwm/xdg-desktop-portal-hyprland";
inputs.nixpkgs.follows = "nixpkgs"; inputs.nixpkgs.follows = "nixpkgs";
@ -88,28 +77,23 @@
}; };
}; };
outputs = outputs = inputs @ {
inputs@{
self, self,
nixpkgs, nixpkgs,
systems, systems,
... ...
}: }: let
let
inherit (nixpkgs) lib; inherit (nixpkgs) lib;
eachSystem = lib.genAttrs (import systems); eachSystem = lib.genAttrs (import systems);
pkgsFor = eachSystem ( pkgsFor = eachSystem (system:
system:
import nixpkgs { import nixpkgs {
localSystem = system; localSystem = system;
overlays = with self.overlays; [ overlays = with self.overlays; [
hyprland-packages hyprland-packages
hyprland-extras hyprland-extras
]; ];
} });
); pkgsCrossFor = eachSystem (system: crossSystem:
pkgsCrossFor = eachSystem (
system: crossSystem:
import nixpkgs { import nixpkgs {
localSystem = system; localSystem = system;
inherit crossSystem; inherit crossSystem;
@ -117,36 +101,29 @@
hyprland-packages hyprland-packages
hyprland-extras hyprland-extras
]; ];
} });
); pkgsDebugFor = eachSystem (system:
pkgsDebugFor = eachSystem (
system:
import nixpkgs { import nixpkgs {
localSystem = system; localSystem = system;
overlays = with self.overlays; [ overlays = with self.overlays; [
hyprland-debug hyprland-debug
]; ];
} });
); pkgsDebugCrossFor = eachSystem (system: crossSystem:
pkgsDebugCrossFor = eachSystem (
system: crossSystem:
import nixpkgs { import nixpkgs {
localSystem = system; localSystem = system;
inherit crossSystem; inherit crossSystem;
overlays = with self.overlays; [ overlays = with self.overlays; [
hyprland-debug hyprland-debug
]; ];
} });
); in {
in
{
overlays = import ./nix/overlays.nix {inherit self lib inputs;}; overlays = import ./nix/overlays.nix {inherit self lib inputs;};
checks = eachSystem ( checks = eachSystem (system:
system: (lib.filterAttrs
(lib.filterAttrs ( (n: _: (lib.hasPrefix "hyprland" n) && !(lib.hasSuffix "debug" n))
n: _: (lib.hasPrefix "hyprland" n) && !(lib.hasSuffix "debug" n) self.packages.${system})
) self.packages.${system})
// { // {
inherit (self.packages.${system}) xdg-desktop-portal-hyprland; inherit (self.packages.${system}) xdg-desktop-portal-hyprland;
pre-commit-check = inputs.pre-commit-hooks.lib.${system}.run { pre-commit-check = inputs.pre-commit-hooks.lib.${system}.run {
@ -162,16 +139,16 @@
}; };
}; };
} }
// (import ./nix/tests inputs pkgsFor.${system}) // (import ./nix/tests inputs pkgsFor.${system}));
);
packages = eachSystem (system: { packages = eachSystem (system: {
default = self.packages.${system}.hyprland; default = self.packages.${system}.hyprland;
inherit (pkgsFor.${system}) inherit
(pkgsFor.${system})
# hyprland-packages # hyprland-packages
hyprland hyprland
hyprland-unwrapped hyprland-unwrapped
hyprland-with-tests hyprtester
# hyprland-extras # hyprland-extras
xdg-desktop-portal-hyprland xdg-desktop-portal-hyprland
; ;
@ -182,11 +159,9 @@
devShells = eachSystem (system: { devShells = eachSystem (system: {
default = default =
pkgsFor.${system}.mkShell.override pkgsFor.${system}.mkShell.override {
{
inherit (self.packages.${system}.default) stdenv; inherit (self.packages.${system}.default) stdenv;
} } {
{
name = "hyprland-shell"; name = "hyprland-shell";
hardeningDisable = ["fortify"]; hardeningDisable = ["fortify"];
inputsFrom = [pkgsFor.${system}.hyprland]; inputsFrom = [pkgsFor.${system}.hyprland];
@ -201,7 +176,7 @@
homeManagerModules.default = import ./nix/hm-module.nix self; homeManagerModules.default = import ./nix/hm-module.nix self;
# Hydra build jobs # 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 # or similar. Remember to filter large or incompatible attributes here. More eval jobs can
# be added by merging, e.g., self.packages // self.devShells. # be added by merging, e.g., self.packages // self.devShells.
hydraJobs = self.packages; hydraJobs = self.packages;

View file

@ -5,32 +5,11 @@ project(
DESCRIPTION "Control utility for Hyprland" 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 "main.cpp")
add_executable(hyprctl ${HYPRCTL_SRCFILES})
target_link_libraries(hyprctl PUBLIC PkgConfig::hyprctl_deps) 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 # binary
install(TARGETS hyprctl) install(TARGETS hyprctl)

View file

@ -49,7 +49,6 @@ commands:
the same format as in colors in config. Will reset the same format as in colors in config. Will reset
when Hyprland's config is reloaded when Hyprland's config is reloaded
setprop ... Sets a window property setprop ... Sets a window property
getprop ... Gets a window property
splash Get the current splash splash Get the current splash
switchxkblayout ... Sets the xkb layout index for a keyboard switchxkblayout ... Sets the xkb layout index for a keyboard
systeminfo Get system info systeminfo Get system info
@ -74,8 +73,11 @@ flags:
const std::string_view HYPRPAPER_HELP = R"#(usage: hyprctl [flags] hyprpaper <request> const std::string_view HYPRPAPER_HELP = R"#(usage: hyprctl [flags] hyprpaper <request>
requests: requests:
wallpaper Issue a wallpaper to call a config wallpaper dynamically. listactive Lists all active images
Arguments are [mon],[path],[fit_mode]. Fit mode is optional. 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: flags:
See 'hyprctl --help')#"; See 'hyprctl --help')#";
@ -157,18 +159,6 @@ lock:
flags: flags:
See 'hyprctl --help')#"; 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> const std::string_view SWITCHXKBLAYOUT_HELP = R"#(usage: [flags] switchxkblayout <device> <cmd>
device: device:

View file

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

View file

@ -48,7 +48,7 @@ function _hyprctl
set descriptions[22] "Focus the urgent window or the last window" set descriptions[22] "Focus the urgent window or the last window"
set descriptions[23] "Get the list of defined workspace rules" set descriptions[23] "Get the list of defined workspace rules"
set descriptions[24] "Move the active workspace to a monitor" 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[26] "Interact with hyprpaper if present"
set descriptions[29] "Swap the active window with the next or previous in a group" 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" 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 # 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" # 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" | (closewindow) "Close a specified window"
| (workspace) "Change the workspace" | (workspace) "Change the workspace"
| (movetoworkspace) "Move the focused window to a 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" | (togglefloating) "Toggle the current window's floating state"
| (setfloating) "Set the current window's floating state to true" | (setfloating) "Set the current window's floating state to true"
| (settiled) "Set the current window's floating state to false" | (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[22]="Focus the urgent window or the last window"
descriptions[23]="Get the list of defined workspace rules" descriptions[23]="Get the list of defined workspace rules"
descriptions[24]="Move the active workspace to a monitor" 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[26]="Interact with hyprpaper if present"
descriptions[29]="Swap the active window with the next or previous in a group" 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" descriptions[30]="Move the cursor to the corner of the active window"

View file

@ -26,12 +26,9 @@
#include <filesystem> #include <filesystem>
#include <cstdarg> #include <cstdarg>
#include <hyprutils/string/String.hpp> #include <hyprutils/string/String.hpp>
#include <hyprutils/memory/Casts.hpp>
using namespace Hyprutils::String; using namespace Hyprutils::String;
using namespace Hyprutils::Memory;
#include "Strings.hpp" #include "Strings.hpp"
#include "hyprpaper/Hyprpaper.hpp"
std::string instanceSignature; std::string instanceSignature;
bool quiet = false; 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); 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)"); log("Couldn't connect to " + socketPath + ". (4)");
return 4; return 4;
} }
@ -228,9 +225,6 @@ int request(std::string_view arg, int minArgs = 0, bool needRoll = false) {
constexpr size_t BUFFER_SIZE = 8192; constexpr size_t BUFFER_SIZE = 8192;
char buffer[BUFFER_SIZE] = {0}; char buffer[BUFFER_SIZE] = {0};
// read all data until server closes the connection
// this handles partial writes on the server side under high load
while (true) {
sizeWritten = read(SERVERSOCKET, buffer, BUFFER_SIZE); sizeWritten = read(SERVERSOCKET, buffer, BUFFER_SIZE);
if (sizeWritten < 0) { if (sizeWritten < 0) {
@ -240,11 +234,14 @@ int request(std::string_view arg, int minArgs = 0, bool needRoll = false) {
return 6; return 6;
} }
if (sizeWritten == 0) { reply += std::string(buffer, sizeWritten);
// server closed connection, we're done
break;
}
while (sizeWritten == BUFFER_SIZE) {
sizeWritten = read(SERVERSOCKET, buffer, BUFFER_SIZE);
if (sizeWritten < 0) {
log("Couldn't read (6)");
return 6;
}
reply += std::string(buffer, sizeWritten); reply += std::string(buffer, sizeWritten);
} }
@ -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); 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)"); log("Couldn't connect to " + socketPath + ". (3)");
return 3; return 3;
} }
@ -306,6 +303,10 @@ int requestIPC(std::string_view filename, std::string_view arg) {
return 0; return 0;
} }
int requestHyprpaper(std::string_view arg) {
return requestIPC(".hyprpaper.sock", arg);
}
int requestHyprsunset(std::string_view arg) { int requestHyprsunset(std::string_view arg) {
return requestIPC(".hyprsunset.sock", arg); return requestIPC(".hyprsunset.sock", arg);
} }
@ -424,8 +425,6 @@ int main(int argc, char** argv) {
std::println("{}", PLUGIN_HELP); std::println("{}", PLUGIN_HELP);
} else if (cmd == "setprop") { } else if (cmd == "setprop") {
std::println("{}", SETPROP_HELP); std::println("{}", SETPROP_HELP);
} else if (cmd == "getprop") {
std::println("{}", GETPROP_HELP);
} else if (cmd == "switchxkblayout") { } else if (cmd == "switchxkblayout") {
std::println("{}", SWITCHXKBLAYOUT_HELP); std::println("{}", SWITCHXKBLAYOUT_HELP);
} else { } else {
@ -476,7 +475,7 @@ int main(int argc, char** argv) {
const auto INSTANCES = instances(); 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"); log("no such instance\n");
return 1; return 1;
} }
@ -497,12 +496,9 @@ int main(int argc, char** argv) {
if (fullRequest.contains("/--batch")) if (fullRequest.contains("/--batch"))
batchRequest(fullRequest, json); batchRequest(fullRequest, json);
else if (fullRequest.contains("/hyprpaper")) { else if (fullRequest.contains("/hyprpaper"))
auto result = Hyprpaper::makeHyprpaperRequest(fullRequest); exitStatus = requestHyprpaper(fullRequest);
if (!result) else if (fullRequest.contains("/hyprsunset"))
log(std::format("error: {}", result.error()));
exitStatus = !result;
} else if (fullRequest.contains("/hyprsunset"))
exitStatus = requestHyprsunset(fullRequest); exitStatus = requestHyprsunset(fullRequest);
else if (fullRequest.contains("/switchxkblayout")) else if (fullRequest.contains("/switchxkblayout"))
exitStatus = request(fullRequest, 2); 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,208 +0,0 @@
#include "Hyprpaper.hpp"
#include "../helpers/Memory.hpp"
#include <optional>
#include <format>
#include <filesystem>
#include <print>
#include <hyprpaper_core-client.hpp>
#include <hyprutils/string/VarList2.hpp>
using namespace Hyprutils::String;
using namespace std::string_literals;
constexpr const char* SOCKET_NAME = ".hyprpaper.sock";
static SP<CCHyprpaperCoreImpl> g_coreImpl;
constexpr const uint32_t PROTOCOL_VERSION_SUPPORTED = 2;
//
static hyprpaperCoreWallpaperFitMode fitFromString(const std::string_view& sv) {
if (sv == "contain")
return HYPRPAPER_CORE_WALLPAPER_FIT_MODE_CONTAIN;
if (sv == "fit" || sv == "stretch")
return HYPRPAPER_CORE_WALLPAPER_FIT_MODE_STRETCH;
if (sv == "tile")
return HYPRPAPER_CORE_WALLPAPER_FIT_MODE_TILE;
return HYPRPAPER_CORE_WALLPAPER_FIT_MODE_COVER;
}
static std::expected<std::string, std::string> resolvePath(const std::string_view& sv) {
std::error_code ec;
auto can = std::filesystem::canonical(sv, ec);
if (ec)
return std::unexpected(std::format("invalid path: {}", ec.message()));
return can;
}
static std::expected<std::string, std::string> getFullPath(const std::string_view& sv) {
if (sv.empty())
return std::unexpected("empty path");
if (sv[0] == '~') {
static auto HOME = getenv("HOME");
if (!HOME || HOME[0] == '\0')
return std::unexpected("home path but no $HOME");
return resolvePath(std::string{HOME} + "/"s + std::string{sv.substr(1)});
}
return resolvePath(sv);
}
static std::expected<void, std::string> doWallpaper(const std::string_view& RHS) {
CVarList2 args(std::string{RHS}, 0, ',');
const std::string MONITOR = std::string{args[0]};
const auto& PATH_RAW = args[1];
const auto& FIT = args[2];
if (PATH_RAW.empty())
return std::unexpected("not enough args");
const auto RTDIR = getenv("XDG_RUNTIME_DIR");
if (!RTDIR || RTDIR[0] == '\0')
return std::unexpected("can't send: no XDG_RUNTIME_DIR");
const auto HIS = getenv("HYPRLAND_INSTANCE_SIGNATURE");
if (!HIS || HIS[0] == '\0')
return std::unexpected("can't send: no HYPRLAND_INSTANCE_SIGNATURE (not running under hyprland)");
const auto PATH = getFullPath(PATH_RAW);
if (!PATH)
return std::unexpected(std::format("bad path: {}", PATH_RAW));
auto socketPath = RTDIR + "/hypr/"s + HIS + "/"s + SOCKET_NAME;
auto socket = Hyprwire::IClientSocket::open(socketPath);
if (!socket)
return std::unexpected("can't send: failed to connect to hyprpaper (is it running?)");
g_coreImpl = makeShared<CCHyprpaperCoreImpl>(PROTOCOL_VERSION_SUPPORTED);
socket->addImplementation(g_coreImpl);
if (!socket->waitForHandshake())
return std::unexpected("can't send: wire handshake failed");
auto spec = socket->getSpec(g_coreImpl->protocol()->specName());
if (!spec)
return std::unexpected("can't send: hyprpaper doesn't have the spec?!");
auto manager = makeShared<CCHyprpaperCoreManagerObject>(socket->bindProtocol(g_coreImpl->protocol(), std::min(PROTOCOL_VERSION_SUPPORTED, spec->specVer())));
if (!manager)
return std::unexpected("wire error: couldn't create manager");
auto wallpaper = makeShared<CCHyprpaperWallpaperObject>(manager->sendGetWallpaperObject());
if (!wallpaper)
return std::unexpected("wire error: couldn't create wallpaper object");
bool canExit = false;
std::optional<std::string> err;
wallpaper->setFailed([&canExit, &err](uint32_t code) {
canExit = true;
switch (code) {
case HYPRPAPER_CORE_APPLYING_ERROR_INVALID_PATH: err = std::format("failed to set wallpaper: Invalid path", code); break;
case HYPRPAPER_CORE_APPLYING_ERROR_INVALID_MONITOR: err = std::format("failed to set wallpaper: Invalid monitor", code); break;
default: err = std::format("failed to set wallpaper: unknown error, code {}", code); break;
}
});
wallpaper->setSuccess([&canExit]() { canExit = true; });
wallpaper->sendPath(PATH->c_str());
wallpaper->sendMonitorName(MONITOR.c_str());
if (!FIT.empty())
wallpaper->sendFitMode(fitFromString(FIT));
wallpaper->sendApply();
while (!canExit) {
socket->dispatchEvents(true);
}
if (err)
return std::unexpected(*err);
return {};
}
static std::expected<void, std::string> doListActive() {
const auto RTDIR = getenv("XDG_RUNTIME_DIR");
if (!RTDIR || RTDIR[0] == '\0')
return std::unexpected("can't send: no XDG_RUNTIME_DIR");
const auto HIS = getenv("HYPRLAND_INSTANCE_SIGNATURE");
if (!HIS || HIS[0] == '\0')
return std::unexpected("can't send: no HYPRLAND_INSTANCE_SIGNATURE (not running under hyprland)");
auto socketPath = RTDIR + "/hypr/"s + HIS + "/"s + SOCKET_NAME;
auto socket = Hyprwire::IClientSocket::open(socketPath);
if (!socket)
return std::unexpected("can't send: failed to connect to hyprpaper (is it running?)");
g_coreImpl = makeShared<CCHyprpaperCoreImpl>(PROTOCOL_VERSION_SUPPORTED);
socket->addImplementation(g_coreImpl);
if (!socket->waitForHandshake())
return std::unexpected("can't send: wire handshake failed");
auto spec = socket->getSpec(g_coreImpl->protocol()->specName());
if (!spec)
return std::unexpected("can't send: hyprpaper doesn't have the spec?!");
if (spec->specVer() < 2)
return std::unexpected("can't send: hyprpaper protocol version too low (hyprpaper too old)");
auto manager = makeShared<CCHyprpaperCoreManagerObject>(socket->bindProtocol(g_coreImpl->protocol(), std::min(PROTOCOL_VERSION_SUPPORTED, spec->specVer())));
if (!manager)
return std::unexpected("wire error: couldn't create manager");
auto status = makeShared<CCHyprpaperStatusObject>(manager->sendGetStatusObject());
status->setActiveWallpaper([](const char* mon, const char* wp) { std::println("{}: {}", mon, wp); });
socket->roundtrip();
return {};
}
std::expected<void, std::string> Hyprpaper::makeHyprpaperRequest(const std::string_view& rq) {
if (!rq.contains(' '))
return std::unexpected("Invalid request");
if (!rq.starts_with("/hyprpaper "))
return std::unexpected("Invalid request");
std::string_view LHS, RHS;
auto spacePos = rq.find(' ', 12);
LHS = rq.substr(11, spacePos - 11);
RHS = rq.substr(spacePos + 1);
if (LHS == "wallpaper")
return doWallpaper(RHS);
else if (LHS == "listactive")
return doListActive();
else
return std::unexpected("invalid hyprpaper request");
return {};
}

View file

@ -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 URL: https://github.com/hyprwm/Hyprland
Description: Hyprland header files Description: Hyprland header files
Version: @HYPRLAND_VERSION@ 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 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) pkg_check_modules(hyprpm_deps REQUIRED IMPORTED_TARGET tomlplusplus hyprutils>=0.7.0)
find_package(glaze 6.0.1 QUIET) find_package(glaze QUIET)
if (NOT glaze_FOUND) if (NOT glaze_FOUND)
set(GLAZE_VERSION v6.0.1) set(GLAZE_VERSION v5.1.1)
message(STATUS "glaze dependency not found, retrieving ${GLAZE_VERSION} with FetchContent") message(STATUS "glaze dependency not found, retrieving ${GLAZE_VERSION} with FetchContent")
include(FetchContent) include(FetchContent)
FetchContent_Declare( FetchContent_Declare(
@ -21,7 +21,6 @@ if (NOT glaze_FOUND)
GIT_REPOSITORY https://github.com/stephenberry/glaze.git GIT_REPOSITORY https://github.com/stephenberry/glaze.git
GIT_TAG ${GLAZE_VERSION} GIT_TAG ${GLAZE_VERSION}
GIT_SHALLOW TRUE GIT_SHALLOW TRUE
EXCLUDE_FROM_ALL
) )
FetchContent_MakeAvailable(glaze) FetchContent_MakeAvailable(glaze)
endif() endif()

View file

@ -29,8 +29,8 @@ function _hyprpm
set descriptions[6] "Show help menu" set descriptions[6] "Show help menu"
set descriptions[7] "Check and update all plugins if needed" set descriptions[7] "Check and update all plugins if needed"
set descriptions[8] "Install a new plugin repository from git" set descriptions[8] "Install a new plugin repository from git"
set descriptions[9] "Enable too much logging" set descriptions[9] "Enable too much loggin"
set descriptions[10] "Enable too much logging" set descriptions[10] "Enable too much loggin"
set descriptions[11] "Force an operation ignoring checks (e.g. update -f)" set descriptions[11] "Force an operation ignoring checks (e.g. update -f)"
set descriptions[12] "Disable shallow cloning of Hyprland sources" set descriptions[12] "Disable shallow cloning of Hyprland sources"
set descriptions[13] "Remove a plugin repository" 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)" <FLAGS> ::= (--notify | -n) "Send a hyprland notification for important events (e.g. load fail)"
| (--help | -h) "Show help menu" | (--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)" | (--force | -f) "Force an operation ignoring checks (e.g. update -f)"
| (--no-shallow | -s) "Disable shallow cloning of Hyprland sources" | (--no-shallow | -s) "Disable shallow cloning of Hyprland sources"
; ;

View file

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

View file

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

View file

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

View file

@ -6,9 +6,6 @@
#include <sys/un.h> #include <sys/un.h>
#include <unistd.h> #include <unistd.h>
#include <cstring> #include <cstring>
#include <hyprutils/memory/Casts.hpp>
using namespace Hyprutils::Memory;
static int getUID() { static int getUID() {
const auto UID = 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); 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)")); std::println("{}", failureString("Couldn't connect to " + socketPath + ". (4)"));
return ""; 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 url;
std::string rev; std::string rev;
std::string name; std::string name;
std::string author;
std::vector<SPlugin> plugins; std::vector<SPlugin> plugins;
std::string hash; 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 <cstdio>
#include <iostream> #include <iostream>
#include <filesystem> #include <filesystem>
#include <string>
#include <print> #include <print>
#include <fstream> #include <fstream>
#include <algorithm> #include <algorithm>
@ -27,10 +26,8 @@
#include <hyprutils/string/String.hpp> #include <hyprutils/string/String.hpp>
#include <hyprutils/os/Process.hpp> #include <hyprutils/os/Process.hpp>
#include <hyprutils/memory/Casts.hpp>
using namespace Hyprutils::String; using namespace Hyprutils::String;
using namespace Hyprutils::OS; using namespace Hyprutils::OS;
using namespace Hyprutils::Memory;
static std::string execAndGet(std::string cmd) { static std::string execAndGet(std::string cmd) {
cmd += " 2>&1"; cmd += " 2>&1";
@ -79,33 +76,40 @@ SHyprlandVersion CPluginManager::getHyprlandVersion(bool running) {
else else
onceInstalled = true; 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 (!HLVERCALL.contains("Tag:")) {
std::println(stderr, "\n{}", failureString("You don't seem to be running Hyprland."));
if (!jsonQuery) {
std::println("{}", failureString("failed to get the current hyprland version. Are you running hyprland?"));
return SHyprlandVersion{}; return SHyprlandVersion{};
} }
auto hlbranch = (*jsonQuery)["branch"].get_string(); std::string hlcommit = HLVERCALL.substr(HLVERCALL.find("at commit") + 10);
auto hlcommit = (*jsonQuery)["commit"].get_string(); hlcommit = hlcommit.substr(0, hlcommit.find_first_of(' '));
auto abiHash = (*jsonQuery)["abiHash"].get_string();
auto hldate = (*jsonQuery)["commit_date"].get_string();
auto hlcommits = (*jsonQuery)["commits"].get_string();
auto flags = (*jsonQuery)["flags"].get_array(); std::string hlbranch = HLVERCALL.substr(HLVERCALL.find("from branch") + 12);
bool isNix = std::ranges::any_of(flags, [](const auto& f) { return f.is_string() && f.get_string() == std::string_view{"nix"}; }); hlbranch = hlbranch.substr(0, hlbranch.find(" at commit "));
size_t commits = 0; 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 { try {
commits = std::stoull(hlcommits); commits = std::stoi(hlcommits);
} catch (...) { ; } } catch (...) { ; }
if (m_bVerbose) if (m_bVerbose)
std::println("{}", verboseString("parsed commit {} at branch {} on {}, commits {}, nix: {}", hlcommit, hlbranch, hldate, commits, isNix)); std::println("{}", verboseString("parsed commit {} at branch {} on {}, commits {}", hlcommit, hlbranch, hldate, commits));
auto ver = SHyprlandVersion{hlbranch, hlcommit, hldate, abiHash, commits, isNix}; auto ver = SHyprlandVersion{hlbranch, hlcommit, hldate, commits};
if (running) if (running)
verRunning = ver; verRunning = ver;
@ -131,24 +135,16 @@ bool CPluginManager::createSafeDirectory(const std::string& path) {
return true; return true;
} }
bool CPluginManager::validArg(const std::string& s) {
return !s.contains("'") && !s.ends_with("\\") && !s.starts_with("\\");
}
bool CPluginManager::addNewPluginRepo(const std::string& url, const std::string& rev) { bool CPluginManager::addNewPluginRepo(const std::string& url, const std::string& rev) {
const auto HLVER = getHyprlandVersion(); const auto HLVER = getHyprlandVersion();
if (!validArg(url) || !validArg(rev)) {
std::println(stderr, "\n{}", failureString("url or rev invalid"));
return false;
}
if (!hasDeps()) { if (!hasDeps()) {
std::println(stderr, "\n{}", failureString("Could not clone the plugin repository. Dependencies not satisfied. Hyprpm requires: cmake, cpio, pkg-config, git, g++, gcc")); std::println(stderr, "\n{}",
failureString("Could not clone the plugin repository. Dependencies not satisfied. Hyprpm requires: cmake, meson, cpio, pkg-config, git, g++, gcc"));
return false; 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.")); std::println(stderr, "\n{}", failureString("Could not clone the plugin repository. Repository already installed."));
return false; return false;
} }
@ -156,14 +152,14 @@ bool CPluginManager::addNewPluginRepo(const std::string& url, const std::string&
auto GLOBALSTATE = DataState::getGlobalState(); auto GLOBALSTATE = DataState::getGlobalState();
if (!GLOBALSTATE.dontWarnInstall) { if (!GLOBALSTATE.dontWarnInstall) {
std::println("{}!{} Disclaimer: {}", Colors::YELLOW, Colors::RED, Colors::RESET); 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" "Run them at your own risk.\n"
"This message will not appear again."); "This message will not appear again.");
GLOBALSTATE.dontWarnInstall = true; GLOBALSTATE.dontWarnInstall = true;
DataState::updateGlobalState(GLOBALSTATE); 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.")); std::println("\n{}", failureString("Cannot find headers in the global state. Try running hyprpm update first."));
return false; return false;
} }
@ -207,7 +203,7 @@ bool CPluginManager::addNewPluginRepo(const std::string& url, const std::string&
progress.printMessageAbove(infoString("Cloning {}", url)); progress.printMessageAbove(infoString("Cloning {}", url));
std::string ret = execAndGet(std::format("cd {} && git clone --recursive '{}' {}", getTempRoot(), url, USERNAME)); std::string ret = execAndGet(std::format("cd {} && git clone --recursive {} {}", getTempRoot(), url, USERNAME));
if (!std::filesystem::exists(m_szWorkingPluginDirectory + "/.git")) { if (!std::filesystem::exists(m_szWorkingPluginDirectory + "/.git")) {
std::println(stderr, "\n{}", failureString("Could not clone the plugin repository. shell returned:\n{}", ret)); std::println(stderr, "\n{}", failureString("Could not clone the plugin repository. shell returned:\n{}", ret));
@ -314,14 +310,8 @@ bool CPluginManager::addNewPluginRepo(const std::string& url, const std::string&
progress.printMessageAbove(infoString("Building {}", p.name)); progress.printMessageAbove(infoString("Building {}", p.name));
for (auto const& bs : p.buildSteps) { for (auto const& bs : p.buildSteps) {
const auto CMD_RAW = nixDevelopIfNeeded(std::format("cd {} && PKG_CONFIG_PATH=\"{}\" {}", m_szWorkingPluginDirectory, getPkgConfigPath(), bs), HLVER); const std::string& cmd = std::format("cd {} && PKG_CONFIG_PATH=\"{}/share/pkgconfig\" {}", m_szWorkingPluginDirectory, DataState::getHeadersPath(), bs);
out += " -> " + cmd + "\n" + execAndGet(cmd) + "\n";
if (!CMD_RAW) {
progress.printMessageAbove(failureString("Failed to build {}: {}", p.name, CMD_RAW.error()));
break;
}
out += " -> " + *CMD_RAW + "\n" + execAndGet(*CMD_RAW) + "\n";
} }
if (m_bVerbose) if (m_bVerbose)
@ -351,10 +341,7 @@ bool CPluginManager::addNewPluginRepo(const std::string& url, const std::string&
std::string repohash = execAndGet("cd " + m_szWorkingPluginDirectory + " && git rev-parse HEAD"); std::string repohash = execAndGet("cd " + m_szWorkingPluginDirectory + " && git rev-parse HEAD");
if (repohash.length() > 0) if (repohash.length() > 0)
repohash.pop_back(); repohash.pop_back();
auto lastSlash = url.find_last_of('/'); repo.name = pManifest->m_repository.name.empty() ? url.substr(url.find_last_of('/') + 1) : pManifest->m_repository.name;
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.url = url;
repo.rev = rev; repo.rev = rev;
repo.hash = repohash; repo.hash = repohash;
@ -377,13 +364,13 @@ bool CPluginManager::addNewPluginRepo(const std::string& url, const std::string&
return true; return true;
} }
bool CPluginManager::removePluginRepo(const SPluginRepoIdentifier identifier) { bool CPluginManager::removePluginRepo(const std::string& urlOrName) {
if (!DataState::pluginRepoExists(identifier)) { if (!DataState::pluginRepoExists(urlOrName)) {
std::println(stderr, "\n{}", failureString("Could not remove the repository. Repository is not installed.")); std::println(stderr, "\n{}", failureString("Could not remove the repository. Repository is not installed."));
return false; 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] "; << "Are you sure? [Y/n] ";
std::fflush(stdout); std::fflush(stdout);
std::string input; std::string input;
@ -394,7 +381,7 @@ bool CPluginManager::removePluginRepo(const SPluginRepoIdentifier identifier) {
return false; return false;
} }
DataState::removePluginRepo(identifier); DataState::removePluginRepo(urlOrName);
return true; return true;
} }
@ -406,7 +393,7 @@ eHeadersErrors CPluginManager::headersValid() {
return HEADERS_MISSING; return HEADERS_MISSING;
// find headers commit // find headers commit
const std::string& cmd = std::format("PKG_CONFIG_PATH=\"{}\" pkgconf --cflags --keep-system-cflags hyprland", getPkgConfigPath()); const std::string& cmd = std::format("PKG_CONFIG_PATH=\"{}/share/pkgconfig\" pkgconf --cflags --keep-system-cflags hyprland", DataState::getHeadersPath());
auto headers = execAndGet(cmd); auto headers = execAndGet(cmd);
if (!headers.contains("-I/")) if (!headers.contains("-I/"))
@ -455,22 +442,17 @@ eHeadersErrors CPluginManager::headersValid() {
if (hash != HLVER.hash) if (hash != HLVER.hash)
return HEADERS_MISMATCHED; return HEADERS_MISMATCHED;
// check ABI hash too
const auto GLOBALSTATE = DataState::getGlobalState();
if (GLOBALSTATE.headersAbiCompiled != HLVER.abiHash)
return HEADERS_ABI_MISMATCH;
return HEADERS_OK; return HEADERS_OK;
} }
bool CPluginManager::updateHeaders(bool force) { bool CPluginManager::updateHeaders(bool force) {
DataState::ensureStateStoreExists(); DataState::ensureStateStoreExists();
const auto HLVER = getHyprlandVersion(false); const auto HLVER = getHyprlandVersion(false);
if (!hasDeps()) { if (!hasDeps()) {
std::println("\n{}", failureString("Could not update. Dependencies not satisfied. Hyprpm requires: cmake, cpio, pkg-config, git, g++, gcc")); std::println("\n{}", failureString("Could not update. Dependencies not satisfied. Hyprpm requires: cmake, meson, cpio, pkg-config, git, g++, gcc"));
return false; return false;
} }
@ -512,11 +494,11 @@ bool CPluginManager::updateHeaders(bool force) {
progress.printMessageAbove(verboseString("will shallow since: {}", SHALLOW_DATE)); progress.printMessageAbove(verboseString("will shallow since: {}", SHALLOW_DATE));
std::string ret = std::string ret =
execAndGet(std::format("cd {} && git clone --recursive '{}' hyprland-{}{}", getTempRoot(), HL_URL, USERNAME, (bShallow ? " --shallow-since='" + SHALLOW_DATE + "'" : ""))); execAndGet(std::format("cd {} && git clone --recursive {} hyprland-{}{}", getTempRoot(), HL_URL, USERNAME, (bShallow ? " --shallow-since='" + SHALLOW_DATE + "'" : "")));
if (!std::filesystem::exists(WORKINGDIR)) { if (!std::filesystem::exists(WORKINGDIR)) {
progress.printMessageAbove(failureString("Clone failed. Retrying without shallow.")); progress.printMessageAbove(failureString("Clone failed. Retrying without shallow."));
ret = execAndGet(std::format("cd {} && git clone --recursive '{}' hyprland-{}", getTempRoot(), HL_URL, USERNAME)); ret = execAndGet(std::format("cd {} && git clone --recursive {} hyprland-{}", getTempRoot(), HL_URL, USERNAME));
} }
if (!std::filesystem::exists(WORKINGDIR + "/.git")) { if (!std::filesystem::exists(WORKINGDIR + "/.git")) {
@ -559,17 +541,8 @@ bool CPluginManager::updateHeaders(bool force) {
if (m_bVerbose) if (m_bVerbose)
progress.printMessageAbove(verboseString("setting PREFIX for cmake to {}", DataState::getHeadersPath())); progress.printMessageAbove(verboseString("setting PREFIX for cmake to {}", DataState::getHeadersPath()));
const auto CONFIGURE_CMD = ret = execAndGet(std::format("cd {} && cmake --no-warn-unused-cli -DCMAKE_BUILD_TYPE:STRING=Release -DCMAKE_INSTALL_PREFIX:STRING=\"{}\" -S . -B ./build -G Ninja", WORKINGDIR,
nixDevelopIfNeeded(std::format("cd {} && cmake --no-warn-unused-cli -DCMAKE_BUILD_TYPE:STRING=Release -DCMAKE_INSTALL_PREFIX:STRING=\"{}\" -S . -B ./build", WORKINGDIR, DataState::getHeadersPath()));
DataState::getHeadersPath()),
HLVER);
if (!CONFIGURE_CMD) {
std::println(stderr, "\n{}", failureString("Could not configure hyprland: {}", CONFIGURE_CMD.error()));
return false;
}
ret = execAndGet(*CONFIGURE_CMD);
if (m_bVerbose) if (m_bVerbose)
progress.printMessageAbove(verboseString("cmake returned: {}", ret)); progress.printMessageAbove(verboseString("cmake returned: {}", ret));
@ -614,20 +587,19 @@ bool CPluginManager::updateHeaders(bool force) {
std::filesystem::remove_all(WORKINGDIR); std::filesystem::remove_all(WORKINGDIR);
auto HEADERSVALID = headersValid(); auto HEADERSVALID = headersValid();
if (HEADERSVALID == HEADERS_OK) {
if (HEADERSVALID == HEADERS_OK || HEADERSVALID == HEADERS_MISMATCHED || HEADERSVALID == HEADERS_ABI_MISMATCH) {
progress.printMessageAbove(successString("installed headers")); progress.printMessageAbove(successString("installed headers"));
progress.m_iSteps = 5; progress.m_iSteps = 5;
progress.m_szCurrentMessage = "Done!"; progress.m_szCurrentMessage = "Done!";
progress.print(); progress.print();
auto GLOBALSTATE = DataState::getGlobalState(); auto GLOBALSTATE = DataState::getGlobalState();
GLOBALSTATE.headersAbiCompiled = HLVER.abiHash; GLOBALSTATE.headersHashCompiled = HLVER.hash;
DataState::updateGlobalState(GLOBALSTATE); DataState::updateGlobalState(GLOBALSTATE);
std::print("\n"); std::print("\n");
} else { } 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.printMessageAbove(infoString("if the problem persists, try running hyprpm purge-cache."));
progress.m_iSteps = 5; progress.m_iSteps = 5;
progress.m_szCurrentMessage = "Failed"; progress.m_szCurrentMessage = "Failed";
@ -657,7 +629,7 @@ bool CPluginManager::updatePlugins(bool forceUpdateAll) {
const auto HLVER = getHyprlandVersion(false); const auto HLVER = getHyprlandVersion(false);
CProgressBar progress; CProgressBar progress;
progress.m_iMaxSteps = (REPOS.size() * 2) + 2; progress.m_iMaxSteps = REPOS.size() * 2 + 2;
progress.m_iSteps = 0; progress.m_iSteps = 0;
progress.m_szCurrentMessage = "Updating repositories"; progress.m_szCurrentMessage = "Updating repositories";
progress.print(); progress.print();
@ -678,7 +650,7 @@ bool CPluginManager::updatePlugins(bool forceUpdateAll) {
progress.printMessageAbove(infoString("Cloning {}", repo.url)); progress.printMessageAbove(infoString("Cloning {}", repo.url));
std::string ret = execAndGet(std::format("cd {} && git clone --recursive '{}' {}", getTempRoot(), repo.url, USERNAME)); std::string ret = execAndGet(std::format("cd {} && git clone --recursive {} {}", getTempRoot(), repo.url, USERNAME));
if (!std::filesystem::exists(m_szWorkingPluginDirectory + "/.git")) { if (!std::filesystem::exists(m_szWorkingPluginDirectory + "/.git")) {
std::println("{}", failureString("could not clone repo: shell returned: {}", ret)); std::println("{}", failureString("could not clone repo: shell returned: {}", ret));
@ -688,7 +660,7 @@ bool CPluginManager::updatePlugins(bool forceUpdateAll) {
if (!repo.rev.empty()) { if (!repo.rev.empty()) {
progress.printMessageAbove(infoString("Plugin has revision set, resetting: {}", repo.rev)); progress.printMessageAbove(infoString("Plugin has revision set, resetting: {}", repo.rev));
std::string ret = execAndGet("git -C " + m_szWorkingPluginDirectory + " reset --hard --recurse-submodules \'" + repo.rev + "\'"); std::string ret = execAndGet("git -C " + m_szWorkingPluginDirectory + " reset --hard --recurse-submodules " + repo.rev);
if (ret.compare(0, 6, "fatal:") == 0) { if (ret.compare(0, 6, "fatal:") == 0) {
std::println(stderr, "\n{}", failureString("could not check out revision {}: shell returned:\n{}", repo.rev, ret)); std::println(stderr, "\n{}", failureString("could not check out revision {}: shell returned:\n{}", repo.rev, ret));
@ -767,14 +739,8 @@ bool CPluginManager::updatePlugins(bool forceUpdateAll) {
progress.printMessageAbove(infoString("Building {}", p.name)); progress.printMessageAbove(infoString("Building {}", p.name));
for (auto const& bs : p.buildSteps) { for (auto const& bs : p.buildSteps) {
const auto CMD_RAW = nixDevelopIfNeeded(std::format("cd {} && PKG_CONFIG_PATH=\"{}\" {}", m_szWorkingPluginDirectory, getPkgConfigPath(), bs), HLVER); const std::string& cmd = std::format("cd {} && PKG_CONFIG_PATH=\"{}/share/pkgconfig\" {}", m_szWorkingPluginDirectory, DataState::getHeadersPath(), bs);
out += " -> " + cmd + "\n" + execAndGet(cmd) + "\n";
if (!CMD_RAW) {
progress.printMessageAbove(failureString("Failed to build {}: {}", p.name, CMD_RAW.error()));
break;
}
out += " -> " + *CMD_RAW + "\n" + execAndGet(*CMD_RAW) + "\n";
} }
if (m_bVerbose) if (m_bVerbose)
@ -804,10 +770,10 @@ bool CPluginManager::updatePlugins(bool forceUpdateAll) {
repohash.pop_back(); repohash.pop_back();
newrepo.hash = repohash; newrepo.hash = repohash;
for (auto const& p : pManifest->m_plugins) { for (auto const& p : pManifest->m_plugins) {
const auto OLDPLUGINIT = std::ranges::find_if(repo.plugins, [&](const auto& other) { return other.name == p.name; }); const auto OLDPLUGINIT = std::find_if(repo.plugins.begin(), repo.plugins.end(), [&](const auto& other) { return other.name == p.name; });
newrepo.plugins.emplace_back(SPlugin{p.name, m_szWorkingPluginDirectory + "/" + p.output, OLDPLUGINIT != repo.plugins.end() ? OLDPLUGINIT->enabled : false}); 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); DataState::addNewPluginRepo(newrepo);
std::filesystem::remove_all(m_szWorkingPluginDirectory); std::filesystem::remove_all(m_szWorkingPluginDirectory);
@ -820,7 +786,7 @@ bool CPluginManager::updatePlugins(bool forceUpdateAll) {
progress.print(); progress.print();
auto GLOBALSTATE = DataState::getGlobalState(); auto GLOBALSTATE = DataState::getGlobalState();
GLOBALSTATE.headersAbiCompiled = HLVER.abiHash; GLOBALSTATE.headersHashCompiled = HLVER.hash;
DataState::updateGlobalState(GLOBALSTATE); DataState::updateGlobalState(GLOBALSTATE);
progress.m_iSteps++; progress.m_iSteps++;
@ -832,23 +798,17 @@ bool CPluginManager::updatePlugins(bool forceUpdateAll) {
return true; return true;
} }
bool CPluginManager::enablePlugin(const SPluginRepoIdentifier identifier) { bool CPluginManager::enablePlugin(const std::string& name) {
bool ret = false; bool ret = DataState::setPluginEnabled(name, true);
switch (identifier.type) {
case IDENTIFIER_NAME:
case IDENTIFIER_AUTHOR_NAME: ret = DataState::setPluginEnabled(identifier, true); break;
default: return false;
}
if (ret) if (ret)
std::println("{}", successString("Enabled {}", identifier.name)); std::println("{}", successString("Enabled {}", name));
return ret; return ret;
} }
bool CPluginManager::disablePlugin(const SPluginRepoIdentifier identifier) { bool CPluginManager::disablePlugin(const std::string& name) {
bool ret = DataState::setPluginEnabled(identifier, false); bool ret = DataState::setPluginEnabled(name, false);
if (ret) if (ret)
std::println("{}", successString("Disabled {}", identifier.name)); std::println("{}", successString("Disabled {}", name));
return ret; return ret;
} }
@ -866,7 +826,7 @@ ePluginLoadStateReturn CPluginManager::ensurePluginsLoadState(bool forceReload)
} }
const auto HYPRPMPATH = DataState::getDataStatePath(); 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) { if (!json) {
std::println(stderr, "PluginManager: couldn't parse plugin list output"); std::println(stderr, "PluginManager: couldn't parse plugin list output");
return LOADSTATE_FAIL; return LOADSTATE_FAIL;
@ -931,7 +891,7 @@ ePluginLoadStateReturn CPluginManager::ensurePluginsLoadState(bool forceReload)
if (!p.enabled) if (!p.enabled)
continue; continue;
if (!forceReload && std::ranges::find_if(loadedPlugins, [&](const auto& other) { return other == p.name; }) != loadedPlugins.end()) if (!forceReload && std::find_if(loadedPlugins.begin(), loadedPlugins.end(), [&](const auto& other) { return other == p.name; }) != loadedPlugins.end())
continue; continue;
if (!loadUnloadPlugin(HYPRPMPATH / repoForName(p.name) / p.filename, true)) { if (!loadUnloadPlugin(HYPRPMPATH / repoForName(p.name) / p.filename, true)) {
@ -951,9 +911,9 @@ bool CPluginManager::loadUnloadPlugin(const std::string& path, bool load) {
auto state = DataState::getGlobalState(); auto state = DataState::getGlobalState();
auto HLVER = getHyprlandVersion(true); auto HLVER = getHyprlandVersion(true);
if (state.headersAbiCompiled != HLVER.abiHash) { if (state.headersHashCompiled != HLVER.hash) {
if (load) 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; return false;
} }
@ -969,7 +929,7 @@ void CPluginManager::listAllPlugins() {
const auto REPOS = DataState::getAllRepositories(); const auto REPOS = DataState::getAllRepositories();
for (auto const& r : REPOS) { 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) { for (auto const& p : r.plugins) {
std::println(" │ Plugin {}", p.name); std::println(" │ Plugin {}", p.name);
@ -985,7 +945,7 @@ void CPluginManager::listAllPlugins() {
} }
void CPluginManager::notify(const eNotifyIcons icon, uint32_t color, int durationMs, const std::string& message) { 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) { std::string CPluginManager::headerError(const eHeadersErrors err) {
@ -994,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_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_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_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: { case HEADERS_DUPLICATED: {
return failureString("Headers duplicated!!! This is a very bad sign.\n" 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" "This could be due to e.g. installing hyprland manually while a system package of hyprland is also installed.\n"
@ -1019,11 +978,8 @@ std::string CPluginManager::headerErrorShort(const eHeadersErrors err) {
} }
bool CPluginManager::hasDeps() { bool CPluginManager::hasDeps() {
if (!m_bNoNix && getHyprlandVersion().isNix)
return true; // dep check not needed if we are on nix
bool hasAllDeps = true; bool hasAllDeps = true;
std::vector<std::string> deps = {"cpio", "cmake", "pkg-config", "g++", "gcc", "git"}; std::vector<std::string> deps = {"meson", "cpio", "cmake", "pkg-config", "g++", "gcc", "git"};
for (auto const& d : deps) { for (auto const& d : deps) {
if (!execAndGet("command -v " + d).contains("/")) { if (!execAndGet("command -v " + d).contains("/")) {
@ -1034,92 +990,3 @@ bool CPluginManager::hasDeps() {
return hasAllDeps; return hasAllDeps;
} }
const std::string& CPluginManager::getPkgConfigPath() {
static const auto str = std::format("{}/share/pkgconfig:$PKG_CONFIG_PATH", DataState::getHeadersPath());
return str;
}
static std::expected<std::string, std::string> getNixDevelopFromPath(const std::string& argv0) {
std::string fullStorePath;
if (argv0.starts_with("/")) {
// we can use this directly
fullStorePath = argv0;
} else {
// use hyprpm, find in path
auto exe = NSys::findInPath("hyprpm");
if (!exe)
return std::unexpected("hyprpm not found in PATH");
fullStorePath = *exe;
}
if (fullStorePath.empty() || !fullStorePath.ends_with("/bin/hyprpm"))
return std::unexpected("couldn't get a real path for hyprpm (1)");
// canonicalize to get the real nix-store path
std::error_code ec;
fullStorePath = std::filesystem::canonical(fullStorePath, ec);
if (ec || fullStorePath.empty() || !fullStorePath.starts_with("/nix"))
return std::unexpected("couldn't get a real path for hyprpm");
fullStorePath = fullStorePath.substr(0, fullStorePath.length() - std::string_view{"/bin/hyprpm"}.length());
auto deriver = trim(execAndGet(std::format("echo \"$(nix-store --query --deriver '{}')\"", fullStorePath)));
if (deriver.starts_with("unknown"))
return std::unexpected("couldn't nix deriver");
return deriver;
}
static std::expected<std::string, std::string> getNixDevelopFromProfile() {
const auto NIX_PROFILE_STR = execAndGet("nix profile list --json");
auto rawJson = glz::read_json<glz::generic>(NIX_PROFILE_STR);
if (!rawJson)
return std::unexpected("failed to parse nix profile list --json");
auto& json = *rawJson;
if (!json.contains("elements") || !json["elements"].is_object())
return std::unexpected("nix profile list --json returned a wonky json");
if (!json["elements"].contains("hyprland") && !json["elements"].contains("Hyprland"))
return std::unexpected("nix profile list --json doesn't contain Hyprland (did you uninstall?)");
auto& hyprlandJson = json["elements"].contains("hyprland") ? json["elements"]["hyprland"] : json["elements"]["Hyprland"];
if (!hyprlandJson.contains("originalUrl"))
return std::unexpected("nix profile list --json's hyprland doesn't contain originalUrl?");
return hyprlandJson["originalUrl"].get_string();
}
std::expected<std::string, std::string> CPluginManager::nixDevelopIfNeeded(const std::string& cmd, const SHyprlandVersion& ver) {
if (m_bNoNix || !ver.isNix)
return cmd;
// Escape single quotes
std::string newCmd = cmd;
replaceInString(newCmd, "'", "\\'");
auto NIX_DEVELOP = getNixDevelopFromPath(m_szArgv0);
if (NIX_DEVELOP)
return std::format("nix develop '{}' --command bash -c $'{}'", *NIX_DEVELOP, newCmd);
else if (m_bVerbose)
std::println("{}", verboseString("Failed nix from path: {}", NIX_DEVELOP.error()));
NIX_DEVELOP = getNixDevelopFromProfile();
if (NIX_DEVELOP)
return std::format("nix develop '{}' --command bash -c $'{}'", *NIX_DEVELOP, newCmd);
else if (m_bVerbose)
std::println("{}", verboseString("Failed nix from profile: {}", NIX_DEVELOP.error()));
return std::unexpected("hyprland is nix, but hyprpm failed to obtain a nix develop shell for build cmd");
}

View file

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

View file

@ -8,7 +8,6 @@
#include <print> #include <print>
#include <filesystem> #include <filesystem>
#include <algorithm> #include <algorithm>
#include <sstream>
#include <hyprutils/os/Process.hpp> #include <hyprutils/os/Process.hpp>
#include <hyprutils/string/VarList.hpp> #include <hyprutils/string/VarList.hpp>
@ -35,13 +34,9 @@ static std::string validSubinsAsStr() {
} }
static bool executableExistsInPath(const std::string& exe) { static bool executableExistsInPath(const std::string& exe) {
return NSys::findInPath(exe).has_value();
}
std::optional<std::string> NSys::findInPath(const std::string& exe) {
const char* PATHENV = std::getenv("PATH"); const char* PATHENV = std::getenv("PATH");
if (!PATHENV) if (!PATHENV)
return std::nullopt; return false;
CVarList paths(PATHENV, 0, ':', true); CVarList paths(PATHENV, 0, ':', true);
std::error_code ec; std::error_code ec;
@ -56,10 +51,10 @@ std::optional<std::string> NSys::findInPath(const std::string& exe) {
if (ec) if (ec)
continue; continue;
if ((perms & std::filesystem::perms::others_exec) != std::filesystem::perms::none) if ((perms & std::filesystem::perms::others_exec) != std::filesystem::perms::none)
return candidate.string(); return true;
} }
return std::nullopt; return false;
} }
static std::string subin() { static std::string subin() {

View file

@ -1,13 +1,11 @@
#pragma once #pragma once
#include <string> #include <string>
#include <optional>
namespace NSys { namespace NSys {
bool isSuperuser(); bool isSuperuser();
int getUID(); int getUID();
int getEUID(); int getEUID();
std::optional<std::string> findInPath(const std::string& exe);
// NOLINTNEXTLINE // NOLINTNEXTLINE
namespace root { namespace root {

View file

@ -13,11 +13,11 @@ using namespace Hyprutils::Utils;
constexpr std::string_view HELP = R"#(┏ hyprpm, a Hyprland Plugin Manager constexpr std::string_view HELP = R"#(┏ hyprpm, a Hyprland Plugin Manager
add <url> [git rev] Install a new plugin repository from git. Git revision add [url] [git rev] Install a new plugin repository from git. Git revision
is optional, when set, commit locks are ignored. is optional, when set, commit locks are ignored.
remove <url|name|author/name> Remove an installed plugin repository. remove [url/name] Remove an installed plugin repository.
enable <name|author/name> Enable a plugin. enable [name] Enable a plugin.
disable <name|author/name> Disable a plugin. disable [name] Disable a plugin.
update Check and update all plugins if needed. update Check and update all plugins if needed.
reload Reload hyprpm state. Ensure all enabled plugins are loaded. reload Reload hyprpm state. Ensure all enabled plugins are loaded.
list List all installed plugins. list List all installed plugins.
@ -25,9 +25,8 @@ constexpr std::string_view HELP = R"#(┏ hyprpm, a Hyprland Plugin Manager
Flags: Flags:
--no-nix | Disable `nix develop` for build commands, even if Hyprland is nix. --notify | -n Send a hyprland notification for important events (including both successes and fail events).
--notify | -n Send a hyprland notification confirming successful plugin load. --notify-fail | -nn Send a hyprland notification for fail events only.
Warnings/Errors trigger notifications regardless of this flag.
--help | -h Show this menu. --help | -h Show this menu.
--verbose | -v Enable too much logging. --verbose | -v Enable too much logging.
--force | -f Force an operation ignoring checks (e.g. update -f). --force | -f Force an operation ignoring checks (e.g. update -f).
@ -48,7 +47,7 @@ int main(int argc, char** argv, char** envp) {
} }
std::vector<std::string> command; std::vector<std::string> command;
bool notify = false, verbose = false, force = false, noShallow = false, noNix = false; bool notify = false, notifyFail = false, verbose = false, force = false, noShallow = false;
std::string customHlUrl; std::string customHlUrl;
for (int i = 1; i < argc; ++i) { for (int i = 1; i < argc; ++i) {
@ -59,13 +58,9 @@ int main(int argc, char** argv, char** envp) {
} else if (ARGS[i] == "--notify" || ARGS[i] == "-n") { } else if (ARGS[i] == "--notify" || ARGS[i] == "-n") {
notify = true; notify = true;
} else if (ARGS[i] == "--notify-fail" || ARGS[i] == "-nn") { } else if (ARGS[i] == "--notify-fail" || ARGS[i] == "-nn") {
// TODO: Deprecated since v.053.0. Remove in version>0.56.0 notifyFail = notify = true;
std::println(stderr, "{}", failureString("Deprececated flag."));
g_pPluginManager->notify(ICON_INFO, 0, 10000, "[hyprpm] -n flag is deprecated, see hyprpm --help.");
} else if (ARGS[i] == "--verbose" || ARGS[i] == "-v") { } else if (ARGS[i] == "--verbose" || ARGS[i] == "-v") {
verbose = true; verbose = true;
} else if (ARGS[i] == "--no-nix") {
noNix = true;
} else if (ARGS[i] == "--no-shallow" || ARGS[i] == "-s") { } else if (ARGS[i] == "--no-shallow" || ARGS[i] == "-s") {
noShallow = true; noShallow = true;
} else if (ARGS[i] == "--hl-url") { } else if (ARGS[i] == "--hl-url") {
@ -94,9 +89,7 @@ int main(int argc, char** argv, char** envp) {
g_pPluginManager = std::make_unique<CPluginManager>(); g_pPluginManager = std::make_unique<CPluginManager>();
g_pPluginManager->m_bVerbose = verbose; g_pPluginManager->m_bVerbose = verbose;
g_pPluginManager->m_bNoShallow = noShallow; g_pPluginManager->m_bNoShallow = noShallow;
g_pPluginManager->m_bNoNix = noNix;
g_pPluginManager->m_szCustomHlUrl = customHlUrl; g_pPluginManager->m_szCustomHlUrl = customHlUrl;
g_pPluginManager->m_szArgv0 = argv[0];
if (command[0] == "add") { if (command[0] == "add") {
if (command.size() < 2) { if (command.size() < 2) {
@ -111,7 +104,7 @@ int main(int argc, char** argv, char** envp) {
const auto HLVER = g_pPluginManager->getHyprlandVersion(); const auto HLVER = g_pPluginManager->getHyprlandVersion();
auto GLOBALSTATE = DataState::getGlobalState(); auto GLOBALSTATE = DataState::getGlobalState();
if (GLOBALSTATE.headersAbiCompiled != HLVER.abiHash) { if (GLOBALSTATE.headersHashCompiled != HLVER.hash) {
std::println(stderr, "{}", failureString("Headers outdated, please run hyprpm update.")); std::println(stderr, "{}", failureString("Headers outdated, please run hyprpm update."));
return 1; return 1;
} }
@ -131,7 +124,7 @@ int main(int argc, char** argv, char** envp) {
NSys::root::cacheSudo(); NSys::root::cacheSudo();
CScopeGuard x([] { NSys::root::dropSudo(); }); 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") { } else if (command[0] == "update") {
NSys::root::cacheSudo(); NSys::root::cacheSudo();
CScopeGuard x([] { NSys::root::dropSudo(); }); CScopeGuard x([] { NSys::root::dropSudo(); });
@ -142,7 +135,7 @@ int main(int argc, char** argv, char** envp) {
if (headers) { if (headers) {
const auto HLVER = g_pPluginManager->getHyprlandVersion(false); const auto HLVER = g_pPluginManager->getHyprlandVersion(false);
auto GLOBALSTATE = DataState::getGlobalState(); 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); bool ret1 = g_pPluginManager->updatePlugins(!headersValid || force || COMPILEDOUTDATED);
@ -156,16 +149,15 @@ int main(int argc, char** argv, char** envp) {
if (ret2 != LOADSTATE_OK) if (ret2 != LOADSTATE_OK)
return 1; return 1;
} else { } else if (notify)
g_pPluginManager->notify(ICON_ERROR, 0, 10000, "[hyprpm] Couldn't update headers"); g_pPluginManager->notify(ICON_ERROR, 0, 10000, "[hyprpm] Couldn't update headers");
}
} else if (command[0] == "enable") { } else if (command[0] == "enable") {
if (command.size() < 2) { if (command.size() < 2) {
std::println(stderr, "{}", failureString("Not enough args for enable.")); std::println(stderr, "{}", failureString("Not enough args for enable."));
return 1; 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?)")); std::println(stderr, "{}", failureString("Couldn't enable plugin (missing?)"));
return 1; return 1;
} }
@ -186,7 +178,7 @@ int main(int argc, char** argv, char** envp) {
return 1; 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?)")); std::println(stderr, "{}", failureString("Couldn't disable plugin (missing?)"));
return 1; return 1;
} }
@ -202,6 +194,7 @@ int main(int argc, char** argv, char** envp) {
auto ret = g_pPluginManager->ensurePluginsLoadState(force); auto ret = g_pPluginManager->ensurePluginsLoadState(force);
if (ret != LOADSTATE_OK) { if (ret != LOADSTATE_OK) {
if (notify) {
switch (ret) { switch (ret) {
case LOADSTATE_FAIL: case LOADSTATE_FAIL:
case LOADSTATE_PARTIAL_FAIL: g_pPluginManager->notify(ICON_ERROR, 0, 10000, "[hyprpm] Failed to load plugins"); break; case LOADSTATE_PARTIAL_FAIL: g_pPluginManager->notify(ICON_ERROR, 0, 10000, "[hyprpm] Failed to load plugins"); break;
@ -210,9 +203,10 @@ int main(int argc, char** argv, char** envp) {
break; break;
default: break; default: break;
} }
}
return 1; return 1;
} else if (notify) { } else if (notify && !notifyFail) {
g_pPluginManager->notify(ICON_OK, 0, 4000, "[hyprpm] Loaded plugins"); g_pPluginManager->notify(ICON_OK, 0, 4000, "[hyprpm] Loaded plugins");
} }
} else if (command[0] == "purge-cache") { } 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 <algorithm>
#include <sstream> #include <sstream>
#include <hyprutils/memory/Casts.hpp>
#include "../helpers/Colors.hpp"
using namespace Hyprutils::Memory; #include "../helpers/Colors.hpp"
static winsize getTerminalSize() { static winsize getTerminalSize() {
winsize w{}; winsize w{};
@ -46,7 +44,7 @@ void CProgressBar::print() {
percentDone = m_fPercentage; percentDone = m_fPercentage;
else { else {
// check for divide-by-zero // 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) // clamp to ensure no overflows (sanity check)
percentDone = std::clamp(percentDone, 0.0f, 1.0f); percentDone = std::clamp(percentDone, 0.0f, 1.0f);
@ -56,7 +54,7 @@ void CProgressBar::print() {
std::ostringstream oss; std::ostringstream oss;
oss << ' ' << Colors::GREEN; 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; size_t i = 0;
for (; i < filled; ++i) for (; i < filled; ++i)
@ -71,7 +69,7 @@ void CProgressBar::print() {
oss << Colors::RESET; oss << Colors::RESET;
if (m_fPercentage >= 0.0f) if (m_fPercentage >= 0.0f)
oss << " " << std::format("{}%", sc<int>(percentDone * 100.0)) << ' '; oss << " " << std::format("{}%", static_cast<int>(percentDone * 100.0)) << ' ';
else else
oss << " " << std::format("{} / {}", m_iSteps, m_iMaxSteps) << ' '; oss << " " << std::format("{} / {}", m_iSteps, m_iMaxSteps) << ' ';

View file

@ -5,7 +5,6 @@ project(hyprtester DESCRIPTION "Hyprland test suite")
include(GNUInstallDirs) include(GNUInstallDirs)
set(CMAKE_CXX_STANDARD 26) set(CMAKE_CXX_STANDARD 26)
set(CMAKE_EXPORT_COMPILE_COMMANDS TRUE)
find_package(PkgConfig REQUIRED) find_package(PkgConfig REQUIRED)
@ -29,76 +28,3 @@ install(FILES ${CMAKE_CURRENT_SOURCE_DIR}/test.conf
install(FILES ${CMAKE_CURRENT_SOURCE_DIR}/plugin/hyprtestplugin.so install(FILES ${CMAKE_CURRENT_SOURCE_DIR}/plugin/hyprtestplugin.so
DESTINATION ${CMAKE_INSTALL_PREFIX}/lib) 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)
protocolnew("unstable/keyboard-shortcuts-inhibit" "keyboard-shortcuts-inhibit-unstable-v1" false)
clientNew("pointer-warp" PROTOS "pointer-warp-v1" "xdg-shell")
clientNew("pointer-scroll" PROTOS "xdg-shell")
clientNew("child-window" PROTOS "xdg-shell")
clientNew("shortcut-inhibitor" PROTOS "xdg-shell" "keyboard-shortcuts-inhibit-unstable-v1")

View file

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

View file

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

View file

@ -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` INCLUDES = `pkg-config --cflags pixman-1 libdrm pangocairo libinput libudev wayland-server xkbcommon`
LIBS = `pkg-config --libs pangocairo` LIBS = `pkg-config --libs pangocairo`

View file

@ -6,23 +6,11 @@
#define private public #define private public
#include <src/config/ConfigManager.hpp> #include <src/config/ConfigManager.hpp>
#include <src/config/ConfigDescriptions.hpp> #include <src/config/ConfigDescriptions.hpp>
#include <src/managers/input/InputManager.hpp> #include <src/layout/IHyprLayout.hpp>
#include <src/managers/PointerManager.hpp> #include <src/managers/LayoutManager.hpp>
#include <src/managers/input/trackpad/TrackpadGestures.hpp>
#include <src/desktop/rule/windowRule/WindowRuleEffectContainer.hpp>
#include <src/desktop/rule/layerRule/LayerRuleEffectContainer.hpp>
#include <src/desktop/rule/windowRule/WindowRuleApplicator.hpp>
#include <src/desktop/view/LayerSurface.hpp>
#include <src/Compositor.hpp> #include <src/Compositor.hpp>
#include <src/desktop/state/FocusState.hpp>
#include <src/layout/LayoutManager.hpp>
#undef private #undef private
#include <hyprutils/utils/ScopeGuard.hpp>
#include <hyprutils/string/VarList.hpp>
using namespace Hyprutils::Utils;
using namespace Hyprutils::String;
#include "globals.hpp" #include "globals.hpp"
// Do NOT change this function. // Do NOT change this function.
@ -47,306 +35,15 @@ static SDispatchResult test(std::string in) {
// Trigger a snap move event for the active window // Trigger a snap move event for the active window
static SDispatchResult snapMove(std::string in) { static SDispatchResult snapMove(std::string in) {
const auto PLASTWINDOW = Desktop::focusState()->window(); const auto PLASTWINDOW = g_pCompositor->m_lastWindow.lock();
if (!PLASTWINDOW->m_isFloating) if (!PLASTWINDOW->m_isFloating)
return {.success = false, .error = "Window must be floating"}; return {.success = false, .error = "Window must be floating"};
Vector2D pos = PLASTWINDOW->m_realPosition->goal(); Vector2D pos = PLASTWINDOW->m_realPosition->goal();
Vector2D size = PLASTWINDOW->m_realSize->goal(); Vector2D size = PLASTWINDOW->m_realSize->goal();
g_layoutManager->performSnap(pos, size, PLASTWINDOW->layoutTarget(), MBIND_MOVE, -1, size); g_pLayoutManager->getCurrentLayout()->performSnap(pos, size, PLASTWINDOW, MBIND_MOVE, -1, size);
*PLASTWINDOW->m_realPosition = pos.round();
PLASTWINDOW->layoutTarget()->setPositionGlobal(CBox{pos, size});
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 click(std::string in) {
CVarList2 data(std::move(in));
uint32_t button;
bool pressed;
try {
button = std::stoul(std::string{data[0]});
pressed = std::stoul(std::string{data[1]}) == 1;
} catch (...) { return {.success = false, .error = "invalid input"}; }
Log::logger->log(Log::DEBUG, "tester: mouse button {} state {}", button, pressed);
g_mouse->m_pointerEvents.button.emit(IPointer::SButtonEvent{
.timeMs = sc<uint32_t>(Time::millis(Time::steadyNow())),
.button = button,
.state = pressed ? WL_POINTER_BUTTON_STATE_PRESSED : WL_POINTER_BUTTON_STATE_RELEASED,
.mouse = true,
});
return {};
}
static SDispatchResult keybind(std::string in) {
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 windowRuleIDX = 0;
//
static SDispatchResult addWindowRule(std::string in) {
windowRuleIDX = Desktop::Rule::windowEffects()->registerEffect("plugin_rule");
if (Desktop::Rule::windowEffects()->registerEffect("plugin_rule") != windowRuleIDX)
return {.success = false, .error = "re-registering returned a different id?"};
return {};
}
static SDispatchResult checkWindowRule(std::string in) {
const auto PLASTWINDOW = Desktop::focusState()->window();
if (!PLASTWINDOW)
return {.success = false, .error = "No window"};
if (!PLASTWINDOW->m_ruleApplicator->m_otherProps.props.contains(windowRuleIDX))
return {.success = false, .error = "No rule"};
if (PLASTWINDOW->m_ruleApplicator->m_otherProps.props[windowRuleIDX]->effect != "effect")
return {.success = false, .error = "Effect isn't \"effect\""};
return {};
}
static Desktop::Rule::CLayerRuleEffectContainer::storageType layerRuleIDX = 0;
static SDispatchResult addLayerRule(std::string in) {
layerRuleIDX = Desktop::Rule::layerEffects()->registerEffect("plugin_rule");
if (Desktop::Rule::layerEffects()->registerEffect("plugin_rule") != layerRuleIDX)
return {.success = false, .error = "re-registering returned a different id?"};
return {};
}
static SDispatchResult checkLayerRule(std::string in) {
if (g_pCompositor->m_layers.size() != 3)
return {.success = false, .error = "Layers under test not here"};
for (const auto& layer : g_pCompositor->m_layers) {
if (layer->m_namespace == "rule-layer") {
if (!layer->m_ruleApplicator->m_otherProps.props.contains(layerRuleIDX))
return {.success = false, .error = "No rule"};
if (layer->m_ruleApplicator->m_otherProps.props[layerRuleIDX]->effect != "effect")
return {.success = false, .error = "Effect isn't \"effect\""};
} else if (layer->m_namespace == "norule-layer") {
if (layer->m_ruleApplicator->m_otherProps.props.contains(layerRuleIDX))
return {.success = false, .error = "Rule even though it shouldn't"};
} else
return {.success = false, .error = "Unrecognized layer"};
}
return {};
}
static SDispatchResult floatingFocusOnFullscreen(std::string in) {
const auto PLASTWINDOW = Desktop::focusState()->window();
if (!PLASTWINDOW)
return {.success = false, .error = "No window"};
if (!PLASTWINDOW->m_isFloating)
return {.success = false, .error = "Window must be floating"};
if (PLASTWINDOW->m_alpha != 1.f)
return {.success = false, .error = "floating window doesnt restore it opacity when focused on fullscreen workspace"};
if (!PLASTWINDOW->m_createdOverFullscreen)
return {.success = false, .error = "floating window doesnt get flagged as createdOverFullscreen"};
return {}; return {};
} }
@ -356,32 +53,10 @@ APICALL EXPORT PLUGIN_DESCRIPTION_INFO PLUGIN_INIT(HANDLE handle) {
HyprlandAPI::addDispatcherV2(PHANDLE, "plugin:test:test", ::test); HyprlandAPI::addDispatcherV2(PHANDLE, "plugin:test:test", ::test);
HyprlandAPI::addDispatcherV2(PHANDLE, "plugin:test:snapmove", ::snapMove); HyprlandAPI::addDispatcherV2(PHANDLE, "plugin:test:snapmove", ::snapMove);
HyprlandAPI::addDispatcherV2(PHANDLE, "plugin:test:vkb", ::vkb);
HyprlandAPI::addDispatcherV2(PHANDLE, "plugin:test:alt", ::pressAlt);
HyprlandAPI::addDispatcherV2(PHANDLE, "plugin:test:gesture", ::simulateGesture);
HyprlandAPI::addDispatcherV2(PHANDLE, "plugin:test:scroll", ::scroll);
HyprlandAPI::addDispatcherV2(PHANDLE, "plugin:test:click", ::click);
HyprlandAPI::addDispatcherV2(PHANDLE, "plugin:test:keybind", ::keybind);
HyprlandAPI::addDispatcherV2(PHANDLE, "plugin:test:add_window_rule", ::addWindowRule);
HyprlandAPI::addDispatcherV2(PHANDLE, "plugin:test:check_window_rule", ::checkWindowRule);
HyprlandAPI::addDispatcherV2(PHANDLE, "plugin:test:add_layer_rule", ::addLayerRule);
HyprlandAPI::addDispatcherV2(PHANDLE, "plugin:test:check_layer_rule", ::checkLayerRule);
HyprlandAPI::addDispatcherV2(PHANDLE, "plugin:test:floating_focus_on_fullscreen", ::floatingFocusOnFullscreen);
// 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"}; return {"hyprtestplugin", "hyprtestplugin", "Vaxry", "1.0"};
} }
APICALL EXPORT void PLUGIN_EXIT() { 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 <csignal>
#include <cerrno> #include <cerrno>
#include <print> #include <print>
#include <hyprutils/memory/Casts.hpp>
using namespace Hyprutils::Memory;
static int getUID() { static int getUID() {
const auto UID = 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); 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); std::println("Couldn't connect to {}. (3)", socketPath);
return ""; return "";
} }

View file

@ -17,22 +17,16 @@
#include "shared.hpp" #include "shared.hpp"
#include "hyprctlCompat.hpp" #include "hyprctlCompat.hpp"
#include "tests/main/tests.hpp" #include "tests/main/tests.hpp"
#include "tests/clients/tests.hpp"
#include "tests/plugin/plugin.hpp" #include "tests/plugin/plugin.hpp"
#include <filesystem> #include <filesystem>
#include <hyprutils/os/Process.hpp> #include <hyprutils/os/Process.hpp>
#include <hyprutils/memory/WeakPtr.hpp> #include <hyprutils/memory/WeakPtr.hpp>
#include <hyprutils/memory/Casts.hpp>
using namespace Hyprutils::Memory;
#include <csignal> #include <csignal>
#include <cerrno> #include <cerrno>
#include <chrono> #include <chrono>
#include <thread> #include <thread>
#include <print> #include <print>
#include <string_view>
#include <span>
#include "Log.hpp" #include "Log.hpp"
@ -97,13 +91,10 @@ int main(int argc, char** argv, char** envp) {
std::string binaryPath = ""; std::string binaryPath = "";
std::string pluginPath = std::filesystem::current_path().string(); std::string pluginPath = std::filesystem::current_path().string();
if (argc > 1) { std::vector<std::string> args{argv + 1, argv + argc};
std::span<char*> args{argv + 1, sc<std::size_t>(argc - 1)};
for (auto it = args.begin(); it != args.end(); it++) { for (auto it = args.begin(); it != args.end(); it++) {
std::string_view value = *it; if (*it == "--config" || *it == "-c") {
if (value == "--config" || value == "-c") {
if (std::next(it) == args.end()) { if (std::next(it) == args.end()) {
help(); help();
@ -128,7 +119,7 @@ int main(int argc, char** argv, char** envp) {
it++; it++;
continue; continue;
} else if (value == "--binary" || value == "-b") { } else if (*it == "--binary" || *it == "-b") {
if (std::next(it) == args.end()) { if (std::next(it) == args.end()) {
help(); help();
@ -153,7 +144,7 @@ int main(int argc, char** argv, char** envp) {
it++; it++;
continue; continue;
} else if (value == "--plugin" || value == "-p") { } else if (*it == "--plugin" || *it == "-p") {
if (std::next(it) == args.end()) { if (std::next(it) == args.end()) {
help(); help();
@ -178,7 +169,7 @@ int main(int argc, char** argv, char** envp) {
it++; it++;
continue; continue;
} else if (value == "--help" || value == "-h") { } else if (*it == "--help" || *it == "-h") {
help(); help();
return 0; return 0;
@ -189,7 +180,6 @@ int main(int argc, char** argv, char** envp) {
return 1; return 1;
} }
} }
}
NLog::log("{}launching hl", Colors::YELLOW); NLog::log("{}launching hl", Colors::YELLOW);
if (!launchHyprland(configPath, binaryPath)) { if (!launchHyprland(configPath, binaryPath)) {
@ -228,24 +218,13 @@ int main(int argc, char** argv, char** envp) {
NLog::log("{}Loaded plugin", Colors::YELLOW); NLog::log("{}Loaded plugin", Colors::YELLOW);
NLog::log("{}Running main tests", Colors::YELLOW);
for (const auto& fn : testFns) { for (const auto& fn : testFns) {
EXPECT(fn(), true); 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); NLog::log("{}running plugin test", Colors::YELLOW);
EXPECT(testPlugin(), true); EXPECT(testPlugin(), true);
NLog::log("{}running vkb test from plugin", Colors::YELLOW);
EXPECT(testVkb(), true);
// kill hyprland // kill hyprland
NLog::log("{}dispatching exit", Colors::YELLOW); NLog::log("{}dispatching exit", Colors::YELLOW);
getFromSocket("/dispatch exit"); getFromSocket("/dispatch exit");

View file

@ -18,17 +18,6 @@ namespace Colors {
constexpr const char* RESET = "\x1b[0m"; 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) \ #define EXPECT(expr, val) \
if (const auto RESULT = expr; RESULT != (val)) { \ if (const auto RESULT = expr; RESULT != (val)) { \
NLog::log("{}Failed: {}{}, expected {}, got {}. Source: {}@{}.", Colors::RED, Colors::RESET, #expr, val, RESULT, __FILE__, __LINE__); \ NLog::log("{}Failed: {}{}, expected {}, got {}. Source: {}@{}.", Colors::RED, Colors::RESET, #expr, val, RESULT, __FILE__, __LINE__); \
@ -39,16 +28,6 @@ namespace Colors {
TESTS_PASSED++; \ TESTS_PASSED++; \
} }
#define EXPECT_NOT(expr, val) \
if (const auto RESULT = expr; RESULT == (val)) { \
NLog::log("{}Failed: {}{}, expected not {}, got {}. Source: {}@{}.", Colors::RED, Colors::RESET, #expr, val, RESULT, __FILE__, __LINE__); \
ret = 1; \
TESTS_FAILED++; \
} else { \
NLog::log("{}Passed: {}{}. Got {}", Colors::GREEN, Colors::RESET, #expr, val); \
TESTS_PASSED++; \
}
#define EXPECT_VECTOR2D(expr, val) \ #define EXPECT_VECTOR2D(expr, val) \
do { \ do { \
const auto& RESULT = expr; \ const auto& RESULT = expr; \
@ -66,7 +45,7 @@ namespace Colors {
#define EXPECT_CONTAINS(haystack, needle) \ #define EXPECT_CONTAINS(haystack, needle) \
if (const auto EXPECTED = needle; !std::string{haystack}.contains(EXPECTED)) { \ 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}); \ std::string{haystack}); \
ret = 1; \ ret = 1; \
TESTS_FAILED++; \ TESTS_FAILED++; \

View file

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

View file

@ -1,151 +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 <unistd.h>
#include <csignal>
#include <thread>
using namespace Hyprutils::OS;
using namespace Hyprutils::Memory;
#define SP CSharedPointer
struct SClient {
SP<CProcess> proc;
std::array<char, 1024> readBuf;
CFileDescriptor readFd, writeFd;
struct pollfd fds;
};
static int ret = 0;
static bool waitForWindow(SP<CProcess> proc, int windowsBefore) {
int counter = 0;
while (Tests::processAlive(proc->pid()) && Tests::windowCount() == windowsBefore) {
counter++;
std::this_thread::sleep_for(std::chrono::milliseconds(100));
if (counter > 50)
return false;
}
NLog::log("{}Waited {} milliseconds for window to open", Colors::YELLOW, counter * 100);
return Tests::processAlive(proc->pid());
}
static bool startClient(SClient& client) {
NLog::log("{}Attempting to start child-window client", Colors::YELLOW);
client.proc = makeShared<CProcess>(binaryDir + "/child-window", std::vector<std::string>{});
client.proc->addEnv("WAYLAND_DISPLAY", WLDISPLAY);
int procInPipeFd[2], procOutPipeFd[2];
if (pipe(procInPipeFd) != 0 || pipe(procOutPipeFd) != 0) {
NLog::log("{}Unable to open pipe to client", Colors::RED);
return false;
}
client.writeFd = CFileDescriptor(procInPipeFd[1]);
client.proc->setStdinFD(procInPipeFd[0]);
client.readFd = CFileDescriptor(procOutPipeFd[0]);
client.proc->setStdoutFD(procOutPipeFd[1]);
if (!client.proc->runAsync()) {
NLog::log("{}Failed to run client", Colors::RED);
return false;
}
close(procInPipeFd[0]);
close(procOutPipeFd[1]);
if (!waitForWindow(client.proc, Tests::windowCount())) {
NLog::log("{}Window took too long to open", Colors::RED);
return false;
}
NLog::log("{}Started child-window client", Colors::YELLOW);
return true;
}
static void stopClient(SClient& client) {
std::string cmd = "exit\n";
write(client.writeFd.get(), cmd.c_str(), cmd.length());
kill(client.proc->pid(), SIGKILL);
client.proc.reset();
}
static bool createChild(SClient& client) {
std::string cmd = "toplevel\n";
if ((size_t)write(client.writeFd.get(), cmd.c_str(), cmd.length()) != cmd.length())
return false;
if (!waitForWindow(client.proc, Tests::windowCount()))
NLog::log("{}Child window took too long to open", Colors::RED);
if (getFromSocket("/dispatch focuswindow class:child-test-child") != "ok") {
NLog::log("{}Failed to focus child window", Colors::RED);
return false;
}
return true;
}
static bool test() {
SClient client;
if (!startClient(client))
return false;
OK(getFromSocket("/dispatch setfloating class:child-test-parent"));
OK(getFromSocket("/dispatch pin class:child-test-parent"));
createChild(client);
EXPECT(Tests::windowCount(), 2)
EXPECT_COUNT_STRING(getFromSocket("/clients"), "pinned: 1", 2);
stopClient(client);
NLog::log("{}Reloading config", Colors::YELLOW);
OK(getFromSocket("/reload"));
Tests::killAllWindows();
EXPECT(Tests::windowCount(), 0);
// test that child windows (shouldBeFloated) are not auto-grouped
NLog::log("{}Test child windows are not auto-grouped", Colors::GREEN);
auto kitty = Tests::spawnKitty();
if (!kitty) {
NLog::log("{}Error: kitty did not spawn", Colors::RED);
return false;
}
// create group and enable auto-grouping
OK(getFromSocket("/dispatch togglegroup"));
OK(getFromSocket("/keyword group:auto_group true"));
SClient client2;
if (!startClient(client2))
return false;
EXPECT(Tests::windowCount(), 2);
createChild(client2);
EXPECT(Tests::windowCount(), 3);
// child has set_parent so shouldBeFloated returns true, it should not be auto-grouped
EXPECT_COUNT_STRING(getFromSocket("/clients"), "grouped: 0", 1);
stopClient(client2);
Tests::killAllWindows();
EXPECT(Tests::windowCount(), 0);
return !ret;
}
REGISTER_CLIENT_TEST_FN(test);

View file

@ -1,156 +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 <unistd.h>
#include <csignal>
#include <thread>
using namespace Hyprutils::OS;
using namespace Hyprutils::Memory;
#define SP CSharedPointer
struct SClient {
SP<CProcess> proc;
std::array<char, 1024> readBuf;
CFileDescriptor readFd, writeFd;
struct pollfd fds;
};
static int ret = 0;
static bool 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]);
const int COUNT_BEFORE = Tests::windowCount();
client.proc->runAsync();
close(pipeFds1[0]);
close(pipeFds2[1]);
client.fds = {.fd = client.readFd.get(), .events = POLLIN};
if (poll(&client.fds, 1, 1000) != 1 || !(client.fds.revents & POLLIN))
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
int counter = 0;
while (Tests::processAlive(client.proc->pid()) && Tests::windowCount() == COUNT_BEFORE) {
counter++;
std::this_thread::sleep_for(std::chrono::milliseconds(100));
if (counter > 50) {
NLog::log("{}pointer-scroll client took too long to open", Colors::RED);
return false;
}
}
if (getFromSocket(std::format("/dispatch setprop pid:{} no_anim 1", client.proc->pid())) != "ok") {
NLog::log("{}Failed to disable animations for client window", Colors::RED, ret);
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,194 +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 <unistd.h>
#include <csignal>
#include <thread>
using namespace Hyprutils::OS;
using namespace Hyprutils::Memory;
#define SP CSharedPointer
struct SClient {
SP<CProcess> proc;
std::array<char, 1024> readBuf;
CFileDescriptor readFd, writeFd;
struct pollfd fds;
};
static int ret = 0;
static bool 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]);
const int COUNT_BEFORE = Tests::windowCount();
client.proc->runAsync();
close(pipeFds1[0]);
close(pipeFds2[1]);
client.fds = {.fd = client.readFd.get(), .events = POLLIN};
if (poll(&client.fds, 1, 1000) != 1 || !(client.fds.revents & POLLIN))
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
int counter = 0;
while (Tests::processAlive(client.proc->pid()) && Tests::windowCount() == COUNT_BEFORE) {
counter++;
std::this_thread::sleep_for(std::chrono::milliseconds(100));
if (counter > 50) {
NLog::log("{}pointer-warp client took too long to open", Colors::RED);
return false;
}
}
if (getFromSocket(std::format("/dispatch setprop pid:{} no_anim 1", client.proc->pid())) != "ok") {
NLog::log("{}Failed to disable animations for client window", Colors::RED, ret);
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,180 +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>
#include <filesystem>
using namespace Hyprutils::OS;
using namespace Hyprutils::Memory;
#define SP CSharedPointer
struct SClient {
SP<CProcess> proc;
std::array<char, 1024> readBuf;
CFileDescriptor readFd, writeFd;
struct pollfd fds;
};
static int ret = 0;
static bool startClient(SClient& client) {
Tests::killAllWindows();
client.proc = makeShared<CProcess>(binaryDir + "/shortcut-inhibitor", std::vector<std::string>{});
client.proc->addEnv("WAYLAND_DISPLAY", WLDISPLAY);
int pipeFds1[2], pipeFds2[2];
if (pipe(pipeFds1) != 0 || pipe(pipeFds2) != 0) {
NLog::log("{}Unable to open pipe to client", Colors::RED);
return false;
}
client.writeFd = CFileDescriptor(pipeFds1[1]);
client.proc->setStdinFD(pipeFds1[0]);
client.readFd = CFileDescriptor(pipeFds2[0]);
client.proc->setStdoutFD(pipeFds2[1]);
const int COUNT_BEFORE = Tests::windowCount();
client.proc->runAsync();
close(pipeFds1[0]);
close(pipeFds2[1]);
client.fds = {.fd = client.readFd.get(), .events = POLLIN};
if (poll(&client.fds, 1, 1000) != 1 || !(client.fds.revents & POLLIN)) {
NLog::log("{}shortcut-inhibitor client failed poll", Colors::RED);
return false;
}
client.readBuf.fill(0);
if (read(client.readFd.get(), client.readBuf.data(), client.readBuf.size() - 1) == -1) {
NLog::log("{}shortcut-inhibitor client read failed", Colors::RED);
return false;
}
std::string ret = std::string{client.readBuf.data()};
if (ret.find("started") == std::string::npos) {
NLog::log("{}Failed to start shortcut-inhibitor client, read {}", Colors::RED, ret);
return false;
}
// wait for window to appear
int counter = 0;
while (Tests::processAlive(client.proc->pid()) && Tests::windowCount() == COUNT_BEFORE) {
counter++;
std::this_thread::sleep_for(std::chrono::milliseconds(100));
if (counter > 50) {
NLog::log("{}shortcut-inhibitor client took too long to open", Colors::RED);
return false;
}
}
if (!Tests::processAlive(client.proc->pid())) {
NLog::log("{}shortcut-inhibitor client not alive", Colors::RED);
return false;
}
if (getFromSocket(std::format("/dispatch focuswindow pid:{}", client.proc->pid())) != "ok") {
NLog::log("{}Failed to focus shortcut-inhibitor client", Colors::RED, ret);
return false;
}
std::string command = "on\n";
if (write(client.writeFd.get(), command.c_str(), command.length()) == -1) {
NLog::log("{}shortcut-inhibitor client write failed", Colors::RED);
return false;
}
client.readBuf.fill(0);
if (read(client.readFd.get(), client.readBuf.data(), client.readBuf.size() - 1) == -1)
return false;
ret = std::string{client.readBuf.data()};
if (ret.find("inhibiting") == std::string::npos) {
NLog::log("{}shortcut-inhibitor client didn't return inhibiting", Colors::RED);
return false;
}
NLog::log("{}Started shortcut-inhibitor client", Colors::YELLOW);
return true;
}
static void stopClient(SClient& client) {
std::string cmd = "off\n";
write(client.writeFd.get(), cmd.c_str(), cmd.length());
kill(client.proc->pid(), SIGKILL);
client.proc.reset();
}
static std::string flagFile = "/tmp/hyprtester-keybinds.txt";
static bool checkFlag() {
bool exists = std::filesystem::exists(flagFile);
std::filesystem::remove(flagFile);
return exists;
}
static bool attemptCheckFlag(int attempts, int intervalMs) {
for (int i = 0; i < attempts; i++) {
if (checkFlag())
return true;
std::this_thread::sleep_for(std::chrono::milliseconds(intervalMs));
}
return false;
}
static bool test() {
SClient client;
if (!startClient(client))
return false;
NLog::log("{}Testing keybinds", Colors::GREEN);
//basic keybind test
EXPECT(checkFlag(), false);
EXPECT(getFromSocket("/keyword bind SUPER,Y,exec,touch " + flagFile), "ok");
OK(getFromSocket("/dispatch plugin:test:keybind 1,7,29"));
EXPECT(attemptCheckFlag(20, 50), false);
OK(getFromSocket("/dispatch plugin:test:keybind 0,0,29"));
EXPECT(getFromSocket("/keyword unbind SUPER,Y"), "ok");
//keybind bypass flag test
EXPECT(checkFlag(), false);
EXPECT(getFromSocket("/keyword bindp SUPER,Y,exec,touch " + flagFile), "ok");
OK(getFromSocket("/dispatch plugin:test:keybind 1,7,29"));
EXPECT(attemptCheckFlag(20, 50), true);
OK(getFromSocket("/dispatch plugin:test:keybind 0,0,29"));
EXPECT(getFromSocket("/keyword unbind SUPER,Y"), "ok");
NLog::log("{}Testing gestures", Colors::GREEN);
//basic gesture test
OK(getFromSocket("/dispatch plugin:test:gesture right,3"));
EXPECT_NOT_CONTAINS(getFromSocket("/activewindow"), "floating: 1");
//gesture bypass flag test
OK(getFromSocket("/dispatch plugin:test:gesture right,2"));
EXPECT_CONTAINS(getFromSocket("/activewindow"), "floating: 1");
stopClient(client);
NLog::log("{}Reloading the config", Colors::YELLOW);
OK(getFromSocket("/reload"));
return !ret;
}
REGISTER_CLIENT_TEST_FN(test);

View file

@ -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,254 +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();
OK(getFromSocket("/reload"));
}
static void test13349() {
// Test if dwindle properly uses a focal point to place a new window.
// exposed by #13349 as a regression from #12890
for (auto const& win : {"a", "b", "c"}) {
if (!Tests::spawnKitty(win)) {
NLog::log("{}Failed to spawn kitty with win class `{}`", Colors::RED, win);
++TESTS_FAILED;
ret = 1;
return;
}
}
OK(getFromSocket("/dispatch focuswindow class:c"));
{
auto str = getFromSocket("/activewindow");
EXPECT_CONTAINS(str, "at: 967,547");
EXPECT_CONTAINS(str, "size: 931,511");
}
OK(getFromSocket("/dispatch movewindow l"));
{
auto str = getFromSocket("/activewindow");
EXPECT_CONTAINS(str, "at: 497,22");
EXPECT_CONTAINS(str, "size: 456,1036");
}
OK(getFromSocket("/dispatch movewindow r"));
{
auto str = getFromSocket("/activewindow");
EXPECT_CONTAINS(str, "at: 967,22");
EXPECT_CONTAINS(str, "size: 456,1036");
}
// clean up
NLog::log("{}Killing all windows", Colors::YELLOW);
Tests::killAllWindows();
}
static void testSplit() {
// Test various split methods
Tests::spawnKitty("a");
// these must not crash
EXPECT_NOT(getFromSocket("/dispatch layoutmsg swapsplit"), "ok");
EXPECT_NOT(getFromSocket("/dispatch layoutmsg splitratio 1 exact"), "ok");
Tests::spawnKitty("b");
OK(getFromSocket("/dispatch focuswindow class:a"));
OK(getFromSocket("/dispatch layoutmsg splitratio -0.2"));
{
auto str = getFromSocket("/activewindow");
EXPECT_CONTAINS(str, "at: 22,22");
EXPECT_CONTAINS(str, "size: 743,1036");
}
OK(getFromSocket("/dispatch layoutmsg splitratio 1.6 exact"));
{
auto str = getFromSocket("/activewindow");
EXPECT_CONTAINS(str, "at: 22,22");
EXPECT_CONTAINS(str, "size: 1495,1036");
}
EXPECT_NOT(getFromSocket("/dispatch layoutmsg splitratio fhne exact"), "ok");
EXPECT_NOT(getFromSocket("/dispatch layoutmsg splitratio exact"), "ok");
EXPECT_NOT(getFromSocket("/dispatch layoutmsg splitratio -....9"), "ok");
EXPECT_NOT(getFromSocket("/dispatch layoutmsg splitratio ..9"), "ok");
EXPECT_NOT(getFromSocket("/dispatch layoutmsg splitratio"), "ok");
OK(getFromSocket("/dispatch layoutmsg togglesplit"));
{
auto str = getFromSocket("/activewindow");
EXPECT_CONTAINS(str, "at: 22,22");
EXPECT_CONTAINS(str, "size: 1876,823");
}
OK(getFromSocket("/dispatch layoutmsg swapsplit"));
{
auto str = getFromSocket("/activewindow");
EXPECT_CONTAINS(str, "at: 22,859");
EXPECT_CONTAINS(str, "size: 1876,199");
}
NLog::log("{}Killing all windows", Colors::YELLOW);
Tests::killAllWindows();
}
static void testRotatesplit() {
OK(getFromSocket("r/keyword general:gaps_in 0"));
OK(getFromSocket("r/keyword general:gaps_out 0"));
OK(getFromSocket("r/keyword general:border_size 0"));
for (auto const& win : {"a", "b"}) {
if (!Tests::spawnKitty(win)) {
NLog::log("{}Failed to spawn kitty with win class `{}`", Colors::RED, win);
++TESTS_FAILED;
ret = 1;
return;
}
}
{
auto str = getFromSocket("/clients");
EXPECT_CONTAINS(str, "at: 0,0");
EXPECT_CONTAINS(str, "size: 960,1080");
}
// test 4 repeated rotations by 90 degrees
OK(getFromSocket("/dispatch layoutmsg rotatesplit"));
{
auto str = getFromSocket("/clients");
EXPECT_CONTAINS(str, "at: 0,0");
EXPECT_CONTAINS(str, "size: 1920,540");
}
OK(getFromSocket("/dispatch layoutmsg rotatesplit"));
{
auto str = getFromSocket("/clients");
EXPECT_CONTAINS(str, "at: 960,0");
EXPECT_CONTAINS(str, "size: 960,1080");
}
OK(getFromSocket("/dispatch layoutmsg rotatesplit"));
{
auto str = getFromSocket("/clients");
EXPECT_CONTAINS(str, "at: 0,540");
EXPECT_CONTAINS(str, "size: 1920,540");
}
OK(getFromSocket("/dispatch layoutmsg rotatesplit"));
{
auto str = getFromSocket("/clients");
EXPECT_CONTAINS(str, "at: 0,0");
EXPECT_CONTAINS(str, "size: 960,1080");
}
// test different angles
OK(getFromSocket("/dispatch layoutmsg rotatesplit 180"));
{
auto str = getFromSocket("/clients");
EXPECT_CONTAINS(str, "at: 960,0");
EXPECT_CONTAINS(str, "size: 960,1080");
}
OK(getFromSocket("/dispatch layoutmsg rotatesplit 270"));
{
auto str = getFromSocket("/clients");
EXPECT_CONTAINS(str, "at: 0,540");
EXPECT_CONTAINS(str, "size: 1920,540");
}
OK(getFromSocket("/dispatch layoutmsg rotatesplit 360"));
{
auto str = getFromSocket("/clients");
EXPECT_CONTAINS(str, "at: 0,0");
EXPECT_CONTAINS(str, "size: 1920,540");
}
// test negative angles
OK(getFromSocket("/dispatch layoutmsg rotatesplit -90"));
{
auto str = getFromSocket("/clients");
EXPECT_CONTAINS(str, "at: 0,0");
EXPECT_CONTAINS(str, "size: 960,1080");
}
OK(getFromSocket("/dispatch layoutmsg rotatesplit -180"));
{
auto str = getFromSocket("/clients");
EXPECT_CONTAINS(str, "at: 960,0");
EXPECT_CONTAINS(str, "size: 960,1080");
}
NLog::log("{}Killing all windows", Colors::YELLOW);
Tests::killAllWindows();
OK(getFromSocket("/reload"));
}
static bool test() {
NLog::log("{}Testing Dwindle layout", Colors::GREEN);
// test
NLog::log("{}Testing float clamp", Colors::GREEN);
testFloatClamp();
NLog::log("{}Testing #13349", Colors::GREEN);
test13349();
NLog::log("{}Testing splits", Colors::GREEN);
testSplit();
NLog::log("{}Testing rotatesplit", Colors::GREEN);
testRotatesplit();
// clean up
NLog::log("Cleaning up", Colors::YELLOW);
getFromSocket("/dispatch workspace 1");
OK(getFromSocket("/reload"));
return !ret;
}
REGISTER_TEST_FN(test);

View file

@ -1,61 +0,0 @@
#include "tests.hpp"
#include "../../shared.hpp"
#include "../../hyprctlCompat.hpp"
#include <chrono>
#include <format>
#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
const static auto SLEEP_DURATIONS = std::array{1, 10};
static bool test() {
NLog::log("{}Testing process spawning", Colors::GREEN);
for (const auto duration : SLEEP_DURATIONS) {
// Note: POSIX sleep does not support fractional seconds, so
// can't sleep for less than 1 second.
OK(getFromSocket(std::format("/dispatch exec sleep {}", duration)));
// Ensure that sleep is our child
const std::string sleepPidS = Tests::execAndGet("pgrep sleep");
pid_t sleepPid;
try {
sleepPid = std::stoull(sleepPidS);
} catch (...) {
NLog::log("{}Sleep was not spawned or several sleeps are running: pgrep returned '{}'", Colors::RED, sleepPidS);
continue;
}
const std::string sleepParentComm = Tests::execAndGet("cat \"/proc/$(ps -o ppid:1= -p " + sleepPidS + ")/comm\"");
NLog::log("{}Expecting that sleep's parent is Hyprland", Colors::YELLOW);
EXPECT_CONTAINS(sleepParentComm, "Hyprland");
std::this_thread::sleep_for(std::chrono::seconds(duration));
// Ensure that sleep did not become a zombie
EXPECT(Tests::processAlive(sleepPid), false);
// kill all
NLog::log("{}Killing all windows", Colors::YELLOW);
Tests::killAllWindows();
NLog::log("{}Expecting 0 windows", Colors::YELLOW);
EXPECT(Tests::windowCount(), 0);
return !ret;
}
return false;
}
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

@ -127,34 +127,6 @@ static bool test() {
ret = 1; ret = 1;
} }
// test movegroupwindow: focus should follow the moved window
NLog::log("{}Test movegroupwindow focus follows window", Colors::YELLOW);
try {
auto str = getFromSocket("/activewindow");
auto activeBeforeMove = std::stoull(str.substr(7, str.find(" -> ") - 7), nullptr, 16);
OK(getFromSocket("/dispatch movegroupwindow f"));
str = getFromSocket("/activewindow");
auto activeAfterMove = std::stoull(str.substr(7, str.find(" -> ") - 7), nullptr, 16);
EXPECT(activeAfterMove, activeBeforeMove);
} catch (...) {
NLog::log("{}Fail at getting prop", Colors::RED);
ret = 1;
}
// and backwards
NLog::log("{}Test movegroupwindow backwards", Colors::YELLOW);
try {
auto str = getFromSocket("/activewindow");
auto activeBeforeMove = std::stoull(str.substr(7, str.find(" -> ") - 7), nullptr, 16);
OK(getFromSocket("/dispatch movegroupwindow b"));
str = getFromSocket("/activewindow");
auto activeAfterMove = std::stoull(str.substr(7, str.find(" -> ") - 7), nullptr, 16);
EXPECT(activeAfterMove, activeBeforeMove);
} catch (...) {
NLog::log("{}Fail at getting prop", Colors::RED);
ret = 1;
}
NLog::log("{}Disable autogrouping", Colors::YELLOW); NLog::log("{}Disable autogrouping", Colors::YELLOW);
OK(getFromSocket("/keyword group:auto_group false")); OK(getFromSocket("/keyword group:auto_group false"));
@ -201,99 +173,6 @@ static bool test() {
NLog::log("{}Expecting 0 windows", Colors::YELLOW); NLog::log("{}Expecting 0 windows", Colors::YELLOW);
EXPECT(Tests::windowCount(), 0); EXPECT(Tests::windowCount(), 0);
// test movewindoworgroup: direction should be respected when extracting from group
NLog::log("{}Test movewindoworgroup respects direction out of group", Colors::YELLOW);
OK(getFromSocket("/keyword group:groupbar:enabled 0"));
{
auto kittyE = Tests::spawnKitty();
if (!kittyE) {
NLog::log("{}Error: kitty did not spawn", Colors::RED);
return false;
}
// group kitty, and new windows should be auto-grouped
OK(getFromSocket("/dispatch togglegroup"));
auto kittyF = Tests::spawnKitty();
if (!kittyF) {
NLog::log("{}Error: kitty did not spawn", Colors::RED);
return false;
}
EXPECT(Tests::windowCount(), 2);
// both windows should be grouped at the same position
{
auto str = getFromSocket("/clients");
EXPECT_COUNT_STRING(str, "at: 22,22", 2);
}
// move active window out of group to the right
NLog::log("{}Test movewindoworgroup r", Colors::YELLOW);
OK(getFromSocket("/dispatch movewindoworgroup r"));
// the group should stay at x=22, the extracted window should be to the right
{
auto str = getFromSocket("/clients");
EXPECT_COUNT_STRING(str, "at: 22,22", 1);
}
// move it back into the group
OK(getFromSocket("/dispatch moveintogroup l"));
// move active window out of group downward
NLog::log("{}Test movewindoworgroup d", Colors::YELLOW);
OK(getFromSocket("/dispatch movewindoworgroup d"));
// the group should stay at y=22, the extracted window should be below
{
auto str = getFromSocket("/clients");
EXPECT_COUNT_STRING(str, "at: 22,22", 1);
}
Tests::killAllWindows();
EXPECT(Tests::windowCount(), 0);
}
// test that we deny a floated window getting auto-grouped into a tiled group.
NLog::log("{}Test that we deny a floated window getting auto-grouped into a tiled group.", Colors::GREEN);
OK(getFromSocket("/keyword windowrule[kitty-tiled]:match:class kitty_tiled"));
OK(getFromSocket("/keyword windowrule[kitty-tiled]:tile yes"));
auto kittyProcE = Tests::spawnKitty("kitty_tiled");
if (!kittyProcE) {
NLog::log("{}Error: kitty did not spawn", Colors::RED);
return false;
}
OK(getFromSocket("/dispatch togglegroup"));
OK(getFromSocket("/keyword windowrule[kitty-floated]:match:class kitty_floated"));
OK(getFromSocket("/keyword windowrule[kitty-floated]:float yes"));
auto kittyProcF = Tests::spawnKitty("kitty_floated");
if (!kittyProcF) {
NLog::log("{}Error: kitty did not spawn", Colors::RED);
return false;
}
EXPECT(Tests::windowCount(), 2);
{
auto clients = getFromSocket("/clients");
auto classPos = clients.find("class: kitty_floated");
if (classPos == std::string::npos) {
NLog::log("{}Could not find kitty_floated in clients output", Colors::RED);
ret = 1;
} else {
auto entryStart = clients.rfind("Window ", classPos);
auto entryEnd = clients.find("\n\n", classPos);
auto windowEntry = clients.substr(entryStart, entryEnd - entryStart);
EXPECT_CONTAINS(windowEntry, "floating: 1");
EXPECT_CONTAINS(windowEntry, "grouped: 0");
}
}
Tests::killAllWindows();
EXPECT(Tests::windowCount(), 0);
return !ret; return !ret;
} }

View file

@ -1,194 +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]})");
// expr-based min/max _size
getFromSocket("/dispatch setfloating class:kitty"); // need to set floating for tests below
getFromSocket("/dispatch setprop class:kitty max_size 90+10 25*2"); // set max to the same as min above, forcing window to 100*50
EXPECT(getCommandStdOut("hyprctl getprop class:kitty max_size"), "100 50");
EXPECT(getCommandStdOut("hyprctl getprop class:kitty max_size -j"), R"({"max_size": [100,50]})");
getFromSocket("/dispatch setprop class:kitty min_size window_w*0.5 window_h-10");
EXPECT(getCommandStdOut("hyprctl getprop class:kitty min_size"), "50 40");
EXPECT(getCommandStdOut("hyprctl getprop class:kitty min_size -j"), R"({"min_size": [50,40]})");
getFromSocket("/dispatch settiled class:kitty"); // go back to tiled for consistency
// opacity
EXPECT(getCommandStdOut("hyprctl getprop class:kitty opacity"), "1");
EXPECT(getCommandStdOut("hyprctl getprop class:kitty opacity -j"), R"({"opacity": 1})");
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,545 +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 testBindsAfterScroll() {
NLog::log("{}Testing binds after scroll", Colors::GREEN);
clearFlag();
OK(getFromSocket("/keyword binds Alt_R,w,exec,touch " + flagFile));
// press keybind before scroll
OK(getFromSocket("/dispatch plugin:test:keybind 1,0,108")); // Alt_R press
OK(getFromSocket("/dispatch plugin:test:keybind 1,4,25")); // w press
EXPECT(attemptCheckFlag(20, 50), true);
OK(getFromSocket("/dispatch plugin:test:keybind 0,4,25")); // w release
OK(getFromSocket("/dispatch plugin:test:keybind 0,0,108")); // Alt_R release
// scroll
OK(getFromSocket("/dispatch plugin:test:scroll 120"));
OK(getFromSocket("/dispatch plugin:test:scroll -120"));
OK(getFromSocket("/dispatch plugin:test:scroll 120"));
// press keybind after scroll
OK(getFromSocket("/dispatch plugin:test:keybind 1,0,108")); // Alt_R press
OK(getFromSocket("/dispatch plugin:test:keybind 1,4,25")); // w press
EXPECT(attemptCheckFlag(20, 50), true);
OK(getFromSocket("/dispatch plugin:test:keybind 0,4,25")); // w release
OK(getFromSocket("/dispatch plugin:test:keybind 0,0,108")); // Alt_R release
clearFlag();
OK(getFromSocket("/keyword unbind Alt_R,w"));
}
static void testSubmapUniversal() {
NLog::log("{}Testing submap universal", Colors::GREEN);
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();
testBindsAfterScroll();
clearFlag();
return !ret;
}
REGISTER_TEST_FN(test)

View file

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

View file

@ -1,127 +0,0 @@
#include "../shared.hpp"
#include "../../shared.hpp"
#include "../../hyprctlCompat.hpp"
#include "tests.hpp"
static int ret = 0;
static void swar() {
OK(getFromSocket("/keyword layout:single_window_aspect_ratio 1 1"));
Tests::spawnKitty();
{
auto str = getFromSocket("/activewindow");
EXPECT_CONTAINS(str, "at: 442,22");
EXPECT_CONTAINS(str, "size: 1036,1036");
}
Tests::spawnKitty();
OK(getFromSocket("/dispatch killwindow activewindow"));
Tests::waitUntilWindowsN(1);
{
auto str = getFromSocket("/activewindow");
EXPECT_CONTAINS(str, "at: 442,22");
EXPECT_CONTAINS(str, "size: 1036,1036");
}
// don't use swar on maximized
OK(getFromSocket("/dispatch fullscreen 1"));
{
auto str = getFromSocket("/activewindow");
EXPECT_CONTAINS(str, "at: 22,22");
EXPECT_CONTAINS(str, "size: 1876,1036");
}
// clean up
NLog::log("{}Killing all windows", Colors::YELLOW);
Tests::killAllWindows();
}
// Don't crash when focus after global geometry changes
static void testCrashOnGeomUpdate() {
Tests::spawnKitty();
Tests::spawnKitty();
Tests::spawnKitty();
// move the layout
OK(getFromSocket("/keyword monitor HEADLESS-2,1920x1080@60,1000x0,1"));
// shouldnt crash
OK(getFromSocket("/dispatch movefocus r"));
OK(getFromSocket("/reload"));
NLog::log("{}Killing all windows", Colors::YELLOW);
Tests::killAllWindows();
}
// Test if size + pos is preserved after fs cycle
static void testPosPreserve() {
Tests::spawnKitty();
OK(getFromSocket("/dispatch setfloating class:kitty"));
OK(getFromSocket("/dispatch resizewindowpixel exact 1337 69, class:kitty"));
OK(getFromSocket("/dispatch movewindowpixel exact 420 420, class:kitty"));
{
auto str = getFromSocket("/activewindow");
EXPECT_CONTAINS(str, "at: 420,420");
EXPECT_CONTAINS(str, "size: 1337,69");
}
OK(getFromSocket("/dispatch fullscreen"));
OK(getFromSocket("/dispatch fullscreen"));
{
auto str = getFromSocket("/activewindow");
EXPECT_CONTAINS(str, "size: 1337,69");
}
OK(getFromSocket("/dispatch movewindow r"));
{
auto str = getFromSocket("/activewindow");
EXPECT_CONTAINS(str, "at: 581,420");
EXPECT_CONTAINS(str, "size: 1337,69");
}
OK(getFromSocket("/dispatch fullscreen"));
OK(getFromSocket("/dispatch fullscreen"));
{
auto str = getFromSocket("/activewindow");
EXPECT_CONTAINS(str, "at: 581,420");
EXPECT_CONTAINS(str, "size: 1337,69");
}
NLog::log("{}Killing all windows", Colors::YELLOW);
Tests::killAllWindows();
}
static bool test() {
NLog::log("{}Testing layout generic", Colors::GREEN);
// setup
OK(getFromSocket("/dispatch workspace 10"));
// test
NLog::log("{}Testing `single_window_aspect_ratio`", Colors::GREEN);
swar();
testCrashOnGeomUpdate();
testPosPreserve();
// clean up
NLog::log("Cleaning up", Colors::YELLOW);
OK(getFromSocket("/dispatch workspace 1"));
OK(getFromSocket("/reload"));
return !ret;
}
REGISTER_TEST_FN(test);

View file

@ -1,183 +0,0 @@
#include "../shared.hpp"
#include "../../shared.hpp"
#include "../../hyprctlCompat.hpp"
#include "tests.hpp"
static int ret = 0;
// reqs 1 master 3 slaves
static void testOrientations() {
OK(getFromSocket("/keyword master:orientation top"));
// top
{
auto str = getFromSocket("/activewindow");
EXPECT_CONTAINS(str, "at: 22,22");
EXPECT_CONTAINS(str, "size: 1876");
}
// cycle = top, right, bottom, center, left
// right
OK(getFromSocket("/dispatch layoutmsg orientationnext"));
{
auto str = getFromSocket("/activewindow");
EXPECT_CONTAINS(str, "at: 873,22");
EXPECT_CONTAINS(str, "size: 1025,1036");
}
// bottom
OK(getFromSocket("/dispatch layoutmsg orientationnext"));
{
auto str = getFromSocket("/activewindow");
EXPECT_CONTAINS(str, "at: 22,495");
EXPECT_CONTAINS(str, "size: 1876");
}
// center
OK(getFromSocket("/dispatch layoutmsg orientationnext"));
{
auto str = getFromSocket("/activewindow");
EXPECT_CONTAINS(str, "at: 450,22");
EXPECT_CONTAINS(str, "size: 1020,1036");
}
// left
OK(getFromSocket("/dispatch layoutmsg orientationnext"));
{
auto str = getFromSocket("/activewindow");
EXPECT_CONTAINS(str, "at: 22,22");
EXPECT_CONTAINS(str, "size: 1025,1036");
}
}
static void focusMasterPrevious() {
// setup
NLog::log("{}Spawning 1 master and 3 slave windows", Colors::YELLOW);
// order of windows set according to new_status = master (set in test.conf)
for (auto const& win : {"slave1", "slave2", "slave3", "master"}) {
if (!Tests::spawnKitty(win)) {
NLog::log("{}Failed to spawn kitty with win class `{}`", Colors::RED, win);
++TESTS_FAILED;
ret = 1;
return;
}
}
NLog::log("{}Ensuring focus is on master before testing", Colors::YELLOW);
OK(getFromSocket("/dispatch layoutmsg focusmaster master"));
EXPECT_CONTAINS(getFromSocket("/activewindow"), "class: master");
// test
NLog::log("{}Testing fallback to focusmaster auto", Colors::YELLOW);
OK(getFromSocket("/dispatch layoutmsg focusmaster previous"));
EXPECT_CONTAINS(getFromSocket("/activewindow"), "class: slave1");
NLog::log("{}Testing focusing from slave to master", Colors::YELLOW);
OK(getFromSocket("/dispatch layoutmsg cyclenext noloop"));
EXPECT_CONTAINS(getFromSocket("/activewindow"), "class: slave2");
OK(getFromSocket("/dispatch layoutmsg focusmaster previous"));
EXPECT_CONTAINS(getFromSocket("/activewindow"), "class: master");
NLog::log("{}Testing focusing on previous window", Colors::YELLOW);
OK(getFromSocket("/dispatch layoutmsg focusmaster previous"));
EXPECT_CONTAINS(getFromSocket("/activewindow"), "class: slave2");
NLog::log("{}Testing focusing back to master", Colors::YELLOW);
OK(getFromSocket("/dispatch layoutmsg focusmaster previous"));
EXPECT_CONTAINS(getFromSocket("/activewindow"), "class: master");
testOrientations();
// clean up
NLog::log("{}Killing all windows", Colors::YELLOW);
Tests::killAllWindows();
}
static void testFsBehavior() {
// Master will re-send data to fullscreen / maximized windows, which can interfere with misc:on_focus_under_fullscreen
// check that it doesn't.
for (auto const& win : {"master", "slave1", "slave2"}) {
if (!Tests::spawnKitty(win)) {
NLog::log("{}Failed to spawn kitty with win class `{}`", Colors::RED, win);
++TESTS_FAILED;
ret = 1;
return;
}
}
OK(getFromSocket("/dispatch focuswindow class:master"));
OK(getFromSocket("/dispatch fullscreen 1"));
{
auto str = getFromSocket("/activewindow");
EXPECT_CONTAINS(str, "at: 22,22");
EXPECT_CONTAINS(str, "size: 1876,1036");
EXPECT_CONTAINS(str, "class: master");
}
OK(getFromSocket("/keyword misc:on_focus_under_fullscreen 1"));
Tests::spawnKitty("new_master");
{
auto str = getFromSocket("/activewindow");
EXPECT_CONTAINS(str, "at: 22,22");
EXPECT_CONTAINS(str, "size: 1876,1036");
EXPECT_CONTAINS(str, "class: new_master");
EXPECT_CONTAINS(str, "fullscreen: 1");
}
OK(getFromSocket("/keyword misc:on_focus_under_fullscreen 0"));
Tests::spawnKitty("ignored");
{
auto str = getFromSocket("/activewindow");
EXPECT_CONTAINS(str, "at: 22,22");
EXPECT_CONTAINS(str, "size: 1876,1036");
EXPECT_CONTAINS(str, "class: new_master");
EXPECT_CONTAINS(str, "fullscreen: 1");
}
OK(getFromSocket("/keyword misc:on_focus_under_fullscreen 2"));
Tests::spawnKitty("vaxwashere");
{
auto str = getFromSocket("/activewindow");
EXPECT_CONTAINS(str, "class: vaxwashere");
EXPECT_CONTAINS(str, "fullscreen: 0");
}
NLog::log("{}Killing all windows", Colors::YELLOW);
Tests::killAllWindows();
}
static bool test() {
NLog::log("{}Testing Master layout", Colors::GREEN);
// setup
OK(getFromSocket("/dispatch workspace name:master"));
OK(getFromSocket("/keyword general:layout master"));
// test
NLog::log("{}Testing `focusmaster previous` layoutmsg", Colors::GREEN);
focusMasterPrevious();
NLog::log("{}Testing fs behavior", Colors::GREEN);
testFsBehavior();
// clean up
NLog::log("Cleaning up", Colors::YELLOW);
OK(getFromSocket("/dispatch workspace 1"));
OK(getFromSocket("/reload"));
return !ret;
}
REGISTER_TEST_FN(test);

View file

@ -18,121 +18,6 @@ using namespace Hyprutils::Memory;
#define UP CUniquePointer #define UP CUniquePointer
#define SP CSharedPointer #define SP CSharedPointer
// Uncomment once test vm can run hyprland-dialog
// static void testAnrDialogs() {
// NLog::log("{}Testing ANR dialogs", Colors::YELLOW);
//
// OK(getFromSocket("/keyword misc:enable_anr_dialog true"));
// OK(getFromSocket("/keyword misc:anr_missed_pings 1"));
//
// NLog::log("{}ANR dialog: regular workspaces", Colors::YELLOW);
// {
// OK(getFromSocket("/dispatch workspace 2"));
//
// auto kitty = Tests::spawnKitty("bad_kitty");
//
// if (!kitty) {
// ret = 1;
// return;
// }
//
// {
// auto str = getFromSocket("/activewindow");
// EXPECT_CONTAINS(str, "workspace: 2");
// }
//
// OK(getFromSocket("/dispatch workspace 1"));
//
// ::kill(kitty->pid(), SIGSTOP);
// Tests::waitUntilWindowsN(2);
//
// {
// auto str = getFromSocket("/activeworkspace");
// EXPECT_CONTAINS(str, "windows: 0");
// }
//
// {
// OK(getFromSocket("/dispatch focuswindow class:hyprland-dialog"))
// auto str = getFromSocket("/activewindow");
// EXPECT_CONTAINS(str, "workspace: 2");
// }
// }
//
// Tests::killAllWindows();
//
// NLog::log("{}ANR dialog: named workspaces", Colors::YELLOW);
// {
// OK(getFromSocket("/dispatch workspace name:yummy"));
//
// auto kitty = Tests::spawnKitty("bad_kitty");
//
// if (!kitty) {
// ret = 1;
// return;
// }
//
// {
// auto str = getFromSocket("/activewindow");
// EXPECT_CONTAINS(str, "yummy");
// }
//
// OK(getFromSocket("/dispatch workspace 1"));
//
// ::kill(kitty->pid(), SIGSTOP);
// Tests::waitUntilWindowsN(2);
//
// {
// auto str = getFromSocket("/activeworkspace");
// EXPECT_CONTAINS(str, "windows: 0");
// }
//
// {
// OK(getFromSocket("/dispatch focuswindow class:hyprland-dialog"))
// auto str = getFromSocket("/activewindow");
// EXPECT_CONTAINS(str, "yummy");
// }
// }
//
// Tests::killAllWindows();
//
// NLog::log("{}ANR dialog: special workspaces", Colors::YELLOW);
// {
// OK(getFromSocket("/dispatch workspace special:apple"));
//
// auto kitty = Tests::spawnKitty("bad_kitty");
//
// if (!kitty) {
// ret = 1;
// return;
// }
//
// {
// auto str = getFromSocket("/activewindow");
// EXPECT_CONTAINS(str, "special:apple");
// }
//
// OK(getFromSocket("/dispatch togglespecialworkspace apple"));
// OK(getFromSocket("/dispatch workspace 1"));
//
// ::kill(kitty->pid(), SIGSTOP);
// Tests::waitUntilWindowsN(2);
//
// {
// auto str = getFromSocket("/activeworkspace");
// EXPECT_CONTAINS(str, "windows: 0");
// }
//
// {
// OK(getFromSocket("/dispatch focuswindow class:hyprland-dialog"))
// auto str = getFromSocket("/activewindow");
// EXPECT_CONTAINS(str, "special:apple");
// }
// }
//
// OK(getFromSocket("/reload"));
// Tests::killAllWindows();
// }
static bool test() { static bool test() {
NLog::log("{}Testing config: misc:", Colors::GREEN); NLog::log("{}Testing config: misc:", Colors::GREEN);
@ -168,7 +53,7 @@ static bool test() {
NLog::log("{}Testing new_window_takes_over_fullscreen", Colors::YELLOW); 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"); Tests::spawnKitty("kitty_A");
@ -188,16 +73,7 @@ static bool test() {
EXPECT_CONTAINS(str, "kitty_A"); EXPECT_CONTAINS(str, "kitty_A");
} }
OK(getFromSocket("/dispatch focuswindow class:kitty_B")); OK(getFromSocket("/keyword misc:new_window_takes_over_fullscreen 1"));
{
// 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"));
Tests::spawnKitty("kitty_C"); Tests::spawnKitty("kitty_C");
@ -207,7 +83,7 @@ static bool test() {
EXPECT_CONTAINS(str, "kitty_C"); 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"); Tests::spawnKitty("kitty_D");
@ -217,7 +93,7 @@ static bool test() {
EXPECT_CONTAINS(str, "kitty_D"); 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(); Tests::killAllWindows();
@ -255,74 +131,6 @@ static bool test() {
EXPECT_CONTAINS(str, "fullscreen: 2"); 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 // kill all
NLog::log("{}Killing all windows", Colors::YELLOW); NLog::log("{}Killing all windows", Colors::YELLOW);
Tests::killAllWindows(); 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

@ -1,228 +0,0 @@
#include "../shared.hpp"
#include "../../shared.hpp"
#include "../../hyprctlCompat.hpp"
#include "tests.hpp"
static int ret = 0;
static void testFocusCycling() {
for (auto const& win : {"a", "b", "c", "d"}) {
if (!Tests::spawnKitty(win)) {
NLog::log("{}Failed to spawn kitty with win class `{}`", Colors::RED, win);
++TESTS_FAILED;
ret = 1;
return;
}
}
OK(getFromSocket("/dispatch focuswindow class:a"));
OK(getFromSocket("/dispatch movefocus r"));
{
auto str = getFromSocket("/activewindow");
EXPECT_CONTAINS(str, "class: b");
}
OK(getFromSocket("/dispatch movefocus r"));
{
auto str = getFromSocket("/activewindow");
EXPECT_CONTAINS(str, "class: c");
}
OK(getFromSocket("/dispatch movefocus r"));
{
auto str = getFromSocket("/activewindow");
EXPECT_CONTAINS(str, "class: d");
}
OK(getFromSocket("/dispatch movewindow l"));
{
auto str = getFromSocket("/activewindow");
EXPECT_CONTAINS(str, "class: d");
}
OK(getFromSocket("/dispatch movefocus u"));
{
auto str = getFromSocket("/activewindow");
EXPECT_CONTAINS(str, "class: c");
}
// clean up
NLog::log("{}Killing all windows", Colors::YELLOW);
Tests::killAllWindows();
}
static void testFocusWrapping() {
for (auto const& win : {"a", "b", "c", "d"}) {
if (!Tests::spawnKitty(win)) {
NLog::log("{}Failed to spawn kitty with win class `{}`", Colors::RED, win);
++TESTS_FAILED;
ret = 1;
return;
}
}
// set wrap_focus to true
OK(getFromSocket("/keyword scrolling:wrap_focus true"));
OK(getFromSocket("/dispatch focuswindow class:a"));
OK(getFromSocket("/dispatch layoutmsg focus l"));
{
auto str = getFromSocket("/activewindow");
EXPECT_CONTAINS(str, "class: d");
}
OK(getFromSocket("/dispatch layoutmsg focus r"));
{
auto str = getFromSocket("/activewindow");
EXPECT_CONTAINS(str, "class: a");
}
// set wrap_focus to false
OK(getFromSocket("/keyword scrolling:wrap_focus false"));
OK(getFromSocket("/dispatch focuswindow class:a"));
OK(getFromSocket("/dispatch layoutmsg focus l"));
{
auto str = getFromSocket("/activewindow");
EXPECT_CONTAINS(str, "class: a");
}
OK(getFromSocket("/dispatch focuswindow class:d"));
OK(getFromSocket("/dispatch layoutmsg focus r"));
{
auto str = getFromSocket("/activewindow");
EXPECT_CONTAINS(str, "class: d");
}
// clean up
NLog::log("{}Killing all windows", Colors::YELLOW);
Tests::killAllWindows();
}
static void testSwapcolWrapping() {
for (auto const& win : {"a", "b", "c", "d"}) {
if (!Tests::spawnKitty(win)) {
NLog::log("{}Failed to spawn kitty with win class `{}`", Colors::RED, win);
++TESTS_FAILED;
ret = 1;
return;
}
}
// set wrap_swapcol to true
OK(getFromSocket("/keyword scrolling:wrap_swapcol true"));
OK(getFromSocket("/dispatch focuswindow class:a"));
OK(getFromSocket("/dispatch layoutmsg swapcol l"));
OK(getFromSocket("/dispatch layoutmsg focus l"));
{
auto str = getFromSocket("/activewindow");
EXPECT_CONTAINS(str, "class: c");
}
// clean up
NLog::log("{}Killing all windows", Colors::YELLOW);
Tests::killAllWindows();
for (auto const& win : {"a", "b", "c", "d"}) {
if (!Tests::spawnKitty(win)) {
NLog::log("{}Failed to spawn kitty with win class `{}`", Colors::RED, win);
++TESTS_FAILED;
ret = 1;
return;
}
}
OK(getFromSocket("/dispatch focuswindow class:d"));
OK(getFromSocket("/dispatch layoutmsg swapcol r"));
OK(getFromSocket("/dispatch layoutmsg focus r"));
{
auto str = getFromSocket("/activewindow");
EXPECT_CONTAINS(str, "class: b");
}
// clean up
NLog::log("{}Killing all windows", Colors::YELLOW);
Tests::killAllWindows();
for (auto const& win : {"a", "b", "c", "d"}) {
if (!Tests::spawnKitty(win)) {
NLog::log("{}Failed to spawn kitty with win class `{}`", Colors::RED, win);
++TESTS_FAILED;
ret = 1;
return;
}
}
// set wrap_swapcol to false
OK(getFromSocket("/keyword scrolling:wrap_swapcol false"));
OK(getFromSocket("/dispatch focuswindow class:a"));
OK(getFromSocket("/dispatch layoutmsg swapcol l"));
OK(getFromSocket("/dispatch layoutmsg focus r"));
{
auto str = getFromSocket("/activewindow");
EXPECT_CONTAINS(str, "class: b");
}
OK(getFromSocket("/dispatch focuswindow class:d"));
OK(getFromSocket("/dispatch layoutmsg swapcol r"));
OK(getFromSocket("/dispatch layoutmsg focus l"));
{
auto str = getFromSocket("/activewindow");
EXPECT_CONTAINS(str, "class: c");
}
// clean up
NLog::log("{}Killing all windows", Colors::YELLOW);
Tests::killAllWindows();
}
static bool test() {
NLog::log("{}Testing Scroll layout", Colors::GREEN);
// setup
OK(getFromSocket("/dispatch workspace name:scroll"));
OK(getFromSocket("/keyword general:layout scrolling"));
// test
NLog::log("{}Testing focus cycling", Colors::GREEN);
testFocusCycling();
// test
NLog::log("{}Testing focus wrap", Colors::GREEN);
testFocusWrapping();
// test
NLog::log("{}Testing swapcol wrap", Colors::GREEN);
testSwapcolWrapping();
// clean up
NLog::log("Cleaning up", Colors::YELLOW);
OK(getFromSocket("/dispatch workspace 1"));
OK(getFromSocket("/reload"));
return !ret;
}
REGISTER_TEST_FN(test);

View file

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

File diff suppressed because it is too large Load diff

View file

@ -6,7 +6,6 @@
#include <chrono> #include <chrono>
#include <hyprutils/os/Process.hpp> #include <hyprutils/os/Process.hpp>
#include <hyprutils/memory/WeakPtr.hpp> #include <hyprutils/memory/WeakPtr.hpp>
#include <hyprutils/utils/ScopeGuard.hpp>
#include <csignal> #include <csignal>
#include <cerrno> #include <cerrno>
#include "../shared.hpp" #include "../shared.hpp"
@ -15,402 +14,16 @@ static int ret = 0;
using namespace Hyprutils::OS; using namespace Hyprutils::OS;
using namespace Hyprutils::Memory; using namespace Hyprutils::Memory;
using namespace Hyprutils::Utils;
#define UP CUniquePointer #define UP CUniquePointer
#define SP CSharedPointer #define SP CSharedPointer
static bool testSpecialWorkspaceFullscreen() {
NLog::log("{}Testing special workspace fullscreen detection", Colors::YELLOW);
CScopeGuard guard = {[&]() {
NLog::log("{}Cleaning up special workspace fullscreen test", Colors::YELLOW);
// Close special workspace if open
auto monitors = getFromSocket("/monitors");
if (monitors.contains("(special:test_fs_special)") && !monitors.contains("special workspace: 0 ()"))
getFromSocket("/dispatch togglespecialworkspace test_fs_special");
Tests::killAllWindows();
OK(getFromSocket("/reload"));
}};
getFromSocket("/dispatch workspace 1");
EXPECT(Tests::windowCount(), 0);
NLog::log("{}Test 1: Fullscreen detection on special workspace", Colors::YELLOW);
OK(getFromSocket("/dispatch workspace special:test_fs_special"));
if (!Tests::spawnKitty("kitty_special"))
return false;
{
auto str = getFromSocket("/activewindow");
EXPECT_CONTAINS(str, "class: kitty_special");
EXPECT_CONTAINS(str, "(special:test_fs_special)");
}
OK(getFromSocket("/dispatch fullscreen 0"));
{
auto str = getFromSocket("/activewindow");
EXPECT_CONTAINS(str, "fullscreen: 2");
}
{
auto str = getFromSocket("/monitors");
EXPECT_CONTAINS(str, "(special:test_fs_special)");
}
NLog::log("{}Test 2: Special workspace fullscreen precedence", Colors::YELLOW);
// Close special workspace before spawning on regular workspace
OK(getFromSocket("/dispatch togglespecialworkspace test_fs_special"));
getFromSocket("/dispatch workspace 1");
if (!Tests::spawnKitty("kitty_regular"))
return false;
OK(getFromSocket("/dispatch fullscreen 0"));
{
auto str = getFromSocket("/activewindow");
EXPECT_CONTAINS(str, "class: kitty_regular");
EXPECT_CONTAINS(str, "fullscreen: 2");
}
OK(getFromSocket("/dispatch togglespecialworkspace test_fs_special"));
OK(getFromSocket("/dispatch focuswindow class:kitty_special"));
{
auto str = getFromSocket("/activewindow");
EXPECT_CONTAINS(str, "class: kitty_special");
}
NLog::log("{}Test 3: Toggle special workspace hides it", Colors::YELLOW);
OK(getFromSocket("/dispatch togglespecialworkspace test_fs_special"));
OK(getFromSocket("/dispatch focuswindow class:kitty_regular"));
{
auto str = getFromSocket("/activewindow");
EXPECT_CONTAINS(str, "class: kitty_regular");
EXPECT_CONTAINS(str, "fullscreen: 2");
}
{
auto str = getFromSocket("/monitors");
EXPECT_CONTAINS(str, "special workspace: 0 ()");
}
return true;
}
static bool testAsymmetricGaps() {
NLog::log("{}Testing asymmetric gap splits", Colors::YELLOW);
{
CScopeGuard guard = {[&]() {
NLog::log("{}Cleaning up asymmetric gap test", Colors::YELLOW);
Tests::killAllWindows();
OK(getFromSocket("/reload"));
}};
OK(getFromSocket("/dispatch workspace name:gap_split_test"));
OK(getFromSocket("r/keyword general:gaps_in 0"));
OK(getFromSocket("r/keyword general:border_size 0"));
OK(getFromSocket("r/keyword dwindle:split_width_multiplier 1.0"));
OK(getFromSocket("r/keyword workspace name:gap_split_test,gapsout:0 1000 0 0"));
NLog::log("{}Testing default split (force_split = 0)", Colors::YELLOW);
OK(getFromSocket("r/keyword dwindle:force_split 0"));
if (!Tests::spawnKitty("gaps_kitty_A") || !Tests::spawnKitty("gaps_kitty_B"))
return false;
NLog::log("{}Expecting vertical split (B below A)", Colors::YELLOW);
OK(getFromSocket("/dispatch focuswindow class:gaps_kitty_A"));
EXPECT_CONTAINS(getFromSocket("/activewindow"), "at: 0,0");
OK(getFromSocket("/dispatch focuswindow class:gaps_kitty_B"));
EXPECT_CONTAINS(getFromSocket("/activewindow"), "at: 0,540");
Tests::killAllWindows();
EXPECT(Tests::windowCount(), 0);
NLog::log("{}Testing force_split = 1", Colors::YELLOW);
OK(getFromSocket("r/keyword dwindle:force_split 1"));
if (!Tests::spawnKitty("gaps_kitty_A") || !Tests::spawnKitty("gaps_kitty_B"))
return false;
NLog::log("{}Expecting vertical split (B above A)", Colors::YELLOW);
OK(getFromSocket("/dispatch focuswindow class:gaps_kitty_B"));
EXPECT_CONTAINS(getFromSocket("/activewindow"), "at: 0,0");
OK(getFromSocket("/dispatch focuswindow class:gaps_kitty_A"));
EXPECT_CONTAINS(getFromSocket("/activewindow"), "at: 0,540");
NLog::log("{}Expecting horizontal split (C left of B)", Colors::YELLOW);
OK(getFromSocket("/dispatch focuswindow class:gaps_kitty_B"));
if (!Tests::spawnKitty("gaps_kitty_C"))
return false;
OK(getFromSocket("/dispatch focuswindow class:gaps_kitty_C"));
EXPECT_CONTAINS(getFromSocket("/activewindow"), "at: 0,0");
OK(getFromSocket("/dispatch focuswindow class:gaps_kitty_B"));
EXPECT_CONTAINS(getFromSocket("/activewindow"), "at: 460,0");
Tests::killAllWindows();
EXPECT(Tests::windowCount(), 0);
NLog::log("{}Testing force_split = 2", Colors::YELLOW);
OK(getFromSocket("r/keyword dwindle:force_split 2"));
if (!Tests::spawnKitty("gaps_kitty_A") || !Tests::spawnKitty("gaps_kitty_B"))
return false;
NLog::log("{}Expecting vertical split (B below A)", Colors::YELLOW);
OK(getFromSocket("/dispatch focuswindow class:gaps_kitty_A"));
EXPECT_CONTAINS(getFromSocket("/activewindow"), "at: 0,0");
OK(getFromSocket("/dispatch focuswindow class:gaps_kitty_B"));
EXPECT_CONTAINS(getFromSocket("/activewindow"), "at: 0,540");
NLog::log("{}Expecting horizontal split (C right of A)", Colors::YELLOW);
OK(getFromSocket("/dispatch focuswindow class:gaps_kitty_A"));
if (!Tests::spawnKitty("gaps_kitty_C"))
return false;
OK(getFromSocket("/dispatch focuswindow class:gaps_kitty_A"));
EXPECT_CONTAINS(getFromSocket("/activewindow"), "at: 0,0");
OK(getFromSocket("/dispatch focuswindow class:gaps_kitty_C"));
EXPECT_CONTAINS(getFromSocket("/activewindow"), "at: 460,0");
}
// kill all
NLog::log("{}Killing all windows", Colors::YELLOW);
Tests::killAllWindows();
return true;
}
static void testMultimonBAF() {
NLog::log("{}Testing multimon back and forth", Colors::YELLOW);
OK(getFromSocket("/keyword binds:workspace_back_and_forth 1"));
OK(getFromSocket("/dispatch focusmonitor HEADLESS-2"));
OK(getFromSocket("/dispatch workspace 1"));
Tests::spawnKitty();
OK(getFromSocket("/dispatch workspace 2"));
Tests::spawnKitty();
OK(getFromSocket("/dispatch focusmonitor HEADLESS-3"));
OK(getFromSocket("/dispatch workspace 3"));
Tests::spawnKitty();
OK(getFromSocket("/dispatch workspace 3"));
{
auto str = getFromSocket("/activeworkspace");
EXPECT_CONTAINS(str, "workspace ID 2 ");
}
OK(getFromSocket("/dispatch workspace 4"));
OK(getFromSocket("/dispatch workspace 4"));
{
auto str = getFromSocket("/activeworkspace");
EXPECT_CONTAINS(str, "workspace ID 2 ");
}
OK(getFromSocket("/dispatch workspace 2"));
{
auto str = getFromSocket("/activeworkspace");
EXPECT_CONTAINS(str, "workspace ID 4 ");
}
OK(getFromSocket("/dispatch workspace 3"));
OK(getFromSocket("/dispatch workspace 3"));
{
auto str = getFromSocket("/activeworkspace");
EXPECT_CONTAINS(str, "workspace ID 4 ");
}
OK(getFromSocket("/dispatch workspace 2"));
OK(getFromSocket("/dispatch workspace 3"));
OK(getFromSocket("/dispatch workspace 1"));
OK(getFromSocket("/dispatch workspace 1"));
{
auto str = getFromSocket("/activeworkspace");
EXPECT_CONTAINS(str, "workspace ID 3 ");
}
Tests::killAllWindows();
}
static void testMultimonFocus() {
NLog::log("{}Testing multimon focus and move", Colors::YELLOW);
OK(getFromSocket("/keyword input:follow_mouse 0"));
OK(getFromSocket("/dispatch focusmonitor HEADLESS-3"));
OK(getFromSocket("/dispatch workspace 8"));
OK(getFromSocket("/dispatch focusmonitor HEADLESS-2"));
OK(getFromSocket("/dispatch workspace 7"));
for (auto const& win : {"a", "b"}) {
if (!Tests::spawnKitty(win)) {
NLog::log("{}Failed to spawn kitty with win class `{}`", Colors::RED, win);
++TESTS_FAILED;
ret = 1;
return;
}
}
OK(getFromSocket("/dispatch focuswindow class:a"));
OK(getFromSocket("/dispatch movefocus r"));
{
auto str = getFromSocket("/activeworkspace");
EXPECT_CONTAINS(str, "workspace ID 7 ");
}
{
auto str = getFromSocket("/activewindow");
EXPECT_CONTAINS(str, "class: b");
}
OK(getFromSocket("/dispatch movefocus r"));
{
auto str = getFromSocket("/activeworkspace");
EXPECT_CONTAINS(str, "workspace ID 8 ");
}
Tests::spawnKitty("c");
{
auto str = getFromSocket("/activewindow");
EXPECT_CONTAINS(str, "class: c");
}
{
auto str = getFromSocket("/activeworkspace");
EXPECT_CONTAINS(str, "workspace ID 8 ");
}
OK(getFromSocket("/dispatch movefocus l"));
{
auto str = getFromSocket("/activewindow");
EXPECT_CONTAINS(str, "class: b");
}
{
auto str = getFromSocket("/activeworkspace");
EXPECT_CONTAINS(str, "workspace ID 7 ");
}
OK(getFromSocket("/dispatch movewindow r"));
{
auto str = getFromSocket("/activewindow");
EXPECT_CONTAINS(str, "class: b");
}
{
auto str = getFromSocket("/activeworkspace");
EXPECT_CONTAINS(str, "workspace ID 8 ");
}
OK(getFromSocket("/dispatch movefocus r"));
{
auto str = getFromSocket("/activewindow");
EXPECT_CONTAINS(str, "class: c");
}
{
auto str = getFromSocket("/activeworkspace");
EXPECT_CONTAINS(str, "workspace ID 8 ");
}
OK(getFromSocket("/dispatch movefocus l"));
OK(getFromSocket("/keyword general:no_focus_fallback true"));
OK(getFromSocket("/keyword binds:window_direction_monitor_fallback false"));
EXPECT_NOT(getFromSocket("/dispatch movefocus l"), "ok");
{
auto str = getFromSocket("/activewindow");
EXPECT_CONTAINS(str, "class: b");
}
{
auto str = getFromSocket("/activeworkspace");
EXPECT_CONTAINS(str, "workspace ID 8 ");
}
OK(getFromSocket("/dispatch movewindow l"));
{
auto str = getFromSocket("/activewindow");
EXPECT_CONTAINS(str, "class: b");
}
{
auto str = getFromSocket("/activeworkspace");
EXPECT_CONTAINS(str, "workspace ID 8 ");
}
OK(getFromSocket("/reload"));
Tests::killAllWindows();
}
static void testDynamicWsEffects() {
// test dynamic workspace effects, they shouldn't lag
OK(getFromSocket("/dispatch workspace 69"));
Tests::spawnKitty("bitch");
OK(getFromSocket("r/keyword workspace 69,bordersize:20"));
OK(getFromSocket("r/keyword workspace 69,rounding:false"));
EXPECT(getFromSocket("/getprop class:bitch border_size"), "20");
EXPECT(getFromSocket("/getprop class:bitch rounding"), "0");
OK(getFromSocket("/reload"));
Tests::killAllWindows();
}
static bool test() { static bool test() {
NLog::log("{}Testing workspaces", Colors::GREEN); NLog::log("{}Testing workspaces", Colors::GREEN);
EXPECT(Tests::windowCount(), 0);
// test on workspace "window" // test on workspace "window"
NLog::log("{}Switching to workspace 1", Colors::YELLOW); NLog::log("{}Switching to workspace 1", Colors::YELLOW);
getFromSocket("/dispatch workspace 1"); OK(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"));
NLog::log("{}Spawning kittyProc on ws 1", Colors::YELLOW); NLog::log("{}Spawning kittyProc on ws 1", Colors::YELLOW);
auto kittyProcA = Tests::spawnKitty(); auto kittyProcA = Tests::spawnKitty();
@ -452,11 +65,6 @@ static bool test() {
NLog::log("{}Switching to workspace 1", Colors::YELLOW); NLog::log("{}Switching to workspace 1", Colors::YELLOW);
OK(getFromSocket("/dispatch workspace 1")); 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); NLog::log("{}Switching to workspace m+1", Colors::YELLOW);
OK(getFromSocket("/dispatch workspace m+1")); OK(getFromSocket("/dispatch workspace m+1"));
@ -602,7 +210,7 @@ static bool test() {
EXPECT_CONTAINS(str, "special:HELLO"); 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"); getFromSocket("/dispatch workspace 3");
{ {
@ -727,19 +335,12 @@ static bool test() {
EXPECT_CONTAINS(str, "class: kitty_B"); EXPECT_CONTAINS(str, "class: kitty_B");
} }
// kill all
NLog::log("{}Killing all windows", Colors::YELLOW);
Tests::killAllWindows();
testMultimonBAF();
testMultimonFocus();
// destroy the headless output // destroy the headless output
OK(getFromSocket("/output remove HEADLESS-3")); OK(getFromSocket("/output remove HEADLESS-3"));
testSpecialWorkspaceFullscreen(); // kill all
testAsymmetricGaps(); NLog::log("{}Killing all windows", Colors::YELLOW);
testDynamicWsEffects(); Tests::killAllWindows();
NLog::log("{}Expecting 0 windows", Colors::YELLOW); NLog::log("{}Expecting 0 windows", Colors::YELLOW);
EXPECT(Tests::windowCount(), 0); EXPECT(Tests::windowCount(), 0);

View file

@ -19,13 +19,3 @@ bool testPlugin() {
} }
return true; 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 #pragma once
bool testPlugin(); bool testPlugin();
bool testVkb();

View file

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

View file

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

View file

@ -22,7 +22,6 @@ monitor=HEADLESS-3,1920x1080@60,auto-right,1
monitor=HEADLESS-4,1920x1080@60,auto-right,1 monitor=HEADLESS-4,1920x1080@60,auto-right,1
monitor=HEADLESS-5,1920x1080@60,auto-right,1 monitor=HEADLESS-5,1920x1080@60,auto-right,1
monitor=HEADLESS-6,1920x1080@60,auto-right,1 monitor=HEADLESS-6,1920x1080@60,auto-right,1
monitor=HEADLESS-PERSISTENT-TEST,1920x1080@60,auto-right,1
monitor=,disabled monitor=,disabled
@ -46,7 +45,6 @@ $menu = wofi --show drun
# Autostart necessary processes (like notifications daemons, status bars, etc.) # Autostart necessary processes (like notifications daemons, status bars, etc.)
# Or execute your favorite apps at launch like this: # Or execute your favorite apps at launch like this:
exec-once = sleep 0 # Terminates very quickly
# exec-once = $terminal # exec-once = $terminal
# exec-once = nm-applet & # exec-once = nm-applet &
# exec-once = waybar & hyprpaper & firefox # exec-once = waybar & hyprpaper & firefox
@ -152,11 +150,6 @@ animations {
animation = workspacesOut, 1, 1.94, almostLinear, fade animation = workspacesOut, 1, 1.94, almostLinear, fade
} }
device {
name = test-mouse-1
enabled = true
}
# Ref https://wiki.hyprland.org/Configuring/Workspace-Rules/ # Ref https://wiki.hyprland.org/Configuring/Workspace-Rules/
# "Smart gaps" / "No gaps when only" # "Smart gaps" / "No gaps when only"
# uncomment all if you wish to use that. # uncomment all if you wish to use that.
@ -171,7 +164,6 @@ device {
dwindle { dwindle {
pseudotile = true # Master switch for pseudotiling. Enabling is bound to mainMod + P in the keybinds section below pseudotile = true # Master switch for pseudotiling. Enabling is bound to mainMod + P in the keybinds section below
preserve_split = true # You probably want this preserve_split = true # You probably want this
split_bias = 1
} }
# See https://wiki.hyprland.org/Configuring/Master-Layout/ for more # See https://wiki.hyprland.org/Configuring/Master-Layout/ for more
@ -179,17 +171,6 @@ master {
new_status = master new_status = master
} }
scrolling {
fullscreen_on_one_column = true
column_width = 0.5
focus_fit_method = 1
follow_focus = true
follow_min_visible = 1
explicit_column_widths = 0.25, 0.333, 0.5, 0.667, 0.75, 1.0
wrap_focus = true
wrap_swapcol = true
}
# https://wiki.hyprland.org/Configuring/Variables/#misc # https://wiki.hyprland.org/Configuring/Variables/#misc
misc { misc {
force_default_wallpaper = -1 # Set to 0 or 1 to disable the anime mascot wallpapers force_default_wallpaper = -1 # Set to 0 or 1 to disable the anime mascot wallpapers
@ -220,7 +201,7 @@ input {
# https://wiki.hyprland.org/Configuring/Variables/#gestures # https://wiki.hyprland.org/Configuring/Variables/#gestures
gestures { gestures {
workspace_swipe = false
} }
# Example per-device config # Example per-device config
@ -230,10 +211,6 @@ device {
sensitivity = -0.5 sensitivity = -0.5
} }
debug {
disable_logs = false
}
################### ###################
### KEYBINDINGS ### ### KEYBINDINGS ###
@ -250,7 +227,7 @@ bind = $mainMod, E, exec, $fileManager
bind = $mainMod, V, togglefloating, bind = $mainMod, V, togglefloating,
bind = $mainMod, R, exec, $menu bind = $mainMod, R, exec, $menu
bind = $mainMod, P, pseudo, # dwindle bind = $mainMod, P, pseudo, # dwindle
bind = $mainMod, J, layoutmsg, togglesplit, # dwindle bind = $mainMod, J, togglesplit, # dwindle
# Move focus with mainMod + arrow keys # Move focus with mainMod + arrow keys
bind = $mainMod, left, movefocus, l bind = $mainMod, left, movefocus, l
@ -308,106 +285,29 @@ bindl = , XF86AudioPause, exec, playerctl play-pause
bindl = , XF86AudioPlay, exec, playerctl play-pause bindl = , XF86AudioPlay, exec, playerctl play-pause
bindl = , XF86AudioPrev, exec, playerctl previous 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 ### ### WINDOWS AND WORKSPACES ###
############################## ##############################
windowrule { # See https://wiki.hyprland.org/Configuring/Window-Rules/ for more
# See https://wiki.hyprland.org/Configuring/Workspace-Rules/ for workspace rules
# Example windowrule v1
# windowrule = float, ^(kitty)$
# Example windowrule v2
# windowrulev2 = float,class:^(kitty)$,title:^(kitty)$
# Ignore maximize requests from apps. You'll probably like this. # Ignore maximize requests from apps. You'll probably like this.
name = suppress-maximize-events windowrulev2 = suppressevent maximize, class:.*
match:class = .*
suppress_event = maximize
}
windowrule {
# Fix some dragging issues with XWayland # Fix some dragging issues with XWayland
name = fix-xwayland-drags windowrulev2 = nofocus,class:^$,title:^$,xwayland:1,floating:1,fullscreen:0,pinned:0
match:class = ^$
match:title = ^$
match:xwayland = true
match:float = true
match:fullscreen = false
match:pin = false
no_focus = true
}
# Workspace "windows" is a smart gaps one
workspace = n[s:window] w[tv1], gapsout:0, gapsin:0 workspace = n[s:window] w[tv1], gapsout:0, gapsin:0
workspace = n[s:window] f[1], gapsout:0, gapsin:0 workspace = n[s:window] f[1], gapsout:0, gapsin:0
windowrulev2 = bordersize 0, floating:0, onworkspace:n[s:window] w[tv1]
windowrule { windowrulev2 = rounding 0, floating:0, onworkspace:n[s:window] w[tv1]
name = smart-gaps-1 windowrulev2 = bordersize 0, floating:0, onworkspace:n[s:window] f[1]
match:float = false windowrulev2 = rounding 0, floating:0, onworkspace:n[s:window] f[1]
match:workspace = n[s:window] w[tv1]
border_size = 0
rounding = 0
}
windowrule {
name = smart-gaps-2
match:float = false
match:workspace = n[s:window] f[1]
border_size = 0
rounding = 0
}
windowrule {
name = wr-kitty-stuff
match:class = wr_kitty
float = true
size = 200 200
pin = false
}
windowrule {
name = tagged-kitty-floats
match:tag = tag_kitty
float = true
}
windowrule {
name = static-kitty-tag
match:class = tag_kitty
tag = +tag_kitty
}
gesture = 3, left, dispatcher, exec, kitty
gesture = 3, right, float
gesture = 3, up, close
gesture = 3, down, fullscreen
gesture = 3, down, mod:ALT, float
gesture = 3, horizontal, mod:ALT, workspace
gesture = 5, up, dispatcher, sendshortcut, , e, activewindow
gesture = 5, down, dispatcher, sendshortcut, , x, activewindow
gesture = 5, left, dispatcher, sendshortcut, , i, activewindow
gesture = 5, right, dispatcher, sendshortcut, , t, activewindow
gesture = 4, right, dispatcher, sendshortcut, , return, activewindow
gesture = 4, left, dispatcher, movecursortocorner, 1
gesturep = 2, right, float

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

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