pax_global_header00006660000000000000000000000064151367007260014521gustar00rootroot0000000000000052 comment=64b991cb917e28a51a50987a113ac4bf014ad0b7 hyprpaper-0.8.3/000077500000000000000000000000001513670072600135435ustar00rootroot00000000000000hyprpaper-0.8.3/.clang-format000066400000000000000000000034161513670072600161220ustar00rootroot00000000000000--- Language: Cpp BasedOnStyle: LLVM AccessModifierOffset: -2 AlignAfterOpenBracket: Align AlignConsecutiveMacros: true AlignConsecutiveAssignments: true AlignEscapedNewlines: Right AlignOperands: false AlignTrailingComments: true AllowAllArgumentsOnNextLine: true AllowAllConstructorInitializersOnNextLine: true AllowAllParametersOfDeclarationOnNextLine: true AllowShortBlocksOnASingleLine: true AllowShortCaseLabelsOnASingleLine: true AllowShortFunctionsOnASingleLine: Empty AllowShortIfStatementsOnASingleLine: Never AllowShortLambdasOnASingleLine: All AllowShortLoopsOnASingleLine: false AlwaysBreakAfterDefinitionReturnType: None AlwaysBreakAfterReturnType: None AlwaysBreakBeforeMultilineStrings: false AlwaysBreakTemplateDeclarations: Yes BreakBeforeBraces: Attach BreakBeforeTernaryOperators: false BreakConstructorInitializers: AfterColon ColumnLimit: 180 CompactNamespaces: false ConstructorInitializerAllOnOneLineOrOnePerLine: false ExperimentalAutoDetectBinPacking: false FixNamespaceComments: false IncludeBlocks: Preserve IndentCaseLabels: true IndentWidth: 4 PointerAlignment: Left ReflowComments: false SortIncludes: false SortUsingDeclarations: false SpaceAfterCStyleCast: false SpaceAfterLogicalNot: false SpaceAfterTemplateKeyword: true SpaceBeforeCtorInitializerColon: true SpaceBeforeInheritanceColon: true SpaceBeforeParens: ControlStatements SpaceBeforeRangeBasedForLoopColon: true SpaceInEmptyParentheses: false SpacesBeforeTrailingComments: 1 SpacesInAngles: false SpacesInCStyleCastParentheses: false SpacesInContainerLiterals: false SpacesInParentheses: false SpacesInSquareBrackets: false Standard: Auto TabWidth: 4 UseTab: Never AllowShortEnumsOnASingleLine: false BraceWrapping: AfterEnum: false AlignConsecutiveDeclarations: AcrossEmptyLines NamespaceIndentation: All hyprpaper-0.8.3/.github/000077500000000000000000000000001513670072600151035ustar00rootroot00000000000000hyprpaper-0.8.3/.github/workflows/000077500000000000000000000000001513670072600171405ustar00rootroot00000000000000hyprpaper-0.8.3/.github/workflows/nix-build.yaml000066400000000000000000000032631513670072600217230ustar00rootroot00000000000000name: Build Hyprpaper (Nix) on: [push, pull_request, workflow_dispatch] jobs: nix: name: "Build" runs-on: ubuntu-latest steps: - name: Clone repository uses: actions/checkout@v3 with: submodules: recursive - name: Install Nix uses: nixbuild/nix-quick-install-action@v31 with: nix_conf: | keep-env-derivations = true keep-outputs = true - name: Restore and save Nix store uses: nix-community/cache-nix-action@v6 with: # restore and save a cache using this key primary-key: nix-${{ runner.os }}-${{ hashFiles('**/*.nix', '**/flake.lock') }} # if there's no cache hit, restore a cache by this prefix restore-prefixes-first-match: nix-${{ runner.os }}- # collect garbage until the Nix store size (in bytes) is at most this number # before trying to save a new cache # 1G = 1073741824 gc-max-store-size-linux: 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 - uses: cachix/cachix-action@v12 with: name: hyprland authToken: '${{ secrets.CACHIX_AUTH_TOKEN }}' - name: Build Hyprpaper with default settings run: nix build --print-build-logs --accept-flake-config hyprpaper-0.8.3/.gitignore000066400000000000000000000005421513670072600155340ustar00rootroot00000000000000CMakeLists.txt.user CMakeCache.txt CMakeFiles CMakeScripts Testing cmake_install.cmake install_manifest.txt compile_commands.json CTestTestfile.cmake _deps build/ result /.vscode/ /.idea *.o *-protocol.c *-protocol.h .ccls-cache protocols/*.hpp protocols/*.cpp hw-protocols/*.hpp hw-protocols/*.cpp .cache/ hyprctl/hyprctl gmon.out *.out *.tar.gz hyprpaper-0.8.3/CMakeLists.txt000066400000000000000000000127311513670072600163070ustar00rootroot00000000000000cmake_minimum_required(VERSION 3.12) file(READ "${CMAKE_SOURCE_DIR}/VERSION" VER_RAW) string(STRIP ${VER_RAW} VERSION) project( hyprpaper DESCRIPTION "A blazing fast wayland wallpaper utility" VERSION ${VERSION}) add_compile_definitions(HYPRPAPER_VERSION="${VERSION}") set(CMAKE_MESSAGE_LOG_LEVEL "STATUS") message(STATUS "Configuring hyprpaper!") configure_file(systemd/hyprpaper.service.in systemd/hyprpaper.service @ONLY) # Get git info hash and branch execute_process( COMMAND git rev-parse --abbrev-ref HEAD WORKING_DIRECTORY ${CMAKE_SOURCE_DIR} OUTPUT_VARIABLE GIT_BRANCH OUTPUT_STRIP_TRAILING_WHITESPACE) execute_process( COMMAND git rev-parse HEAD WORKING_DIRECTORY ${CMAKE_SOURCE_DIR} OUTPUT_VARIABLE GIT_COMMIT_HASH OUTPUT_STRIP_TRAILING_WHITESPACE) execute_process( COMMAND bash -c "git show ${GIT_COMMIT_HASH} | head -n 5 | tail -n 1 | sed -s 's/\#//g; s/^[[:space:]]*//'" WORKING_DIRECTORY ${CMAKE_SOURCE_DIR} OUTPUT_VARIABLE GIT_COMMIT_MESSAGE OUTPUT_STRIP_TRAILING_WHITESPACE) execute_process( COMMAND bash -c "git diff-index --quiet HEAD -- || echo \"dirty\"" WORKING_DIRECTORY ${CMAKE_SOURCE_DIR} OUTPUT_VARIABLE GIT_DIRTY OUTPUT_STRIP_TRAILING_WHITESPACE) include_directories(. hw-protocols protocols) set(CMAKE_CXX_STANDARD 23) add_compile_options(-DWLR_USE_UNSTABLE) add_compile_options(-Wall -Wextra -Wno-unused-parameter -Wno-unused-value -Wno-missing-field-initializers -Wno-narrowing) find_package(Threads REQUIRED) find_package(PkgConfig REQUIRED) find_package(hyprwayland-scanner 0.4.0 REQUIRED) pkg_check_modules( deps REQUIRED IMPORTED_TARGET hyprlang>=0.6.0 hyprutils>=0.2.4 wayland-client hyprtoolkit>=0.4.1 hyprwire pixman-1 libdrm) file(GLOB_RECURSE SRCFILES "src/*.cpp") add_executable(hyprpaper ${SRCFILES}) # Wayland 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}") function(protocolnew protoPath protoName external) if(external) set(path ${CMAKE_SOURCE_DIR}/${protoPath}) else() set(path ${WAYLAND_PROTOCOLS_DIR}/${protoPath}) endif() add_custom_command( OUTPUT ${CMAKE_SOURCE_DIR}/protocols/${protoName}.cpp ${CMAKE_SOURCE_DIR}/protocols/${protoName}.hpp COMMAND hyprwayland-scanner --client ${path}/${protoName}.xml ${CMAKE_SOURCE_DIR}/protocols/ WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}) target_sources(hyprpaper PRIVATE protocols/${protoName}.cpp protocols/${protoName}.hpp) endfunction() function(protocolWayland) add_custom_command( OUTPUT ${CMAKE_SOURCE_DIR}/protocols/wayland.cpp ${CMAKE_SOURCE_DIR}/protocols/wayland.hpp COMMAND hyprwayland-scanner --wayland-enums --client ${WAYLAND_SCANNER_PKGDATA_DIR}/wayland.xml ${CMAKE_SOURCE_DIR}/protocols/ WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}) target_sources(hyprpaper PRIVATE protocols/wayland.cpp protocols/wayland.hpp) endfunction() protocolwayland() protocolnew("protocols" "wlr-layer-shell-unstable-v1" true) protocolnew("stable/linux-dmabuf" "linux-dmabuf-v1" false) protocolnew("staging/fractional-scale" "fractional-scale-v1" false) protocolnew("stable/viewporter" "viewporter" false) protocolnew("stable/xdg-shell" "xdg-shell" false) protocolnew("staging/cursor-shape" "cursor-shape-v1" false) protocolnew("stable/tablet" "tablet-v2" false) # Hyprwire function(hyprprotocolServer protoPath protoName) set(path ${CMAKE_SOURCE_DIR}/${protoPath}) add_custom_command( OUTPUT ${CMAKE_SOURCE_DIR}/hw-protocols/${protoName}-server.cpp ${CMAKE_SOURCE_DIR}/hw-protocols/${protoName}-server.hpp COMMAND hyprwire-scanner ${path}/${protoName}.xml ${CMAKE_SOURCE_DIR}/hw-protocols/ WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}) target_sources(hyprpaper PRIVATE hw-protocols/${protoName}-server.cpp hw-protocols/${protoName}-server.hpp ) endfunction() hyprprotocolServer(hw-protocols hyprpaper_core) # string(REPLACE "\"" " " GIT_COMMIT_MESSAGE_ESCAPED "${GIT_COMMIT_MESSAGE}") target_compile_definitions(hyprpaper PRIVATE "-DGIT_COMMIT_HASH=\"${GIT_COMMIT_HASH}\"") target_compile_definitions(hyprpaper PRIVATE "-DGIT_BRANCH=\"${GIT_BRANCH}\"") target_compile_definitions(hyprpaper PRIVATE "-DGIT_COMMIT_MESSAGE=\"${GIT_COMMIT_MESSAGE_ESCAPED}\"") target_compile_definitions(hyprpaper PRIVATE "-DGIT_DIRTY=\"${GIT_DIRTY}\"") target_link_libraries(hyprpaper rt) set(CPACK_PROJECT_NAME ${PROJECT_NAME}) set(CPACK_PROJECT_VERSION ${PROJECT_VERSION}) include(CPack) target_link_libraries(hyprpaper PkgConfig::deps) target_link_libraries( hyprpaper OpenGL GLESv2 pthread magic ${CMAKE_THREAD_LIBS_INIT} wayland-cursor) if(CMAKE_BUILD_TYPE MATCHES Debug OR CMAKE_BUILD_TYPE MATCHES DEBUG) set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -pg -no-pie -fno-builtin") set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -pg -no-pie -fno-builtin") set(CMAKE_SHARED_LINKER_FLAGS "${CMAKE_SHARED_LINKER_FLAGS} -pg -no-pie -fno-builtin") endif(CMAKE_BUILD_TYPE MATCHES Debug OR CMAKE_BUILD_TYPE MATCHES DEBUG) include(GNUInstallDirs) install(TARGETS hyprpaper) install(FILES ${CMAKE_BINARY_DIR}/systemd/hyprpaper.service DESTINATION "lib/systemd/user") hyprpaper-0.8.3/LICENSE000066400000000000000000000027641513670072600145610ustar00rootroot00000000000000BSD 3-Clause License Copyright (c) 2022, Hypr Development All rights reserved. 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. hyprpaper-0.8.3/README.md000066400000000000000000000021011513670072600150140ustar00rootroot00000000000000# hyprpaper Hyprpaper is a simple and fast wallpaper utility for Hyprland with the ability to dynamically change wallpapers through sockets. # Features - Per-output wallpapers - fill, tile, cover or contain modes - fractional scaling support - IPC for fast wallpaper switches # Installation [Arch Linux](https://archlinux.org/packages/extra/x86_64/hyprpaper/): `pacman -S hyprpaper` [OpenSuse Linux](https://software.opensuse.org/package/hyprpaper): `zypper install hyprpaper` ## Manual: ### Dependencies The development files of these packages need to be installed on the system for `hyprpaper` to build correctly. (Development packages are usually suffixed with `-dev` or `-devel` in most distros' repos). - hyprtoolkit - hyprlang - hyprutils - hyprwire ### Building Building is done via CMake: ```sh cmake --no-warn-unused-cli -DCMAKE_BUILD_TYPE:STRING=Release -DCMAKE_INSTALL_PREFIX:PATH=/usr -S . -B ./build cmake --build ./build --config Release --target hyprpaper -j`nproc 2>/dev/null || getconf _NPROCESSORS_CONF` ``` Install with: ```sh cmake --install ./build ```hyprpaper-0.8.3/VERSION000066400000000000000000000000061513670072600146070ustar00rootroot000000000000000.8.3 hyprpaper-0.8.3/flake.lock000066400000000000000000000131451513670072600155030ustar00rootroot00000000000000{ "nodes": { "aquamarine": { "inputs": { "hyprutils": [ "hyprutils" ], "hyprwayland-scanner": [ "hyprwayland-scanner" ], "nixpkgs": [ "nixpkgs" ], "systems": [ "systems" ] }, "locked": { "lastModified": 1765900596, "narHash": "sha256-+hn8v9jkkLP9m+o0Nm5SiEq10W0iWDSotH2XfjU45fA=", "owner": "hyprwm", "repo": "aquamarine", "rev": "d83c97f8f5c0aae553c1489c7d9eff3eadcadace", "type": "github" }, "original": { "owner": "hyprwm", "repo": "aquamarine", "type": "github" } }, "hyprgraphics": { "inputs": { "hyprutils": [ "hyprutils" ], "nixpkgs": [ "nixpkgs" ], "systems": [ "systems" ] }, "locked": { "lastModified": 1763733840, "narHash": "sha256-JnET78yl5RvpGuDQy3rCycOCkiKoLr5DN1fPhRNNMco=", "owner": "hyprwm", "repo": "hyprgraphics", "rev": "8f1bec691b2d198c60cccabca7a94add2df4ed1a", "type": "github" }, "original": { "owner": "hyprwm", "repo": "hyprgraphics", "type": "github" } }, "hyprlang": { "inputs": { "hyprutils": [ "hyprutils" ], "nixpkgs": [ "nixpkgs" ], "systems": [ "systems" ] }, "locked": { "lastModified": 1764612430, "narHash": "sha256-54ltTSbI6W+qYGMchAgCR6QnC1kOdKXN6X6pJhOWxFg=", "owner": "hyprwm", "repo": "hyprlang", "rev": "0d00dc118981531aa731150b6ea551ef037acddd", "type": "github" }, "original": { "owner": "hyprwm", "repo": "hyprlang", "type": "github" } }, "hyprtoolkit": { "inputs": { "aquamarine": [ "aquamarine" ], "hyprgraphics": [ "hyprgraphics" ], "hyprlang": [ "hyprlang" ], "hyprutils": [ "hyprutils" ], "hyprwayland-scanner": [ "hyprwayland-scanner" ], "nixpkgs": [ "nixpkgs" ], "systems": [ "systems" ] }, "locked": { "lastModified": 1765999299, "narHash": "sha256-C0tMHhVMEN2XlgMVeuJSbY64h9UhR2AKk5Hxxlxx6cA=", "owner": "hyprwm", "repo": "hyprtoolkit", "rev": "0a5d2c25d018112434e802212a1ad57ca1e24819", "type": "github" }, "original": { "owner": "hyprwm", "repo": "hyprtoolkit", "type": "github" } }, "hyprutils": { "inputs": { "nixpkgs": [ "nixpkgs" ], "systems": [ "systems" ] }, "locked": { "lastModified": 1766002182, "narHash": "sha256-Ji2ty5d6yuToq59SZfpG0T5B5SkF3UiHoDl8VMyQp14=", "owner": "hyprwm", "repo": "hyprutils", "rev": "1c527b30feb7bed959ac07ae034a6105e6b65fd3", "type": "github" }, "original": { "owner": "hyprwm", "repo": "hyprutils", "type": "github" } }, "hyprwayland-scanner": { "inputs": { "nixpkgs": [ "nixpkgs" ], "systems": [ "systems" ] }, "locked": { "lastModified": 1763640274, "narHash": "sha256-Uan1Nl9i4TF/kyFoHnTq1bd/rsWh4GAK/9/jDqLbY5A=", "owner": "hyprwm", "repo": "hyprwayland-scanner", "rev": "f6cf414ca0e16a4d30198fd670ec86df3c89f671", "type": "github" }, "original": { "owner": "hyprwm", "repo": "hyprwayland-scanner", "type": "github" } }, "hyprwire": { "inputs": { "hyprutils": [ "hyprutils" ], "nixpkgs": [ "nixpkgs" ], "systems": [ "systems" ] }, "locked": { "lastModified": 1765741352, "narHash": "sha256-jTJQy1m2XkcZJABajYVP249fCWPl3GbLe3Z8KiQmZqg=", "owner": "hyprwm", "repo": "hyprwire", "rev": "b8ca85082fd5c3cdd3d11027492cd0332b517078", "type": "github" }, "original": { "owner": "hyprwm", "repo": "hyprwire", "type": "github" } }, "nixpkgs": { "locked": { "lastModified": 1765779637, "narHash": "sha256-KJ2wa/BLSrTqDjbfyNx70ov/HdgNBCBBSQP3BIzKnv4=", "owner": "NixOS", "repo": "nixpkgs", "rev": "1306659b587dc277866c7b69eb97e5f07864d8c4", "type": "github" }, "original": { "owner": "NixOS", "ref": "nixos-unstable", "repo": "nixpkgs", "type": "github" } }, "root": { "inputs": { "aquamarine": "aquamarine", "hyprgraphics": "hyprgraphics", "hyprlang": "hyprlang", "hyprtoolkit": "hyprtoolkit", "hyprutils": "hyprutils", "hyprwayland-scanner": "hyprwayland-scanner", "hyprwire": "hyprwire", "nixpkgs": "nixpkgs", "systems": "systems" } }, "systems": { "locked": { "lastModified": 1689347949, "narHash": "sha256-12tWmuL2zgBgZkdoB6qXZsgJEH9LR3oUgpaQq2RbI80=", "owner": "nix-systems", "repo": "default-linux", "rev": "31732fcf5e8fea42e59c2488ad31a0e651500f68", "type": "github" }, "original": { "owner": "nix-systems", "repo": "default-linux", "type": "github" } } }, "root": "root", "version": 7 } hyprpaper-0.8.3/flake.nix000066400000000000000000000070371513670072600153540ustar00rootroot00000000000000{ description = "Hyprpaper is a blazing fast Wayland wallpaper utility with IPC controls"; inputs = { nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; systems.url = "github:nix-systems/default-linux"; aquamarine = { url = "github:hyprwm/aquamarine"; inputs.nixpkgs.follows = "nixpkgs"; inputs.systems.follows = "systems"; inputs.hyprutils.follows = "hyprutils"; inputs.hyprwayland-scanner.follows = "hyprwayland-scanner"; }; hyprgraphics = { url = "github:hyprwm/hyprgraphics"; inputs.nixpkgs.follows = "nixpkgs"; inputs.systems.follows = "systems"; inputs.hyprutils.follows = "hyprutils"; }; hyprutils = { url = "github:hyprwm/hyprutils"; inputs.nixpkgs.follows = "nixpkgs"; inputs.systems.follows = "systems"; }; hyprlang = { url = "github:hyprwm/hyprlang"; inputs.nixpkgs.follows = "nixpkgs"; inputs.systems.follows = "systems"; inputs.hyprutils.follows = "hyprutils"; }; hyprwayland-scanner = { url = "github:hyprwm/hyprwayland-scanner"; inputs.nixpkgs.follows = "nixpkgs"; inputs.systems.follows = "systems"; }; hyprwire = { url = "github:hyprwm/hyprwire"; inputs.nixpkgs.follows = "nixpkgs"; inputs.systems.follows = "systems"; inputs.hyprutils.follows = "hyprutils"; }; hyprtoolkit = { url = "github:hyprwm/hyprtoolkit"; inputs.nixpkgs.follows = "nixpkgs"; inputs.systems.follows = "systems"; inputs.aquamarine.follows = "aquamarine"; inputs.hyprutils.follows = "hyprutils"; inputs.hyprlang.follows = "hyprlang"; inputs.hyprgraphics.follows = "hyprgraphics"; inputs.hyprwayland-scanner.follows = "hyprwayland-scanner"; }; }; outputs = { self, nixpkgs, systems, ... } @ inputs: let inherit (nixpkgs) lib; eachSystem = lib.genAttrs (import systems); pkgsFor = eachSystem (system: import nixpkgs { localSystem.system = system; overlays = with self.overlays; [hyprpaper]; }); mkDate = longDate: (lib.concatStringsSep "-" [ (builtins.substring 0 4 longDate) (builtins.substring 4 2 longDate) (builtins.substring 6 2 longDate) ]); version = lib.removeSuffix "\n" (builtins.readFile ./VERSION); in { overlays = { default = self.overlays.hyprpaper; hyprpaper = lib.composeManyExtensions [ inputs.aquamarine.overlays.default inputs.hyprgraphics.overlays.default inputs.hyprlang.overlays.default inputs.hyprutils.overlays.default inputs.hyprwayland-scanner.overlays.default inputs.hyprtoolkit.overlays.default inputs.hyprwire.overlays.default (final: prev: rec { hyprpaper = final.callPackage ./nix/default.nix { stdenv = final.gcc15Stdenv; version = version + "+date=" + (mkDate (self.lastModifiedDate or "19700101")) + "_" + (self.shortRev or "dirty"); commit = self.rev or ""; }; hyprpaper-debug = hyprpaper.override {debug = true;}; }) ]; }; packages = eachSystem (system: { default = self.packages.${system}.hyprpaper; inherit (pkgsFor.${system}) hyprpaper hyprpaper-debug; }); homeManagerModules = { default = self.homeManagerModules.hyprpaper; hyprpaper = builtins.throw "hyprpaper: the flake HM module has been removed. Use the module from Home Manager upstream."; }; formatter = eachSystem (system: pkgsFor.${system}.alejandra); }; } hyprpaper-0.8.3/hw-protocols/000077500000000000000000000000001513670072600162035ustar00rootroot00000000000000hyprpaper-0.8.3/hw-protocols/hyprpaper_core.xml000066400000000000000000000126011513670072600217470ustar00rootroot00000000000000 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. This is the core manager object for hyprpaper operations Creates a wallpaper object Emitted when a new monitor is added. Emitted when a monitor is removed. Destroys this object. Children remain alive until destroyed. This is an object describing a wallpaper Set a file path for the wallpaper. This has to be an absolute path from the fs root. This is required. Set a fit mode for the wallpaper. This is set to cover by default. 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. 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. Wallpaper was applied successfully. Wallpaper was not applied. See the error field for more information. Destroys this object. hyprpaper-0.8.3/nix/000077500000000000000000000000001513670072600143415ustar00rootroot00000000000000hyprpaper-0.8.3/nix/default.nix000066400000000000000000000027251513670072600165130ustar00rootroot00000000000000{ lib, stdenv, pkg-config, cmake, aquamarine, cairo, expat, file, fribidi, hyprgraphics, hyprlang, hyprutils, hyprtoolkit, hyprwire, hyprwayland-scanner, libGL, libdatrie, libdrm, libjpeg, libjxl, libselinux, libsepol, libthai, libwebp, pango, pcre, pcre2, util-linux, wayland, wayland-protocols, wayland-scanner, xorg, commit, debug ? false, version ? "git", }: stdenv.mkDerivation { pname = "hyprpaper" + lib.optionalString debug "-debug"; inherit version; src = ../.; prePatch = '' substituteInPlace src/main.cpp \ --replace GIT_COMMIT_HASH '"${commit}"' ''; depsBuildBuild = [ pkg-config ]; cmakeBuildType = if debug then "Debug" else "Release"; nativeBuildInputs = [ cmake hyprwayland-scanner hyprwire pkg-config wayland-scanner ]; buildInputs = [ aquamarine cairo expat file fribidi hyprgraphics hyprlang hyprutils hyprtoolkit hyprwire libGL libdatrie libdrm libjpeg libjxl libselinux libsepol libthai libwebp pango pcre pcre2 wayland wayland-protocols xorg.libXdmcp util-linux ]; meta = with lib; { description = "A blazing fast wayland wallpaper utility with IPC controls"; homepage = "https://github.com/hyprwm/hyprpaper"; license = licenses.bsd3; mainProgram = "hyprpaper"; platforms = platforms.linux; }; } hyprpaper-0.8.3/protocols/000077500000000000000000000000001513670072600155675ustar00rootroot00000000000000hyprpaper-0.8.3/protocols/wlr-layer-shell-unstable-v1.xml000066400000000000000000000320741513670072600235010ustar00rootroot00000000000000 Copyright © 2017 Drew DeVault Permission to use, copy, modify, distribute, and sell this software and its documentation for any purpose is hereby granted without fee, provided that the above copyright notice appear in all copies and that both that copyright notice and this permission notice appear in supporting documentation, and that the name of the copyright holders not be used in advertising or publicity pertaining to distribution of the software without specific, written prior permission. The copyright holders make no representations about the suitability of this software for any purpose. It is provided "as is" without express or implied warranty. THE COPYRIGHT HOLDERS DISCLAIM ALL WARRANTIES WITH REGARD TO THIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS, IN NO EVENT SHALL THE COPYRIGHT HOLDERS BE LIABLE FOR ANY SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. Clients can use this interface to assign the surface_layer role to wl_surfaces. Such surfaces are assigned to a "layer" of the output and rendered with a defined z-depth respective to each other. They may also be anchored to the edges and corners of a screen and specify input handling semantics. This interface should be suitable for the implementation of many desktop shell components, and a broad number of other applications that interact with the desktop. Create a layer surface for an existing surface. This assigns the role of layer_surface, or raises a protocol error if another role is already assigned. Creating a layer surface from a wl_surface which has a buffer attached or committed is a client error, and any attempts by a client to attach or manipulate a buffer prior to the first layer_surface.configure call must also be treated as errors. You may pass NULL for output to allow the compositor to decide which output to use. Generally this will be the one that the user most recently interacted with. Clients can specify a namespace that defines the purpose of the layer surface. These values indicate which layers a surface can be rendered in. They are ordered by z depth, bottom-most first. Traditional shell surfaces will typically be rendered between the bottom and top layers. Fullscreen shell surfaces are typically rendered at the top layer. Multiple surfaces can share a single layer, and ordering within a single layer is undefined. An interface that may be implemented by a wl_surface, for surfaces that are designed to be rendered as a layer of a stacked desktop-like environment. Layer surface state (size, anchor, exclusive zone, margin, interactivity) is double-buffered, and will be applied at the time wl_surface.commit of the corresponding wl_surface is called. Sets the size of the surface in surface-local coordinates. The compositor will display the surface centered with respect to its anchors. If you pass 0 for either value, the compositor will assign it and inform you of the assignment in the configure event. You must set your anchor to opposite edges in the dimensions you omit; not doing so is a protocol error. Both values are 0 by default. Size is double-buffered, see wl_surface.commit. Requests that the compositor anchor the surface to the specified edges and corners. If two orthoginal edges are specified (e.g. 'top' and 'left'), then the anchor point will be the intersection of the edges (e.g. the top left corner of the output); otherwise the anchor point will be centered on that edge, or in the center if none is specified. Anchor is double-buffered, see wl_surface.commit. Requests that the compositor avoids occluding an area of the surface with other surfaces. The compositor's use of this information is implementation-dependent - do not assume that this region will not actually be occluded. A positive value is only meaningful if the surface is anchored to an edge, rather than a corner. The zone is the number of surface-local coordinates from the edge that are considered exclusive. Surfaces that do not wish to have an exclusive zone may instead specify how they should interact with surfaces that do. If set to zero, the surface indicates that it would like to be moved to avoid occluding surfaces with a positive excluzive zone. If set to -1, the surface indicates that it would not like to be moved to accommodate for other surfaces, and the compositor should extend it all the way to the edges it is anchored to. For example, a panel might set its exclusive zone to 10, so that maximized shell surfaces are not shown on top of it. A notification might set its exclusive zone to 0, so that it is moved to avoid occluding the panel, but shell surfaces are shown underneath it. A wallpaper or lock screen might set their exclusive zone to -1, so that they stretch below or over the panel. The default value is 0. Exclusive zone is double-buffered, see wl_surface.commit. Requests that the surface be placed some distance away from the anchor point on the output, in surface-local coordinates. Setting this value for edges you are not anchored to has no effect. The exclusive zone includes the margin. Margin is double-buffered, see wl_surface.commit. Set to 1 to request that the seat send keyboard events to this layer surface. For layers below the shell surface layer, the seat will use normal focus semantics. For layers above the shell surface layers, the seat will always give exclusive keyboard focus to the top-most layer which has keyboard interactivity set to true. Layer surfaces receive pointer, touch, and tablet events normally. If you do not want to receive them, set the input region on your surface to an empty region. Events is double-buffered, see wl_surface.commit. This assigns an xdg_popup's parent to this layer_surface. This popup should have been created via xdg_surface::get_popup with the parent set to NULL, and this request must be invoked before committing the popup's initial state. See the documentation of xdg_popup for more details about what an xdg_popup is and how it is used. When a configure event is received, if a client commits the surface in response to the configure event, then the client must make an ack_configure request sometime before the commit request, passing along the serial of the configure event. If the client receives multiple configure events before it can respond to one, it only has to ack the last configure event. A client is not required to commit immediately after sending an ack_configure request - it may even ack_configure several times before its next surface commit. A client may send multiple ack_configure requests before committing, but only the last request sent before a commit indicates which configure event the client really is responding to. This request destroys the layer surface. The configure event asks the client to resize its surface. Clients should arrange their surface for the new states, and then send an ack_configure request with the serial sent in this configure event at some point before committing the new surface. The client is free to dismiss all but the last configure event it received. The width and height arguments specify the size of the window in surface-local coordinates. The size is a hint, in the sense that the client is free to ignore it if it doesn't resize, pick a smaller size (to satisfy aspect ratio or resize in steps of NxM pixels). If the client picks a smaller size and is anchored to two opposite anchors (e.g. 'top' and 'bottom'), the surface will be centered on this axis. If the width or height arguments are zero, it means the client should decide its own window dimension. The closed event is sent by the compositor when the surface will no longer be shown. The output may have been destroyed or the user may have asked for it to be removed. Further changes to the surface will be ignored. The client should destroy the resource after receiving this event, and create a new surface if they so choose. hyprpaper-0.8.3/src/000077500000000000000000000000001513670072600143325ustar00rootroot00000000000000hyprpaper-0.8.3/src/config/000077500000000000000000000000001513670072600155775ustar00rootroot00000000000000hyprpaper-0.8.3/src/config/ConfigManager.cpp000066400000000000000000000222341513670072600210060ustar00rootroot00000000000000#include "ConfigManager.hpp" #include #include #include #include #include #include #include #include #include "../helpers/Logger.hpp" #include "WallpaperMatcher.hpp" #include using namespace std::string_literals; [[nodiscard]] static bool isImage(const std::filesystem::path& path) { static constexpr std::array exts{".jpg", ".jpeg", ".png", ".bmp", ".webp", ".svg"}; auto ext = path.extension().string(); std::transform(ext.begin(), ext.end(), ext.begin(), ::tolower); if (std::ranges::any_of(exts, [&ext](const auto& e) { return ext == e; })) return true; magic_t magic = magic_open(MAGIC_MIME_TYPE); if (magic == nullptr) return false; Hyprutils::Utils::CScopeGuard guard{[&magic] { magic_close(magic); }}; if (magic_load(magic, nullptr) != 0) return false; const auto* result = magic_file(magic, path.string().c_str()); if (result == nullptr) return false; return std::string(result).starts_with("image/"); } // Forward declaration for the source handler static Hyprlang::CParseResult handleSource(const char* COMMAND, const char* VALUE); static std::string getMainConfigPath() { static const auto paths = Hyprutils::Path::findConfig("hyprpaper"); return paths.first.value_or(""); } CConfigManager::CConfigManager(const std::string& configPath) : m_config(configPath.empty() ? getMainConfigPath().c_str() : configPath.c_str(), Hyprlang::SConfigOptions{.throwAllErrors = true, .allowMissingConfig = true}) { m_currentConfigPath = configPath.empty() ? getMainConfigPath() : configPath; } bool CConfigManager::init() { m_config.addConfigValue("splash", Hyprlang::INT{1}); m_config.addConfigValue("splash_offset", Hyprlang::INT{20}); m_config.addConfigValue("splash_opacity", Hyprlang::FLOAT{0.8}); m_config.addConfigValue("ipc", Hyprlang::INT{1}); m_config.addSpecialCategory("wallpaper", Hyprlang::SSpecialCategoryOptions{.key = "monitor"}); m_config.addSpecialConfigValue("wallpaper", "monitor", Hyprlang::STRING{""}); m_config.addSpecialConfigValue("wallpaper", "path", Hyprlang::STRING{""}); m_config.addSpecialConfigValue("wallpaper", "fit_mode", Hyprlang::STRING{"cover"}); m_config.addSpecialConfigValue("wallpaper", "timeout", Hyprlang::INT{0}); m_config.registerHandler(&handleSource, "source", Hyprlang::SHandlerOptions{}); m_config.commence(); auto result = m_config.parse(); if (result.error) { g_logger->log(LOG_ERR, "Config has errors:\n{}", result.getError()); return false; } g_matcher->addStates(getSettings()); return true; } Hyprlang::CConfig* CConfigManager::hyprlang() { return &m_config; } const std::string& CConfigManager::getCurrentConfigPath() const { return m_currentConfigPath; } static std::expected resolvePath(const std::string_view& sv) { std::error_code ec; const auto CAN = std::filesystem::canonical(sv, ec); if (ec) return std::unexpected(std::format("invalid path: {}", ec.message())); return CAN; } static std::expected getPath(const std::string_view& sv, const std::string& basePath = "") { if (sv.empty()) return std::unexpected("empty path"); std::string path{sv}; if (sv[0] == '~') { static auto HOME = getenv("HOME"); if (!HOME || HOME[0] == '\0') return std::unexpected("home path but no $HOME"); path = std::string{HOME} + "/"s + std::string{sv.substr(1)}; } else if (!std::filesystem::path(sv).is_absolute() && !basePath.empty()) { // Make relative paths relative to the base path's directory auto baseDir = std::filesystem::path(basePath).parent_path(); path = (baseDir / sv).string(); } return resolvePath(path); } static std::expected, std::string> getFullPath(const std::string& sv) { if (sv.empty()) return std::unexpected("empty path"); static constexpr const size_t maxImagesCount{1024}; std::vector result; const auto resolved = getPath(sv); if (!resolved) return std::unexpected(resolved.error()); const auto resolvedPath = resolved.value(); if (!std::filesystem::exists(resolvedPath)) return std::unexpected(std::format("File '{}' does not exist", resolvedPath)); if (std::filesystem::is_directory(resolvedPath)) for (const auto& entry : std::filesystem::directory_iterator(resolvedPath, std::filesystem::directory_options::skip_permission_denied)) { if (entry.is_regular_file() && isImage(entry.path())) result.push_back(entry.path()); if (result.size() >= maxImagesCount) break; } else if (isImage(resolvedPath)) result.push_back(resolvedPath); else return std::unexpected(std::format("File '{}' is neither an image nor a directory", resolvedPath)); return result; } std::vector CConfigManager::getSettings() { std::vector result; auto keys = m_config.listKeysForSpecialCategory("wallpaper"); result.reserve(keys.size()); for (auto& key : keys) { std::string monitor, fitMode, path; int timeout; try { monitor = std::any_cast(m_config.getSpecialConfigValue("wallpaper", "monitor", key.c_str())); fitMode = std::any_cast(m_config.getSpecialConfigValue("wallpaper", "fit_mode", key.c_str())); path = std::any_cast(m_config.getSpecialConfigValue("wallpaper", "path", key.c_str())); timeout = std::any_cast(m_config.getSpecialConfigValue("wallpaper", "timeout", key.c_str())); } catch (...) { g_logger->log(LOG_ERR, "Failed parsing wallpaper for key {}", key); continue; } const auto RESOLVE_PATH = getFullPath(path); if (!RESOLVE_PATH) { g_logger->log(LOG_ERR, "Failed to resolve path {}: {}", path, RESOLVE_PATH.error()); continue; } if (RESOLVE_PATH.value().empty()) { g_logger->log(LOG_ERR, "Provided path(s) '{}' does not contain a valid image", path); continue; } result.emplace_back(SSetting{.monitor = std::move(monitor), .fitMode = std::move(fitMode), .paths = RESOLVE_PATH.value(), .timeout = timeout}); } return result; } static Hyprlang::CParseResult handleSource(const char* COMMAND, const char* VALUE) { Hyprlang::CParseResult result; const auto value = Hyprutils::String::trim(VALUE); if (value.empty()) { result.setError("source= requires a file path"); return result; } const auto RESOLVED_PATH = getPath(value, g_config->getCurrentConfigPath()); if (!RESOLVED_PATH) { result.setError(std::format("source= path error: {}", RESOLVED_PATH.error()).c_str()); return result; } const auto& PATH = RESOLVED_PATH.value(); g_logger->log(LOG_DEBUG, "source: including '{}'", PATH); // Support glob patterns glob_t globResult; Hyprutils::Utils::CScopeGuard scopeGuard([&globResult]() { globfree(&globResult); }); int globStatus = glob(PATH.c_str(), GLOB_TILDE | GLOB_NOSORT, nullptr, &globResult); if (globStatus == GLOB_NOMATCH) { // No glob match - try as a literal path std::error_code ec; const auto exists = std::filesystem::exists(PATH, ec); if (ec || !exists) { result.setError(std::format("source file '{}' not found", PATH).c_str()); return result; } // Parse the single file g_logger->log(LOG_DEBUG, "source: parsing file '{}'", PATH); auto parseResult = g_config->hyprlang()->parseFile(PATH.c_str()); if (parseResult.error) result.setError(std::format("error parsing '{}': {}", PATH, parseResult.getError()).c_str()); return result; } if (globStatus != 0) { result.setError(std::format("glob error for pattern '{}'", PATH).c_str()); return result; } // Process all matched files g_logger->log(LOG_DEBUG, "source: glob matched {} file(s)", globResult.gl_pathc); for (size_t i = 0; i < globResult.gl_pathc; i++) { const std::string matchedPath = globResult.gl_pathv[i]; std::error_code ec; const auto isFile = std::filesystem::is_regular_file(matchedPath, ec); if (ec || !isFile) { g_logger->log(LOG_WARN, "source: skipping non-regular file '{}'", matchedPath); continue; } g_logger->log(LOG_DEBUG, "source: parsing file '{}'", matchedPath); auto parseResult = g_config->hyprlang()->parseFile(matchedPath.c_str()); if (parseResult.error) g_logger->log(LOG_ERR, "error parsing '{}': {}", matchedPath, parseResult.getError()); } return result; } hyprpaper-0.8.3/src/config/ConfigManager.hpp000066400000000000000000000017171513670072600210160ustar00rootroot00000000000000#pragma once #include "../helpers/Memory.hpp" #include #include class CConfigManager { public: CConfigManager(const std::string& configPath); ~CConfigManager() = default; CConfigManager(const CConfigManager&) = delete; CConfigManager(CConfigManager&) = delete; CConfigManager(CConfigManager&&) = delete; struct SSetting { std::string monitor, fitMode; std::vector paths; int timeout = 0; uint32_t id = 0; }; constexpr static const uint32_t SETTING_INVALID = 0; bool init(); Hyprlang::CConfig* hyprlang(); std::vector getSettings(); const std::string& getCurrentConfigPath() const; private: Hyprlang::CConfig m_config; std::string m_currentConfigPath; }; inline UP g_config; hyprpaper-0.8.3/src/config/WallpaperMatcher.cpp000066400000000000000000000071151513670072600215420ustar00rootroot00000000000000#include "WallpaperMatcher.hpp" #include using namespace std::string_literals; // Check if a monitor string represents a wildcard (matches all monitors) // Empty string or "*" are both treated as wildcards. // "*" is preferred since hyprlang's special category system doesn't properly // return entries with empty string keys from listKeysForSpecialCategory(). static bool isWildcard(const std::string& monitor) { return monitor.empty() || monitor == "*"; } void CWallpaperMatcher::addState(CConfigManager::SSetting&& s) { s.id = ++m_maxId; std::erase_if(m_settings, [&s](const auto& e) { return e.monitor == s.monitor; }); m_settings.emplace_back(std::move(s)); recalcStates(); } void CWallpaperMatcher::addStates(std::vector&& s) { for (auto& ss : s) { ss.id = ++m_maxId; } std::erase_if(m_settings, [&s](const auto& e) { return std::ranges::any_of(s, [&e](const auto& el) { return el.monitor == e.monitor; }); }); m_settings.append_range(std::move(s)); recalcStates(); } void CWallpaperMatcher::registerOutput(const std::string_view& s, const std::string_view& desc) { m_monitorNames.emplace_back(std::make_pair<>(s, desc)); recalcStates(); } void CWallpaperMatcher::unregisterOutput(const std::string_view& s) { std::erase_if(m_monitorNames, [&s](const auto& e) { return e.first == s; }); std::erase_if(m_monitorStates, [&s](const auto& e) { return e.name == s; }); recalcStates(); } bool CWallpaperMatcher::outputExists(const std::string_view& s) { return std::ranges::any_of(m_monitorNames, [&s](const auto& e) { return e.first == s || "desc:" + e.second == s; }); } std::optional> CWallpaperMatcher::getSetting(const std::string_view& monName, const std::string_view& monDesc) { for (const auto& m : m_monitorStates) { if (m.name != monName) continue; for (const auto& s : m_settings) { if (s.id != m.currentID) continue; return s; } return std::nullopt; } return std::nullopt; } std::optional> CWallpaperMatcher::matchSetting(const std::string_view& monName, const std::string_view& monDesc) { // match explicit for (const auto& s : m_settings) { if (s.monitor != monName && !("desc:"s + std::string{monDesc}).starts_with(s.monitor)) continue; return s; } // match wildcard (empty string or "*") for (const auto& s : m_settings) { if (isWildcard(s.monitor)) return s; } return std::nullopt; } CWallpaperMatcher::SMonitorState& CWallpaperMatcher::getState(const std::string_view& monName) { for (auto& s : m_monitorStates) { if (s.name == monName) return s; } return m_monitorStates.emplace_back(); } void CWallpaperMatcher::recalcStates() { std::vector namesChanged; for (const auto& [name, desc] : m_monitorNames) { const auto STATE = matchSetting(name, desc); auto& activeState = getState(name); if (!STATE) activeState = {.name = name, .desc = desc}; else { activeState.name = name; activeState.desc = desc; if (activeState.currentID != STATE->get().id) { activeState.currentID = STATE->get().id; namesChanged.emplace_back(name); } } } for (const auto& n : namesChanged) { m_events.monitorConfigChanged.emit(n); } } hyprpaper-0.8.3/src/config/WallpaperMatcher.hpp000066400000000000000000000037531513670072600215530ustar00rootroot00000000000000#pragma once #include #include #include "ConfigManager.hpp" #include class CWallpaperMatcher { public: template using rw = std::reference_wrapper; CWallpaperMatcher() = default; ~CWallpaperMatcher() = default; CWallpaperMatcher(const CWallpaperMatcher&) = delete; CWallpaperMatcher(CWallpaperMatcher&) = delete; CWallpaperMatcher(CWallpaperMatcher&&) = delete; void addState(CConfigManager::SSetting&&); void addStates(std::vector&&); void registerOutput(const std::string_view&, const std::string_view&); void unregisterOutput(const std::string_view&); bool outputExists(const std::string_view&); std::optional> getSetting(const std::string_view& monName, const std::string_view& monDesc); struct { Hyprutils::Signal::CSignalT monitorConfigChanged; } m_events; private: void recalcStates(); std::optional> matchSetting(const std::string_view& monName, const std::string_view& monDesc); std::vector m_settings; struct SMonitorState { std::string name, desc; uint32_t currentID = CConfigManager::SETTING_INVALID; }; std::vector> m_monitorNames; std::vector m_monitorStates; uint32_t m_maxId = 0; SMonitorState& getState(const std::string_view& monName); }; inline UP g_matcher = makeUnique(); hyprpaper-0.8.3/src/defines.hpp000066400000000000000000000023131513670072600164570ustar00rootroot00000000000000#pragma once #include #include #include "helpers/Logger.hpp" // git stuff #ifndef GIT_COMMIT_HASH #define GIT_COMMIT_HASH "?" #endif #ifndef GIT_BRANCH #define GIT_BRANCH "?" #endif #ifndef GIT_COMMIT_MESSAGE #define GIT_COMMIT_MESSAGE "?" #endif #ifndef GIT_DIRTY #define GIT_DIRTY "?" #endif #define ASSERT(expr) \ if (!(expr)) { \ g_logger->log(LOG_CRIT, "Failed assertion at line {} in {}: {} was false", __LINE__, \ ([]() constexpr -> std::string { return std::string(__FILE__).substr(std::string(__FILE__).find("/src/") + 1); })(), #expr); \ std::abort(); \ } hyprpaper-0.8.3/src/helpers/000077500000000000000000000000001513670072600157745ustar00rootroot00000000000000hyprpaper-0.8.3/src/helpers/GlobalState.hpp000066400000000000000000000002261513670072600207060ustar00rootroot00000000000000#pragma once #include "Memory.hpp" struct SGlobalState { bool verbose = false; }; inline UP g_state = makeUnique();hyprpaper-0.8.3/src/helpers/Logger.hpp000066400000000000000000000005671513670072600177340ustar00rootroot00000000000000#pragma once #include #include "Memory.hpp" #define LOG_DEBUG Hyprutils::CLI::LOG_DEBUG #define LOG_ERR Hyprutils::CLI::LOG_ERR #define LOG_WARN Hyprutils::CLI::LOG_WARN #define LOG_TRACE Hyprutils::CLI::LOG_TRACE #define LOG_CRIT Hyprutils::CLI::LOG_CRIT inline UP g_logger = makeUnique(); hyprpaper-0.8.3/src/helpers/Memory.hpp000066400000000000000000000004351513670072600177570ustar00rootroot00000000000000#pragma once #include #include #include using namespace Hyprutils::Memory; #define SP CSharedPointer #define WP CWeakPointer #define UP CUniquePointer #define ASP CAtomicSharedPointerhyprpaper-0.8.3/src/ipc/000077500000000000000000000000001513670072600151055ustar00rootroot00000000000000hyprpaper-0.8.3/src/ipc/HyprlandSocket.cpp000066400000000000000000000046721513670072600205540ustar00rootroot00000000000000#include "HyprlandSocket.hpp" #include #include #include #include #include #include #include #include using namespace Hyprutils::Memory; static int getUID() { const auto UID = getuid(); const auto PWUID = getpwuid(UID); return PWUID ? PWUID->pw_uid : UID; } static std::string getRuntimeDir() { const auto XDG = getenv("XDG_RUNTIME_DIR"); if (!XDG) { const std::string USERID = std::to_string(getUID()); return "/run/user/" + USERID + "/hypr"; } return std::string{XDG} + "/hypr"; } std::expected HyprlandSocket::getFromSocket(const std::string& cmd) { static const auto HIS = getenv("HYPRLAND_INSTANCE_SIGNATURE"); if (!HIS || HIS[0] == '\0') return std::unexpected("HYPRLAND_INSTANCE_SIGNATURE empty: are we under hyprland?"); const auto SERVERSOCKET = socket(AF_UNIX, SOCK_STREAM, 0); auto t = timeval{.tv_sec = 5, .tv_usec = 0}; setsockopt(SERVERSOCKET, SOL_SOCKET, SO_RCVTIMEO, &t, sizeof(struct timeval)); if (SERVERSOCKET < 0) return std::unexpected("couldn't open a socket (1)"); sockaddr_un serverAddress = {0}; serverAddress.sun_family = AF_UNIX; std::string socketPath = getRuntimeDir() + "/" + HIS + "/.socket.sock"; strncpy(serverAddress.sun_path, socketPath.c_str(), sizeof(serverAddress.sun_path) - 1); if (connect(SERVERSOCKET, rc(&serverAddress), SUN_LEN(&serverAddress)) < 0) return std::unexpected(std::format("couldn't connect to the hyprland socket at {}", socketPath)); auto sizeWritten = write(SERVERSOCKET, cmd.c_str(), cmd.length()); if (sizeWritten < 0) return std::unexpected("couldn't write (4)"); std::string reply = ""; char buffer[8192] = {0}; sizeWritten = read(SERVERSOCKET, buffer, 8192); if (sizeWritten < 0) { if (errno == EWOULDBLOCK) return std::unexpected("Hyprland IPC didn't respond in time"); return std::unexpected("couldn't read (5)"); } reply += std::string(buffer, sizeWritten); while (sizeWritten == 8192) { sizeWritten = read(SERVERSOCKET, buffer, 8192); if (sizeWritten < 0) { return std::unexpected("couldn't read (5)"); } reply += std::string(buffer, sizeWritten); } close(SERVERSOCKET); return reply; }hyprpaper-0.8.3/src/ipc/HyprlandSocket.hpp000066400000000000000000000002741513670072600205530ustar00rootroot00000000000000#pragma once #include #include #include namespace HyprlandSocket { std::expected getFromSocket(const std::string& cmd); };hyprpaper-0.8.3/src/ipc/IPC.cpp000066400000000000000000000112151513670072600162240ustar00rootroot00000000000000#include "IPC.hpp" #include "../helpers/Logger.hpp" #include "../config/WallpaperMatcher.hpp" #include "../ui/UI.hpp" #include using namespace IPC; using namespace std::string_literals; constexpr const char* SOCKET_NAME = ".hyprpaper.sock"; static SP g_coreImpl; CWallpaperObject::CWallpaperObject(SP&& obj) : m_object(std::move(obj)) { m_object->setDestroy([this]() { std::erase_if(g_IPCSocket->m_wallpaperObjects, [this](const auto& e) { return e.get() == this; }); }); m_object->setOnDestroy([this]() { std::erase_if(g_IPCSocket->m_wallpaperObjects, [this](const auto& e) { return e.get() == this; }); }); m_object->setPath([this](const char* s) { if (m_inert) m_object->error(HYPRPAPER_CORE_WALLPAPER_ERRORS_INERT_WALLPAPER_OBJECT, "Object is inert"); m_path = s; }); m_object->setFitMode([this](hyprpaperCoreWallpaperFitMode f) { if (m_inert) m_object->error(HYPRPAPER_CORE_WALLPAPER_ERRORS_INERT_WALLPAPER_OBJECT, "Object is inert"); if (f > HYPRPAPER_CORE_WALLPAPER_FIT_MODE_TILE) m_object->error(HYPRPAPER_CORE_APPLYING_ERROR_UNKNOWN_ERROR, "Invalid fit mode"); m_fitMode = f; }); m_object->setMonitorName([this](const char* s) { if (m_inert) m_object->error(HYPRPAPER_CORE_WALLPAPER_ERRORS_INERT_WALLPAPER_OBJECT, "Object is inert"); m_monitor = s; }); m_object->setApply([this]() { if (m_inert) m_object->error(HYPRPAPER_CORE_WALLPAPER_ERRORS_INERT_WALLPAPER_OBJECT, "Object is inert"); apply(); }); } static std::string fitModeToStr(hyprpaperCoreWallpaperFitMode m) { switch (m) { case HYPRPAPER_CORE_WALLPAPER_FIT_MODE_CONTAIN: return "contain"; case HYPRPAPER_CORE_WALLPAPER_FIT_MODE_COVER: return "cover"; case HYPRPAPER_CORE_WALLPAPER_FIT_MODE_TILE: return "tile"; case HYPRPAPER_CORE_WALLPAPER_FIT_MODE_STRETCH: return "fit"; default: return "cover"; } } void CWallpaperObject::apply() { m_inert = true; if (!m_monitor.empty() && !g_matcher->outputExists(m_monitor)) { m_object->sendFailed(HYPRPAPER_CORE_APPLYING_ERROR_INVALID_MONITOR); return; } if (m_path.empty()) { m_object->sendFailed(HYPRPAPER_CORE_APPLYING_ERROR_INVALID_PATH); return; } if (m_path[0] != '/') { m_object->sendFailed(HYPRPAPER_CORE_APPLYING_ERROR_INVALID_PATH); return; } std::error_code ec; if (!std::filesystem::exists(m_path, ec) || ec) { m_object->sendFailed(HYPRPAPER_CORE_APPLYING_ERROR_INVALID_PATH); return; } g_matcher->addState(CConfigManager::SSetting{ .monitor = std::move(m_monitor), .fitMode = fitModeToStr(m_fitMode), .paths = std::vector{std::move(m_path)}, }); m_object->sendSuccess(); } CSocket::CSocket() { const auto RTDIR = getenv("XDG_RUNTIME_DIR"); if (!RTDIR) return; const auto HIS = getenv("HYPRLAND_INSTANCE_SIGNATURE"); if (!HIS) { g_logger->log(LOG_WARN, "not running under hyprland, IPC will be disabled."); return; } m_socketPath = RTDIR + "/hypr/"s + HIS + "/"s + SOCKET_NAME; std::error_code ec; std::filesystem::remove(m_socketPath, ec); m_socket = Hyprwire::IServerSocket::open(m_socketPath); if (!m_socket) return; g_coreImpl = makeShared(1, [this](SP obj) { auto manager = m_managers.emplace_back(makeShared(std::move(obj))); manager->setDestroy([this, weak = WP{manager}] { std::erase(m_managers, weak); }); manager->setOnDestroy([this, weak = WP{manager}] { std::erase(m_managers, weak); }); manager->setGetWallpaperObject([this, weak = WP{manager}](uint32_t id) { if (!weak) return; m_wallpaperObjects.emplace_back(makeShared( makeShared(m_socket->createObject(weak->getObject()->client(), weak->getObject(), "hyprpaper_wallpaper", id)))); }); }); m_socket->addImplementation(g_coreImpl); g_ui->backend()->addFd(m_socket->extractLoopFD(), [this]() { m_socket->dispatchEvents(); }); } void CSocket::onNewDisplay(const std::string& sv) { for (const auto& m : m_managers) { m->sendAddMonitor(sv.c_str()); } } void CSocket::onRemovedDisplay(const std::string& sv) { for (const auto& m : m_managers) { m->sendRemoveMonitor(sv.c_str()); } } hyprpaper-0.8.3/src/ipc/IPC.hpp000066400000000000000000000022701513670072600162320ustar00rootroot00000000000000#pragma once #include "../helpers/Memory.hpp" #include #include namespace IPC { class CWallpaperObject { public: CWallpaperObject(SP&& obj); ~CWallpaperObject() = default; private: void apply(); SP m_object; std::string m_path; hyprpaperCoreWallpaperFitMode m_fitMode = HYPRPAPER_CORE_WALLPAPER_FIT_MODE_COVER; std::string m_monitor; bool m_inert = false; }; class CSocket { public: CSocket(); ~CSocket() = default; void onNewDisplay(const std::string& sv); void onRemovedDisplay(const std::string& sv); private: SP m_socket; std::string m_socketPath = ""; std::vector> m_managers; std::vector> m_wallpaperObjects; friend class CWallpaperObject; }; inline UP g_IPCSocket; };hyprpaper-0.8.3/src/main.cpp000066400000000000000000000030421513670072600157610ustar00rootroot00000000000000#include "defines.hpp" #include "helpers/Logger.hpp" #include "helpers/GlobalState.hpp" #include "ui/UI.hpp" #include "config/ConfigManager.hpp" #include using namespace Hyprutils::CLI; int main(int argc, const char** argv, const char** envp) { CArgumentParser parser({argv, argc}); ASSERT(parser.registerStringOption("config", "c", "Set a custom config path")); ASSERT(parser.registerBoolOption("verbose", "", "Enable more logging")); ASSERT(parser.registerBoolOption("version", "v", "Show hyprpaper's version")); ASSERT(parser.registerBoolOption("help", "h", "Show the help menu")); if (const auto ret = parser.parse(); !ret) { g_logger->log(LOG_ERR, "Failed parsing arguments: {}", ret.error()); return 1; } if (parser.getBool("help").value_or(false)) { std::println("{}", parser.getDescription(std::format("hyprpaper v{}", HYPRPAPER_VERSION))); return 0; } if (parser.getBool("version").value_or(false)) { std::println("hyprpaper v{}", HYPRPAPER_VERSION); return 0; } if (parser.getBool("verbose").value_or(false)) { g_logger->setLogLevel(LOG_TRACE); g_state->verbose = true; } g_logger->log(LOG_DEBUG, "Welcome to hyprpaper!\nbuilt from commit {} ({})", GIT_COMMIT_HASH, GIT_COMMIT_MESSAGE); g_config = makeUnique(std::string{parser.getString("config").value_or("")}); if (!g_config->init()) return 1; g_ui = makeUnique(); g_ui->run(); return 0; } hyprpaper-0.8.3/src/ui/000077500000000000000000000000001513670072600147475ustar00rootroot00000000000000hyprpaper-0.8.3/src/ui/UI.cpp000066400000000000000000000203241513670072600157710ustar00rootroot00000000000000#include "UI.hpp" #include "../defines.hpp" #include "../helpers/Logger.hpp" #include "../helpers/GlobalState.hpp" #include "../ipc/HyprlandSocket.hpp" #include "../ipc/IPC.hpp" #include "../config/WallpaperMatcher.hpp" #include #include CUI::CUI() = default; CUI::~CUI() { m_targets.clear(); } static std::string_view pruneDesc(const std::string_view& sv) { if (sv.contains('(')) return Hyprutils::String::trim(sv.substr(0, sv.find_last_of('('))); return sv; } class CWallpaperTarget::CImagesData { public: CImagesData(Hyprtoolkit::eImageFitMode fitMode, const std::vector& images, const int timeout = 0) : fitMode(fitMode), images(images), timeout(timeout > 0 ? timeout : 30) {} const Hyprtoolkit::eImageFitMode fitMode; const std::vector images; const int timeout; std::string nextImage() { current = (current + 1) % images.size(); return images[current]; } private: size_t current = 0; }; CWallpaperTarget::CWallpaperTarget(SP backend, SP output, const std::vector& path, Hyprtoolkit::eImageFitMode fitMode, const int timeout) : m_monitorName(output->port()), m_backend(backend) { static const auto SPLASH_REPLY = HyprlandSocket::getFromSocket("/splash"); static const auto PENABLESPLASH = Hyprlang::CSimpleConfigValue(g_config->hyprlang(), "splash"); static const auto PSPLASHOFFSET = Hyprlang::CSimpleConfigValue(g_config->hyprlang(), "splash_offset"); static const auto PSPLASHALPHA = Hyprlang::CSimpleConfigValue(g_config->hyprlang(), "splash_opacity"); ASSERT(path.size() > 0); m_window = Hyprtoolkit::CWindowBuilder::begin() ->type(Hyprtoolkit::HT_WINDOW_LAYER) ->prefferedOutput(output) ->anchor(0xF) ->layer(0) ->preferredSize({0, 0}) ->exclusiveZone(-1) ->appClass("hyprpaper") ->commence(); m_bg = Hyprtoolkit::CRectangleBuilder::begin() ->size({Hyprtoolkit::CDynamicSize::HT_SIZE_PERCENT, Hyprtoolkit::CDynamicSize::HT_SIZE_PERCENT, {1, 1}}) ->color([] { return Hyprtoolkit::CHyprColor{0xFF000000}; }) ->commence(); m_null = Hyprtoolkit::CNullBuilder::begin()->size({Hyprtoolkit::CDynamicSize::HT_SIZE_PERCENT, Hyprtoolkit::CDynamicSize::HT_SIZE_PERCENT, {1, 1}})->commence(); m_image = Hyprtoolkit::CImageBuilder::begin() ->path(std::string{path.front()}) ->size({Hyprtoolkit::CDynamicSize::HT_SIZE_PERCENT, Hyprtoolkit::CDynamicSize::HT_SIZE_PERCENT, {1.F, 1.F}}) ->sync(true) ->fitMode(fitMode) ->commence(); m_image->setPositionMode(Hyprtoolkit::IElement::HT_POSITION_ABSOLUTE); m_image->setPositionFlag(Hyprtoolkit::IElement::HT_POSITION_FLAG_CENTER, true); if (path.size() > 1) { m_imagesData = makeUnique(fitMode, path, timeout); m_timer = m_backend->addTimer(std::chrono::milliseconds(std::chrono::seconds(m_imagesData->timeout)), [this](ASP self, void*) { onRepeatTimer(); }, nullptr); } m_window->m_rootElement->addChild(m_bg); m_window->m_rootElement->addChild(m_null); m_null->addChild(m_image); if (!SPLASH_REPLY) g_logger->log(LOG_ERR, "Can't get splash: {}", SPLASH_REPLY.error()); if (SPLASH_REPLY && *PENABLESPLASH) { m_splash = Hyprtoolkit::CTextBuilder::begin() ->text(std::string{SPLASH_REPLY.value()}) ->fontSize({Hyprtoolkit::CFontSize::HT_FONT_TEXT, 1.15F}) ->color([] { return g_ui->backend()->getPalette()->m_colors.text; }) ->a(*PSPLASHALPHA) ->commence(); m_splash->setPositionMode(Hyprtoolkit::IElement::HT_POSITION_ABSOLUTE); m_splash->setPositionFlag(Hyprtoolkit::IElement::HT_POSITION_FLAG_HCENTER, true); m_splash->setPositionFlag(Hyprtoolkit::IElement::HT_POSITION_FLAG_BOTTOM, true); m_splash->setAbsolutePosition({0.F, sc(-*PSPLASHOFFSET)}); m_null->addChild(m_splash); } m_window->open(); } CWallpaperTarget::~CWallpaperTarget() { if (m_timer && !m_timer->passed()) m_timer->cancel(); } void CWallpaperTarget::onRepeatTimer() { ASSERT(m_imagesData); m_image->rebuild() ->path(m_imagesData->nextImage()) ->size({Hyprtoolkit::CDynamicSize::HT_SIZE_PERCENT, Hyprtoolkit::CDynamicSize::HT_SIZE_PERCENT, {1.F, 1.F}}) ->sync(true) ->fitMode(m_imagesData->fitMode) ->commence(); m_timer = m_backend->addTimer(std::chrono::milliseconds(std::chrono::seconds(m_imagesData->timeout)), [this](ASP self, void*) { onRepeatTimer(); }, nullptr); } void CUI::registerOutput(const SP& mon) { g_matcher->registerOutput(mon->port(), pruneDesc(mon->desc())); if (IPC::g_IPCSocket) IPC::g_IPCSocket->onNewDisplay(mon->port()); mon->m_events.removed.listenStatic([this, m = WP{mon}] { g_matcher->unregisterOutput(m->port()); if (IPC::g_IPCSocket) IPC::g_IPCSocket->onRemovedDisplay(m->port()); std::erase_if(m_targets, [&m](const auto& e) { return e->m_monitorName == m->port(); }); }); } bool CUI::run() { static const auto PENABLEIPC = Hyprlang::CSimpleConfigValue(g_config->hyprlang(), "ipc"); // Hyprtoolkit::IBackend::SBackendCreationData data; data.pLogConnection = makeShared(*g_logger); data.pLogConnection->setName("hyprtoolkit"); data.pLogConnection->setLogLevel(g_state->verbose ? LOG_TRACE : LOG_ERR); m_backend = Hyprtoolkit::IBackend::createWithData(data); if (!m_backend) return false; if (*PENABLEIPC) IPC::g_IPCSocket = makeUnique(); const auto MONITORS = m_backend->getOutputs(); for (const auto& m : MONITORS) { registerOutput(m); } m_listeners.newMon = m_backend->m_events.outputAdded.listen([this](SP mon) { registerOutput(mon); }); g_logger->log(LOG_DEBUG, "Found {} output(s)", MONITORS.size()); // load the config now, then bind for (const auto& m : MONITORS) { targetChanged(m); } m_listeners.targetChanged = g_matcher->m_events.monitorConfigChanged.listen([this](const std::string_view& m) { targetChanged(m); }); m_backend->enterLoop(); return true; } SP CUI::backend() { return m_backend; } static Hyprtoolkit::eImageFitMode toFitMode(const std::string_view& sv) { if (sv.starts_with("contain")) return Hyprtoolkit::IMAGE_FIT_MODE_CONTAIN; if (sv.starts_with("cover")) return Hyprtoolkit::IMAGE_FIT_MODE_COVER; if (sv.starts_with("tile")) return Hyprtoolkit::IMAGE_FIT_MODE_TILE; if (sv.starts_with("fill")) return Hyprtoolkit::IMAGE_FIT_MODE_STRETCH; return Hyprtoolkit::IMAGE_FIT_MODE_COVER; } void CUI::targetChanged(const std::string_view& monName) { const auto MONITORS = m_backend->getOutputs(); SP monitor; for (const auto& m : MONITORS) { if (m->port() != monName) continue; monitor = m; } if (!monitor) { g_logger->log(LOG_ERR, "targetChanged but {} has no output?", monName); return; } targetChanged(monitor); } void CUI::targetChanged(const SP& mon) { const auto TARGET = g_matcher->getSetting(mon->port(), pruneDesc(mon->desc())); if (!TARGET) { g_logger->log(LOG_DEBUG, "Monitor {} has no target: no wp will be created", mon->port()); return; } std::erase_if(m_targets, [&mon](const auto& e) { return e->m_monitorName == mon->port(); }); m_targets.emplace_back(makeShared(m_backend, mon, TARGET->get().paths, toFitMode(TARGET->get().fitMode), TARGET->get().timeout)); } hyprpaper-0.8.3/src/ui/UI.hpp000066400000000000000000000037621513670072600160050ustar00rootroot00000000000000#pragma once #include #include #include #include #include #include #include #include #include #include "../helpers/Memory.hpp" class CWallpaperTarget { public: CWallpaperTarget(SP backend, SP output, const std::vector& path, Hyprtoolkit::eImageFitMode fitMode = Hyprtoolkit::IMAGE_FIT_MODE_COVER, const int timeout = 0); ~CWallpaperTarget(); CWallpaperTarget(const CWallpaperTarget&) = delete; CWallpaperTarget(CWallpaperTarget&) = delete; CWallpaperTarget(CWallpaperTarget&&) = delete; std::string m_monitorName; private: void onRepeatTimer(); class CImagesData; UP m_imagesData; ASP m_timer; SP m_backend; SP m_window; SP m_null; SP m_bg; SP m_image; SP m_splash; }; class CUI { public: CUI(); ~CUI(); bool run(); SP backend(); private: void targetChanged(const SP& mon); void targetChanged(const std::string_view& monName); void registerOutput(const SP& mon); SP m_backend; std::vector> m_targets; struct { Hyprutils::Signal::CHyprSignalListener targetChanged; Hyprutils::Signal::CHyprSignalListener newMon; } m_listeners; }; inline UP g_ui; hyprpaper-0.8.3/systemd/000077500000000000000000000000001513670072600152335ustar00rootroot00000000000000hyprpaper-0.8.3/systemd/hyprpaper.service.in000066400000000000000000000006521513670072600212370ustar00rootroot00000000000000[Unit] Description=Fast, IPC-controlled wallpaper utility for Hyprland. Documentation=https://wiki.hyprland.org/Hypr-Ecosystem/hyprpaper/ PartOf=graphical-session.target Requires=graphical-session.target After=graphical-session.target ConditionEnvironment=WAYLAND_DISPLAY [Service] Type=simple ExecStart=@CMAKE_INSTALL_PREFIX@/bin/hyprpaper Slice=session.slice Restart=on-failure [Install] WantedBy=graphical-session.target