├── .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 |
--------------------------------------------------------------------------------