├── .clang-format ├── .github └── workflows │ └── nix-build.yaml ├── .gitignore ├── CMakeLists.txt ├── LICENSE ├── README.md ├── VERSION ├── flake.lock ├── flake.nix ├── nix └── default.nix ├── protocols └── wlr-layer-shell-unstable-v1.xml ├── src ├── Hyprpaper.cpp ├── Hyprpaper.hpp ├── config │ ├── ConfigManager.cpp │ └── ConfigManager.hpp ├── debug │ └── Log.hpp ├── defines.hpp ├── helpers │ ├── MiscFunctions.cpp │ ├── MiscFunctions.hpp │ ├── Monitor.cpp │ ├── Monitor.hpp │ └── PoolBuffer.hpp ├── includes.hpp ├── ipc │ ├── Socket.cpp │ └── Socket.hpp ├── main.cpp └── render │ ├── LayerSurface.cpp │ ├── LayerSurface.hpp │ ├── WallpaperTarget.cpp │ └── WallpaperTarget.hpp └── systemd └── hyprpaper.service.in /.clang-format: -------------------------------------------------------------------------------- 1 | --- 2 | Language: Cpp 3 | BasedOnStyle: LLVM 4 | 5 | AccessModifierOffset: -2 6 | AlignAfterOpenBracket: Align 7 | AlignConsecutiveMacros: true 8 | AlignConsecutiveAssignments: true 9 | AlignEscapedNewlines: Right 10 | AlignOperands: false 11 | AlignTrailingComments: true 12 | AllowAllArgumentsOnNextLine: true 13 | AllowAllConstructorInitializersOnNextLine: true 14 | AllowAllParametersOfDeclarationOnNextLine: true 15 | AllowShortBlocksOnASingleLine: true 16 | AllowShortCaseLabelsOnASingleLine: true 17 | AllowShortFunctionsOnASingleLine: Empty 18 | AllowShortIfStatementsOnASingleLine: Never 19 | AllowShortLambdasOnASingleLine: All 20 | AllowShortLoopsOnASingleLine: false 21 | AlwaysBreakAfterDefinitionReturnType: None 22 | AlwaysBreakAfterReturnType: None 23 | AlwaysBreakBeforeMultilineStrings: false 24 | AlwaysBreakTemplateDeclarations: Yes 25 | BreakBeforeBraces: Attach 26 | BreakBeforeTernaryOperators: false 27 | BreakConstructorInitializers: AfterColon 28 | ColumnLimit: 180 29 | CompactNamespaces: false 30 | ConstructorInitializerAllOnOneLineOrOnePerLine: false 31 | ExperimentalAutoDetectBinPacking: false 32 | FixNamespaceComments: false 33 | IncludeBlocks: Preserve 34 | IndentCaseLabels: true 35 | IndentWidth: 4 36 | PointerAlignment: Left 37 | ReflowComments: false 38 | SortIncludes: false 39 | SortUsingDeclarations: false 40 | SpaceAfterCStyleCast: false 41 | SpaceAfterLogicalNot: false 42 | SpaceAfterTemplateKeyword: true 43 | SpaceBeforeCtorInitializerColon: true 44 | SpaceBeforeInheritanceColon: true 45 | SpaceBeforeParens: ControlStatements 46 | SpaceBeforeRangeBasedForLoopColon: true 47 | SpaceInEmptyParentheses: false 48 | SpacesBeforeTrailingComments: 1 49 | SpacesInAngles: false 50 | SpacesInCStyleCastParentheses: false 51 | SpacesInContainerLiterals: false 52 | SpacesInParentheses: false 53 | SpacesInSquareBrackets: false 54 | Standard: Auto 55 | TabWidth: 4 56 | UseTab: Never 57 | 58 | AllowShortEnumsOnASingleLine: false 59 | 60 | BraceWrapping: 61 | AfterEnum: false 62 | 63 | AlignConsecutiveDeclarations: AcrossEmptyLines 64 | 65 | NamespaceIndentation: All 66 | -------------------------------------------------------------------------------- /.github/workflows/nix-build.yaml: -------------------------------------------------------------------------------- 1 | name: Build Hyprpaper (Nix) 2 | 3 | on: [push, pull_request, workflow_dispatch] 4 | jobs: 5 | nix: 6 | name: "Build" 7 | runs-on: ubuntu-latest 8 | steps: 9 | - name: Clone repository 10 | uses: actions/checkout@v3 11 | with: 12 | submodules: recursive 13 | - name: Install nix 14 | uses: cachix/install-nix-action@v20 15 | with: 16 | install_url: https://nixos.org/nix/install 17 | extra_nix_config: | 18 | auto-optimise-store = true 19 | access-tokens = github.com=${{ secrets.GITHUB_TOKEN }} 20 | experimental-features = nix-command flakes 21 | - uses: cachix/cachix-action@v12 22 | with: 23 | name: hyprland 24 | authToken: '${{ secrets.CACHIX_AUTH_TOKEN }}' 25 | - name: Build Hyprpaper with default settings 26 | run: nix build --print-build-logs --accept-flake-config 27 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | CMakeLists.txt.user 2 | CMakeCache.txt 3 | CMakeFiles 4 | CMakeScripts 5 | Testing 6 | cmake_install.cmake 7 | install_manifest.txt 8 | compile_commands.json 9 | CTestTestfile.cmake 10 | _deps 11 | 12 | build/ 13 | result 14 | /.vscode/ 15 | /.idea 16 | 17 | *.o 18 | *-protocol.c 19 | *-protocol.h 20 | .ccls-cache 21 | 22 | protocols/*.hpp 23 | protocols/*.cpp 24 | 25 | .cache/ 26 | 27 | hyprctl/hyprctl 28 | 29 | gmon.out 30 | *.out 31 | *.tar.gz 32 | -------------------------------------------------------------------------------- /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.12) 2 | 3 | file(READ "${CMAKE_SOURCE_DIR}/VERSION" VER_RAW) 4 | string(STRIP ${VER_RAW} VERSION) 5 | 6 | project( 7 | hyprpaper 8 | DESCRIPTION "A blazing fast wayland wallpaper utility" 9 | VERSION ${VERSION}) 10 | 11 | set(CMAKE_MESSAGE_LOG_LEVEL "STATUS") 12 | 13 | message(STATUS "Configuring hyprpaper!") 14 | 15 | configure_file(systemd/hyprpaper.service.in systemd/hyprpaper.service @ONLY) 16 | 17 | # Get git info hash and branch 18 | execute_process( 19 | COMMAND git rev-parse --abbrev-ref HEAD 20 | WORKING_DIRECTORY ${CMAKE_SOURCE_DIR} 21 | OUTPUT_VARIABLE GIT_BRANCH 22 | OUTPUT_STRIP_TRAILING_WHITESPACE) 23 | 24 | execute_process( 25 | COMMAND git rev-parse HEAD 26 | WORKING_DIRECTORY ${CMAKE_SOURCE_DIR} 27 | OUTPUT_VARIABLE GIT_COMMIT_HASH 28 | OUTPUT_STRIP_TRAILING_WHITESPACE) 29 | 30 | execute_process( 31 | COMMAND bash -c "git show ${GIT_COMMIT_HASH} | head -n 5 | tail -n 1 | sed -s 's/\#//g'" 32 | WORKING_DIRECTORY ${CMAKE_SOURCE_DIR} 33 | OUTPUT_VARIABLE GIT_COMMIT_MESSAGE 34 | OUTPUT_STRIP_TRAILING_WHITESPACE) 35 | 36 | execute_process( 37 | COMMAND bash -c "git diff-index --quiet HEAD -- || echo \"dirty\"" 38 | WORKING_DIRECTORY ${CMAKE_SOURCE_DIR} 39 | OUTPUT_VARIABLE GIT_DIRTY 40 | OUTPUT_STRIP_TRAILING_WHITESPACE) 41 | 42 | include_directories(.) 43 | set(CMAKE_CXX_STANDARD 23) 44 | add_compile_options(-DWLR_USE_UNSTABLE) 45 | add_compile_options(-Wall -Wextra -Wno-unused-parameter -Wno-unused-value 46 | -Wno-missing-field-initializers -Wno-narrowing) 47 | 48 | find_package(Threads REQUIRED) 49 | find_package(PkgConfig REQUIRED) 50 | find_package(hyprwayland-scanner 0.4.0 REQUIRED) 51 | 52 | pkg_check_modules( 53 | deps 54 | REQUIRED 55 | IMPORTED_TARGET 56 | wayland-client 57 | wayland-protocols>=1.35 58 | cairo 59 | pango 60 | pangocairo 61 | hyprlang>=0.6.0 62 | hyprutils>=0.2.4 63 | hyprgraphics) 64 | 65 | file(GLOB_RECURSE SRCFILES "src/*.cpp") 66 | 67 | add_executable(hyprpaper ${SRCFILES}) 68 | 69 | pkg_get_variable(WAYLAND_PROTOCOLS_DIR wayland-protocols pkgdatadir) 70 | message(STATUS "Found wayland-protocols at ${WAYLAND_PROTOCOLS_DIR}") 71 | pkg_get_variable(WAYLAND_SCANNER_PKGDATA_DIR wayland-scanner pkgdatadir) 72 | message( 73 | STATUS "Found wayland-scanner pkgdatadir at ${WAYLAND_SCANNER_PKGDATA_DIR}") 74 | 75 | function(protocolnew protoPath protoName external) 76 | if(external) 77 | set(path ${CMAKE_SOURCE_DIR}/${protoPath}) 78 | else() 79 | set(path ${WAYLAND_PROTOCOLS_DIR}/${protoPath}) 80 | endif() 81 | add_custom_command( 82 | OUTPUT ${CMAKE_SOURCE_DIR}/protocols/${protoName}.cpp 83 | ${CMAKE_SOURCE_DIR}/protocols/${protoName}.hpp 84 | COMMAND hyprwayland-scanner --client ${path}/${protoName}.xml 85 | ${CMAKE_SOURCE_DIR}/protocols/ 86 | WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}) 87 | target_sources(hyprpaper PRIVATE protocols/${protoName}.cpp 88 | protocols/${protoName}.hpp) 89 | endfunction() 90 | function(protocolWayland) 91 | add_custom_command( 92 | OUTPUT ${CMAKE_SOURCE_DIR}/protocols/wayland.cpp 93 | ${CMAKE_SOURCE_DIR}/protocols/wayland.hpp 94 | COMMAND hyprwayland-scanner --wayland-enums --client 95 | ${WAYLAND_SCANNER_PKGDATA_DIR}/wayland.xml ${CMAKE_SOURCE_DIR}/protocols/ 96 | WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}) 97 | target_sources(hyprpaper PRIVATE protocols/wayland.cpp protocols/wayland.hpp) 98 | endfunction() 99 | 100 | protocolwayland() 101 | 102 | protocolnew("protocols" "wlr-layer-shell-unstable-v1" true) 103 | protocolnew("stable/linux-dmabuf" "linux-dmabuf-v1" false) 104 | protocolnew("staging/fractional-scale" "fractional-scale-v1" false) 105 | protocolnew("stable/viewporter" "viewporter" false) 106 | protocolnew("stable/xdg-shell" "xdg-shell" false) 107 | protocolnew("staging/cursor-shape" "cursor-shape-v1" false) 108 | protocolnew("stable/tablet" "tablet-v2" false) 109 | 110 | target_compile_definitions(hyprpaper 111 | PRIVATE "-DGIT_COMMIT_HASH=\"${GIT_COMMIT_HASH}\"") 112 | target_compile_definitions(hyprpaper PRIVATE "-DGIT_BRANCH=\"${GIT_BRANCH}\"") 113 | target_compile_definitions( 114 | hyprpaper PRIVATE "-DGIT_COMMIT_MESSAGE=\"${GIT_COMMIT_MESSAGE}\"") 115 | target_compile_definitions(hyprpaper PRIVATE "-DGIT_DIRTY=\"${GIT_DIRTY}\"") 116 | 117 | target_link_libraries(hyprpaper rt) 118 | 119 | set(CPACK_PROJECT_NAME ${PROJECT_NAME}) 120 | set(CPACK_PROJECT_VERSION ${PROJECT_VERSION}) 121 | include(CPack) 122 | 123 | target_link_libraries(hyprpaper PkgConfig::deps) 124 | 125 | target_link_libraries( 126 | hyprpaper 127 | OpenGL 128 | GLESv2 129 | pthread 130 | magic 131 | ${CMAKE_THREAD_LIBS_INIT} 132 | wayland-cursor) 133 | 134 | if(CMAKE_BUILD_TYPE MATCHES Debug OR CMAKE_BUILD_TYPE MATCHES DEBUG) 135 | set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -pg -no-pie -fno-builtin") 136 | set(CMAKE_EXE_LINKER_FLAGS 137 | "${CMAKE_EXE_LINKER_FLAGS} -pg -no-pie -fno-builtin") 138 | set(CMAKE_SHARED_LINKER_FLAGS 139 | "${CMAKE_SHARED_LINKER_FLAGS} -pg -no-pie -fno-builtin") 140 | endif(CMAKE_BUILD_TYPE MATCHES Debug OR CMAKE_BUILD_TYPE MATCHES DEBUG) 141 | 142 | include(GNUInstallDirs) 143 | 144 | install(TARGETS hyprpaper) 145 | install(FILES ${CMAKE_BINARY_DIR}/systemd/hyprpaper.service DESTINATION "lib/systemd/user") 146 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2022, Hypr Development 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | 1. Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | 2. Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | 3. Neither the name of the copyright holder nor the names of its 17 | contributors may be used to endorse or promote products derived from 18 | this software without specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 21 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 22 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 23 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 24 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 25 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 26 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 27 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 28 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # hyprpaper 2 | 3 | Hyprpaper is a blazing fast wallpaper utility for Hyprland with the ability to dynamically change wallpapers through sockets. It will work on all wlroots-based compositors, though. 4 | 5 | # Features 6 | - Per-output wallpapers 7 | - fill, tile or contain modes 8 | - fractional scaling support 9 | - IPC for blazing fast wallpaper switches 10 | - preloading targets into memory 11 | 12 | # Installation 13 | 14 | [Arch Linux](https://archlinux.org/packages/extra/x86_64/hyprpaper/): `pacman -S hyprpaper` 15 | 16 | [OpenSuse Linux](https://software.opensuse.org/package/hyprpaper): `zypper install hyprpaper` 17 | 18 | ## Manual: 19 | 20 | ### Dependencies 21 | The development files of these packages need to be installed on the system for `hyprpaper` to build correctly. 22 | (Development packages are usually suffixed with `-dev` or `-devel` in most distros' repos). 23 | - wayland 24 | - wayland-protocols 25 | - pango 26 | - cairo 27 | - file 28 | - libglvnd 29 | - libglvnd-core 30 | - libjpeg-turbo 31 | - libwebp 32 | - libjxl 33 | - hyprlang 34 | - hyprutils 35 | - hyprwayland-scanner 36 | - hyprgraphics 37 | 38 | To install all of these in Fedora, run this command: 39 | ``` 40 | sudo dnf install wayland-devel wayland-protocols-devel hyprlang-devel pango-devel cairo-devel file-devel libglvnd-devel libglvnd-core-devel libjpeg-turbo-devel libwebp-devel libjxl-devel gcc-c++ hyprutils-devel hyprwayland-scanner 41 | ``` 42 | 43 | On Arch: 44 | ``` 45 | sudo pacman -S ninja gcc wayland-protocols libjpeg-turbo libwebp libjxl pango cairo pkgconf cmake libglvnd wayland hyprutils hyprwayland-scanner hyprlang 46 | ``` 47 | 48 | On OpenSUSE: 49 | ``` 50 | sudo zypper install ninja gcc-c++ wayland-protocols-devel Mesa-libGLESv3-devel file-devel hyprutils-devel hyprwayland-scanner 51 | ``` 52 | 53 | ### Building 54 | 55 | Building is done via CMake: 56 | 57 | ```sh 58 | cmake --no-warn-unused-cli -DCMAKE_BUILD_TYPE:STRING=Release -DCMAKE_INSTALL_PREFIX:PATH=/usr -S . -B ./build 59 | cmake --build ./build --config Release --target hyprpaper -j`nproc 2>/dev/null || getconf _NPROCESSORS_CONF` 60 | ``` 61 | 62 | Install with: 63 | 64 | ```sh 65 | cmake --install ./build 66 | ``` 67 | 68 | # Usage 69 | 70 | Hyprpaper is controlled by the config, like this: 71 | 72 | *~/.config/hypr/hyprpaper.conf* 73 | ``` 74 | preload = /path/to/image.png 75 | #if more than one preload is desired then continue to preload other backgrounds 76 | preload = /path/to/next_image.png 77 | # .. more preloads 78 | 79 | #set the default wallpaper(s) seen on initial workspace(s) --depending on the number of monitors used 80 | wallpaper = monitor1,/path/to/image.png 81 | #if more than one monitor in use, can load a 2nd image 82 | wallpaper = monitor2,/path/to/next_image.png 83 | # .. more monitors 84 | 85 | #enable splash text rendering over the wallpaper 86 | splash = true 87 | 88 | #fully disable ipc 89 | # ipc = off 90 | 91 | 92 | ``` 93 | 94 | Preload will tell Hyprland to load a particular image (supported formats: png, jpg, jpeg, jpeg xl, webp). Wallpaper will apply the wallpaper to the selected output (`monitor` is the monitor's name, easily can be retrieved with `hyprctl monitors`. You can leave it empty to set all monitors without an active wallpaper. You can also use `desc:` followed by the monitor's description without the (PORT) at the end) 95 | 96 | You may add `contain:` or `tile:` before the file path in `wallpaper=` to set the mode to either contain or tile, respectively, instead of cover: 97 | 98 | ``` 99 | wallpaper = monitor,contain:/path/to/image.jpg 100 | ``` 101 | 102 | A Wallpaper ***cannot*** be applied without preloading. The config is ***not*** reloaded dynamically. 103 | 104 | ## Important note to the inner workings 105 | Preload does exactly what it says. It loads the entire wallpaper into memory. This can result in around 8 - 20MB of mem usage. It is not recommended to preload every wallpaper you have, as it will be a) taking a couple seconds at the beginning to load and b) take 100s of MBs of disk and RAM usage. 106 | 107 | Preload is meant only for situations in which you want a wallpaper to switch INSTANTLY when you issue a wallpaper keyword (e.g. wallpaper per workspace) 108 | 109 | In any and all cases when you don't mind waiting 300ms for the wallpaper to change, consider making a script that: 110 | - preloads the new wallpaper 111 | - sets the new wallpaper 112 | - unloads the old wallpaper (to free memory) 113 | 114 | # IPC 115 | You can use `hyprctl hyprpaper` (if on Hyprland) to issue a keyword, for example 116 | 117 | Example: 118 | 119 | If your wallpapers are stored in *~/Pictures*, then make sure you have already preloaded the desired wallpapers in hyprpaper.conf. 120 | 121 | *~/.config/hypr/hyprpaper.conf* 122 | ``` 123 | preload = ~/Pictures/myepicpng.png 124 | preload = ~/Pictures/myepicpngToo.png 125 | preload = ~/Pictures/myepicpngAlso.png 126 | #... continue as desired, but be mindful of the impact on memory. 127 | ``` 128 | 129 | In the actual configuration for Hyprland, *hyprland.conf*, variables can be set for ease of reading and to be used as shortcuts in the bind command. The following example uses $w shorthand wallpaper variables: 130 | 131 | *~/.config/hypr/hyprland.conf* 132 | ``` 133 | $w1 = hyprctl hyprpaper wallpaper "DP-1,~/Pictures/myepicpng.png" 134 | $w2 = hyprctl hyprpaper wallpaper "DP-1,~/Pictures/myepicpngToo.png" 135 | $w3 = hyprctl hyprpaper wallpaper "DP-1,~/Pictures/myepicpngAlso.png" 136 | #yes use quotes around desired monitor and wallpaper 137 | #... continued with desired amount 138 | ``` 139 | With the variables created we can now "exec" the actions. 140 | 141 | Remember in Hyprland we can bind more than one action to a key so in the case where we'd like to change the wallpaper when we switch workspace we have to ensure that the actions are bound to the same key such as... 142 | 143 | *~/.config/hypr/hyprland.conf* 144 | ``` 145 | bind=SUPER,1,workspace,1 #Superkey + 1 switches to workspace 1 146 | bind=SUPER,1,exec,$w1 #SuperKey + 1 switches to wallpaper $w1 on DP-1 as defined in the variable 147 | 148 | bind=SUPER,2,workspace,2 #Superkey + 2 switches to workspace 2 149 | bind=SUPER,2,exec,$w2 #SuperKey + 2 switches to wallpaper $w2 on DP-1 as defined in the variable 150 | 151 | bind=SUPER,3,workspace,3 #Superkey + 3 switches to workspace 3 152 | bind=SUPER,3,exec,$w3 #SuperKey + 3 switches to wallpaper $w3 on DP-1 as defined in the variable 153 | 154 | #... and so on 155 | ``` 156 | Because the default behavior in Hyprland is to also switch the workspace whenever movetoworkspace is used to move a window to another workspace you may want to include the following: 157 | 158 | ``` 159 | bind=SUPERSHIFT,1,movetoworkspace,1 #Superkey + Shift + 1 moves windows and switches to workspace 1 160 | bind=SUPERSHIFT,1,exec,$w1 #SuperKey + Shift + 1 switches to wallpaper $w1 on DP-1 as defined in the variable 161 | ``` 162 | 163 | ## Getting information from hyprpaper 164 | You can also use `hyprctl hyprpaper` to get information about the state of hyprpaper using the following commands: 165 | ``` 166 | listloaded - lists the wallpapers that are currently preloaded (useful for dynamically preloading and unloading) 167 | listactive - prints the active wallpapers hyprpaper is displaying, along with its accociated monitor 168 | ``` 169 | 170 | # Battery life 171 | Since the IPC has to tick every now and then, and poll in the background, battery life might be a tiny bit worse with IPC on. If you want to fully disable it, use 172 | ``` 173 | ipc = off 174 | ``` 175 | in the config. 176 | 177 | # Misc 178 | You can set `splash = true` to enable the splash rendering over the wallpaper. 179 | 180 | The value for `splash_offset` sets, in percentage, the splash rendering offset relative to the bottom of the display. 181 | 182 | ## Unloading 183 | If you use a lot of wallpapers, consider unloading those that you no longer need. This will mean you need to load them again if you wish to use them for a second time, but will free the memory used by the preloaded bitmap. (Usually 8 - 20MB, depending on the resolution) 184 | 185 | You can issue a `hyprctl hyprpaper unload [PATH]` to do that. 186 | 187 | You can also issue a `hyprctl hyprpaper unload all` to unload all inactive wallpapers. 188 | 189 |
190 | 191 | For other compositors, the socket works like socket1 of Hyprland, and is located in `/tmp/hypr/.hyprpaper.sock` (this path only when Hyprland is not running!) 192 | -------------------------------------------------------------------------------- /VERSION: -------------------------------------------------------------------------------- 1 | 0.7.5 2 | -------------------------------------------------------------------------------- /flake.lock: -------------------------------------------------------------------------------- 1 | { 2 | "nodes": { 3 | "hyprgraphics": { 4 | "inputs": { 5 | "hyprutils": [ 6 | "hyprutils" 7 | ], 8 | "nixpkgs": [ 9 | "nixpkgs" 10 | ], 11 | "systems": [ 12 | "systems" 13 | ] 14 | }, 15 | "locked": { 16 | "lastModified": 1745015490, 17 | "narHash": "sha256-apEJ9zoSzmslhJ2vOKFcXTMZLUFYzh1ghfB6Rbw3Low=", 18 | "owner": "hyprwm", 19 | "repo": "hyprgraphics", 20 | "rev": "60754910946b4e2dc1377b967b7156cb989c5873", 21 | "type": "github" 22 | }, 23 | "original": { 24 | "owner": "hyprwm", 25 | "repo": "hyprgraphics", 26 | "type": "github" 27 | } 28 | }, 29 | "hyprlang": { 30 | "inputs": { 31 | "hyprutils": [ 32 | "hyprutils" 33 | ], 34 | "nixpkgs": [ 35 | "nixpkgs" 36 | ], 37 | "systems": [ 38 | "systems" 39 | ] 40 | }, 41 | "locked": { 42 | "lastModified": 1746655412, 43 | "narHash": "sha256-kVQ0bHVtX6baYxRWWIh4u3LNJZb9Zcm2xBeDPOGz5BY=", 44 | "owner": "hyprwm", 45 | "repo": "hyprlang", 46 | "rev": "557241780c179cf7ef224df392f8e67dab6cef83", 47 | "type": "github" 48 | }, 49 | "original": { 50 | "owner": "hyprwm", 51 | "repo": "hyprlang", 52 | "type": "github" 53 | } 54 | }, 55 | "hyprutils": { 56 | "inputs": { 57 | "nixpkgs": [ 58 | "nixpkgs" 59 | ], 60 | "systems": [ 61 | "systems" 62 | ] 63 | }, 64 | "locked": { 65 | "lastModified": 1746635225, 66 | "narHash": "sha256-W9G9bb0zRYDBRseHbVez0J8qVpD5QbizX67H/vsudhM=", 67 | "owner": "hyprwm", 68 | "repo": "hyprutils", 69 | "rev": "674ea57373f08b7609ce93baff131117a0dfe70d", 70 | "type": "github" 71 | }, 72 | "original": { 73 | "owner": "hyprwm", 74 | "repo": "hyprutils", 75 | "type": "github" 76 | } 77 | }, 78 | "hyprwayland-scanner": { 79 | "inputs": { 80 | "nixpkgs": [ 81 | "nixpkgs" 82 | ], 83 | "systems": [ 84 | "systems" 85 | ] 86 | }, 87 | "locked": { 88 | "lastModified": 1739870480, 89 | "narHash": "sha256-SiDN5BGxa/1hAsqhgJsS03C3t2QrLgBT8u+ENJ0Qzwc=", 90 | "owner": "hyprwm", 91 | "repo": "hyprwayland-scanner", 92 | "rev": "206367a08dc5ac4ba7ad31bdca391d098082e64b", 93 | "type": "github" 94 | }, 95 | "original": { 96 | "owner": "hyprwm", 97 | "repo": "hyprwayland-scanner", 98 | "type": "github" 99 | } 100 | }, 101 | "nixpkgs": { 102 | "locked": { 103 | "lastModified": 1746461020, 104 | "narHash": "sha256-7+pG1I9jvxNlmln4YgnlW4o+w0TZX24k688mibiFDUE=", 105 | "owner": "NixOS", 106 | "repo": "nixpkgs", 107 | "rev": "3730d8a308f94996a9ba7c7138ede69c1b9ac4ae", 108 | "type": "github" 109 | }, 110 | "original": { 111 | "owner": "NixOS", 112 | "ref": "nixos-unstable", 113 | "repo": "nixpkgs", 114 | "type": "github" 115 | } 116 | }, 117 | "root": { 118 | "inputs": { 119 | "hyprgraphics": "hyprgraphics", 120 | "hyprlang": "hyprlang", 121 | "hyprutils": "hyprutils", 122 | "hyprwayland-scanner": "hyprwayland-scanner", 123 | "nixpkgs": "nixpkgs", 124 | "systems": "systems" 125 | } 126 | }, 127 | "systems": { 128 | "locked": { 129 | "lastModified": 1689347949, 130 | "narHash": "sha256-12tWmuL2zgBgZkdoB6qXZsgJEH9LR3oUgpaQq2RbI80=", 131 | "owner": "nix-systems", 132 | "repo": "default-linux", 133 | "rev": "31732fcf5e8fea42e59c2488ad31a0e651500f68", 134 | "type": "github" 135 | }, 136 | "original": { 137 | "owner": "nix-systems", 138 | "repo": "default-linux", 139 | "type": "github" 140 | } 141 | } 142 | }, 143 | "root": "root", 144 | "version": 7 145 | } 146 | -------------------------------------------------------------------------------- /flake.nix: -------------------------------------------------------------------------------- 1 | { 2 | description = "Hyprpaper is a blazing fast Wayland wallpaper utility with IPC controls"; 3 | 4 | inputs = { 5 | nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; 6 | systems.url = "github:nix-systems/default-linux"; 7 | 8 | hyprgraphics = { 9 | url = "github:hyprwm/hyprgraphics"; 10 | inputs.nixpkgs.follows = "nixpkgs"; 11 | inputs.systems.follows = "systems"; 12 | inputs.hyprutils.follows = "hyprutils"; 13 | }; 14 | 15 | hyprutils = { 16 | url = "github:hyprwm/hyprutils"; 17 | inputs.nixpkgs.follows = "nixpkgs"; 18 | inputs.systems.follows = "systems"; 19 | }; 20 | 21 | hyprlang = { 22 | url = "github:hyprwm/hyprlang"; 23 | inputs.nixpkgs.follows = "nixpkgs"; 24 | inputs.systems.follows = "systems"; 25 | inputs.hyprutils.follows = "hyprutils"; 26 | }; 27 | 28 | hyprwayland-scanner = { 29 | url = "github:hyprwm/hyprwayland-scanner"; 30 | inputs.nixpkgs.follows = "nixpkgs"; 31 | inputs.systems.follows = "systems"; 32 | }; 33 | }; 34 | 35 | outputs = { 36 | self, 37 | nixpkgs, 38 | systems, 39 | ... 40 | } @ inputs: let 41 | inherit (nixpkgs) lib; 42 | eachSystem = lib.genAttrs (import systems); 43 | 44 | pkgsFor = eachSystem (system: 45 | import nixpkgs { 46 | localSystem.system = system; 47 | overlays = with self.overlays; [hyprpaper]; 48 | }); 49 | mkDate = longDate: (lib.concatStringsSep "-" [ 50 | (builtins.substring 0 4 longDate) 51 | (builtins.substring 4 2 longDate) 52 | (builtins.substring 6 2 longDate) 53 | ]); 54 | version = lib.removeSuffix "\n" (builtins.readFile ./VERSION); 55 | in { 56 | overlays = { 57 | default = self.overlays.hyprpaper; 58 | hyprpaper = lib.composeManyExtensions [ 59 | inputs.hyprgraphics.overlays.default 60 | inputs.hyprlang.overlays.default 61 | inputs.hyprutils.overlays.default 62 | inputs.hyprwayland-scanner.overlays.default 63 | (final: prev: rec { 64 | hyprpaper = final.callPackage ./nix/default.nix { 65 | stdenv = final.gcc14Stdenv; 66 | version = version + "+date=" + (mkDate (self.lastModifiedDate or "19700101")) + "_" + (self.shortRev or "dirty"); 67 | commit = self.rev or ""; 68 | }; 69 | hyprpaper-debug = hyprpaper.override {debug = true;}; 70 | }) 71 | ]; 72 | }; 73 | 74 | packages = eachSystem (system: { 75 | default = self.packages.${system}.hyprpaper; 76 | inherit (pkgsFor.${system}) hyprpaper hyprpaper-debug; 77 | }); 78 | 79 | homeManagerModules = { 80 | default = self.homeManagerModules.hyprpaper; 81 | hyprpaper = builtins.throw "hyprpaper: the flake HM module has been removed. Use the module from Home Manager upstream."; 82 | }; 83 | 84 | formatter = eachSystem (system: pkgsFor.${system}.alejandra); 85 | }; 86 | } 87 | -------------------------------------------------------------------------------- /nix/default.nix: -------------------------------------------------------------------------------- 1 | { 2 | lib, 3 | stdenv, 4 | pkg-config, 5 | cmake, 6 | cairo, 7 | expat, 8 | file, 9 | fribidi, 10 | hyprgraphics, 11 | hyprlang, 12 | hyprutils, 13 | hyprwayland-scanner, 14 | libdatrie, 15 | libGL, 16 | libjpeg, 17 | libjxl, 18 | libselinux, 19 | libsepol, 20 | libthai, 21 | libwebp, 22 | pango, 23 | pcre, 24 | pcre2, 25 | util-linux, 26 | wayland, 27 | wayland-protocols, 28 | wayland-scanner, 29 | xorg, 30 | commit, 31 | debug ? false, 32 | version ? "git", 33 | }: 34 | stdenv.mkDerivation { 35 | pname = "hyprpaper" + lib.optionalString debug "-debug"; 36 | inherit version; 37 | 38 | src = ../.; 39 | 40 | prePatch = '' 41 | substituteInPlace src/main.cpp \ 42 | --replace GIT_COMMIT_HASH '"${commit}"' 43 | ''; 44 | 45 | depsBuildBuild = [ 46 | pkg-config 47 | ]; 48 | 49 | cmakeBuildType = 50 | if debug 51 | then "Debug" 52 | else "Release"; 53 | 54 | nativeBuildInputs = [ 55 | cmake 56 | hyprwayland-scanner 57 | pkg-config 58 | wayland-scanner 59 | ]; 60 | 61 | buildInputs = [ 62 | cairo 63 | expat 64 | file 65 | fribidi 66 | hyprgraphics 67 | hyprlang 68 | hyprutils 69 | libdatrie 70 | libGL 71 | libjpeg 72 | libjxl 73 | libselinux 74 | libsepol 75 | libthai 76 | libwebp 77 | pango 78 | pcre 79 | pcre2 80 | wayland 81 | wayland-protocols 82 | xorg.libXdmcp 83 | util-linux 84 | ]; 85 | 86 | meta = with lib; { 87 | description = "A blazing fast wayland wallpaper utility with IPC controls"; 88 | homepage = "https://github.com/hyprwm/hyprpaper"; 89 | license = licenses.bsd3; 90 | mainProgram = "hyprpaper"; 91 | platforms = platforms.linux; 92 | }; 93 | } 94 | -------------------------------------------------------------------------------- /protocols/wlr-layer-shell-unstable-v1.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Copyright © 2017 Drew DeVault 5 | 6 | Permission to use, copy, modify, distribute, and sell this 7 | software and its documentation for any purpose is hereby granted 8 | without fee, provided that the above copyright notice appear in 9 | all copies and that both that copyright notice and this permission 10 | notice appear in supporting documentation, and that the name of 11 | the copyright holders not be used in advertising or publicity 12 | pertaining to distribution of the software without specific, 13 | written prior permission. The copyright holders make no 14 | representations about the suitability of this software for any 15 | purpose. It is provided "as is" without express or implied 16 | warranty. 17 | 18 | THE COPYRIGHT HOLDERS DISCLAIM ALL WARRANTIES WITH REGARD TO THIS 19 | SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND 20 | FITNESS, IN NO EVENT SHALL THE COPYRIGHT HOLDERS BE LIABLE FOR ANY 21 | SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 22 | WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN 23 | AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, 24 | ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF 25 | THIS SOFTWARE. 26 | 27 | 28 | 29 | 30 | Clients can use this interface to assign the surface_layer role to 31 | wl_surfaces. Such surfaces are assigned to a "layer" of the output and 32 | rendered with a defined z-depth respective to each other. They may also be 33 | anchored to the edges and corners of a screen and specify input handling 34 | semantics. This interface should be suitable for the implementation of 35 | many desktop shell components, and a broad number of other applications 36 | that interact with the desktop. 37 | 38 | 39 | 40 | 41 | Create a layer surface for an existing surface. This assigns the role of 42 | layer_surface, or raises a protocol error if another role is already 43 | assigned. 44 | 45 | Creating a layer surface from a wl_surface which has a buffer attached 46 | or committed is a client error, and any attempts by a client to attach 47 | or manipulate a buffer prior to the first layer_surface.configure call 48 | must also be treated as errors. 49 | 50 | You may pass NULL for output to allow the compositor to decide which 51 | output to use. Generally this will be the one that the user most 52 | recently interacted with. 53 | 54 | Clients can specify a namespace that defines the purpose of the layer 55 | surface. 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | These values indicate which layers a surface can be rendered in. They 73 | are ordered by z depth, bottom-most first. Traditional shell surfaces 74 | will typically be rendered between the bottom and top layers. 75 | Fullscreen shell surfaces are typically rendered at the top layer. 76 | Multiple surfaces can share a single layer, and ordering within a 77 | single layer is undefined. 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | An interface that may be implemented by a wl_surface, for surfaces that 90 | are designed to be rendered as a layer of a stacked desktop-like 91 | environment. 92 | 93 | Layer surface state (size, anchor, exclusive zone, margin, interactivity) 94 | is double-buffered, and will be applied at the time wl_surface.commit of 95 | the corresponding wl_surface is called. 96 | 97 | 98 | 99 | 100 | Sets the size of the surface in surface-local coordinates. The 101 | compositor will display the surface centered with respect to its 102 | anchors. 103 | 104 | If you pass 0 for either value, the compositor will assign it and 105 | inform you of the assignment in the configure event. You must set your 106 | anchor to opposite edges in the dimensions you omit; not doing so is a 107 | protocol error. Both values are 0 by default. 108 | 109 | Size is double-buffered, see wl_surface.commit. 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | Requests that the compositor anchor the surface to the specified edges 118 | and corners. If two orthoginal edges are specified (e.g. 'top' and 119 | 'left'), then the anchor point will be the intersection of the edges 120 | (e.g. the top left corner of the output); otherwise the anchor point 121 | will be centered on that edge, or in the center if none is specified. 122 | 123 | Anchor is double-buffered, see wl_surface.commit. 124 | 125 | 126 | 127 | 128 | 129 | 130 | Requests that the compositor avoids occluding an area of the surface 131 | with other surfaces. The compositor's use of this information is 132 | implementation-dependent - do not assume that this region will not 133 | actually be occluded. 134 | 135 | A positive value is only meaningful if the surface is anchored to an 136 | edge, rather than a corner. The zone is the number of surface-local 137 | coordinates from the edge that are considered exclusive. 138 | 139 | Surfaces that do not wish to have an exclusive zone may instead specify 140 | how they should interact with surfaces that do. If set to zero, the 141 | surface indicates that it would like to be moved to avoid occluding 142 | surfaces with a positive excluzive zone. If set to -1, the surface 143 | indicates that it would not like to be moved to accommodate for other 144 | surfaces, and the compositor should extend it all the way to the edges 145 | it is anchored to. 146 | 147 | For example, a panel might set its exclusive zone to 10, so that 148 | maximized shell surfaces are not shown on top of it. A notification 149 | might set its exclusive zone to 0, so that it is moved to avoid 150 | occluding the panel, but shell surfaces are shown underneath it. A 151 | wallpaper or lock screen might set their exclusive zone to -1, so that 152 | they stretch below or over the panel. 153 | 154 | The default value is 0. 155 | 156 | Exclusive zone is double-buffered, see wl_surface.commit. 157 | 158 | 159 | 160 | 161 | 162 | 163 | Requests that the surface be placed some distance away from the anchor 164 | point on the output, in surface-local coordinates. Setting this value 165 | for edges you are not anchored to has no effect. 166 | 167 | The exclusive zone includes the margin. 168 | 169 | Margin is double-buffered, see wl_surface.commit. 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | Set to 1 to request that the seat send keyboard events to this layer 180 | surface. For layers below the shell surface layer, the seat will use 181 | normal focus semantics. For layers above the shell surface layers, the 182 | seat will always give exclusive keyboard focus to the top-most layer 183 | which has keyboard interactivity set to true. 184 | 185 | Layer surfaces receive pointer, touch, and tablet events normally. If 186 | you do not want to receive them, set the input region on your surface 187 | to an empty region. 188 | 189 | Events is double-buffered, see wl_surface.commit. 190 | 191 | 192 | 193 | 194 | 195 | 196 | This assigns an xdg_popup's parent to this layer_surface. This popup 197 | should have been created via xdg_surface::get_popup with the parent set 198 | to NULL, and this request must be invoked before committing the popup's 199 | initial state. 200 | 201 | See the documentation of xdg_popup for more details about what an 202 | xdg_popup is and how it is used. 203 | 204 | 205 | 206 | 207 | 208 | 209 | When a configure event is received, if a client commits the 210 | surface in response to the configure event, then the client 211 | must make an ack_configure request sometime before the commit 212 | request, passing along the serial of the configure event. 213 | 214 | If the client receives multiple configure events before it 215 | can respond to one, it only has to ack the last configure event. 216 | 217 | A client is not required to commit immediately after sending 218 | an ack_configure request - it may even ack_configure several times 219 | before its next surface commit. 220 | 221 | A client may send multiple ack_configure requests before committing, but 222 | only the last request sent before a commit indicates which configure 223 | event the client really is responding to. 224 | 225 | 226 | 227 | 228 | 229 | 230 | This request destroys the layer surface. 231 | 232 | 233 | 234 | 235 | 236 | The configure event asks the client to resize its surface. 237 | 238 | Clients should arrange their surface for the new states, and then send 239 | an ack_configure request with the serial sent in this configure event at 240 | some point before committing the new surface. 241 | 242 | The client is free to dismiss all but the last configure event it 243 | received. 244 | 245 | The width and height arguments specify the size of the window in 246 | surface-local coordinates. 247 | 248 | The size is a hint, in the sense that the client is free to ignore it if 249 | it doesn't resize, pick a smaller size (to satisfy aspect ratio or 250 | resize in steps of NxM pixels). If the client picks a smaller size and 251 | is anchored to two opposite anchors (e.g. 'top' and 'bottom'), the 252 | surface will be centered on this axis. 253 | 254 | If the width or height arguments are zero, it means the client should 255 | decide its own window dimension. 256 | 257 | 258 | 259 | 260 | 261 | 262 | 263 | 264 | The closed event is sent by the compositor when the surface will no 265 | longer be shown. The output may have been destroyed or the user may 266 | have asked for it to be removed. Further changes to the surface will be 267 | ignored. The client should destroy the resource after receiving this 268 | event, and create a new surface if they so choose. 269 | 270 | 271 | 272 | 273 | 274 | 275 | 276 | 277 | 278 | 279 | 280 | 281 | 282 | 283 | 284 | 285 | 286 | -------------------------------------------------------------------------------- /src/Hyprpaper.cpp: -------------------------------------------------------------------------------- 1 | #include "Hyprpaper.hpp" 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | static void setMallocThreshold() { 10 | #ifdef M_TRIM_THRESHOLD 11 | // The default is 128 pages, 12 | // which is very large and can lead to a lot of memory used for no reason 13 | // because trimming hasn't happened 14 | static const int PAGESIZE = sysconf(_SC_PAGESIZE); 15 | mallopt(M_TRIM_THRESHOLD, 6 * PAGESIZE); 16 | #endif 17 | } 18 | 19 | CHyprpaper::CHyprpaper() { 20 | setMallocThreshold(); 21 | } 22 | 23 | static void handleGlobal(CCWlRegistry* registry, uint32_t name, const char* interface, uint32_t version) { 24 | if (strcmp(interface, wl_compositor_interface.name) == 0) { 25 | g_pHyprpaper->m_pCompositor = makeShared((wl_proxy*)wl_registry_bind((wl_registry*)registry->resource(), name, &wl_compositor_interface, 4)); 26 | } else if (strcmp(interface, wl_shm_interface.name) == 0) { 27 | g_pHyprpaper->m_pSHM = makeShared((wl_proxy*)wl_registry_bind((wl_registry*)registry->resource(), name, &wl_shm_interface, 1)); 28 | } else if (strcmp(interface, wl_output_interface.name) == 0) { 29 | g_pHyprpaper->m_mtTickMutex.lock(); 30 | 31 | const auto PMONITOR = g_pHyprpaper->m_vMonitors.emplace_back(std::make_unique()).get(); 32 | PMONITOR->wayland_name = name; 33 | PMONITOR->name = ""; 34 | PMONITOR->output = makeShared((wl_proxy*)wl_registry_bind((wl_registry*)registry->resource(), name, &wl_output_interface, 4)); 35 | PMONITOR->registerListeners(); 36 | 37 | g_pHyprpaper->m_mtTickMutex.unlock(); 38 | } else if (strcmp(interface, wl_seat_interface.name) == 0) { 39 | g_pHyprpaper->createSeat(makeShared((wl_proxy*)wl_registry_bind((wl_registry*)registry->resource(), name, &wl_seat_interface, 7))); 40 | } else if (strcmp(interface, zwlr_layer_shell_v1_interface.name) == 0) { 41 | g_pHyprpaper->m_pLayerShell = makeShared((wl_proxy*)wl_registry_bind((wl_registry*)registry->resource(), name, &zwlr_layer_shell_v1_interface, 1)); 42 | } else if (strcmp(interface, wp_fractional_scale_manager_v1_interface.name) == 0 && !g_pHyprpaper->m_bNoFractionalScale) { 43 | g_pHyprpaper->m_pFractionalScale = 44 | makeShared((wl_proxy*)wl_registry_bind((wl_registry*)registry->resource(), name, &wp_fractional_scale_manager_v1_interface, 1)); 45 | } else if (strcmp(interface, wp_viewporter_interface.name) == 0) { 46 | g_pHyprpaper->m_pViewporter = makeShared((wl_proxy*)wl_registry_bind((wl_registry*)registry->resource(), name, &wp_viewporter_interface, 1)); 47 | } else if (strcmp(interface, wp_cursor_shape_manager_v1_interface.name) == 0) { 48 | g_pHyprpaper->m_pCursorShape = 49 | makeShared((wl_proxy*)wl_registry_bind((wl_registry*)registry->resource(), name, &wp_cursor_shape_manager_v1_interface, 1)); 50 | } 51 | } 52 | 53 | static void handleGlobalRemove(CCWlRegistry* registry, uint32_t name) { 54 | for (auto& m : g_pHyprpaper->m_vMonitors) { 55 | if (m->wayland_name == name) { 56 | Debug::log(LOG, "Destroying output {}", m->name); 57 | g_pHyprpaper->clearWallpaperFromMonitor(m->name); 58 | std::erase_if(g_pHyprpaper->m_vMonitors, [&](const auto& other) { return other->wayland_name == name; }); 59 | return; 60 | } 61 | } 62 | } 63 | 64 | void CHyprpaper::init() { 65 | 66 | if (!lockSingleInstance()) { 67 | Debug::log(CRIT, "Cannot launch multiple instances of Hyprpaper at once!"); 68 | exit(1); 69 | } 70 | 71 | removeOldHyprpaperImages(); 72 | 73 | m_sDisplay = (wl_display*)wl_display_connect(nullptr); 74 | 75 | if (!m_sDisplay) { 76 | Debug::log(CRIT, "No wayland compositor running!"); 77 | exit(1); 78 | } 79 | 80 | // run 81 | auto REGISTRY = makeShared((wl_proxy*)wl_display_get_registry(m_sDisplay)); 82 | REGISTRY->setGlobal(::handleGlobal); 83 | REGISTRY->setGlobalRemove(::handleGlobalRemove); 84 | 85 | wl_display_roundtrip(m_sDisplay); 86 | 87 | while (m_vMonitors.size() < 1 || m_vMonitors[0]->name.empty()) { 88 | wl_display_dispatch(m_sDisplay); 89 | } 90 | 91 | g_pConfigManager = std::make_unique(); 92 | g_pIPCSocket = std::make_unique(); 93 | 94 | g_pConfigManager->parse(); 95 | 96 | preloadAllWallpapersFromConfig(); 97 | 98 | if (std::any_cast(g_pConfigManager->config->getConfigValue("ipc"))) 99 | g_pIPCSocket->initialize(); 100 | 101 | do { 102 | std::lock_guard lg(m_mtTickMutex); 103 | tick(true); 104 | } while (wl_display_dispatch(m_sDisplay) != -1); 105 | 106 | unlockSingleInstance(); 107 | } 108 | 109 | void CHyprpaper::tick(bool force) { 110 | bool reload = g_pIPCSocket && g_pIPCSocket->mainThreadParseRequest(); 111 | 112 | if (!reload && !force) 113 | return; 114 | 115 | preloadAllWallpapersFromConfig(); 116 | ensurePoolBuffersPresent(); 117 | 118 | recheckAllMonitors(); 119 | } 120 | 121 | bool CHyprpaper::isPreloaded(const std::string& path) { 122 | for (auto& [pt, wt] : m_mWallpaperTargets) { 123 | if (pt == path) 124 | return true; 125 | } 126 | 127 | return false; 128 | } 129 | 130 | void CHyprpaper::unloadWallpaper(const std::string& path) { 131 | bool found = false; 132 | 133 | for (auto& [ewp, cls] : m_mWallpaperTargets) { 134 | if (ewp == path) { 135 | // found 136 | found = true; 137 | break; 138 | } 139 | } 140 | 141 | if (!found) { 142 | Debug::log(LOG, "Cannot unload a target that was not loaded!"); 143 | return; 144 | } 145 | 146 | // clean buffers 147 | for (auto it = m_vBuffers.begin(); it != m_vBuffers.end();) { 148 | 149 | if (it->get()->target != path) { 150 | it++; 151 | continue; 152 | } 153 | 154 | const auto PRELOADPATH = it->get()->name; 155 | 156 | Debug::log(LOG, "Unloading target {}, preload path {}", path, PRELOADPATH); 157 | 158 | std::filesystem::remove(PRELOADPATH); 159 | 160 | destroyBuffer(it->get()); 161 | 162 | it = m_vBuffers.erase(it); 163 | } 164 | 165 | m_mWallpaperTargets.erase(path); // will free the cairo surface 166 | } 167 | 168 | void CHyprpaper::preloadAllWallpapersFromConfig() { 169 | if (g_pConfigManager->m_dRequestedPreloads.empty()) 170 | return; 171 | 172 | for (auto& wp : g_pConfigManager->m_dRequestedPreloads) { 173 | 174 | // check if it doesnt exist 175 | bool exists = false; 176 | for (auto& [ewp, cls] : m_mWallpaperTargets) { 177 | if (ewp == wp) { 178 | Debug::log(LOG, "Ignoring request to preload {} as it already is preloaded!", ewp); 179 | exists = true; 180 | break; 181 | } 182 | } 183 | 184 | if (exists) 185 | continue; 186 | 187 | m_mWallpaperTargets[wp] = CWallpaperTarget(); 188 | if (std::filesystem::is_symlink(wp)) { 189 | auto real_wp = std::filesystem::read_symlink(wp); 190 | std::filesystem::path absolute_path = std::filesystem::path(wp).parent_path() / real_wp; 191 | absolute_path = absolute_path.lexically_normal(); 192 | m_mWallpaperTargets[wp].create(absolute_path); 193 | } else { 194 | m_mWallpaperTargets[wp].create(wp); 195 | } 196 | } 197 | 198 | g_pConfigManager->m_dRequestedPreloads.clear(); 199 | } 200 | 201 | void CHyprpaper::recheckAllMonitors() { 202 | for (auto& m : m_vMonitors) { 203 | recheckMonitor(m.get()); 204 | } 205 | } 206 | 207 | void CHyprpaper::createSeat(SP pSeat) { 208 | m_pSeat = pSeat; 209 | 210 | pSeat->setCapabilities([this](CCWlSeat* r, wl_seat_capability caps) { 211 | if (caps & WL_SEAT_CAPABILITY_POINTER) { 212 | m_pSeatPointer = makeShared(m_pSeat->sendGetPointer()); 213 | if (!m_pCursorShape) 214 | Debug::log(WARN, "No cursor-shape-v1 support from the compositor: cursor will be blank"); 215 | else 216 | m_pSeatCursorShapeDevice = makeShared(m_pCursorShape->sendGetPointer(m_pSeatPointer->resource())); 217 | 218 | m_pSeatPointer->setEnter([this](CCWlPointer* r, uint32_t serial, wl_proxy* surface, wl_fixed_t x, wl_fixed_t y) { 219 | if (!m_pCursorShape) { 220 | m_pSeatPointer->sendSetCursor(serial, nullptr, 0, 0); 221 | return; 222 | } 223 | 224 | m_pSeatCursorShapeDevice->sendSetShape(serial, wpCursorShapeDeviceV1Shape::WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_DEFAULT); 225 | }); 226 | } else 227 | Debug::log(LOG, "No pointer capability from the compositor"); 228 | }); 229 | } 230 | 231 | void CHyprpaper::recheckMonitor(SMonitor* pMonitor) { 232 | ensureMonitorHasActiveWallpaper(pMonitor); 233 | 234 | if (pMonitor->wantsACK) { 235 | pMonitor->wantsACK = false; 236 | pMonitor->pCurrentLayerSurface->pLayerSurface->sendAckConfigure(pMonitor->configureSerial); 237 | } 238 | 239 | if (pMonitor->wantsReload) { 240 | pMonitor->wantsReload = false; 241 | renderWallpaperForMonitor(pMonitor); 242 | } 243 | } 244 | 245 | void CHyprpaper::removeOldHyprpaperImages() { 246 | int cleaned = 0; 247 | uint64_t memoryFreed = 0; 248 | 249 | for (const auto& entry : std::filesystem::directory_iterator(std::string(getenv("XDG_RUNTIME_DIR")))) { 250 | if (entry.is_directory()) 251 | continue; 252 | 253 | const auto FILENAME = entry.path().filename().string(); 254 | 255 | if (FILENAME.contains(".hyprpaper_")) { 256 | // unlink it 257 | 258 | memoryFreed += entry.file_size(); 259 | if (!std::filesystem::remove(entry.path())) 260 | Debug::log(LOG, "Couldn't remove {}", entry.path().string()); 261 | cleaned++; 262 | } 263 | } 264 | 265 | if (cleaned != 0) 266 | Debug::log(LOG, "Cleaned old hyprpaper preloads ({}), removing {:.1f}MB", cleaned, ((float)memoryFreed) / 1000000.f); 267 | } 268 | 269 | SMonitor* CHyprpaper::getMonitorFromName(const std::string& monname) { 270 | bool useDesc = false; 271 | std::string desc = ""; 272 | if (monname.find("desc:") == 0) { 273 | useDesc = true; 274 | desc = monname.substr(5); 275 | } 276 | 277 | for (auto& m : m_vMonitors) { 278 | if (useDesc && m->description.find(desc) == 0) 279 | return m.get(); 280 | 281 | if (m->name == monname) 282 | return m.get(); 283 | } 284 | 285 | return nullptr; 286 | } 287 | 288 | void CHyprpaper::ensurePoolBuffersPresent() { 289 | bool anyNewBuffers = false; 290 | 291 | for (auto& [file, wt] : m_mWallpaperTargets) { 292 | for (auto& m : m_vMonitors) { 293 | 294 | if (m->size == Vector2D()) 295 | continue; 296 | 297 | auto it = std::find_if(m_vBuffers.begin(), m_vBuffers.end(), [wt = &wt, &m](const std::unique_ptr& el) { 298 | auto scale = std::round((m->pCurrentLayerSurface && m->pCurrentLayerSurface->pFractionalScaleInfo ? m->pCurrentLayerSurface->fScale : m->scale) * 120.0) / 120.0; 299 | return el->target == wt->m_szPath && vectorDeltaLessThan(el->pixelSize, m->size * scale, 1); 300 | }); 301 | 302 | if (it == m_vBuffers.end()) { 303 | // create 304 | const auto PBUFFER = m_vBuffers.emplace_back(std::make_unique()).get(); 305 | auto scale = std::round((m->pCurrentLayerSurface && m->pCurrentLayerSurface->pFractionalScaleInfo ? m->pCurrentLayerSurface->fScale : m->scale) * 120.0) / 120.0; 306 | createBuffer(PBUFFER, m->size.x * scale, m->size.y * scale, WL_SHM_FORMAT_ARGB8888); 307 | 308 | PBUFFER->target = wt.m_szPath; 309 | 310 | Debug::log(LOG, "Buffer created for target {}, Shared Memory usage: {:.1f}MB", wt.m_szPath, PBUFFER->size / 1000000.f); 311 | 312 | anyNewBuffers = true; 313 | } 314 | } 315 | } 316 | 317 | if (anyNewBuffers) { 318 | uint64_t bytesUsed = 0; 319 | 320 | for (auto& bf : m_vBuffers) { 321 | bytesUsed += bf->size; 322 | } 323 | 324 | Debug::log(LOG, "Total SM usage for all buffers: {:.1f}MB", bytesUsed / 1000000.f); 325 | } 326 | } 327 | 328 | void CHyprpaper::clearWallpaperFromMonitor(const std::string& monname) { 329 | 330 | const auto PMONITOR = getMonitorFromName(monname); 331 | 332 | if (!PMONITOR) 333 | return; 334 | 335 | auto it = m_mMonitorActiveWallpaperTargets.find(PMONITOR); 336 | 337 | if (it != m_mMonitorActiveWallpaperTargets.end()) 338 | m_mMonitorActiveWallpaperTargets.erase(it); 339 | 340 | PMONITOR->hasATarget = true; 341 | 342 | if (PMONITOR->pCurrentLayerSurface) { 343 | 344 | PMONITOR->pCurrentLayerSurface = nullptr; 345 | 346 | PMONITOR->wantsACK = false; 347 | PMONITOR->wantsReload = false; 348 | PMONITOR->initialized = false; 349 | PMONITOR->readyForLS = true; 350 | } 351 | } 352 | 353 | void CHyprpaper::ensureMonitorHasActiveWallpaper(SMonitor* pMonitor) { 354 | if (!pMonitor->readyForLS || !pMonitor->hasATarget) 355 | return; 356 | 357 | auto it = m_mMonitorActiveWallpaperTargets.find(pMonitor); 358 | 359 | if (it == m_mMonitorActiveWallpaperTargets.end()) { 360 | m_mMonitorActiveWallpaperTargets[pMonitor] = nullptr; 361 | it = m_mMonitorActiveWallpaperTargets.find(pMonitor); 362 | } 363 | 364 | if (it->second) 365 | return; // has 366 | 367 | // get the target 368 | for (auto& [mon, path1] : m_mMonitorActiveWallpapers) { 369 | if (mon.find("desc:") != 0) 370 | continue; 371 | 372 | if (pMonitor->description.find(mon.substr(5)) == 0) { 373 | for (auto& [path2, target] : m_mWallpaperTargets) { 374 | if (path1 == path2) { 375 | it->second = ⌖ 376 | break; 377 | } 378 | } 379 | break; 380 | } 381 | } 382 | 383 | if (!it->second) { 384 | for (auto& [mon, path1] : m_mMonitorActiveWallpapers) { 385 | if (mon == pMonitor->name) { 386 | for (auto& [path2, target] : m_mWallpaperTargets) { 387 | if (path1 == path2) { 388 | it->second = ⌖ 389 | break; 390 | } 391 | } 392 | break; 393 | } 394 | } 395 | } 396 | 397 | if (!it->second) { 398 | // try to find a wildcard 399 | for (auto& [mon, path1] : m_mMonitorActiveWallpapers) { 400 | if (mon.empty()) { 401 | for (auto& [path2, target] : m_mWallpaperTargets) { 402 | if (path1 == path2) { 403 | it->second = ⌖ 404 | break; 405 | } 406 | } 407 | break; 408 | } 409 | } 410 | } 411 | 412 | if (!it->second) { 413 | pMonitor->hasATarget = false; 414 | Debug::log(WARN, "Monitor {} does not have a target! A wallpaper will not be created.", pMonitor->name); 415 | return; 416 | } 417 | 418 | // create it for thy if it doesnt have 419 | if (!pMonitor->pCurrentLayerSurface) 420 | createLSForMonitor(pMonitor); 421 | else 422 | pMonitor->wantsReload = true; 423 | } 424 | 425 | void CHyprpaper::createLSForMonitor(SMonitor* pMonitor) { 426 | pMonitor->pCurrentLayerSurface = pMonitor->layerSurfaces.emplace_back(std::make_unique(pMonitor)).get(); 427 | } 428 | 429 | bool CHyprpaper::setCloexec(const int& FD) { 430 | long flags = fcntl(FD, F_GETFD); 431 | if (flags == -1) { 432 | return false; 433 | } 434 | 435 | if (fcntl(FD, F_SETFD, flags | FD_CLOEXEC) == -1) { 436 | return false; 437 | } 438 | 439 | return true; 440 | } 441 | 442 | int CHyprpaper::createPoolFile(size_t size, std::string& name) { 443 | const auto XDGRUNTIMEDIR = getenv("XDG_RUNTIME_DIR"); 444 | if (!XDGRUNTIMEDIR) { 445 | Debug::log(CRIT, "XDG_RUNTIME_DIR not set!"); 446 | exit(1); 447 | } 448 | 449 | name = std::string(XDGRUNTIMEDIR) + "/.hyprpaper_XXXXXX"; 450 | 451 | const auto FD = mkstemp((char*)name.c_str()); 452 | if (FD < 0) { 453 | Debug::log(CRIT, "createPoolFile: fd < 0"); 454 | exit(1); 455 | } 456 | 457 | if (!setCloexec(FD)) { 458 | close(FD); 459 | Debug::log(CRIT, "createPoolFile: !setCloexec"); 460 | exit(1); 461 | } 462 | 463 | if (ftruncate(FD, size) < 0) { 464 | close(FD); 465 | Debug::log(CRIT, "createPoolFile: ftruncate < 0"); 466 | exit(1); 467 | } 468 | 469 | return FD; 470 | } 471 | 472 | void CHyprpaper::createBuffer(SPoolBuffer* pBuffer, int32_t w, int32_t h, uint32_t format) { 473 | const size_t STRIDE = w * 4; 474 | const size_t SIZE = STRIDE * h; 475 | 476 | std::string name; 477 | const auto FD = createPoolFile(SIZE, name); 478 | 479 | if (FD == -1) { 480 | Debug::log(CRIT, "Unable to create pool file!"); 481 | exit(1); 482 | } 483 | 484 | const auto DATA = mmap(nullptr, SIZE, PROT_READ | PROT_WRITE, MAP_SHARED, FD, 0); 485 | auto POOL = makeShared(g_pHyprpaper->m_pSHM->sendCreatePool(FD, SIZE)); 486 | pBuffer->buffer = makeShared(POOL->sendCreateBuffer(0, w, h, STRIDE, format)); 487 | POOL.reset(); 488 | 489 | close(FD); 490 | 491 | pBuffer->size = SIZE; 492 | pBuffer->data = DATA; 493 | pBuffer->surface = cairo_image_surface_create_for_data((unsigned char*)DATA, CAIRO_FORMAT_ARGB32, w, h, STRIDE); 494 | pBuffer->cairo = cairo_create(pBuffer->surface); 495 | pBuffer->pixelSize = Vector2D(w, h); 496 | pBuffer->name = name; 497 | } 498 | 499 | void CHyprpaper::destroyBuffer(SPoolBuffer* pBuffer) { 500 | pBuffer->buffer.reset(); 501 | cairo_destroy(pBuffer->cairo); 502 | cairo_surface_destroy(pBuffer->surface); 503 | munmap(pBuffer->data, pBuffer->size); 504 | 505 | pBuffer->buffer = nullptr; 506 | } 507 | 508 | SPoolBuffer* CHyprpaper::getPoolBuffer(SMonitor* pMonitor, CWallpaperTarget* pWallpaperTarget) { 509 | const auto IT = std::find_if(m_vBuffers.begin(), m_vBuffers.end(), [&](const std::unique_ptr& el) { 510 | auto scale = 511 | std::round((pMonitor->pCurrentLayerSurface && pMonitor->pCurrentLayerSurface->pFractionalScaleInfo ? pMonitor->pCurrentLayerSurface->fScale : pMonitor->scale) * 512 | 120.0) / 513 | 120.0; 514 | return el->target == pWallpaperTarget->m_szPath && vectorDeltaLessThan(el->pixelSize, pMonitor->size * scale, 1); 515 | }); 516 | 517 | if (IT == m_vBuffers.end()) 518 | return nullptr; 519 | return IT->get(); 520 | } 521 | 522 | void CHyprpaper::renderWallpaperForMonitor(SMonitor* pMonitor) { 523 | static auto PRENDERSPLASH = Hyprlang::CSimpleConfigValue(g_pConfigManager->config.get(), "splash"); 524 | static auto PSPLASHOFFSET = Hyprlang::CSimpleConfigValue(g_pConfigManager->config.get(), "splash_offset"); 525 | 526 | if (!m_mMonitorActiveWallpaperTargets[pMonitor]) 527 | recheckMonitor(pMonitor); 528 | 529 | const auto PWALLPAPERTARGET = m_mMonitorActiveWallpaperTargets[pMonitor]; 530 | const auto CONTAIN = m_mMonitorWallpaperRenderData[pMonitor->name].contain; 531 | const auto TILE = m_mMonitorWallpaperRenderData[pMonitor->name].tile; 532 | 533 | if (!PWALLPAPERTARGET) { 534 | Debug::log(CRIT, "wallpaper target null in render??"); 535 | exit(1); 536 | } 537 | 538 | auto* PBUFFER = getPoolBuffer(pMonitor, PWALLPAPERTARGET); 539 | 540 | if (!PBUFFER) { 541 | Debug::log(LOG, "Pool buffer missing for available target??"); 542 | ensurePoolBuffersPresent(); 543 | 544 | PBUFFER = getPoolBuffer(pMonitor, PWALLPAPERTARGET); 545 | 546 | if (!PBUFFER) { 547 | Debug::log(LOG, "Pool buffer failed #2. Ignoring WP."); 548 | return; 549 | } 550 | } 551 | 552 | const double SURFACESCALE = pMonitor->pCurrentLayerSurface && pMonitor->pCurrentLayerSurface->pFractionalScaleInfo ? pMonitor->pCurrentLayerSurface->fScale : pMonitor->scale; 553 | const Vector2D DIMENSIONS = Vector2D{std::round(pMonitor->size.x * SURFACESCALE), std::round(pMonitor->size.y * SURFACESCALE)}; 554 | 555 | const auto PCAIRO = PBUFFER->cairo; 556 | cairo_save(PCAIRO); 557 | cairo_set_operator(PCAIRO, CAIRO_OPERATOR_CLEAR); 558 | cairo_paint(PCAIRO); 559 | cairo_restore(PCAIRO); 560 | 561 | // always draw a black background behind the wallpaper 562 | cairo_set_source_rgb(PCAIRO, 0, 0, 0); 563 | cairo_rectangle(PCAIRO, 0, 0, DIMENSIONS.x, DIMENSIONS.y); 564 | cairo_fill(PCAIRO); 565 | cairo_surface_flush(PBUFFER->surface); 566 | 567 | // get scale 568 | // we always do cover 569 | double scale; 570 | Vector2D origin; 571 | 572 | const bool LOWASPECTRATIO = pMonitor->size.x / pMonitor->size.y > PWALLPAPERTARGET->m_vSize.x / PWALLPAPERTARGET->m_vSize.y; 573 | if ((CONTAIN && !LOWASPECTRATIO) || (!CONTAIN && LOWASPECTRATIO)) { 574 | scale = DIMENSIONS.x / PWALLPAPERTARGET->m_vSize.x; 575 | origin.y = -(PWALLPAPERTARGET->m_vSize.y * scale - DIMENSIONS.y) / 2.0 / scale; 576 | } else { 577 | scale = DIMENSIONS.y / PWALLPAPERTARGET->m_vSize.y; 578 | origin.x = -(PWALLPAPERTARGET->m_vSize.x * scale - DIMENSIONS.x) / 2.0 / scale; 579 | } 580 | 581 | Debug::log(LOG, "Image data for {}: {} at [{:.2f}, {:.2f}], scale: {:.2f} (original image size: [{}, {}])", pMonitor->name, PWALLPAPERTARGET->m_szPath, origin.x, origin.y, 582 | scale, (int)PWALLPAPERTARGET->m_vSize.x, (int)PWALLPAPERTARGET->m_vSize.y); 583 | 584 | if (TILE) { 585 | cairo_pattern_t* pattern = cairo_pattern_create_for_surface(PWALLPAPERTARGET->m_pCairoSurface->cairo()); 586 | cairo_pattern_set_extend(pattern, CAIRO_EXTEND_REPEAT); 587 | cairo_set_source(PCAIRO, pattern); 588 | } else { 589 | cairo_scale(PCAIRO, scale, scale); 590 | cairo_set_source_surface(PCAIRO, PWALLPAPERTARGET->m_pCairoSurface->cairo(), origin.x, origin.y); 591 | } 592 | 593 | cairo_paint(PCAIRO); 594 | 595 | if (*PRENDERSPLASH && getenv("HYPRLAND_INSTANCE_SIGNATURE")) { 596 | auto SPLASH = execAndGet("hyprctl splash"); 597 | if (!SPLASH.empty()) 598 | SPLASH.pop_back(); 599 | 600 | Debug::log(LOG, "Rendering splash: {}", SPLASH); 601 | 602 | cairo_select_font_face(PCAIRO, "Sans", CAIRO_FONT_SLANT_NORMAL, CAIRO_FONT_WEIGHT_NORMAL); 603 | 604 | const auto FONTSIZE = (int)(DIMENSIONS.y / 76.0 / scale); 605 | cairo_set_font_size(PCAIRO, FONTSIZE); 606 | 607 | static auto PSPLASHCOLOR = Hyprlang::CSimpleConfigValue(g_pConfigManager->config.get(), "splash_color"); 608 | 609 | Debug::log(LOG, "Splash color: {:x}", *PSPLASHCOLOR); 610 | 611 | cairo_set_source_rgba(PCAIRO, ((*PSPLASHCOLOR >> 16) & 0xFF) / 255.0, ((*PSPLASHCOLOR >> 8) & 0xFF) / 255.0, (*PSPLASHCOLOR & 0xFF) / 255.0, 612 | ((*PSPLASHCOLOR >> 24) & 0xFF) / 255.0); 613 | 614 | cairo_text_extents_t textExtents; 615 | cairo_text_extents(PCAIRO, SPLASH.c_str(), &textExtents); 616 | 617 | cairo_move_to(PCAIRO, ((DIMENSIONS.x - textExtents.width * scale) / 2.0) / scale, ((DIMENSIONS.y * (100 - *PSPLASHOFFSET)) / 100 - textExtents.height * scale) / scale); 618 | 619 | Debug::log(LOG, "Splash font size: {}, pos: {:.2f}, {:.2f}", FONTSIZE, (DIMENSIONS.x - textExtents.width) / 2.0 / scale, 620 | ((DIMENSIONS.y * (100 - *PSPLASHOFFSET)) / 100 - textExtents.height * scale) / scale); 621 | 622 | cairo_show_text(PCAIRO, SPLASH.c_str()); 623 | 624 | cairo_surface_flush(PWALLPAPERTARGET->m_pCairoSurface->cairo()); 625 | } 626 | 627 | cairo_restore(PCAIRO); 628 | 629 | if (pMonitor->pCurrentLayerSurface) { 630 | pMonitor->pCurrentLayerSurface->pSurface->sendAttach(PBUFFER->buffer.get(), 0, 0); 631 | pMonitor->pCurrentLayerSurface->pSurface->sendSetBufferScale(pMonitor->pCurrentLayerSurface->pFractionalScaleInfo ? 1 : pMonitor->scale); 632 | pMonitor->pCurrentLayerSurface->pSurface->sendDamageBuffer(0, 0, 0xFFFF, 0xFFFF); 633 | 634 | // our wps are always opaque 635 | auto opaqueRegion = makeShared(g_pHyprpaper->m_pCompositor->sendCreateRegion()); 636 | opaqueRegion->sendAdd(0, 0, PBUFFER->pixelSize.x, PBUFFER->pixelSize.y); 637 | pMonitor->pCurrentLayerSurface->pSurface->sendSetOpaqueRegion(opaqueRegion.get()); 638 | 639 | if (pMonitor->pCurrentLayerSurface->pFractionalScaleInfo) { 640 | Debug::log(LOG, "Submitting viewport dest size {}x{} for {:x}", static_cast(std::round(pMonitor->size.x)), static_cast(std::round(pMonitor->size.y)), 641 | (uintptr_t)pMonitor->pCurrentLayerSurface); 642 | pMonitor->pCurrentLayerSurface->pViewport->sendSetDestination(static_cast(std::round(pMonitor->size.x)), static_cast(std::round(pMonitor->size.y))); 643 | } 644 | pMonitor->pCurrentLayerSurface->pSurface->sendCommit(); 645 | } 646 | 647 | // check if we dont need to remove a wallpaper 648 | if (pMonitor->layerSurfaces.size() > 1) { 649 | for (auto it = pMonitor->layerSurfaces.begin(); it != pMonitor->layerSurfaces.end(); it++) { 650 | if (pMonitor->pCurrentLayerSurface != it->get()) { 651 | pMonitor->layerSurfaces.erase(it); 652 | break; 653 | } 654 | } 655 | } 656 | } 657 | 658 | bool CHyprpaper::lockSingleInstance() { 659 | const std::string XDG_RUNTIME_DIR = getenv("XDG_RUNTIME_DIR"); 660 | 661 | const auto LOCKFILE = XDG_RUNTIME_DIR + "/hyprpaper.lock"; 662 | 663 | if (std::filesystem::exists(LOCKFILE)) { 664 | std::ifstream ifs(LOCKFILE); 665 | std::string content((std::istreambuf_iterator(ifs)), (std::istreambuf_iterator())); 666 | 667 | try { 668 | kill(std::stoull(content), 0); 669 | 670 | if (errno != ESRCH) 671 | return false; 672 | } catch (std::exception& e) { ; } 673 | } 674 | 675 | // create lockfile 676 | std::ofstream ofs(LOCKFILE, std::ios::trunc); 677 | 678 | ofs << std::to_string(getpid()); 679 | 680 | ofs.close(); 681 | 682 | return true; 683 | } 684 | 685 | void CHyprpaper::unlockSingleInstance() { 686 | const std::string XDG_RUNTIME_DIR = getenv("XDG_RUNTIME_DIR"); 687 | const auto LOCKFILE = XDG_RUNTIME_DIR + "/hyprpaper.lock"; 688 | unlink(LOCKFILE.c_str()); 689 | } 690 | -------------------------------------------------------------------------------- /src/Hyprpaper.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "config/ConfigManager.hpp" 4 | #include "defines.hpp" 5 | #include "helpers/MiscFunctions.hpp" 6 | #include "helpers/Monitor.hpp" 7 | #include "helpers/PoolBuffer.hpp" 8 | #include "ipc/Socket.hpp" 9 | #include "render/WallpaperTarget.hpp" 10 | #include 11 | 12 | #include "protocols/cursor-shape-v1.hpp" 13 | #include "protocols/fractional-scale-v1.hpp" 14 | #include "protocols/linux-dmabuf-v1.hpp" 15 | #include "protocols/viewporter.hpp" 16 | #include "protocols/wayland.hpp" 17 | #include "protocols/wlr-layer-shell-unstable-v1.hpp" 18 | 19 | struct SWallpaperRenderData { 20 | bool contain = false; 21 | bool tile = false; 22 | }; 23 | 24 | class CHyprpaper { 25 | public: 26 | // important 27 | wl_display* m_sDisplay = nullptr; 28 | SP m_pCompositor; 29 | SP m_pSHM; 30 | SP m_pLayerShell; 31 | SP m_pFractionalScale; 32 | SP m_pViewporter; 33 | SP m_pSeat; 34 | SP m_pSeatPointer; 35 | SP m_pSeatCursorShapeDevice; 36 | SP m_pCursorShape; 37 | 38 | // init the utility 39 | CHyprpaper(); 40 | void init(); 41 | void tick(bool force); 42 | 43 | std::unordered_map m_mWallpaperTargets; 44 | std::unordered_map m_mMonitorActiveWallpapers; 45 | std::unordered_map m_mMonitorWallpaperRenderData; 46 | std::unordered_map m_mMonitorActiveWallpaperTargets; 47 | std::vector> m_vBuffers; 48 | std::vector> m_vMonitors; 49 | 50 | std::string m_szExplicitConfigPath; 51 | bool m_bNoFractionalScale = false; 52 | 53 | void removeOldHyprpaperImages(); 54 | void preloadAllWallpapersFromConfig(); 55 | void recheckAllMonitors(); 56 | void ensureMonitorHasActiveWallpaper(SMonitor*); 57 | void createLSForMonitor(SMonitor*); 58 | void renderWallpaperForMonitor(SMonitor*); 59 | void createBuffer(SPoolBuffer*, int32_t, int32_t, uint32_t); 60 | void destroyBuffer(SPoolBuffer*); 61 | int createPoolFile(size_t, std::string&); 62 | bool setCloexec(const int&); 63 | void clearWallpaperFromMonitor(const std::string&); 64 | SMonitor* getMonitorFromName(const std::string&); 65 | bool isPreloaded(const std::string&); 66 | void recheckMonitor(SMonitor*); 67 | void ensurePoolBuffersPresent(); 68 | SPoolBuffer* getPoolBuffer(SMonitor*, CWallpaperTarget*); 69 | void unloadWallpaper(const std::string&); 70 | void createSeat(SP); 71 | bool lockSingleInstance(); // fails on multi-instance 72 | void unlockSingleInstance(); 73 | 74 | std::mutex m_mtTickMutex; 75 | 76 | SMonitor* m_pLastMonitor = nullptr; 77 | 78 | private: 79 | bool m_bShouldExit = false; 80 | }; 81 | 82 | inline std::unique_ptr g_pHyprpaper; 83 | -------------------------------------------------------------------------------- /src/config/ConfigManager.cpp: -------------------------------------------------------------------------------- 1 | #include "ConfigManager.hpp" 2 | #include "../Hyprpaper.hpp" 3 | #include 4 | #include 5 | 6 | static Hyprlang::CParseResult handleWallpaper(const char* C, const char* V) { 7 | const std::string COMMAND = C; 8 | const std::string VALUE = V; 9 | Hyprlang::CParseResult result; 10 | 11 | if (VALUE.find_first_of(',') == std::string::npos) { 12 | result.setError("wallpaper failed (syntax)"); 13 | return result; 14 | } 15 | 16 | auto MONITOR = VALUE.substr(0, VALUE.find_first_of(',')); 17 | auto WALLPAPER = g_pConfigManager->trimPath(VALUE.substr(VALUE.find_first_of(',') + 1)); 18 | 19 | bool contain = false; 20 | 21 | if (WALLPAPER.find("contain:") == 0) { 22 | WALLPAPER = WALLPAPER.substr(8); 23 | contain = true; 24 | } 25 | 26 | bool tile = false; 27 | 28 | if (WALLPAPER.find("tile:") == 0) { 29 | WALLPAPER = WALLPAPER.substr(5); 30 | tile = true; 31 | } 32 | 33 | if (WALLPAPER[0] == '~') { 34 | static const char* const ENVHOME = getenv("HOME"); 35 | WALLPAPER = std::string(ENVHOME) + WALLPAPER.substr(1); 36 | } 37 | 38 | std::error_code ec; 39 | 40 | if (!std::filesystem::exists(WALLPAPER, ec)) { 41 | result.setError((std::string{"wallpaper failed ("} + (ec ? ec.message() : std::string{"no such file"}) + std::string{": "} + WALLPAPER + std::string{")"}).c_str()); 42 | return result; 43 | } 44 | 45 | if (std::find(g_pConfigManager->m_dRequestedPreloads.begin(), g_pConfigManager->m_dRequestedPreloads.end(), WALLPAPER) == g_pConfigManager->m_dRequestedPreloads.end() && 46 | !g_pHyprpaper->isPreloaded(WALLPAPER)) { 47 | result.setError("wallpaper failed (not preloaded)"); 48 | return result; 49 | } 50 | 51 | g_pHyprpaper->clearWallpaperFromMonitor(MONITOR); 52 | g_pHyprpaper->m_mMonitorActiveWallpapers[MONITOR] = WALLPAPER; 53 | g_pHyprpaper->m_mMonitorWallpaperRenderData[MONITOR].contain = contain; 54 | g_pHyprpaper->m_mMonitorWallpaperRenderData[MONITOR].tile = tile; 55 | 56 | if (MONITOR.empty()) { 57 | for (auto& m : g_pHyprpaper->m_vMonitors) { 58 | if (!m->hasATarget || m->wildcard) { 59 | g_pHyprpaper->clearWallpaperFromMonitor(m->name); 60 | g_pHyprpaper->m_mMonitorActiveWallpapers[m->name] = WALLPAPER; 61 | g_pHyprpaper->m_mMonitorWallpaperRenderData[m->name].contain = contain; 62 | g_pHyprpaper->m_mMonitorWallpaperRenderData[m->name].tile = tile; 63 | } 64 | } 65 | } else { 66 | const auto PMON = g_pHyprpaper->getMonitorFromName(MONITOR); 67 | if (PMON) 68 | PMON->wildcard = false; 69 | } 70 | 71 | return result; 72 | } 73 | 74 | static Hyprlang::CParseResult handlePreload(const char* C, const char* V) { 75 | const std::string COMMAND = C; 76 | const std::string VALUE = V; 77 | auto WALLPAPER = VALUE; 78 | 79 | if (WALLPAPER[0] == '~') { 80 | static const char* const ENVHOME = getenv("HOME"); 81 | WALLPAPER = std::string(ENVHOME) + WALLPAPER.substr(1); 82 | } 83 | 84 | std::error_code ec; 85 | 86 | if (!std::filesystem::exists(WALLPAPER, ec)) { 87 | Hyprlang::CParseResult result; 88 | result.setError(((ec ? ec.message() : std::string{"no such file"}) + std::string{": "} + WALLPAPER).c_str()); 89 | return result; 90 | } 91 | 92 | g_pConfigManager->m_dRequestedPreloads.emplace_back(WALLPAPER); 93 | 94 | return Hyprlang::CParseResult{}; 95 | } 96 | 97 | static Hyprlang::CParseResult handleUnloadAll(const char* C, const char* V) { 98 | const std::string COMMAND = C; 99 | const std::string VALUE = V; 100 | std::vector toUnload; 101 | 102 | for (auto& [name, target] : g_pHyprpaper->m_mWallpaperTargets) { 103 | if (VALUE == "unused") { 104 | bool exists = false; 105 | for (auto& [mon, target2] : g_pHyprpaper->m_mMonitorActiveWallpaperTargets) { 106 | if (&target == target2) { 107 | exists = true; 108 | break; 109 | } 110 | } 111 | 112 | if (exists) 113 | continue; 114 | } 115 | 116 | toUnload.emplace_back(name); 117 | } 118 | 119 | for (auto& tu : toUnload) 120 | g_pHyprpaper->unloadWallpaper(tu); 121 | 122 | return Hyprlang::CParseResult{}; 123 | } 124 | 125 | static Hyprlang::CParseResult handleUnload(const char* C, const char* V) { 126 | const std::string COMMAND = C; 127 | const std::string VALUE = V; 128 | auto WALLPAPER = VALUE; 129 | 130 | if (VALUE == "all" || VALUE == "unused") 131 | return handleUnloadAll(C, V); 132 | 133 | if (WALLPAPER[0] == '~') { 134 | static const char* const ENVHOME = getenv("HOME"); 135 | WALLPAPER = std::string(ENVHOME) + WALLPAPER.substr(1); 136 | } 137 | 138 | g_pHyprpaper->unloadWallpaper(WALLPAPER); 139 | 140 | return Hyprlang::CParseResult{}; 141 | } 142 | 143 | static Hyprlang::CParseResult handleReload(const char* C, const char* V) { 144 | const std::string COMMAND = C; 145 | const std::string VALUE = V; 146 | 147 | auto WALLPAPER = g_pConfigManager->trimPath(VALUE.substr(VALUE.find_first_of(',') + 1)); 148 | 149 | if (WALLPAPER.find("contain:") == 0) { 150 | WALLPAPER = WALLPAPER.substr(8); 151 | } 152 | 153 | if (WALLPAPER.find("tile:") == 0) 154 | WALLPAPER = WALLPAPER.substr(5); 155 | 156 | auto preloadResult = handlePreload(C, WALLPAPER.c_str()); 157 | if (preloadResult.error) 158 | return preloadResult; 159 | 160 | auto MONITOR = VALUE.substr(0, VALUE.find_first_of(',')); 161 | 162 | if (MONITOR.empty()) { 163 | for (auto& m : g_pHyprpaper->m_vMonitors) { 164 | auto OLD_WALLPAPER = g_pHyprpaper->m_mMonitorActiveWallpapers[m->name]; 165 | g_pHyprpaper->unloadWallpaper(OLD_WALLPAPER); 166 | } 167 | } else { 168 | auto OLD_WALLPAPER = g_pHyprpaper->m_mMonitorActiveWallpapers[MONITOR]; 169 | g_pHyprpaper->unloadWallpaper(OLD_WALLPAPER); 170 | } 171 | 172 | auto wallpaperResult = handleWallpaper(C, V); 173 | if (wallpaperResult.error) 174 | return wallpaperResult; 175 | 176 | return Hyprlang::CParseResult{}; 177 | } 178 | 179 | CConfigManager::CConfigManager() { 180 | // Initialize the configuration 181 | // Read file from default location 182 | // or from an explicit location given by user 183 | 184 | std::string configPath = getMainConfigPath(); 185 | 186 | config = std::make_unique(configPath.c_str(), Hyprlang::SConfigOptions{.allowMissingConfig = true}); 187 | 188 | config->addConfigValue("ipc", Hyprlang::INT{1L}); 189 | config->addConfigValue("splash", Hyprlang::INT{0L}); 190 | config->addConfigValue("splash_offset", Hyprlang::FLOAT{2.F}); 191 | config->addConfigValue("splash_color", Hyprlang::INT{0x55ffffff}); 192 | 193 | config->registerHandler(&handleWallpaper, "wallpaper", {.allowFlags = false}); 194 | config->registerHandler(&handleUnload, "unload", {.allowFlags = false}); 195 | config->registerHandler(&handlePreload, "preload", {.allowFlags = false}); 196 | config->registerHandler(&handleUnloadAll, "unloadAll", {.allowFlags = false}); 197 | config->registerHandler(&handleReload, "reload", {.allowFlags = false}); 198 | 199 | config->commence(); 200 | } 201 | 202 | void CConfigManager::parse() { 203 | const auto ERROR = config->parse(); 204 | 205 | if (ERROR.error) 206 | std::cout << "Error in config: \n" << ERROR.getError() << "\n"; 207 | } 208 | 209 | std::string CConfigManager::getMainConfigPath() { 210 | if (!g_pHyprpaper->m_szExplicitConfigPath.empty()) 211 | return g_pHyprpaper->m_szExplicitConfigPath; 212 | 213 | static const auto paths = Hyprutils::Path::findConfig("hyprpaper"); 214 | if (paths.first.has_value()) 215 | return paths.first.value(); 216 | else 217 | throw std::runtime_error("Could not find config in HOME, XDG_CONFIG_HOME, XDG_CONFIG_DIRS or /etc/hypr."); 218 | } 219 | 220 | // trim from both ends 221 | std::string CConfigManager::trimPath(std::string path) { 222 | if (path.empty()) 223 | return ""; 224 | 225 | // trims whitespaces, tabs and new line feeds 226 | size_t pathStartIndex = path.find_first_not_of(" \t\r\n"); 227 | size_t pathEndIndex = path.find_last_not_of(" \t\r\n"); 228 | return path.substr(pathStartIndex, pathEndIndex - pathStartIndex + 1); 229 | } 230 | -------------------------------------------------------------------------------- /src/config/ConfigManager.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include "../defines.hpp" 3 | #include 4 | 5 | class CIPCSocket; 6 | 7 | class CConfigManager { 8 | public: 9 | // gets all the data from the config 10 | CConfigManager(); 11 | void parse(); 12 | 13 | std::deque m_dRequestedPreloads; 14 | std::string getMainConfigPath(); 15 | std::string trimPath(std::string path); 16 | 17 | std::unique_ptr config; 18 | 19 | private: 20 | friend class CIPCSocket; 21 | }; 22 | 23 | inline std::unique_ptr g_pConfigManager; 24 | -------------------------------------------------------------------------------- /src/debug/Log.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include 3 | #include 4 | #include 5 | 6 | enum eLogLevel { 7 | TRACE = 0, 8 | INFO, 9 | LOG, 10 | WARN, 11 | ERR, 12 | CRIT, 13 | NONE 14 | }; 15 | 16 | #define RASSERT(expr, reason, ...) \ 17 | if (!(expr)) { \ 18 | Debug::log(CRIT, "\n==========================================================================================\nASSERTION FAILED! \n\n{}\n\nat: line {} in {}", \ 19 | std::format(reason, ##__VA_ARGS__), __LINE__, \ 20 | ([]() constexpr -> std::string { return std::string(__FILE__).substr(std::string(__FILE__).find_last_of('/') + 1); })().c_str()); \ 21 | std::abort(); \ 22 | } 23 | 24 | #define ASSERT(expr) RASSERT(expr, "?") 25 | 26 | namespace Debug { 27 | inline bool quiet = false; 28 | inline bool verbose = false; 29 | 30 | template 31 | void log(eLogLevel level, const std::string& fmt, Args&&... args) { 32 | 33 | if (!verbose && level == TRACE) 34 | return; 35 | 36 | if (quiet) 37 | return; 38 | 39 | if (level != NONE) { 40 | std::cout << '['; 41 | 42 | switch (level) { 43 | case TRACE: std::cout << "TRACE"; break; 44 | case INFO: std::cout << "INFO"; break; 45 | case LOG: std::cout << "LOG"; break; 46 | case WARN: std::cout << "WARN"; break; 47 | case ERR: std::cout << "ERR"; break; 48 | case CRIT: std::cout << "CRITICAL"; break; 49 | default: break; 50 | } 51 | 52 | std::cout << "] "; 53 | } 54 | 55 | std::cout << std::vformat(fmt, std::make_format_args(args...)) << std::endl; 56 | } 57 | }; -------------------------------------------------------------------------------- /src/defines.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "includes.hpp" 4 | #include "debug/Log.hpp" 5 | 6 | // git stuff 7 | #ifndef GIT_COMMIT_HASH 8 | #define GIT_COMMIT_HASH "?" 9 | #endif 10 | #ifndef GIT_BRANCH 11 | #define GIT_BRANCH "?" 12 | #endif 13 | #ifndef GIT_COMMIT_MESSAGE 14 | #define GIT_COMMIT_MESSAGE "?" 15 | #endif 16 | #ifndef GIT_DIRTY 17 | #define GIT_DIRTY "?" 18 | #endif 19 | 20 | #include 21 | using namespace Hyprutils::Math; 22 | 23 | #include 24 | using namespace Hyprutils::Memory; 25 | #define SP Hyprutils::Memory::CSharedPointer 26 | #define WP Hyprutils::Memory::CWeakPointer 27 | -------------------------------------------------------------------------------- /src/helpers/MiscFunctions.cpp: -------------------------------------------------------------------------------- 1 | #include "MiscFunctions.hpp" 2 | #include 3 | #include "../debug/Log.hpp" 4 | #include 5 | 6 | #include 7 | using namespace Hyprutils::OS; 8 | 9 | bool vectorDeltaLessThan(const Vector2D& a, const Vector2D& b, const float& delta) { 10 | return std::abs(a.x - b.x) < delta && std::abs(a.y - b.y) < delta; 11 | } 12 | 13 | bool vectorDeltaLessThan(const Vector2D& a, const Vector2D& b, const Vector2D& delta) { 14 | return std::abs(a.x - b.x) < delta.x && std::abs(a.y - b.y) < delta.y; 15 | } 16 | 17 | std::string execAndGet(const char* cmd) { 18 | CProcess proc("/bin/bash", {"-c", cmd}); 19 | if (!proc.runSync()) 20 | return ""; 21 | return proc.stdOut(); 22 | } 23 | -------------------------------------------------------------------------------- /src/helpers/MiscFunctions.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include 3 | #include "../defines.hpp" 4 | 5 | bool vectorDeltaLessThan(const Vector2D& a, const Vector2D& b, const float& delta); 6 | bool vectorDeltaLessThan(const Vector2D& a, const Vector2D& b, const Vector2D& delta); 7 | std::string execAndGet(const char*); -------------------------------------------------------------------------------- /src/helpers/Monitor.cpp: -------------------------------------------------------------------------------- 1 | #include "Monitor.hpp" 2 | #include "../Hyprpaper.hpp" 3 | #include "MiscFunctions.hpp" 4 | 5 | void SMonitor::registerListeners() { 6 | output->setMode([this](CCWlOutput* r, uint32_t flags, int32_t width, int32_t height, int32_t refresh) { 7 | size = Vector2D(width, height); 8 | 9 | //ensures any transforms are also taken care of when setting the mode 10 | if (transform & 1) 11 | std::swap(size.x, size.y); 12 | }); 13 | 14 | output->setDone([this](CCWlOutput* r) { 15 | readyForLS = true; 16 | std::lock_guard lg(g_pHyprpaper->m_mtTickMutex); 17 | if (g_pConfigManager) // don't tick if this is the first roundtrip 18 | g_pHyprpaper->tick(true); 19 | }); 20 | 21 | output->setScale([this](CCWlOutput* r, int32_t scale_) { scale = scale_; }); 22 | 23 | output->setName([this](CCWlOutput* r, const char* name_) { name = name_; }); 24 | 25 | output->setDescription([this](CCWlOutput* r, const char* desc_) { 26 | std::string desc = desc_; 27 | std::erase(desc, ','); 28 | 29 | description = desc; 30 | }); 31 | 32 | output->setGeometry([this](CCWlOutput* r, int32_t x, int32_t y, int32_t width_mm, int32_t height_mm, int32_t subpixel, const char* make, const char* model, 33 | int32_t transform_) { // 34 | /* 35 | see https://wayland.freedesktop.org/docs/html/apa.html#protocol-spec-wl_output-enum-transform 36 | If there is a difference in parity of the old vs new transforms, the size needs to be swapped. 37 | */ 38 | if ((transform ^ transform_) & 1) 39 | std::swap(size.x, size.y); 40 | 41 | transform = (wl_output_transform)transform_; 42 | }); 43 | } 44 | -------------------------------------------------------------------------------- /src/helpers/Monitor.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "../defines.hpp" 4 | #include "../render/LayerSurface.hpp" 5 | #include "PoolBuffer.hpp" 6 | #include "protocols/wayland.hpp" 7 | 8 | struct SMonitor { 9 | std::string name = ""; 10 | std::string description = ""; 11 | SP output; 12 | uint32_t wayland_name = 0; 13 | Vector2D size; 14 | int scale; 15 | wl_output_transform transform = WL_OUTPUT_TRANSFORM_NORMAL; 16 | 17 | bool readyForLS = false; 18 | bool hasATarget = true; 19 | 20 | bool wildcard = true; 21 | 22 | uint32_t configureSerial = 0; 23 | SPoolBuffer buffer; 24 | 25 | bool wantsReload = false; 26 | bool wantsACK = false; 27 | bool initialized = false; 28 | 29 | std::vector> layerSurfaces; 30 | CLayerSurface* pCurrentLayerSurface = nullptr; 31 | 32 | void registerListeners(); 33 | }; -------------------------------------------------------------------------------- /src/helpers/PoolBuffer.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "../defines.hpp" 4 | #include "protocols/wayland.hpp" 5 | 6 | class CWallpaperTarget; 7 | 8 | struct SPoolBuffer { 9 | SP buffer = nullptr; 10 | cairo_surface_t* surface = nullptr; 11 | cairo_t* cairo = nullptr; 12 | void* data = nullptr; 13 | size_t size = 0; 14 | std::string name = ""; 15 | 16 | std::string target = ""; 17 | Vector2D pixelSize; 18 | }; -------------------------------------------------------------------------------- /src/includes.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | 10 | #include 11 | #include 12 | 13 | #include 14 | #include 15 | #include 16 | #include 17 | #include 18 | #include 19 | #include 20 | #include 21 | #include 22 | #include 23 | #include 24 | #include -------------------------------------------------------------------------------- /src/ipc/Socket.cpp: -------------------------------------------------------------------------------- 1 | #include "Socket.hpp" 2 | #include "../Hyprpaper.hpp" 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | #include 16 | 17 | void CIPCSocket::initialize() { 18 | std::thread([&]() { 19 | const auto SOCKET = socket(AF_UNIX, SOCK_STREAM, 0); 20 | 21 | if (SOCKET < 0) { 22 | Debug::log(ERR, "Couldn't start the hyprpaper Socket. (1) IPC will not work."); 23 | return; 24 | } 25 | 26 | sockaddr_un SERVERADDRESS = {.sun_family = AF_UNIX}; 27 | 28 | const auto HISenv = getenv("HYPRLAND_INSTANCE_SIGNATURE"); 29 | const auto RUNTIMEdir = getenv("XDG_RUNTIME_DIR"); 30 | const std::string USERID = std::to_string(getpwuid(getuid())->pw_uid); 31 | 32 | const auto USERDIR = RUNTIMEdir ? RUNTIMEdir + std::string{"/hypr/"} : "/run/user/" + USERID + "/hypr/"; 33 | 34 | std::string socketPath = HISenv ? USERDIR + std::string(HISenv) + "/.hyprpaper.sock" : USERDIR + ".hyprpaper.sock"; 35 | 36 | if (!HISenv) 37 | mkdir(USERDIR.c_str(), S_IRWXU); 38 | 39 | unlink(socketPath.c_str()); 40 | 41 | strcpy(SERVERADDRESS.sun_path, socketPath.c_str()); 42 | 43 | bind(SOCKET, (sockaddr*)&SERVERADDRESS, SUN_LEN(&SERVERADDRESS)); 44 | 45 | // 10 max queued. 46 | listen(SOCKET, 10); 47 | 48 | sockaddr_in clientAddress = {}; 49 | socklen_t clientSize = sizeof(clientAddress); 50 | 51 | char readBuffer[1024] = {0}; 52 | 53 | Debug::log(LOG, "hyprpaper socket started at {} (fd: {})", socketPath, SOCKET); 54 | while (1) { 55 | const auto ACCEPTEDCONNECTION = accept(SOCKET, (sockaddr*)&clientAddress, &clientSize); 56 | if (ACCEPTEDCONNECTION < 0) { 57 | Debug::log(ERR, "Couldn't listen on the hyprpaper Socket. (3) IPC will not work."); 58 | break; 59 | } else { 60 | do { 61 | Debug::log(LOG, "Accepted incoming socket connection request on fd {}", ACCEPTEDCONNECTION); 62 | std::lock_guard lg(g_pHyprpaper->m_mtTickMutex); 63 | 64 | auto messageSize = read(ACCEPTEDCONNECTION, readBuffer, 1024); 65 | readBuffer[messageSize == 1024 ? 1023 : messageSize] = '\0'; 66 | if (messageSize == 0) 67 | break; 68 | std::string request(readBuffer); 69 | 70 | m_szRequest = request; 71 | m_bRequestReady = true; 72 | 73 | g_pHyprpaper->tick(true); 74 | while (!m_bReplyReady) { // wait for Hyprpaper to finish processing the request 75 | std::this_thread::sleep_for(std::chrono::milliseconds(1)); 76 | } 77 | write(ACCEPTEDCONNECTION, m_szReply.c_str(), m_szReply.length()); 78 | m_bReplyReady = false; 79 | m_szReply = ""; 80 | 81 | } while (1); 82 | Debug::log(LOG, "Closing Accepted Connection"); 83 | close(ACCEPTEDCONNECTION); 84 | } 85 | } 86 | 87 | close(SOCKET); 88 | }).detach(); 89 | } 90 | 91 | bool CIPCSocket::mainThreadParseRequest() { 92 | 93 | if (!m_bRequestReady) 94 | return false; 95 | 96 | std::string copy = m_szRequest; 97 | 98 | if (copy == "") 99 | return false; 100 | 101 | // now we can work on the copy 102 | 103 | Debug::log(LOG, "Received a request: {}", copy); 104 | 105 | // set default reply 106 | m_szReply = "ok"; 107 | m_bReplyReady = true; 108 | m_bRequestReady = false; 109 | 110 | // config commands 111 | if (copy.find("wallpaper") == 0 || copy.find("preload") == 0 || copy.find("unload") == 0 || copy.find("reload") == 0) { 112 | 113 | const auto RESULT = g_pConfigManager->config->parseDynamic(copy.substr(0, copy.find_first_of(' ')).c_str(), copy.substr(copy.find_first_of(' ') + 1).c_str()); 114 | 115 | if (RESULT.error) { 116 | m_szReply = RESULT.getError(); 117 | return false; 118 | } 119 | 120 | return true; 121 | } 122 | 123 | if (copy.find("listloaded") == 0) { 124 | 125 | const auto numWallpapersLoaded = g_pHyprpaper->m_mWallpaperTargets.size(); 126 | Debug::log(LOG, "numWallpapersLoaded: {}", numWallpapersLoaded); 127 | 128 | if (numWallpapersLoaded == 0) { 129 | m_szReply = "no wallpapers loaded"; 130 | return false; 131 | } 132 | 133 | m_szReply = ""; 134 | long unsigned int i = 0; 135 | for (auto& [name, target] : g_pHyprpaper->m_mWallpaperTargets) { 136 | m_szReply += name; 137 | i++; 138 | if (i < numWallpapersLoaded) 139 | m_szReply += '\n'; // dont add newline on last entry 140 | } 141 | 142 | return true; 143 | } 144 | 145 | if (copy.find("listactive") == 0) { 146 | 147 | const auto numWallpapersActive = g_pHyprpaper->m_mMonitorActiveWallpapers.size(); 148 | Debug::log(LOG, "numWallpapersActive: {}", numWallpapersActive); 149 | 150 | if (numWallpapersActive == 0) { 151 | m_szReply = "no wallpapers active"; 152 | return false; 153 | } 154 | 155 | m_szReply = ""; 156 | long unsigned int i = 0; 157 | for (auto& [mon, path1] : g_pHyprpaper->m_mMonitorActiveWallpapers) { 158 | m_szReply += mon + " = " + path1; 159 | i++; 160 | if (i < numWallpapersActive) 161 | m_szReply += '\n'; // dont add newline on last entry 162 | } 163 | 164 | return true; 165 | } 166 | 167 | m_szReply = "invalid command"; 168 | return false; 169 | } 170 | -------------------------------------------------------------------------------- /src/ipc/Socket.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "../defines.hpp" 4 | #include 5 | 6 | class CIPCSocket { 7 | public: 8 | void initialize(); 9 | 10 | bool mainThreadParseRequest(); 11 | 12 | private: 13 | std::mutex m_mtRequestMutex; 14 | std::string m_szRequest = ""; 15 | std::string m_szReply = ""; 16 | 17 | bool m_bRequestReady = false; 18 | bool m_bReplyReady = false; 19 | }; 20 | 21 | inline std::unique_ptr g_pIPCSocket; -------------------------------------------------------------------------------- /src/main.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include "defines.hpp" 3 | #include "Hyprpaper.hpp" 4 | 5 | int main(int argc, char** argv, char** envp) { 6 | Debug::log(LOG, "Welcome to hyprpaper!\nbuilt from commit {} ({})", GIT_COMMIT_HASH, GIT_COMMIT_MESSAGE); 7 | 8 | // parse some args 9 | std::string configPath; 10 | bool noFractional = false; 11 | for (int i = 1; i < argc; ++i) { 12 | if ((!strcmp(argv[i], "-c") || !strcmp(argv[i], "--config")) && argc >= i + 2) { 13 | configPath = std::string(argv[++i]); 14 | Debug::log(LOG, "Using config location {}.", configPath); 15 | } else if (!strcmp(argv[i], "--no-fractional") || !strcmp(argv[i], "-n")) { 16 | noFractional = true; 17 | Debug::log(LOG, "Disabling fractional scaling support!"); 18 | } else { 19 | std::cout << "Hyprpaper usage: hyprpaper [arg [...]].\n\nArguments:\n" 20 | << "--help -h | Show this help message\n" 21 | << "--config -c | Specify config file to use\n" 22 | << "--no-fractional -n | Disable fractional scaling support\n"; 23 | return 1; 24 | } 25 | } 26 | 27 | // starts 28 | g_pHyprpaper = std::make_unique(); 29 | g_pHyprpaper->m_szExplicitConfigPath = configPath; 30 | g_pHyprpaper->m_bNoFractionalScale = noFractional; 31 | g_pHyprpaper->init(); 32 | 33 | return 0; 34 | } 35 | -------------------------------------------------------------------------------- /src/render/LayerSurface.cpp: -------------------------------------------------------------------------------- 1 | #include "LayerSurface.hpp" 2 | 3 | #include "../Hyprpaper.hpp" 4 | 5 | CLayerSurface::CLayerSurface(SMonitor* pMonitor) { 6 | m_pMonitor = pMonitor; 7 | 8 | pSurface = makeShared(g_pHyprpaper->m_pCompositor->sendCreateSurface()); 9 | 10 | if (!pSurface) { 11 | Debug::log(CRIT, "The compositor did not allow hyprpaper a surface!"); 12 | exit(1); 13 | } 14 | 15 | const auto PINPUTREGION = makeShared(g_pHyprpaper->m_pCompositor->sendCreateRegion()); 16 | 17 | if (!PINPUTREGION) { 18 | Debug::log(CRIT, "The compositor did not allow hyprpaper a region!"); 19 | exit(1); 20 | } 21 | 22 | pSurface->sendSetInputRegion(PINPUTREGION.get()); 23 | 24 | pLayerSurface = makeShared( 25 | g_pHyprpaper->m_pLayerShell->sendGetLayerSurface(pSurface->resource(), pMonitor->output->resource(), ZWLR_LAYER_SHELL_V1_LAYER_BACKGROUND, "hyprpaper")); 26 | 27 | if (!pLayerSurface) { 28 | Debug::log(CRIT, "The compositor did not allow hyprpaper a layersurface!"); 29 | exit(1); 30 | } 31 | 32 | pLayerSurface->sendSetSize(0, 0); 33 | pLayerSurface->sendSetAnchor((zwlrLayerSurfaceV1Anchor)(ZWLR_LAYER_SURFACE_V1_ANCHOR_TOP | ZWLR_LAYER_SURFACE_V1_ANCHOR_RIGHT | ZWLR_LAYER_SURFACE_V1_ANCHOR_BOTTOM | 34 | ZWLR_LAYER_SURFACE_V1_ANCHOR_LEFT)); 35 | pLayerSurface->sendSetExclusiveZone(-1); 36 | 37 | pLayerSurface->setConfigure([this](CCZwlrLayerSurfaceV1* r, uint32_t serial, uint32_t x, uint32_t y) { 38 | m_pMonitor->size = Vector2D((double)x, (double)y); 39 | m_pMonitor->wantsReload = true; 40 | m_pMonitor->configureSerial = serial; 41 | m_pMonitor->wantsACK = true; 42 | m_pMonitor->initialized = true; 43 | 44 | Debug::log(LOG, "configure for {}", m_pMonitor->name); 45 | }); 46 | 47 | pLayerSurface->setClosed([this](CCZwlrLayerSurfaceV1* r) { 48 | for (auto& m : g_pHyprpaper->m_vMonitors) { 49 | std::erase_if(m->layerSurfaces, [&](const auto& other) { return other.get() == this; }); 50 | if (m->pCurrentLayerSurface == this) { 51 | if (m->layerSurfaces.empty()) { 52 | m->pCurrentLayerSurface = nullptr; 53 | } else { 54 | m->pCurrentLayerSurface = m->layerSurfaces.begin()->get(); 55 | g_pHyprpaper->recheckMonitor(m.get()); 56 | } 57 | } 58 | } 59 | }); 60 | 61 | pSurface->sendCommit(); 62 | 63 | // fractional scale, if supported by the compositor 64 | if (g_pHyprpaper->m_pFractionalScale && g_pHyprpaper->m_pViewporter) { 65 | pFractionalScaleInfo = makeShared(g_pHyprpaper->m_pFractionalScale->sendGetFractionalScale(pSurface->resource())); 66 | pFractionalScaleInfo->setPreferredScale([this](CCWpFractionalScaleV1* r, uint32_t sc120) { 67 | const double SCALE = sc120 / 120.0; 68 | 69 | Debug::log(LOG, "handlePreferredScale: {:.2f} for {:x}", SCALE, (uintptr_t)this); 70 | 71 | if (fScale != SCALE) { 72 | fScale = SCALE; 73 | std::lock_guard lg(g_pHyprpaper->m_mtTickMutex); 74 | m_pMonitor->wantsReload = true; 75 | g_pHyprpaper->tick(true); 76 | } 77 | }); 78 | 79 | pViewport = makeShared(g_pHyprpaper->m_pViewporter->sendGetViewport(pSurface->resource())); 80 | 81 | pSurface->sendCommit(); 82 | } else 83 | Debug::log(ERR, "No fractional-scale-v1 / wp-viewporter support from the compositor! fractional scaling will not work."); 84 | 85 | wl_display_flush(g_pHyprpaper->m_sDisplay); 86 | } 87 | 88 | CLayerSurface::~CLayerSurface() { 89 | // hyprwayland-scanner will send the destructors automatically. Neat. 90 | pLayerSurface.reset(); 91 | pFractionalScaleInfo.reset(); 92 | pViewport.reset(); 93 | pSurface.reset(); 94 | 95 | wl_display_flush(g_pHyprpaper->m_sDisplay); 96 | } 97 | -------------------------------------------------------------------------------- /src/render/LayerSurface.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "../defines.hpp" 4 | #include "protocols/fractional-scale-v1.hpp" 5 | #include "protocols/viewporter.hpp" 6 | #include "protocols/wayland.hpp" 7 | #include "protocols/wlr-layer-shell-unstable-v1.hpp" 8 | 9 | struct SMonitor; 10 | 11 | class CLayerSurface { 12 | public: 13 | explicit CLayerSurface(SMonitor*); 14 | ~CLayerSurface(); 15 | 16 | SMonitor* m_pMonitor = nullptr; 17 | 18 | SP pLayerSurface = nullptr; 19 | SP pSurface = nullptr; 20 | SP pFractionalScaleInfo = nullptr; 21 | SP pViewport = nullptr; 22 | double fScale = 1.0; 23 | }; 24 | -------------------------------------------------------------------------------- /src/render/WallpaperTarget.cpp: -------------------------------------------------------------------------------- 1 | #include "WallpaperTarget.hpp" 2 | 3 | #include 4 | #include 5 | using namespace Hyprgraphics; 6 | 7 | CWallpaperTarget::~CWallpaperTarget() { 8 | ; 9 | } 10 | 11 | void CWallpaperTarget::create(const std::string& path) { 12 | m_szPath = path; 13 | 14 | const auto BEGINLOAD = std::chrono::system_clock::now(); 15 | 16 | auto loadedImage = CImage(path); 17 | if (!loadedImage.success()) { 18 | Debug::log(CRIT, "Cannot load image {}: {}", path, loadedImage.getError()); 19 | exit(1); 20 | } 21 | 22 | m_vSize = loadedImage.cairoSurface()->size(); 23 | 24 | const auto MS = std::chrono::duration_cast(std::chrono::system_clock::now() - BEGINLOAD).count() / 1000.f; 25 | 26 | Debug::log(LOG, "Preloaded target {} in {:.2f}ms -> Pixel size: [{}, {}]", path, MS, (int)m_vSize.x, (int)m_vSize.y); 27 | 28 | m_pCairoSurface = loadedImage.cairoSurface(); 29 | } 30 | -------------------------------------------------------------------------------- /src/render/WallpaperTarget.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "../defines.hpp" 4 | #include 5 | 6 | class CWallpaperTarget { 7 | public: 8 | ~CWallpaperTarget(); 9 | 10 | void create(const std::string& path); 11 | 12 | std::string m_szPath; 13 | 14 | Vector2D m_vSize; 15 | 16 | bool m_bHasAlpha = true; 17 | 18 | SP m_pCairoSurface; 19 | }; 20 | -------------------------------------------------------------------------------- /systemd/hyprpaper.service.in: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=Fast, IPC-controlled wallpaper utility for Hyprland. 3 | Documentation=https://wiki.hyprland.org/Hypr-Ecosystem/hyprpaper/ 4 | PartOf=graphical-session.target 5 | Requires=graphical-session.target 6 | After=graphical-session.target 7 | ConditionEnvironment=WAYLAND_DISPLAY 8 | 9 | [Service] 10 | Type=simple 11 | ExecStart=@CMAKE_INSTALL_PREFIX@/bin/hyprpaper 12 | Slice=session.slice 13 | Restart=on-failure 14 | 15 | [Install] 16 | WantedBy=graphical-session.target 17 | --------------------------------------------------------------------------------