From 05c4b91f953d62cf003a9d5be84d1c2042644a32 Mon Sep 17 00:00:00 2001 From: vaxerski <43317083+vaxerski@users.noreply.github.com> Date: Fri, 1 Jul 2022 23:05:58 +0200 Subject: [PATCH] initial code commit --- CMakeLists.txt | 76 ++++++ Makefile | 45 ++++ protocols/wlr-layer-shell-unstable-v1.xml | 285 ++++++++++++++++++++++ src/Hyprpaper.cpp | 233 ++++++++++++++++++ src/Hyprpaper.hpp | 38 +++ src/config/ConfigManager.cpp | 141 +++++++++++ src/config/ConfigManager.hpp | 22 ++ src/debug/Log.cpp | 60 +++++ src/debug/Log.hpp | 17 ++ src/defines.hpp | 19 ++ src/events/Events.cpp | 65 +++++ src/events/Events.hpp | 29 +++ src/helpers/Monitor.hpp | 20 ++ src/helpers/PoolBuffer.hpp | 11 + src/helpers/Vector2D.cpp | 23 ++ src/helpers/Vector2D.hpp | 39 +++ src/includes.hpp | 44 ++++ src/main.cpp | 13 + src/render/WallpaperTarget.cpp | 25 ++ src/render/WallpaperTarget.hpp | 16 ++ 20 files changed, 1221 insertions(+) create mode 100644 CMakeLists.txt create mode 100644 Makefile create mode 100644 protocols/wlr-layer-shell-unstable-v1.xml create mode 100644 src/Hyprpaper.cpp create mode 100644 src/Hyprpaper.hpp create mode 100644 src/config/ConfigManager.cpp create mode 100644 src/config/ConfigManager.hpp create mode 100644 src/debug/Log.cpp create mode 100644 src/debug/Log.hpp create mode 100644 src/defines.hpp create mode 100644 src/events/Events.cpp create mode 100644 src/events/Events.hpp create mode 100644 src/helpers/Monitor.hpp create mode 100644 src/helpers/PoolBuffer.hpp create mode 100644 src/helpers/Vector2D.cpp create mode 100644 src/helpers/Vector2D.hpp create mode 100644 src/includes.hpp create mode 100644 src/main.cpp create mode 100644 src/render/WallpaperTarget.cpp create mode 100644 src/render/WallpaperTarget.hpp diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..d482238 --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,76 @@ +cmake_minimum_required(VERSION 3.4) +project(hyprpaper + DESCRIPTION "A blazing fast wayland wallpaper utility" +) + +set(CMAKE_MESSAGE_LOG_LEVEL "STATUS") + +message(STATUS "Configuring hyprpaper!") + +# Get git info +# hash and branch +execute_process( + COMMAND git rev-parse --abbrev-ref HEAD + WORKING_DIRECTORY ${CMAKE_SOURCE_DIR} + OUTPUT_VARIABLE GIT_BRANCH + OUTPUT_STRIP_TRAILING_WHITESPACE) + +execute_process( + COMMAND git rev-parse HEAD + WORKING_DIRECTORY ${CMAKE_SOURCE_DIR} + OUTPUT_VARIABLE GIT_COMMIT_HASH + OUTPUT_STRIP_TRAILING_WHITESPACE) + +execute_process( + COMMAND bash -c "git show ${GIT_COMMIT_HASH} | head -n 5 | tail -n 1" + WORKING_DIRECTORY ${CMAKE_SOURCE_DIR} + OUTPUT_VARIABLE GIT_COMMIT_MESSAGE + OUTPUT_STRIP_TRAILING_WHITESPACE) + +execute_process( + COMMAND bash -c "git diff-index --quiet HEAD -- || echo \"dirty\"" + WORKING_DIRECTORY ${CMAKE_SOURCE_DIR} + OUTPUT_VARIABLE GIT_DIRTY + OUTPUT_STRIP_TRAILING_WHITESPACE) +# +# + +include_directories(.) +add_compile_options(-std=c++20 -DWLR_USE_UNSTABLE ) +add_compile_options(-Wall -Wextra -Wno-unused-parameter -Wno-unused-value -Wno-missing-field-initializers -Wno-narrowing) +find_package(Threads REQUIRED) + +find_package(PkgConfig REQUIRED) +pkg_check_modules(deps REQUIRED IMPORTED_TARGET wayland-server wayland-client wayland-cursor wayland-protocols cairo pango pangocairo wlroots libjpeg) + +file(GLOB_RECURSE SRCFILES "src/*.cpp") + +add_executable(hyprpaper ${SRCFILES}) + +target_compile_definitions(hyprpaper PRIVATE "-DGIT_COMMIT_HASH=\"${GIT_COMMIT_HASH}\"") +target_compile_definitions(hyprpaper PRIVATE "-DGIT_BRANCH=\"${GIT_BRANCH}\"") +target_compile_definitions(hyprpaper PRIVATE "-DGIT_COMMIT_MESSAGE=\"${GIT_COMMIT_MESSAGE}\"") +target_compile_definitions(hyprpaper PRIVATE "-DGIT_DIRTY=\"${GIT_DIRTY}\"") + +target_link_libraries(hyprpaper rt) + +set(CPACK_PROJECT_NAME ${PROJECT_NAME}) +set(CPACK_PROJECT_VERSION ${PROJECT_VERSION}) +include(CPack) + +target_link_libraries(hyprpaper PkgConfig::deps) + +target_link_libraries(hyprpaper + OpenGL + GLESv2 + pthread + ${CMAKE_THREAD_LIBS_INIT} + ${CMAKE_SOURCE_DIR}/wlr-layer-shell-unstable-v1-protocol.o + ${CMAKE_SOURCE_DIR}/xdg-shell-protocol.o +) + +IF(CMAKE_BUILD_TYPE MATCHES Debug OR CMAKE_BUILD_TYPE MATCHES DEBUG) + SET(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -pg -no-pie -fno-builtin") + SET(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -pg -no-pie -fno-builtin") + SET(CMAKE_SHARED_LINKER_FLAGS "${CMAKE_SHARED_LINKER_FLAGS} -pg -no-pie -fno-builtin") +ENDIF(CMAKE_BUILD_TYPE MATCHES Debug OR CMAKE_BUILD_TYPE MATCHES DEBUG) \ No newline at end of file diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..a20ab6f --- /dev/null +++ b/Makefile @@ -0,0 +1,45 @@ +PREFIX = /usr/local +CFLAGS ?= -g -Wall -Wextra -Werror -Wno-unused-parameter -Wno-sign-compare -Wno-unused-function -Wno-unused-variable -Wno-unused-result -Wdeclaration-after-statement + +CFLAGS += -I. -DWLR_USE_UNSTABLE -std=c99 + +WAYLAND_PROTOCOLS=$(shell pkg-config --variable=pkgdatadir wayland-protocols) +WAYLAND_SCANNER=$(shell pkg-config --variable=wayland_scanner wayland-scanner) + +PKGS = wlroots wayland-server +CFLAGS += $(foreach p,$(PKGS),$(shell pkg-config --cflags $(p))) +LDLIBS += $(foreach p,$(PKGS),$(shell pkg-config --libs $(p))) + +wlr-layer-shell-unstable-v1-protocol.h: + $(WAYLAND_SCANNER) client-header \ + protocols/wlr-layer-shell-unstable-v1.xml $@ + +wlr-layer-shell-unstable-v1-protocol.c: + $(WAYLAND_SCANNER) private-code \ + protocols/wlr-layer-shell-unstable-v1.xml $@ + +wlr-layer-shell-unstable-v1-protocol.o: wlr-layer-shell-unstable-v1-protocol.h + +xdg-shell-protocol.h: + $(WAYLAND_SCANNER) client-header \ + $(WAYLAND_PROTOCOLS)/stable/xdg-shell/xdg-shell.xml $@ + +xdg-shell-protocol.c: + $(WAYLAND_SCANNER) private-code \ + $(WAYLAND_PROTOCOLS)/stable/xdg-shell/xdg-shell.xml $@ + +xdg-shell-protocol.o: xdg-shell-protocol.h + +protocols: wlr-layer-shell-unstable-v1-protocol.o xdg-shell-protocol.o + +clear: + rm -rf build + rm -f *.o *-protocol.h *-protocol.c + +release: + mkdir -p build && cmake --no-warn-unused-cli -DCMAKE_BUILD_TYPE:STRING=Release -H./ -B./build -G Ninja + cmake --build ./build --config Release --target all -j 10 + +debug: + mkdir -p build && cmake --no-warn-unused-cli -DCMAKE_BUILD_TYPE:STRING=Debug -H./ -B./build -G Ninja + cmake --build ./build --config Debug --target all -j 10 \ No newline at end of file diff --git a/protocols/wlr-layer-shell-unstable-v1.xml b/protocols/wlr-layer-shell-unstable-v1.xml new file mode 100644 index 0000000..f29eb87 --- /dev/null +++ b/protocols/wlr-layer-shell-unstable-v1.xml @@ -0,0 +1,285 @@ + + + + Copyright © 2017 Drew DeVault + + Permission to use, copy, modify, distribute, and sell this + software and its documentation for any purpose is hereby granted + without fee, provided that the above copyright notice appear in + all copies and that both that copyright notice and this permission + notice appear in supporting documentation, and that the name of + the copyright holders not be used in advertising or publicity + pertaining to distribution of the software without specific, + written prior permission. The copyright holders make no + representations about the suitability of this software for any + purpose. It is provided "as is" without express or implied + warranty. + + THE COPYRIGHT HOLDERS DISCLAIM ALL WARRANTIES WITH REGARD TO THIS + SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND + FITNESS, IN NO EVENT SHALL THE COPYRIGHT HOLDERS BE LIABLE FOR ANY + SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN + AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, + ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF + THIS SOFTWARE. + + + + + Clients can use this interface to assign the surface_layer role to + wl_surfaces. Such surfaces are assigned to a "layer" of the output and + rendered with a defined z-depth respective to each other. They may also be + anchored to the edges and corners of a screen and specify input handling + semantics. This interface should be suitable for the implementation of + many desktop shell components, and a broad number of other applications + that interact with the desktop. + + + + + Create a layer surface for an existing surface. This assigns the role of + layer_surface, or raises a protocol error if another role is already + assigned. + + Creating a layer surface from a wl_surface which has a buffer attached + or committed is a client error, and any attempts by a client to attach + or manipulate a buffer prior to the first layer_surface.configure call + must also be treated as errors. + + You may pass NULL for output to allow the compositor to decide which + output to use. Generally this will be the one that the user most + recently interacted with. + + Clients can specify a namespace that defines the purpose of the layer + surface. + + + + + + + + + + + + + + + + + These values indicate which layers a surface can be rendered in. They + are ordered by z depth, bottom-most first. Traditional shell surfaces + will typically be rendered between the bottom and top layers. + Fullscreen shell surfaces are typically rendered at the top layer. + Multiple surfaces can share a single layer, and ordering within a + single layer is undefined. + + + + + + + + + + + + An interface that may be implemented by a wl_surface, for surfaces that + are designed to be rendered as a layer of a stacked desktop-like + environment. + + Layer surface state (size, anchor, exclusive zone, margin, interactivity) + is double-buffered, and will be applied at the time wl_surface.commit of + the corresponding wl_surface is called. + + + + + Sets the size of the surface in surface-local coordinates. The + compositor will display the surface centered with respect to its + anchors. + + If you pass 0 for either value, the compositor will assign it and + inform you of the assignment in the configure event. You must set your + anchor to opposite edges in the dimensions you omit; not doing so is a + protocol error. Both values are 0 by default. + + Size is double-buffered, see wl_surface.commit. + + + + + + + + Requests that the compositor anchor the surface to the specified edges + and corners. If two orthoginal edges are specified (e.g. 'top' and + 'left'), then the anchor point will be the intersection of the edges + (e.g. the top left corner of the output); otherwise the anchor point + will be centered on that edge, or in the center if none is specified. + + Anchor is double-buffered, see wl_surface.commit. + + + + + + + Requests that the compositor avoids occluding an area of the surface + with other surfaces. The compositor's use of this information is + implementation-dependent - do not assume that this region will not + actually be occluded. + + A positive value is only meaningful if the surface is anchored to an + edge, rather than a corner. The zone is the number of surface-local + coordinates from the edge that are considered exclusive. + + Surfaces that do not wish to have an exclusive zone may instead specify + how they should interact with surfaces that do. If set to zero, the + surface indicates that it would like to be moved to avoid occluding + surfaces with a positive excluzive zone. If set to -1, the surface + indicates that it would not like to be moved to accommodate for other + surfaces, and the compositor should extend it all the way to the edges + it is anchored to. + + For example, a panel might set its exclusive zone to 10, so that + maximized shell surfaces are not shown on top of it. A notification + might set its exclusive zone to 0, so that it is moved to avoid + occluding the panel, but shell surfaces are shown underneath it. A + wallpaper or lock screen might set their exclusive zone to -1, so that + they stretch below or over the panel. + + The default value is 0. + + Exclusive zone is double-buffered, see wl_surface.commit. + + + + + + + Requests that the surface be placed some distance away from the anchor + point on the output, in surface-local coordinates. Setting this value + for edges you are not anchored to has no effect. + + The exclusive zone includes the margin. + + Margin is double-buffered, see wl_surface.commit. + + + + + + + + + + Set to 1 to request that the seat send keyboard events to this layer + surface. For layers below the shell surface layer, the seat will use + normal focus semantics. For layers above the shell surface layers, the + seat will always give exclusive keyboard focus to the top-most layer + which has keyboard interactivity set to true. + + Layer surfaces receive pointer, touch, and tablet events normally. If + you do not want to receive them, set the input region on your surface + to an empty region. + + Events is double-buffered, see wl_surface.commit. + + + + + + + This assigns an xdg_popup's parent to this layer_surface. This popup + should have been created via xdg_surface::get_popup with the parent set + to NULL, and this request must be invoked before committing the popup's + initial state. + + See the documentation of xdg_popup for more details about what an + xdg_popup is and how it is used. + + + + + + + When a configure event is received, if a client commits the + surface in response to the configure event, then the client + must make an ack_configure request sometime before the commit + request, passing along the serial of the configure event. + + If the client receives multiple configure events before it + can respond to one, it only has to ack the last configure event. + + A client is not required to commit immediately after sending + an ack_configure request - it may even ack_configure several times + before its next surface commit. + + A client may send multiple ack_configure requests before committing, but + only the last request sent before a commit indicates which configure + event the client really is responding to. + + + + + + + This request destroys the layer surface. + + + + + + The configure event asks the client to resize its surface. + + Clients should arrange their surface for the new states, and then send + an ack_configure request with the serial sent in this configure event at + some point before committing the new surface. + + The client is free to dismiss all but the last configure event it + received. + + The width and height arguments specify the size of the window in + surface-local coordinates. + + The size is a hint, in the sense that the client is free to ignore it if + it doesn't resize, pick a smaller size (to satisfy aspect ratio or + resize in steps of NxM pixels). If the client picks a smaller size and + is anchored to two opposite anchors (e.g. 'top' and 'bottom'), the + surface will be centered on this axis. + + If the width or height arguments are zero, it means the client should + decide its own window dimension. + + + + + + + + + The closed event is sent by the compositor when the surface will no + longer be shown. The output may have been destroyed or the user may + have asked for it to be removed. Further changes to the surface will be + ignored. The client should destroy the resource after receiving this + event, and create a new surface if they so choose. + + + + + + + + + + + + + + + + + diff --git a/src/Hyprpaper.cpp b/src/Hyprpaper.cpp new file mode 100644 index 0000000..cffe02b --- /dev/null +++ b/src/Hyprpaper.cpp @@ -0,0 +1,233 @@ +#include "Hyprpaper.hpp" + +CHyprpaper::CHyprpaper() { } + +void CHyprpaper::init() { + g_pConfigManager = std::make_unique(); + + m_sDisplay = (wl_display *)wl_display_connect(NULL); + + if (!m_sDisplay) { + Debug::log(CRIT, "No wayland compositor running!"); + exit(1); + return; + } + + preloadAllWallpapersFromConfig(); + + // run + wl_registry *registry = wl_display_get_registry(m_sDisplay); + wl_registry_add_listener(registry, &Events::registryListener, nullptr); + + while (wl_display_dispatch(m_sDisplay) != -1) { + recheckAllMonitors(); + + std::this_thread::sleep_for(std::chrono::milliseconds(1)); + } +} + +void CHyprpaper::preloadAllWallpapersFromConfig() { + for (auto& wp : g_pConfigManager->m_dRequestedPreloads) { + m_mWallpaperTargets[wp] = CWallpaperTarget(); + m_mWallpaperTargets[wp].create(wp); + } +} + +void CHyprpaper::recheckAllMonitors() { + for (auto& m : m_vMonitors) { + ensureMonitorHasActiveWallpaper(&m); + + if (m.wantsACK) { + m.wantsACK = false; + zwlr_layer_surface_v1_ack_configure(m.pLayerSurface, m.configureSerial); + } + + if (m.wantsReload) { + m.wantsReload = false; + renderWallpaperForMonitor(&m); + } + } +} + +void CHyprpaper::ensureMonitorHasActiveWallpaper(SMonitor* pMonitor) { + if (!pMonitor->readyForLS) + return; + + auto it = m_mMonitorActiveWallpaperTargets.find(pMonitor); + + if (it == m_mMonitorActiveWallpaperTargets.end()) { + m_mMonitorActiveWallpaperTargets[pMonitor] = nullptr; + it = m_mMonitorActiveWallpaperTargets.find(pMonitor); + } + + if (it->second) + return; // has + + // create it for thy + createLSForMonitor(pMonitor); + + // get the target + for (auto&[mon, path1] : m_mMonitorActiveWallpapers) { + if (mon == pMonitor->name) { + for (auto&[path2, target] : m_mWallpaperTargets) { + if (path1 == path2) { + it->second = ⌖ + break; + } + } + break; + } + } + + if (!it->second) { + Debug::log(CRIT, "No target for monitor %s!!", pMonitor->name.c_str()); + exit(1); + return; + } +} + +void CHyprpaper::createLSForMonitor(SMonitor* pMonitor) { + pMonitor->pSurface = wl_compositor_create_surface(m_sCompositor); + + if (!pMonitor->pSurface) { + Debug::log(CRIT, "The compositor did not allow hyprpaper a surface!"); + exit(1); + return; + } + + const auto PINPUTREGION = wl_compositor_create_region(m_sCompositor); + + if (!PINPUTREGION) { + Debug::log(CRIT, "The compositor did not allow hyprpaper a region!"); + exit(1); + return; + } + + wl_surface_set_input_region(pMonitor->pSurface, PINPUTREGION); + + pMonitor->pLayerSurface = zwlr_layer_shell_v1_get_layer_surface(g_pHyprpaper->m_sLayerShell, pMonitor->pSurface, pMonitor->output, ZWLR_LAYER_SHELL_V1_LAYER_BACKGROUND, "hyprpaper"); + + if (!pMonitor->pLayerSurface) { + Debug::log(CRIT, "The compositor did not allow hyprpaper a layersurface!"); + exit(1); + return; + } + + zwlr_layer_surface_v1_set_size(pMonitor->pLayerSurface, 0, 0); + zwlr_layer_surface_v1_set_anchor(pMonitor->pLayerSurface, ZWLR_LAYER_SURFACE_V1_ANCHOR_TOP | ZWLR_LAYER_SURFACE_V1_ANCHOR_RIGHT | ZWLR_LAYER_SURFACE_V1_ANCHOR_BOTTOM | ZWLR_LAYER_SURFACE_V1_ANCHOR_LEFT); + zwlr_layer_surface_v1_set_exclusive_zone(pMonitor->pLayerSurface, -1); + zwlr_layer_surface_v1_add_listener(pMonitor->pLayerSurface, &Events::layersurfaceListener, pMonitor); + wl_surface_commit(pMonitor->pSurface); + + wl_region_destroy(PINPUTREGION); +} + +bool CHyprpaper::setCloexec(const int& FD) { + long flags = fcntl(FD, F_GETFD); + if (flags == -1) { + return false; + } + + if (fcntl(FD, F_SETFD, flags | FD_CLOEXEC) == -1) { + return false; + } + + return true; +} + +int CHyprpaper::createPoolFile(size_t size, std::string& name) { + const auto XDGRUNTIMEDIR = getenv("XDG_RUNTIME_DIR"); + if (!XDGRUNTIMEDIR) { + Debug::log(CRIT, "XDG_RUNTIME_DIR not set!"); + exit(1); + } + + name = std::string(XDGRUNTIMEDIR) + "/.hyprpaper_XXXXXX"; + + const auto FD = mkstemp((char*)name.c_str()); + if (FD < 0) { + Debug::log(CRIT, "createPoolFile: fd < 0"); + exit(1); + } + + if (!setCloexec(FD)) { + close(FD); + Debug::log(CRIT, "createPoolFile: !setCloexec"); + exit(1); + } + + if (ftruncate(FD, size) < 0) { + close(FD); + Debug::log(CRIT, "createPoolFile: ftruncate < 0"); + exit(1); + } + + return FD; +} + +void CHyprpaper::createBuffer(SPoolBuffer* pBuffer, int32_t w, int32_t h, uint32_t format) { + const uint STRIDE = w * 4; + const size_t SIZE = STRIDE * h; + + std::string name; + const auto FD = createPoolFile(SIZE, name); + + if (FD == -1) { + Debug::log(CRIT, "Unable to create pool file!"); + exit(1); + } + + const auto DATA = mmap(NULL, SIZE, PROT_READ | PROT_WRITE, MAP_SHARED, FD, 0); + const auto POOL = wl_shm_create_pool(g_pHyprpaper->m_sSHM, FD, SIZE); + pBuffer->buffer = wl_shm_pool_create_buffer(POOL, 0, w, h, STRIDE, format); + wl_shm_pool_destroy(POOL); + + close(FD); + + pBuffer->size = SIZE; + pBuffer->data = DATA; + pBuffer->surface = cairo_image_surface_create_for_data((unsigned char*)DATA, CAIRO_FORMAT_ARGB32, w, h, STRIDE); + pBuffer->cairo = cairo_create(pBuffer->surface); +} + +void CHyprpaper::destroyBuffer(SPoolBuffer* pBuffer) { + wl_buffer_destroy(pBuffer->buffer); + cairo_destroy(pBuffer->cairo); + cairo_surface_destroy(pBuffer->surface); + munmap(pBuffer->data, pBuffer->size); +} + +void CHyprpaper::renderWallpaperForMonitor(SMonitor* pMonitor) { + SPoolBuffer buffer; + createBuffer(&buffer, pMonitor->size.x * pMonitor->scale, pMonitor->size.y * pMonitor->scale, WL_SHM_FORMAT_ARGB8888); + + const auto PCAIRO = buffer.cairo; + cairo_save(PCAIRO); + cairo_set_operator(PCAIRO, CAIRO_OPERATOR_CLEAR); + cairo_paint(PCAIRO); + cairo_restore(PCAIRO); + + // render + // get wp + const auto PWALLPAPERTARGET = m_mMonitorActiveWallpaperTargets[pMonitor]; + + if (!PWALLPAPERTARGET) { + Debug::log(CRIT, "wallpaper target null in render??"); + exit(1); + } + + Debug::log(LOG, "Scale: %i", pMonitor->scale); + cairo_scale(PCAIRO, pMonitor->scale, pMonitor->scale); + cairo_set_source_surface(PCAIRO, PWALLPAPERTARGET->m_pCairoSurface, 0, 0); + + cairo_paint(PCAIRO); + cairo_restore(PCAIRO); + + wl_surface_set_buffer_scale(pMonitor->pSurface, pMonitor->scale); + wl_surface_attach(pMonitor->pSurface, buffer.buffer, 0, 0); + wl_surface_damage_buffer(pMonitor->pSurface, 0, 0, INT32_MAX, INT32_MAX); + wl_surface_commit(pMonitor->pSurface); + + // we will not reuse the buffer, so destroy it immediately + destroyBuffer(&buffer); +} \ No newline at end of file diff --git a/src/Hyprpaper.hpp b/src/Hyprpaper.hpp new file mode 100644 index 0000000..70ffa06 --- /dev/null +++ b/src/Hyprpaper.hpp @@ -0,0 +1,38 @@ +#pragma once + +#include "defines.hpp" +#include "config/ConfigManager.hpp" +#include "render/WallpaperTarget.hpp" +#include "helpers/Monitor.hpp" +#include "events/Events.hpp" +#include "helpers/PoolBuffer.hpp" + +class CHyprpaper { +public: + // important + wl_display* m_sDisplay; + wl_compositor* m_sCompositor; + wl_shm* m_sSHM; + zwlr_layer_shell_v1* m_sLayerShell; + + // init the utility + CHyprpaper(); + void init(); + + std::unordered_map m_mWallpaperTargets; + std::unordered_map m_mMonitorActiveWallpapers; + std::unordered_map m_mMonitorActiveWallpaperTargets; + std::vector m_vMonitors; + + void preloadAllWallpapersFromConfig(); + void recheckAllMonitors(); + void ensureMonitorHasActiveWallpaper(SMonitor*); + void createLSForMonitor(SMonitor*); + void renderWallpaperForMonitor(SMonitor*); + void createBuffer(SPoolBuffer*, int32_t, int32_t, uint32_t); + void destroyBuffer(SPoolBuffer*); + int createPoolFile(size_t, std::string&); + bool setCloexec(const int&); +}; + +inline std::unique_ptr g_pHyprpaper; \ No newline at end of file diff --git a/src/config/ConfigManager.cpp b/src/config/ConfigManager.cpp new file mode 100644 index 0000000..cfbdeff --- /dev/null +++ b/src/config/ConfigManager.cpp @@ -0,0 +1,141 @@ +#include "ConfigManager.hpp" +#include "../Hyprpaper.hpp" + +CConfigManager::CConfigManager() { + // init the entire thing + + const char* const ENVHOME = getenv("HOME"); + const std::string CONFIGPATH = ENVHOME + (std::string) "/.config/hypr/hyprpaper.conf"; + + std::ifstream ifs; + ifs.open(CONFIGPATH); + + if (!ifs.good()) { + Debug::log(CRIT, "Hyprpaper was not provided a config!"); + exit(1); + return; //jic + } + + std::string line = ""; + int linenum = 1; + if (ifs.is_open()) { + while (std::getline(ifs, line)) { + // Read line by line. + try { + parseLine(line); + } catch (...) { + Debug::log(ERR, "Error reading line from config. Line:"); + Debug::log(NONE, "%s", line.c_str()); + + parseError += "Config error at line " + std::to_string(linenum) + ": Line parsing error."; + } + + if (parseError != "" && parseError.find("Config error at line") != 0) { + parseError = "Config error at line " + std::to_string(linenum) + ": " + parseError; + } + + ++linenum; + } + + ifs.close(); + } + + if (parseError != "") { + Debug::log(CRIT, "Exiting because of config parse errors!\n%s", parseError.c_str()); + exit(1); + return; + } +} + +std::string CConfigManager::removeBeginEndSpacesTabs(std::string str) { + while (str[0] == ' ' || str[0] == '\t') { + str = str.substr(1); + } + + while (str.length() != 0 && (str[str.length() - 1] == ' ' || str[str.length() - 1] == '\t')) { + str = str.substr(0, str.length() - 1); + } + + return str; +} + +void CConfigManager::parseLine(std::string& line) { + // first check if its not a comment + const auto COMMENTSTART = line.find_first_of('#'); + if (COMMENTSTART == 0) + return; + + // now, cut the comment off + if (COMMENTSTART != std::string::npos) + line = line.substr(0, COMMENTSTART); + + // remove shit at the beginning + while (line[0] == ' ' || line[0] == '\t') { + line = line.substr(1); + } + + // And parse + // check if command + const auto EQUALSPLACE = line.find_first_of('='); + + if (EQUALSPLACE == std::string::npos) + return; + + const auto COMMAND = removeBeginEndSpacesTabs(line.substr(0, EQUALSPLACE)); + const auto VALUE = removeBeginEndSpacesTabs(line.substr(EQUALSPLACE + 1)); + // + + parseKeyword(COMMAND, VALUE); +} + +void CConfigManager::parseKeyword(const std::string& COMMAND, const std::string& VALUE) { + if (COMMAND == "wallpaper") + handleWallpaper(COMMAND, VALUE); + else if (COMMAND == "preload") + handlePreload(COMMAND, VALUE); + else + parseError = "unknown keyword " + COMMAND; +} + +void CConfigManager::handleWallpaper(const std::string& COMMAND, const std::string& VALUE) { + if (VALUE.find_first_of(',') == std::string::npos) { + parseError = "wallpaper failed (syntax)"; + return; + } + + auto MONITOR = VALUE.substr(0, VALUE.find_first_of(',')); + auto WALLPAPER = VALUE.substr(VALUE.find_first_of(',') + 1); + + if (WALLPAPER[0] == '~') { + static const char* const ENVHOME = getenv("HOME"); + WALLPAPER = std::string(ENVHOME) + WALLPAPER.substr(1); + } + + if (!std::filesystem::exists(WALLPAPER)) { + parseError = "wallpaper failed (no such file)"; + return; + } + + if (std::find(m_dRequestedPreloads.begin(), m_dRequestedPreloads.end(), WALLPAPER) == m_dRequestedPreloads.end()) { + parseError = "wallpaper failed (not preloaded)"; + return; + } + + g_pHyprpaper->m_mMonitorActiveWallpapers[MONITOR] = WALLPAPER; +} + +void CConfigManager::handlePreload(const std::string& COMMAND, const std::string& VALUE) { + auto WALLPAPER = VALUE; + + if (WALLPAPER[0] == '~') { + static const char* const ENVHOME = getenv("HOME"); + WALLPAPER = std::string(ENVHOME) + WALLPAPER.substr(1); + } + + if (!std::filesystem::exists(WALLPAPER)) { + parseError = "preload failed (no such file)"; + return; + } + + m_dRequestedPreloads.emplace_back(WALLPAPER); +} diff --git a/src/config/ConfigManager.hpp b/src/config/ConfigManager.hpp new file mode 100644 index 0000000..c0d0590 --- /dev/null +++ b/src/config/ConfigManager.hpp @@ -0,0 +1,22 @@ +#pragma once +#include "../defines.hpp" + +class CConfigManager { +public: + // gets all the data from the config + CConfigManager(); + + std::deque m_dRequestedPreloads; + +private: + std::string parseError = ""; + + void parseLine(std::string&); + std::string removeBeginEndSpacesTabs(std::string in); + void parseKeyword(const std::string&, const std::string&); + + void handleWallpaper(const std::string&, const std::string&); + void handlePreload(const std::string&, const std::string&); +}; + +inline std::unique_ptr g_pConfigManager; \ No newline at end of file diff --git a/src/debug/Log.cpp b/src/debug/Log.cpp new file mode 100644 index 0000000..ff145d4 --- /dev/null +++ b/src/debug/Log.cpp @@ -0,0 +1,60 @@ +#include "Log.hpp" +#include "../includes.hpp" + +#include +#include + +void Debug::log(LogLevel level, const char* fmt, ...) { + + std::string levelstr = ""; + + switch (level) { + case LOG: + levelstr = "[LOG] "; + break; + case WARN: + levelstr = "[WARN] "; + break; + case ERR: + levelstr = "[ERR] "; + break; + case CRIT: + levelstr = "[CRITICAL] "; + break; + case INFO: + levelstr = "[INFO] "; + break; + default: + break; + } + + char buf[LOGMESSAGESIZE] = ""; + char* outputStr; + int logLen; + + va_list args; + va_start(args, fmt); + logLen = vsnprintf(buf, sizeof buf, fmt, args); + va_end(args); + + if ((long unsigned int)logLen < sizeof buf) { + outputStr = strdup(buf); + } else { + outputStr = (char*)malloc(logLen + 1); + + if (!outputStr) { + printf("CRITICAL: Cannot alloc size %d for log! (Out of memory?)", logLen + 1); + return; + } + + va_start(args, fmt); + vsnprintf(outputStr, logLen + 1U, fmt, args); + va_end(args); + } + + // hyprpaper only logs to stdout + std::cout << levelstr << outputStr << "\n"; + + // free the log + free(outputStr); +} diff --git a/src/debug/Log.hpp b/src/debug/Log.hpp new file mode 100644 index 0000000..8948086 --- /dev/null +++ b/src/debug/Log.hpp @@ -0,0 +1,17 @@ +#pragma once +#include + +#define LOGMESSAGESIZE 1024 + +enum LogLevel { + NONE = -1, + LOG = 0, + WARN, + ERR, + CRIT, + INFO +}; + +namespace Debug { + void log(LogLevel level, const char* fmt, ...); +}; \ No newline at end of file diff --git a/src/defines.hpp b/src/defines.hpp new file mode 100644 index 0000000..738148f --- /dev/null +++ b/src/defines.hpp @@ -0,0 +1,19 @@ +#pragma once + +#include "includes.hpp" +#include "debug/Log.hpp" +#include "helpers/Vector2D.hpp" + +// git stuff +#ifndef GIT_COMMIT_HASH +#define GIT_COMMIT_HASH "?" +#endif +#ifndef GIT_BRANCH +#define GIT_BRANCH "?" +#endif +#ifndef GIT_COMMIT_MESSAGE +#define GIT_COMMIT_MESSAGE "?" +#endif +#ifndef GIT_DIRTY +#define GIT_DIRTY "?" +#endif \ No newline at end of file diff --git a/src/events/Events.cpp b/src/events/Events.cpp new file mode 100644 index 0000000..5b28bdb --- /dev/null +++ b/src/events/Events.cpp @@ -0,0 +1,65 @@ +#include "Events.hpp" +#include "../Hyprpaper.hpp" + +void Events::geometry(void *data, wl_output *output, int32_t x, int32_t y, int32_t width_mm, int32_t height_mm, int32_t subpixel, const char *make, const char *model, int32_t transform) { + // ignored +} + +void Events::mode(void *data, wl_output *output, uint32_t flags, int32_t width, int32_t height, int32_t refresh) { + // ignored +} + +void Events::done(void *data, wl_output *wl_output) { + const auto PMONITOR = (SMonitor*)data; + + PMONITOR->readyForLS = true; + + g_pHyprpaper->recheckAllMonitors(); +} + +void Events::scale(void *data, wl_output *wl_output, int32_t scale) { + const auto PMONITOR = (SMonitor*)data; + + PMONITOR->scale = scale; +} + +void Events::name(void *data, wl_output *wl_output, const char *name) { + const auto PMONITOR = (SMonitor*)data; + + PMONITOR->name = std::string(name); +} + +void Events::description(void *data, wl_output *wl_output, const char *description) { + // i do not care +} + +void Events::ls_configure(void *data, zwlr_layer_surface_v1 *surface, uint32_t serial, uint32_t width, uint32_t height) { + const auto PMONITOR = (SMonitor *)data; + + PMONITOR->size = Vector2D(width, height); + PMONITOR->wantsReload = true; + PMONITOR->configureSerial = serial; + PMONITOR->wantsACK = true; + + Debug::log(LOG, "configure for %s", PMONITOR->name.c_str()); +} + +void Events::handleGlobal(void *data, struct wl_registry *registry, uint32_t name, const char *interface, uint32_t version) { + if (strcmp(interface, wl_compositor_interface.name) == 0) { + g_pHyprpaper->m_sCompositor = (wl_compositor *)wl_registry_bind(registry, name, &wl_compositor_interface, 4); + } else if (strcmp(interface, wl_shm_interface.name) == 0) { + g_pHyprpaper->m_sSHM = (wl_shm *)wl_registry_bind(registry, name, &wl_shm_interface, 1); + } else if (strcmp(interface, wl_output_interface.name) == 0) { + const auto PMONITOR = &g_pHyprpaper->m_vMonitors.emplace_back(); + PMONITOR->wayland_name = name; + PMONITOR->output = (wl_output *)wl_registry_bind(registry, name, &wl_output_interface, 4); + wl_output_add_listener(PMONITOR->output, &Events::outputListener, PMONITOR); + } else if (strcmp(interface, zwlr_layer_shell_v1_interface.name) == 0) { + g_pHyprpaper->m_sLayerShell = (zwlr_layer_shell_v1 *)wl_registry_bind(registry, name, &zwlr_layer_shell_v1_interface, 1); + } +} + +void Events::handleGlobalRemove(void *data, struct wl_registry *registry, uint32_t name) { + // todo +} + diff --git a/src/events/Events.hpp b/src/events/Events.hpp new file mode 100644 index 0000000..58f5e09 --- /dev/null +++ b/src/events/Events.hpp @@ -0,0 +1,29 @@ +#pragma once + +#include "../defines.hpp" + +namespace Events { + void geometry(void *data, wl_output *output, int32_t x, int32_t y, int32_t width_mm, int32_t height_mm, int32_t subpixel, const char *make, const char *model, int32_t transform); + + void mode(void *data, wl_output *output, uint32_t flags, int32_t width, int32_t height, int32_t refresh); + + void done(void *data, wl_output *wl_output); + + void scale(void *data, wl_output *wl_output, int32_t scale); + + void name(void *data, wl_output *wl_output, const char *name); + + void description(void *data, wl_output *wl_output, const char *description); + + void ls_configure(void *data, zwlr_layer_surface_v1 *surface, uint32_t serial, uint32_t width, uint32_t height); + + void handleGlobal(void *data, struct wl_registry *registry, uint32_t name, const char *interface, uint32_t version); + + void handleGlobalRemove(void *data, struct wl_registry *registry, uint32_t name); + + inline const wl_output_listener outputListener = {.geometry = geometry, .mode = mode, .done = done, .scale = scale, .name = name, .description = description}; + + inline const zwlr_layer_surface_v1_listener layersurfaceListener = { .configure = ls_configure }; + + inline const struct wl_registry_listener registryListener = { .global = handleGlobal, .global_remove = handleGlobalRemove }; +}; \ No newline at end of file diff --git a/src/helpers/Monitor.hpp b/src/helpers/Monitor.hpp new file mode 100644 index 0000000..2b0ad81 --- /dev/null +++ b/src/helpers/Monitor.hpp @@ -0,0 +1,20 @@ +#pragma once + +#include "../defines.hpp" + +struct SMonitor { + std::string name = ""; + wl_output* output = nullptr; + uint32_t wayland_name = 0; + Vector2D size; + int scale; + + bool readyForLS = false; + + zwlr_layer_surface_v1* pLayerSurface = nullptr; + wl_surface* pSurface = nullptr; + uint32_t configureSerial = 0; + + bool wantsReload = false; + bool wantsACK = false; +}; \ No newline at end of file diff --git a/src/helpers/PoolBuffer.hpp b/src/helpers/PoolBuffer.hpp new file mode 100644 index 0000000..4f1f870 --- /dev/null +++ b/src/helpers/PoolBuffer.hpp @@ -0,0 +1,11 @@ +#pragma once + +#include "../defines.hpp" + +struct SPoolBuffer { + wl_buffer* buffer; + cairo_surface_t *surface; + cairo_t *cairo; + void* data; + size_t size; +}; \ No newline at end of file diff --git a/src/helpers/Vector2D.cpp b/src/helpers/Vector2D.cpp new file mode 100644 index 0000000..acaace1 --- /dev/null +++ b/src/helpers/Vector2D.cpp @@ -0,0 +1,23 @@ +#include "Vector2D.hpp" + +Vector2D::Vector2D(double xx, double yy) { + x = xx; + y = yy; +} + +Vector2D::Vector2D() { x = 0; y = 0; } +Vector2D::~Vector2D() {} + +double Vector2D::normalize() { + // get max abs + const auto max = abs(x) > abs(y) ? abs(x) : abs(y); + + x /= max; + y /= max; + + return max; +} + +Vector2D Vector2D::floor() { + return Vector2D((int)x, (int)y); +} \ No newline at end of file diff --git a/src/helpers/Vector2D.hpp b/src/helpers/Vector2D.hpp new file mode 100644 index 0000000..7288a08 --- /dev/null +++ b/src/helpers/Vector2D.hpp @@ -0,0 +1,39 @@ +#pragma once + +#include + +class Vector2D { + public: + Vector2D(double, double); + Vector2D(); + ~Vector2D(); + + double x = 0; + double y = 0; + + // returns the scale + double normalize(); + + Vector2D operator+(const Vector2D a) const { + return Vector2D(this->x + a.x, this->y + a.y); + } + Vector2D operator-(const Vector2D a) const { + return Vector2D(this->x - a.x, this->y - a.y); + } + Vector2D operator*(const float a) const { + return Vector2D(this->x * a, this->y * a); + } + Vector2D operator/(const float a) const { + return Vector2D(this->x / a, this->y / a); + } + + bool operator==(const Vector2D& a) const { + return a.x == x && a.y == y; + } + + bool operator!=(const Vector2D& a) const { + return a.x != x || a.y != y; + } + + Vector2D floor(); +}; \ No newline at end of file diff --git a/src/includes.hpp b/src/includes.hpp new file mode 100644 index 0000000..8bcf789 --- /dev/null +++ b/src/includes.hpp @@ -0,0 +1,44 @@ +#pragma once + +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +#define class _class +#define namespace _namespace +#define static + +extern "C" { +#include "wlr-layer-shell-unstable-v1-protocol.h" +#include "xdg-shell-protocol.h" +#include +} + +#undef class +#undef namespace +#undef static + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include \ No newline at end of file diff --git a/src/main.cpp b/src/main.cpp new file mode 100644 index 0000000..23b3aac --- /dev/null +++ b/src/main.cpp @@ -0,0 +1,13 @@ +#include +#include "defines.hpp" +#include "Hyprpaper.hpp" + +int main(int argc, char** argv, char** envp) { + Debug::log(LOG, "Welcome to hyprpaper!\nbuilt from commit %s (%s)", GIT_COMMIT_HASH, GIT_COMMIT_MESSAGE); + + // starts + g_pHyprpaper = std::make_unique(); + g_pHyprpaper->init(); + + return 0; +} \ No newline at end of file diff --git a/src/render/WallpaperTarget.cpp b/src/render/WallpaperTarget.cpp new file mode 100644 index 0000000..5e3499d --- /dev/null +++ b/src/render/WallpaperTarget.cpp @@ -0,0 +1,25 @@ +#include "WallpaperTarget.hpp" + +void CWallpaperTarget::create(const std::string& path) { + m_szPath = path; + + cairo_surface_t* CAIROSURFACE = nullptr; + if (path.find(".png") == path.length() - 4) { + CAIROSURFACE = cairo_image_surface_create_from_png(path.c_str()); + } else if (path.find(".jpg") == path.length() - 4 || path.find(".jpeg") == path.length() - 5) { + Debug::log(ERR, ".jpg images are not yet supported! :("); + exit(1); + return; + } else { + Debug::log(CRIT, "unrecognized image %s", path.c_str()); + exit(1); + return; + } + + if (cairo_surface_status(CAIROSURFACE) != CAIRO_STATUS_SUCCESS) { + Debug::log(CRIT, "Failed to read image %s because of:\n%s", path.c_str(), cairo_status_to_string(cairo_surface_status(CAIROSURFACE))); + exit(1); + } + + m_pCairoSurface = CAIROSURFACE; +} \ No newline at end of file diff --git a/src/render/WallpaperTarget.hpp b/src/render/WallpaperTarget.hpp new file mode 100644 index 0000000..767d890 --- /dev/null +++ b/src/render/WallpaperTarget.hpp @@ -0,0 +1,16 @@ +#pragma once + +#include "../defines.hpp" + +class CWallpaperTarget { +public: + + void create(const std::string& path); + void render(); + + std::string m_szPath; + + Vector2D m_vSize; + + cairo_surface_t* m_pCairoSurface; +}; \ No newline at end of file