diff --git a/CMakeLists.txt b/CMakeLists.txt index 239b142..81a559b 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -57,7 +57,6 @@ pkg_check_modules( IMPORTED_TARGET hyprlang>=0.6.0 hyprutils>=0.2.4 - wayland-client hyprtoolkit>=0.4.1 hyprwire pixman-1 @@ -67,51 +66,11 @@ file(GLOB_RECURSE SRCFILES "src/*.cpp") add_executable(hyprpaper ${SRCFILES}) -# Wayland - -pkg_get_variable(WAYLAND_PROTOCOLS_DIR wayland-protocols pkgdatadir) -message(STATUS "Found wayland-protocols at ${WAYLAND_PROTOCOLS_DIR}") -pkg_get_variable(WAYLAND_SCANNER_PKGDATA_DIR wayland-scanner pkgdatadir) -message( - STATUS "Found wayland-scanner pkgdatadir at ${WAYLAND_SCANNER_PKGDATA_DIR}") - -function(protocolnew protoPath protoName external) - if(external) - set(path ${CMAKE_SOURCE_DIR}/${protoPath}) - else() - set(path ${WAYLAND_PROTOCOLS_DIR}/${protoPath}) - endif() - add_custom_command( - OUTPUT ${CMAKE_SOURCE_DIR}/protocols/${protoName}.cpp - ${CMAKE_SOURCE_DIR}/protocols/${protoName}.hpp - COMMAND hyprwayland-scanner --client ${path}/${protoName}.xml - ${CMAKE_SOURCE_DIR}/protocols/ - WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}) - target_sources(hyprpaper PRIVATE protocols/${protoName}.cpp - protocols/${protoName}.hpp) -endfunction() -function(protocolWayland) - add_custom_command( - OUTPUT ${CMAKE_SOURCE_DIR}/protocols/wayland.cpp - ${CMAKE_SOURCE_DIR}/protocols/wayland.hpp - COMMAND hyprwayland-scanner --wayland-enums --client - ${WAYLAND_SCANNER_PKGDATA_DIR}/wayland.xml ${CMAKE_SOURCE_DIR}/protocols/ - WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}) - target_sources(hyprpaper PRIVATE protocols/wayland.cpp protocols/wayland.hpp) -endfunction() - -protocolwayland() - -protocolnew("protocols" "wlr-layer-shell-unstable-v1" true) -protocolnew("stable/linux-dmabuf" "linux-dmabuf-v1" false) -protocolnew("staging/fractional-scale" "fractional-scale-v1" false) -protocolnew("stable/viewporter" "viewporter" false) -protocolnew("stable/xdg-shell" "xdg-shell" false) -protocolnew("staging/cursor-shape" "cursor-shape-v1" false) -protocolnew("stable/tablet" "tablet-v2" false) - # Hyprwire +pkg_get_variable(HYPRWIRE_PROTOCOLS_DIR hyprwire-protocols pkgdatadir) +message(STATUS "Found hyprwire-protocols at ${HYPRWIRE_PROTOCOLS_DIR}") + function(hyprprotocolServer protoPath protoName) set(path ${CMAKE_SOURCE_DIR}/${protoPath}) add_custom_command( @@ -125,8 +84,27 @@ function(hyprprotocolServer protoPath protoName) ) endfunction() +function(hyprprotocol protoPath protoName external) + if(external) + set(path ${protoPath}) + else() + set(path ${HYPRWIRE_PROTOCOLS_DIR}/${protoPath}) + endif() + add_custom_command( + OUTPUT ${CMAKE_CURRENT_SOURCE_DIR}/hw-protocols/${protoName}-client.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/hw-protocols/${protoName}-client.hpp + COMMAND hyprwire-scanner --client ${path}/${protoName}.xml + ${CMAKE_CURRENT_SOURCE_DIR}/hw-protocols/ + WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}) + target_sources(hyprpaper PRIVATE hw-protocols/${protoName}-client.cpp + hw-protocols/${protoName}-client.hpp + ) +endfunction() + hyprprotocolServer(hw-protocols hyprpaper_core) +hyprprotocol(hyprtavern hp_hyprtavern_core_v1 false) + # string(REPLACE "\"" " " GIT_COMMIT_MESSAGE_ESCAPED "${GIT_COMMIT_MESSAGE}") diff --git a/protocols/wlr-layer-shell-unstable-v1.xml b/protocols/wlr-layer-shell-unstable-v1.xml deleted file mode 100644 index f29eb87..0000000 --- a/protocols/wlr-layer-shell-unstable-v1.xml +++ /dev/null @@ -1,285 +0,0 @@ - - - - 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/ipc/Tavern.cpp b/src/ipc/Tavern.cpp new file mode 100644 index 0000000..8316986 --- /dev/null +++ b/src/ipc/Tavern.cpp @@ -0,0 +1,153 @@ +#include "Tavern.hpp" + +#include "../helpers/Logger.hpp" +#include "IPC.hpp" +#include "../ui/UI.hpp" + +using namespace IPC; + +constexpr const uint32_t PROTOCOL_VERSION = 1; + +static SP impl = makeShared(PROTOCOL_VERSION); + +CTavernPeer::CTavernPeer(SP sock, WP conn) : m_socket(sock), m_connection(conn) { + m_impl = makeShared(1, [this](SP obj) { + auto manager = m_managers.emplace_back(makeShared(std::move(obj))); + + // We disconnect on destroying the manager. TODO: disconnect when the peer disconnects? + manager->setDestroy([this, weak = WP{manager}] { + m_connection->dropPeer(this); + std::erase(m_managers, weak); + }); + manager->setOnDestroy([this, weak = WP{manager}] { + m_connection->dropPeer(this); + std::erase(m_managers, weak); + }); + + manager->setGetWallpaperObject([this, weak = WP{manager}](uint32_t id) { + if (!weak) + return; + + m_wallpaperObjects.emplace_back(makeShared( + makeShared(m_socket->createObject(weak->getObject()->client(), weak->getObject(), "hyprpaper_wallpaper", id)))); + }); + }); + + m_socket->addImplementation(m_impl); +} + +CTavernPeer::~CTavernPeer() { + if (!g_ui || m_fd < 0) + return; + + g_ui->backend()->removeFd(m_fd); +} + +void CTavernPeer::onNewDisplay(const std::string& sv) { + for (const auto& m : m_managers) { + m->sendAddMonitor(sv.c_str()); + } +} + +void CTavernPeer::onRemovedDisplay(const std::string& sv) { + for (const auto& m : m_managers) { + m->sendRemoveMonitor(sv.c_str()); + } +} + +int CTavernPeer::extractFD() { + if (m_fd >= 0) + return m_fd; + + m_fd = m_socket->extractLoopFD(); + return m_fd; +} + +void CTavernPeer::dispatch() { + if (!m_socket->dispatchEvents()) + m_connection->dropPeer(this); +} + +CTavernConnection::CTavernConnection() = default; + +void CTavernConnection::init() { + const auto XDG_RUNTIME_DIR = getenv("XDG_RUNTIME_DIR"); + + if (!XDG_RUNTIME_DIR) { + g_logger->log(LOG_ERR, "CTavernConnection: no runtime dir"); + return; + } + + const auto WL_DISPLAY = getenv("WAYLAND_DISPLAY"); + + m_socket = Hyprwire::IClientSocket::open(XDG_RUNTIME_DIR + std::string{"/hyprtavern/ht.sock"}); + + if (!m_socket) { + g_logger->log(LOG_ERR, "CTavernConnection: tavern is not serving beer, ignoring tavern"); + return; + } + + m_socket->addImplementation(impl); + + if (!m_socket->waitForHandshake()) { + g_logger->log(LOG_ERR, "CTavernConnection: failed a handshake to tavern"); + return; + } + + const auto SPEC = m_socket->getSpec(impl->protocol()->specName()); + + if (!SPEC) { + g_logger->log(LOG_ERR, "CTavernConnection: tavern is bad (no protocol)"); + return; + } + + m_manager = makeShared(m_socket->bindProtocol(impl->protocol(), PROTOCOL_VERSION)); + + m_busObject = makeShared(m_manager->sendGetBusObject("hyprpaper")); + + if (WL_DISPLAY) + m_busObject->sendExposeProperty("GLOBAL:WAYLAND_DISPLAY", WL_DISPLAY); + m_busObject->sendExposeProtocol("hyprpaper_core", 1); + + m_busObject->setNewFd([this](int fd) { + auto sock = Hyprwire::IServerSocket::open(fd); + + if (!sock) { + g_logger->log(LOG_ERR, "CTavernConnection: received a fd {} but it's dead", fd); + return; + } + + auto x = m_peers.emplace_back(makeShared(sock, m_self)); + + g_ui->backend()->addFd(x->extractFD(), [w = WP{x}] { + if (!w) + return; + + w->dispatch(); + }); + }); + + g_ui->backend()->addFd(m_socket->extractLoopFD(), [this] { // + m_socket->dispatchEvents(); + }); +} + +bool CTavernConnection::connected() { + return m_busObject; +} + +void CTavernConnection::dropPeer(CTavernPeer* peer) { + std::erase_if(m_peers, [peer](const auto& e) { return e.get() == peer; }); +} + +void CTavernConnection::onNewDisplay(const std::string& sv) { + for (const auto& p : m_peers) { + p->onNewDisplay(sv); + } +} + +void CTavernConnection::onRemovedDisplay(const std::string& sv) { + for (const auto& p : m_peers) { + p->onRemovedDisplay(sv); + } +} diff --git a/src/ipc/Tavern.hpp b/src/ipc/Tavern.hpp new file mode 100644 index 0000000..d68b488 --- /dev/null +++ b/src/ipc/Tavern.hpp @@ -0,0 +1,59 @@ +#pragma once + +#include "../helpers/Memory.hpp" + +#include +#include +#include + +namespace IPC { + class CWallpaperObject; + class CTavernConnection; + + class CTavernPeer { + public: + CTavernPeer(SP sock, WP conn); + ~CTavernPeer(); + + int extractFD(); + + void onNewDisplay(const std::string& sv); + void onRemovedDisplay(const std::string& sv); + + void dispatch(); + + private: + SP m_socket; + int m_fd = -1; + SP m_impl; + WP m_connection; + + std::vector> m_managers; + std::vector> m_wallpaperObjects; + }; + + class CTavernConnection { + public: + CTavernConnection(); + ~CTavernConnection() = default; + + void init(); + + WP m_self; + + bool connected(); + void dropPeer(CTavernPeer* peer); + + void onNewDisplay(const std::string& sv); + void onRemovedDisplay(const std::string& sv); + + private: + SP m_socket; + SP m_manager; + SP m_busObject; + + std::vector> m_peers; + }; + + inline UP g_tavernConnection; +}; \ No newline at end of file diff --git a/src/ui/UI.cpp b/src/ui/UI.cpp index 072a0eb..171279b 100644 --- a/src/ui/UI.cpp +++ b/src/ui/UI.cpp @@ -2,6 +2,7 @@ #include "../helpers/Logger.hpp" #include "../ipc/HyprlandSocket.hpp" #include "../ipc/IPC.hpp" +#include "../ipc/Tavern.hpp" #include "../config/WallpaperMatcher.hpp" #include @@ -70,12 +71,20 @@ CWallpaperTarget::CWallpaperTarget(SP output, const std::s void CUI::registerOutput(const SP& mon) { g_matcher->registerOutput(mon->port()); + if (IPC::g_IPCSocket) IPC::g_IPCSocket->onNewDisplay(mon->port()); + if (IPC::g_tavernConnection) + IPC::g_tavernConnection->onNewDisplay(mon->port()); + mon->m_events.removed.listenStatic([this, m = WP{mon}] { g_matcher->unregisterOutput(m->port()); + if (IPC::g_IPCSocket) IPC::g_IPCSocket->onRemovedDisplay(m->port()); + if (IPC::g_tavernConnection) + IPC::g_tavernConnection->onRemovedDisplay(m->port()); + std::erase_if(m_targets, [&m](const auto& e) { return e->m_monitorName == m->port(); }); }); } @@ -88,8 +97,12 @@ bool CUI::run() { if (!m_backend) return false; - if (*PENABLEIPC) - IPC::g_IPCSocket = makeUnique(); + if (*PENABLEIPC) { + IPC::g_IPCSocket = makeUnique(); + IPC::g_tavernConnection = makeUnique(); + IPC::g_tavernConnection->m_self = IPC::g_tavernConnection; + IPC::g_tavernConnection->init(); + } const auto MONITORS = m_backend->getOutputs();