diff --git a/.gitmodules b/.gitmodules index e863dde..2690634 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,6 +1,7 @@ [submodule "hyprland-protocols"] path = subprojects/hyprland-protocols - url = https://github.com/hyprwm/hyprland-protocols + url = https://github.com/3l0w/hyprland-protocols + branch = feat/input-capture-impl [submodule "subprojects/sdbus-cpp"] path = subprojects/sdbus-cpp url = https://github.com/Kistler-Group/sdbus-cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 871ca3c..532fc02 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -62,6 +62,8 @@ pkg_check_modules( libpipewire-0.3>=1.1.82 libspa-0.2 libdrm + libeis-1.0 + uuid gbm hyprlang>=0.2.0 hyprutils>=0.2.6 @@ -130,6 +132,8 @@ protocolnew("${HYPRLAND_PROTOCOLS}/protocols" "hyprland-global-shortcuts-v1" true) protocolnew("${HYPRLAND_PROTOCOLS}/protocols" "hyprland-toplevel-export-v1" true) +protocolnew("${HYPRLAND_PROTOCOLS}/protocols" "hyprland-input-capture-v1" + true) protocolnew("stable/linux-dmabuf" "linux-dmabuf-v1" false) # Installation diff --git a/hyprland-share-picker/meson.build b/hyprland-share-picker/meson.build index ef29621..7a42657 100644 --- a/hyprland-share-picker/meson.build +++ b/hyprland-share-picker/meson.build @@ -18,6 +18,6 @@ executable('hyprland-share-picker', sources, ui_files, moc, - dependencies: qtdep, + dependencies: [qtdep, dependency('hyprutils')], install: true ) diff --git a/hyprland.portal b/hyprland.portal index d3e2cbe..cbaafa8 100644 --- a/hyprland.portal +++ b/hyprland.portal @@ -1,4 +1,4 @@ [portal] DBusName=org.freedesktop.impl.portal.desktop.hyprland -Interfaces=org.freedesktop.impl.portal.Screenshot;org.freedesktop.impl.portal.ScreenCast;org.freedesktop.impl.portal.GlobalShortcuts; +Interfaces=org.freedesktop.impl.portal.Screenshot;org.freedesktop.impl.portal.ScreenCast;org.freedesktop.impl.portal.GlobalShortcuts;org.freedesktop.impl.portal.InputCapture; UseIn=wlroots;Hyprland;sway;Wayfire;river; diff --git a/meson.build b/meson.build index bcae2b2..c2a2675 100644 --- a/meson.build +++ b/meson.build @@ -61,6 +61,9 @@ install_data( 'hyprland.portal', install_dir: join_paths(get_option('datadir'), 'xdg-desktop-portal', 'portals'), ) +version = run_command('cat', files('VERSION'), check: true).stdout().strip() + +add_project_arguments(f'-DXDPH_VERSION="@version@"', language : 'cpp') inc = include_directories('.', 'protocols') diff --git a/protocols/meson.build b/protocols/meson.build index f10d4c8..d7805d1 100644 --- a/protocols/meson.build +++ b/protocols/meson.build @@ -22,6 +22,7 @@ client_protocols = [ 'wlr-foreign-toplevel-management-unstable-v1.xml', hl_protocol_dir / 'protocols/hyprland-toplevel-export-v1.xml', hl_protocol_dir / 'protocols/hyprland-global-shortcuts-v1.xml', + hl_protocol_dir / 'protocols/hyprland-input-capture-v1.xml', wl_protocol_dir / 'stable/linux-dmabuf/linux-dmabuf-v1.xml', ] diff --git a/src/core/PortalManager.cpp b/src/core/PortalManager.cpp index 9111870..b02ffce 100644 --- a/src/core/PortalManager.cpp +++ b/src/core/PortalManager.cpp @@ -3,7 +3,6 @@ #include "../helpers/MiscFunctions.hpp" #include -#include #include #include #include @@ -19,12 +18,21 @@ SOutput::SOutput(SP output_) : output(output_) { Debug::log(LOG, "Found output name {}", name); }); - output->setMode([this](CCWlOutput* r, uint32_t flags, int32_t width, int32_t height, int32_t refresh) { // + output->setMode([this](CCWlOutput* r, uint32_t flags, int32_t width_, int32_t height_, int32_t refresh) { refreshRate = refresh; + width = width_; + height = height_; }); - output->setGeometry([this](CCWlOutput* r, int32_t x, int32_t y, int32_t physical_width, int32_t physical_height, int32_t subpixel, const char* make, const char* model, - int32_t transform_) { // - transform = (wl_output_transform)transform_; + output->setGeometry( + [this](CCWlOutput* r, int32_t x_, int32_t y_, int32_t physical_width, int32_t physical_height, int32_t subpixel, const char* make, const char* model, int32_t transform_) { + transform = (wl_output_transform)transform_; + x = x_; + y = y_; + }); + output->setScale([this](CCWlOutput* r, uint32_t factor_) { scale = factor_; }); + output->setDone([](CCWlOutput* r) { + if (g_pPortalManager->m_sPortals.inputCapture != nullptr) + g_pPortalManager->m_sPortals.inputCapture->zonesChanged(); }); } @@ -63,7 +71,9 @@ void CPortalManager::onGlobal(uint32_t name, const char* interface, uint32_t ver m_sPortals.globalShortcuts = std::make_unique(makeShared( (wl_proxy*)wl_registry_bind((wl_registry*)m_sWaylandConnection.registry->resource(), name, &hyprland_global_shortcuts_manager_v1_interface, version))); } - + if (INTERFACE == hyprland_input_capture_manager_v1_interface.name) + m_sPortals.inputCapture = std::make_unique(makeShared( + (wl_proxy*)wl_registry_bind((wl_registry*)m_sWaylandConnection.registry->resource(), name, &hyprland_input_capture_manager_v1_interface, version))); else if (INTERFACE == hyprland_toplevel_export_manager_v1_interface.name) { m_sWaylandConnection.hyprlandToplevelMgr = makeShared( (wl_proxy*)wl_registry_bind((wl_registry*)m_sWaylandConnection.registry->resource(), name, &hyprland_toplevel_export_manager_v1_interface, version)); @@ -269,32 +279,21 @@ void CPortalManager::init() { } void CPortalManager::startEventLoop() { + addFdToEventLoop(m_pConnection->getEventLoopPollData().fd, POLLIN, nullptr); + addFdToEventLoop(wl_display_get_fd(m_sWaylandConnection.display), POLLIN, nullptr); + addFdToEventLoop(pw_loop_get_fd(m_sPipewire.loop), POLLIN, nullptr); - pollfd pollfds[] = { - { - .fd = m_pConnection->getEventLoopPollData().fd, - .events = POLLIN, - }, - { - .fd = wl_display_get_fd(m_sWaylandConnection.display), - .events = POLLIN, - }, - { - .fd = pw_loop_get_fd(m_sPipewire.loop), - .events = POLLIN, - }, - }; - - std::thread pollThr([this, &pollfds]() { + std::thread pollThr([this]() { while (1) { - int ret = poll(pollfds, 3, 5000 /* 5 seconds, reasonable. It's because we might need to terminate */); + + int ret = poll(m_sEventLoopInternals.pollFds.data(), m_sEventLoopInternals.pollFds.size(), 5000 /* 5 seconds, reasonable. It's because we might need to terminate */); if (ret < 0) { Debug::log(CRIT, "[core] Polling fds failed with {}", strerror(errno)); g_pPortalManager->terminate(); } for (size_t i = 0; i < 3; ++i) { - if (pollfds[i].revents & POLLHUP) { + if (m_sEventLoopInternals.pollFds.data()->revents & POLLHUP) { Debug::log(CRIT, "[core] Disconnected from pollfd id {}", i); g_pPortalManager->terminate(); } @@ -367,13 +366,13 @@ void CPortalManager::startEventLoop() { m_mEventLock.lock(); - if (pollfds[0].revents & POLLIN /* dbus */) { + if (m_sEventLoopInternals.pollFds[0].revents & POLLIN /* dbus */) { while (m_pConnection->processPendingEvent()) { ; } } - if (pollfds[1].revents & POLLIN /* wl */) { + if (m_sEventLoopInternals.pollFds[1].revents & POLLIN /* wl */) { wl_display_flush(m_sWaylandConnection.display); if (wl_display_prepare_read(m_sWaylandConnection.display) == 0) { wl_display_read_events(m_sWaylandConnection.display); @@ -383,12 +382,18 @@ void CPortalManager::startEventLoop() { } } - if (pollfds[2].revents & POLLIN /* pw */) { + if (m_sEventLoopInternals.pollFds[2].revents & POLLIN /* pw */) { while (pw_loop_iterate(m_sPipewire.loop, 0) != 0) { ; } } + for (pollfd p : m_sEventLoopInternals.pollFds) { + if (p.revents & POLLIN && m_sEventLoopInternals.pollCallbacks.contains(p.fd)) { + m_sEventLoopInternals.pollCallbacks[p.fd](); + } + } + std::vector toRemove; for (auto& t : m_sTimersThread.timers) { if (t->passed()) { @@ -417,6 +422,7 @@ void CPortalManager::startEventLoop() { m_sPortals.screencopy.reset(); m_sPortals.screenshot.reset(); m_sHelpers.toplevel.reset(); + m_sPortals.inputCapture.reset(); m_pConnection.reset(); pw_loop_destroy(m_sPipewire.loop); @@ -438,6 +444,10 @@ SOutput* CPortalManager::getOutputFromName(const std::string& name) { return nullptr; } +std::vector> const& CPortalManager::getAllOutputs() { + return m_vOutputs; +} + static char* gbm_find_render_node(drmDevice* device) { drmDevice* devices[64]; char* render_node = NULL; @@ -487,6 +497,20 @@ void CPortalManager::addTimer(const CTimer& timer) { m_sTimersThread.loopSignal.notify_all(); } +void CPortalManager::addFdToEventLoop(int fd, short events, std::function callback) { + m_sEventLoopInternals.pollFds.emplace_back(pollfd{.fd = fd, .events = POLLIN}); + + if (callback == nullptr) + return; + + m_sEventLoopInternals.pollCallbacks[fd] = callback; +} + +void CPortalManager::removeFdFromEventLoop(int fd) { + std::erase_if(m_sEventLoopInternals.pollFds, [fd](const pollfd& p) { return p.fd == fd; }); + m_sEventLoopInternals.pollCallbacks.erase(fd); +} + void CPortalManager::terminate() { m_bTerminate = true; diff --git a/src/core/PortalManager.hpp b/src/core/PortalManager.hpp index e0f2f03..c10e121 100644 --- a/src/core/PortalManager.hpp +++ b/src/core/PortalManager.hpp @@ -8,9 +8,11 @@ #include "../portals/Screencopy.hpp" #include "../portals/Screenshot.hpp" #include "../portals/GlobalShortcuts.hpp" +#include "../portals/InputCapture.hpp" #include "../helpers/Timer.hpp" #include "../shared/ToplevelManager.hpp" #include +#include #include #include "hyprland-toplevel-export-v1.hpp" @@ -33,6 +35,11 @@ struct SOutput { uint32_t id = 0; float refreshRate = 60.0; wl_output_transform transform = WL_OUTPUT_TRANSFORM_NORMAL; + uint32_t width = 0; + uint32_t height = 0; + int32_t x = 0; + int32_t y = 0; + int32_t scale = 1; }; struct SDMABUFModifier { @@ -44,13 +51,14 @@ class CPortalManager { public: CPortalManager(); - void init(); + void init(); - void onGlobal(uint32_t name, const char* interface, uint32_t version); - void onGlobalRemoved(uint32_t name); + void onGlobal(uint32_t name, const char* interface, uint32_t version); + void onGlobalRemoved(uint32_t name); - sdbus::IConnection* getConnection(); - SOutput* getOutputFromName(const std::string& name); + sdbus::IConnection* getConnection(); + SOutput* getOutputFromName(const std::string& name); + std::vector> const& getAllOutputs(); struct { pw_loop* loop = nullptr; @@ -60,6 +68,7 @@ class CPortalManager { std::unique_ptr screencopy; std::unique_ptr screenshot; std::unique_ptr globalShortcuts; + std::unique_ptr inputCapture; } m_sPortals; struct { @@ -92,6 +101,9 @@ class CPortalManager { gbm_device* createGBMDevice(drmDevice* dev); + void addFdToEventLoop(int fd, short events, std::function callback); + void removeFdFromEventLoop(int fd); + // terminate after the event loop has been created. Before we can exit() void terminate(); @@ -102,10 +114,12 @@ class CPortalManager { pid_t m_iPID = 0; struct { - std::condition_variable loopSignal; - std::mutex loopMutex; - std::atomic shouldProcess = false; - std::mutex loopRequestMutex; + std::condition_variable loopSignal; + std::mutex loopMutex; + std::atomic shouldProcess = false; + std::mutex loopRequestMutex; + std::vector pollFds; + std::map> pollCallbacks; } m_sEventLoopInternals; struct { diff --git a/src/helpers/MiscFunctions.cpp b/src/helpers/MiscFunctions.cpp index 7307ba3..9119b09 100644 --- a/src/helpers/MiscFunctions.cpp +++ b/src/helpers/MiscFunctions.cpp @@ -7,6 +7,10 @@ #include #include #include +#include +#include +#include +#include #include using namespace Hyprutils::OS; @@ -55,3 +59,84 @@ bool inShellPath(const std::string& exec) { return std::ranges::any_of(paths, [&exec](std::string& path) { return access((path + "/" + exec).c_str(), X_OK) == 0; }); } + +std::string getRandomUUID() { + std::string uuid; + uuid_t uuid_; + uuid_generate_random(uuid_); + return std::format("{:02x}{:02x}{:02x}{:02x}-{:02x}{:02x}-{:02x}{:02x}-{:02x}{:02x}-{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}", (uint16_t)uuid_[0], (uint16_t)uuid_[1], + (uint16_t)uuid_[2], (uint16_t)uuid_[3], (uint16_t)uuid_[4], (uint16_t)uuid_[5], (uint16_t)uuid_[6], (uint16_t)uuid_[7], (uint16_t)uuid_[8], + (uint16_t)uuid_[9], (uint16_t)uuid_[10], (uint16_t)uuid_[11], (uint16_t)uuid_[12], (uint16_t)uuid_[13], (uint16_t)uuid_[14], (uint16_t)uuid_[15]); +} + +std::pair openExclusiveShm() { + // Only absolute paths can be shared across different shm_open() calls + std::string name = "/" + getRandomUUID(); + + for (size_t i = 0; i < 69; ++i) { + int fd = shm_open(name.c_str(), O_RDWR | O_CREAT | O_EXCL, 0600); + if (fd >= 0) + return {fd, name}; + } + + return {-1, ""}; +} + +int allocateSHMFile(size_t len) { + auto [fd, name] = openExclusiveShm(); + if (fd < 0) + return -1; + + shm_unlink(name.c_str()); + + int ret; + do { + ret = ftruncate(fd, len); + } while (ret < 0 && errno == EINTR); + + if (ret < 0) { + close(fd); + return -1; + } + + return fd; +} + +bool allocateSHMFilePair(size_t size, int* rw_fd_ptr, int* ro_fd_ptr) { + auto [fd, name] = openExclusiveShm(); + if (fd < 0) { + return false; + } + + // CLOEXEC is guaranteed to be set by shm_open + int ro_fd = shm_open(name.c_str(), O_RDONLY, 0); + if (ro_fd < 0) { + shm_unlink(name.c_str()); + close(fd); + return false; + } + + shm_unlink(name.c_str()); + + // Make sure the file cannot be re-opened in read-write mode (e.g. via + // "/proc/self/fd/" on Linux) + if (fchmod(fd, 0) != 0) { + close(fd); + close(ro_fd); + return false; + } + + int ret; + do { + ret = ftruncate(fd, size); + } while (ret < 0 && errno == EINTR); + if (ret < 0) { + close(fd); + close(ro_fd); + return false; + } + + *rw_fd_ptr = fd; + *ro_fd_ptr = ro_fd; + return true; +} diff --git a/src/helpers/MiscFunctions.hpp b/src/helpers/MiscFunctions.hpp index f335a83..194d248 100644 --- a/src/helpers/MiscFunctions.hpp +++ b/src/helpers/MiscFunctions.hpp @@ -5,4 +5,6 @@ std::string execAndGet(const char* cmd); void addHyprlandNotification(const std::string& icon, float timeMs, const std::string& color, const std::string& message); bool inShellPath(const std::string& exec); -void sendEmptyDbusMethodReply(sdbus::MethodCall& call, u_int32_t responseCode); \ No newline at end of file +void sendEmptyDbusMethodReply(sdbus::MethodCall& call, u_int32_t responseCode); +int allocateSHMFile(size_t len); +bool allocateSHMFilePair(size_t size, int* rw_fd_ptr, int* ro_fd_ptr); diff --git a/src/main.cpp b/src/main.cpp index cdeb83d..e5c3ed3 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -42,4 +42,4 @@ int main(int argc, char** argv, char** envp) { g_pPortalManager->init(); return 0; -} \ No newline at end of file +} diff --git a/src/meson.build b/src/meson.build index b76a85f..ad66f5b 100644 --- a/src/meson.build +++ b/src/meson.build @@ -9,10 +9,12 @@ executable('xdg-desktop-portal-hyprland', dependency('hyprlang'), dependency('hyprutils'), dependency('libdrm'), + dependency('libeis-1.0'), dependency('libpipewire-0.3'), dependency('sdbus-c++'), dependency('threads'), dependency('wayland-client'), + dependency('uuid'), ], include_directories: inc, install: true, diff --git a/src/portals/InputCapture.cpp b/src/portals/InputCapture.cpp new file mode 100644 index 0000000..ba02e39 --- /dev/null +++ b/src/portals/InputCapture.cpp @@ -0,0 +1,549 @@ +#include "InputCapture.hpp" + +#include "../core/PortalManager.hpp" +#include "../helpers/Log.hpp" +#include "../shared/Session.hpp" +#include "hyprland-input-capture-v1.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +CInputCapturePortal::CInputCapturePortal(SP mgr) : m_sState(mgr) { + Debug::log(LOG, "[input-capture] initializing input capture portal"); + + mgr->setForceRelease([this](CCHyprlandInputCaptureManagerV1* r) { onForceRelease(); }); + + mgr->setMotion([this](CCHyprlandInputCaptureManagerV1* r, wl_fixed_t x, wl_fixed_t y, wl_fixed_t dx, wl_fixed_t dy) { + onMotion(wl_fixed_to_double(x), wl_fixed_to_double(y), wl_fixed_to_double(dx), wl_fixed_to_double(dy)); + }); + + mgr->setKeymap([this](CCHyprlandInputCaptureManagerV1* r, hyprlandInputCaptureManagerV1KeymapFormat format, int32_t fd, uint32_t size) { + onKeymap(format == HYPRLAND_INPUT_CAPTURE_MANAGER_V1_KEYMAP_FORMAT_XKB_V1 ? fd : 0, size); + }); + + mgr->setModifiers([this](CCHyprlandInputCaptureManagerV1* r, uint32_t mods_depressed, uint32_t mods_latched, uint32_t mods_locked, uint32_t group) { + onModifiers(mods_depressed, mods_latched, mods_locked, group); + }); + + mgr->setKey([this](CCHyprlandInputCaptureManagerV1* r, uint32_t key, hyprlandInputCaptureManagerV1KeyState state) { onKey(key, state); }); + + mgr->setButton([this](CCHyprlandInputCaptureManagerV1* r, uint32_t button, hyprlandInputCaptureManagerV1ButtonState state) { onButton(button, state); }); + + mgr->setAxis([this](CCHyprlandInputCaptureManagerV1* r, hyprlandInputCaptureManagerV1Axis axis, double value) { onAxis(axis, value); }); + + mgr->setAxisValue120([this](CCHyprlandInputCaptureManagerV1* r, hyprlandInputCaptureManagerV1Axis axis, int32_t value120) { onAxis(axis, value120); }); + + mgr->setAxisStop([this](CCHyprlandInputCaptureManagerV1* r, hyprlandInputCaptureManagerV1Axis axis) { onAxisStop(axis); }); + + mgr->setFrame([this](CCHyprlandInputCaptureManagerV1* r) { onFrame(); }); + + m_pObject = sdbus::createObject(*g_pPortalManager->getConnection(), OBJECT_PATH); + + m_pObject + ->addVTable( + sdbus::registerMethod("CreateSession") + .implementedAs([this](sdbus::ObjectPath o1, sdbus::ObjectPath o2, std::string s1, std::string s2, std::unordered_map m) { + return onCreateSession(o1, o2, s1, s2, m); + }), + sdbus::registerMethod("GetZones").implementedAs([this](sdbus::ObjectPath o1, sdbus::ObjectPath o2, std::string s, std::unordered_map m) { + return onGetZones(o1, o2, s, m); + }), + sdbus::registerMethod("SetPointerBarriers") + .implementedAs([this](sdbus::ObjectPath o1, sdbus::ObjectPath o2, std::string s, std::unordered_map m, + std::vector> v, uint32_t u) { return onSetPointerBarriers(o1, o2, s, m, v, u); }), + sdbus::registerMethod("Enable").implementedAs( + [this](sdbus::ObjectPath o, std::string s, std::unordered_map m) { return onEnable(o, s, m); }), + + sdbus::registerMethod("Disable").implementedAs( + [this](sdbus::ObjectPath o, std::string s, std::unordered_map m) { return onDisable(o, s, m); }), + + sdbus::registerMethod("Release").implementedAs( + [this](sdbus::ObjectPath o, std::string s, std::unordered_map m) { return onRelease(o, s, m); }), + sdbus::registerMethod("ConnectToEIS").implementedAs([this](sdbus::ObjectPath o, std::string s, std::unordered_map m) { + return onConnectToEIS(o, s, m); + }), + sdbus::registerProperty("SupportedCapabilities").withGetter([] { return (uint32_t)(1 | 2); }), + sdbus::registerProperty("version").withGetter([] { return (uint32_t)(1); }), + sdbus::registerSignal("Activated").withParameters>(), + sdbus::registerSignal("Disabled").withParameters>(), + sdbus::registerSignal("Deactivated").withParameters>(), + sdbus::registerSignal("ZonesChanged").withParameters>()) + .forInterface(INTERFACE_NAME); + + for (auto& o : g_pPortalManager->getAllOutputs()) + Debug::log(LOG, "{} {}x{}", o->name, o->width, o->height); + + Debug::log(LOG, "[input-capture] init successful"); +} + +void complete(sdbus::MethodCall& call) { + auto reply = call.createReply(); + reply << (uint32_t)0; + reply << std::unordered_map{}; + reply.send(); +} + +dbUasv CInputCapturePortal::onCreateSession(sdbus::ObjectPath requestHandle, sdbus::ObjectPath sessionHandle, std::string appID, std::string parentWindow, + std::unordered_map options) { + Debug::log(LOG, "[input-capture] New session:"); + + uint32_t capabilities = options["capabilities"].get(); + + Debug::log(LOG, "[input-capture] | {}", requestHandle.c_str()); + Debug::log(LOG, "[input-capture] | {}", sessionHandle.c_str()); + Debug::log(LOG, "[input-capture] | appid: {}", appID); + Debug::log(LOG, "[input-capture] | parent_window: {}", parentWindow); + Debug::log(LOG, "[input-capture] | capabilities : {}", capabilities); + + std::string sessionId = "input-capture-" + std::to_string(sessionCounter++); + Debug::log(LOG, "[input-capture] | sessionId : {}", sessionId); + + const std::shared_ptr session = std::make_shared(); + + session->appid = appID; + session->requestHandle = requestHandle; + session->sessionHandle = sessionHandle; + session->sessionId = sessionId; + session->capabilities = capabilities; + + session->session = createDBusSession(sessionHandle); + session->session->onDestroy = [session, this]() { + disable(session->sessionHandle); + session->status = STOPPED; + session->eis->stopServer(); + session->eis.reset(); + + Debug::log(LOG, "[input-capture] Session {} destroyed", session->sessionHandle.c_str()); + + session->session.release(); + }; + + session->request = createDBusRequest(requestHandle); + session->request->onDestroy = [session]() { session->request.release(); }; + + session->eis = std::make_unique("eis-" + sessionId, keymap); + + sessions.emplace(sessionHandle, session); + + std::unordered_map results; + results["capabilities"] = sdbus::Variant{(uint)3}; + results["session_id"] = sdbus::Variant{sessionId}; + return {0, results}; +} + +dbUasv CInputCapturePortal::onGetZones(sdbus::ObjectPath requestHandle, sdbus::ObjectPath sessionHandle, std::string appID, std::unordered_map opts) { + Debug::log(LOG, "[input-capture] New GetZones request:"); + Debug::log(LOG, "[input-capture] | {}", requestHandle.c_str()); + Debug::log(LOG, "[input-capture] | {}", sessionHandle.c_str()); + Debug::log(LOG, "[input-capture] | appid: {}", appID); + + if (!sessionValid(sessionHandle)) + return {1, {}}; + + std::vector> zones; + for (auto& o : g_pPortalManager->getAllOutputs()) { + Debug::log(LOG, "[input-capture] | w: {} h: {} x: {} y: {}", o->width, o->height, o->x, o->y); + zones.push_back(sdbus::Struct(o->width, o->height, o->x, o->y)); + } + + std::unordered_map results; + results["zones"] = sdbus::Variant{zones}; + results["zone_set"] = sdbus::Variant{++lastZoneSet}; + return {0, results}; +} + +dbUasv CInputCapturePortal::onSetPointerBarriers(sdbus::ObjectPath requestHandle, sdbus::ObjectPath sessionHandle, std::string appID, + std::unordered_map opts, std::vector> barriers, + uint32_t zoneSet) { + Debug::log(LOG, "[input-capture] New SetPointerBarriers request:"); + + Debug::log(LOG, "[input-capture] | {}", requestHandle.c_str()); + Debug::log(LOG, "[input-capture] | {}", sessionHandle.c_str()); + Debug::log(LOG, "[input-capture] | appid: {}", appID); + + if (!sessionValid(sessionHandle)) + return {1, {}}; + + Debug::log(LOG, "[input-capture] | zoneSet: {}", zoneSet); + + if (zoneSet != lastZoneSet) { + Debug::log(WARN, "[input-capture] Invalid zone set discarding barriers"); + return {0, {}}; //TODO: We should return failed_barries + } + + sessions[sessionHandle]->barriers.clear(); + for (const auto& b : barriers) { + uint id = b.at("barrier_id").get(); + int x1, y1, x2, y2; + + sdbus::Struct p = b.at("position").get>(); + x1 = p.get<0>(); + y1 = p.get<1>(); + x2 = p.get<2>(); + y2 = p.get<3>(); + + Debug::log(LOG, "[input-capture] | barrier: {}, [{}, {}] [{}, {}]", id, x1, y1, x2, y2); + sessions[sessionHandle]->barriers[id] = {id, x1, y1, x2, y2}; + } + + std::vector failedBarriers; + + std::unordered_map results; + results["failed_barriers"] = sdbus::Variant{failedBarriers}; + return {0, results}; +} + +dbUasv CInputCapturePortal::onDisable(sdbus::ObjectPath sessionHandle, std::string appID, std::unordered_map opts) { + Debug::log(LOG, "[input-capture] New Disable request:"); + Debug::log(LOG, "[input-capture] | {}", sessionHandle.c_str()); + Debug::log(LOG, "[input-capture] | appid: {}", appID); + + if (!sessionValid(sessionHandle)) + return {1, {}}; + + disable(sessionHandle); + return {0, {}}; +} + +dbUasv CInputCapturePortal::onEnable(sdbus::ObjectPath sessionHandle, std::string appID, std::unordered_map opts) { + Debug::log(LOG, "[input-capture] New Enable request:"); + Debug::log(LOG, "[input-capture] | {}", sessionHandle.c_str()); + Debug::log(LOG, "[input-capture] | appid: {}", appID); + + if (!sessions.contains(sessionHandle)) { + Debug::log(WARN, "[input-capture] Unknown session handle: {}", sessionHandle.c_str()); + return {1, {}}; + } + + sessions[sessionHandle]->status = ENABLED; + return {0, {}}; +} + +dbUasv CInputCapturePortal::onRelease(sdbus::ObjectPath sessionHandle, std::string appID, std::unordered_map opts) { + Debug::log(LOG, "[input-capture] New Release request:"); + Debug::log(LOG, "[input-capture] | {}", sessionHandle.c_str()); + Debug::log(LOG, "[input-capture] | appid: {}", appID); + + if (!sessionValid(sessionHandle)) + return {1, {}}; + + uint32_t activationId = opts["activation_id"].get(); + if (activationId != sessions[sessionHandle]->activationId) { + Debug::log(WARN, "[input-capture] Invalid activation id {} expected {}", activationId, sessions[sessionHandle]->activationId); + return {1, {}}; + } + + deactivate(sessionHandle); + + //TODO: maybe warp pointer + + return {0, {}}; +} + +sdbus::UnixFd CInputCapturePortal::onConnectToEIS(sdbus::ObjectPath sessionHandle, std::string appID, std::unordered_map opts) { + Debug::log(LOG, "[input-capture] New ConnectToEIS request:"); + Debug::log(LOG, "[input-capture] | {}", sessionHandle.c_str()); + Debug::log(LOG, "[input-capture] | appid: {}", appID); + + if (!sessionValid(sessionHandle)) + return (sdbus::UnixFd)0; + + int sockfd = sessions[sessionHandle]->eis->getFileDescriptor(); + + Debug::log(LOG, "[input-capture] Connected to the socket. File descriptor: {}", sockfd); + return (sdbus::UnixFd)sockfd; +} + +bool CInputCapturePortal::sessionValid(sdbus::ObjectPath sessionHandle) { + if (!sessions.contains(sessionHandle)) { + Debug::log(WARN, "[input-capture] Unknown session handle: {}", sessionHandle.c_str()); + return false; + } + + return sessions[sessionHandle]->status != STOPPED; +} + +void CInputCapturePortal::onForceRelease() { + Debug::log(LOG, "[input-capture] Released every captures"); + for (auto [key, value] : sessions) + disable(value->sessionHandle); +} + +bool get_line_intersection(double p0_x, double p0_y, double p1_x, double p1_y, double p2_x, double p2_y, double p3_x, double p3_y, double* i_x, double* i_y) { + float s1_x, s1_y, s2_x, s2_y; + s1_x = p1_x - p0_x; + s1_y = p1_y - p0_y; + s2_x = p3_x - p2_x; + s2_y = p3_y - p2_y; + + float s, t; + s = (-s1_y * (p0_x - p2_x) + s1_x * (p0_y - p2_y)) / (-s2_x * s1_y + s1_x * s2_y); + t = (s2_x * (p0_y - p2_y) - s2_y * (p0_x - p2_x)) / (-s2_x * s1_y + s1_x * s2_y); + + if (s >= 0 && s <= 1 && t >= 0 && t <= 1) { + // Collision detected + if (i_x != NULL) + *i_x = p0_x + (t * s1_x); + if (i_y != NULL) + *i_y = p0_y + (t * s1_y); + return 1; + } + + return 0; // No collision +} + +bool testCollision(SBarrier barrier, double px, double py, double nx, double ny) { + return get_line_intersection(barrier.x1, barrier.y1, barrier.x2, barrier.y2, px, py, nx, ny, nullptr, nullptr); +} + +uint32_t CInputCapturePortal::SSession::isColliding(double px, double py, double nx, double ny) { + for (const auto& [key, value] : barriers) + if (testCollision(value, px, py, nx, ny)) + return key; + + return 0; +} + +void CInputCapturePortal::onMotion(double x, double y, double dx, double dy) { + for (const auto& [key, session] : sessions) { + int matched = session->isColliding(x, y, x - dx, y - dy); + if (matched != 0) + activate(session->sessionHandle, x, y, matched); + + session->motion(dx, dy); + } +} + +void CInputCapturePortal::onKey(uint32_t id, bool pressed) { + for (const auto& [key, value] : sessions) + value->key(id, pressed); +} + +void CInputCapturePortal::onModifiers(uint32_t modsDepressed, uint32_t modsLatched, uint32_t modsLocked, uint32_t group) { + for (const auto& [key, value] : sessions) + value->modifiers(modsDepressed, modsLatched, modsLocked, group); +} + +void CInputCapturePortal::onKeymap(int32_t fd, uint32_t size) { + keymap.fd = fd; + keymap.size = size; + for (const auto& [key, value] : sessions) + value->keymap(keymap); +} + +void CInputCapturePortal::onButton(uint32_t button, bool pressed) { + for (const auto& [key, session] : sessions) + session->button(button, pressed); +} + +void CInputCapturePortal::onAxis(bool axis, double value) { + for (const auto& [key, session] : sessions) + session->axis(axis, value); +} + +void CInputCapturePortal::onAxisValue120(bool axis, int32_t value120) { + for (const auto& [key, session] : sessions) + session->axisValue120(axis, value120); +} + +void CInputCapturePortal::onAxisStop(bool axis) { + for (const auto& [_, session] : sessions) + session->axisStop(axis); +} + +void CInputCapturePortal::onFrame() { + for (const auto& [_, session] : sessions) + session->frame(); +} + +void CInputCapturePortal::activate(sdbus::ObjectPath sessionHandle, double x, double y, uint32_t borderId) { + if (!sessionValid(sessionHandle)) + return; + + auto session = sessions[sessionHandle]; + if (!session->activate(x, y, borderId)) + return; + + m_sState.manager->sendCapture(); + + std::unordered_map results; + results["activation_id"] = sdbus::Variant{session->activationId}; + results["cursor_position"] = sdbus::Variant{sdbus::Struct(x, y)}; + results["barrier_id"] = sdbus::Variant{borderId}; + + m_pObject->emitSignal("Activated").onInterface(INTERFACE_NAME).withArguments(sessionHandle, results); +} + +bool CInputCapturePortal::SSession::activate(double x, double y, uint32_t borderId) { + if (status != ENABLED) + return false; + + activationId += 5; + status = ACTIVATED; + Debug::log(LOG, "[input-capture] Input captured for {} activationId: {}", sessionHandle.c_str(), activationId); + eis->startEmulating(activationId); + + return true; +} + +void CInputCapturePortal::deactivate(sdbus::ObjectPath sessionHandle) { + if (!sessionValid(sessionHandle)) + return; + + auto session = sessions[sessionHandle]; + if (!session->deactivate()) + return; + + m_sState.manager->sendRelease(); + + std::unordered_map options; + options["activation_id"] = sdbus::Variant{session->activationId}; + + m_pObject->emitSignal("Deactivated").onInterface(INTERFACE_NAME).withArguments(sessionHandle, options); +} + +bool CInputCapturePortal::SSession::deactivate() { + if (status != ACTIVATED) + return false; + + Debug::log(LOG, "[input-capture] Input released for {}", sessionHandle.c_str()); + eis->stopEmulating(); + + status = ENABLED; + + return true; +} + +void CInputCapturePortal::zonesChanged() { + if (sessions.empty()) + return; + + Debug::log(LOG, "[input-capture] Monitor layout has changed, notifing clients"); + lastZoneSet++; + + for (auto& [key, value] : sessions) { + if (!sessionValid(value->sessionHandle)) + continue; + disable(value->sessionHandle); + if (!value->zonesChanged()) + continue; + + std::unordered_map options; + options["zone_set"] = sdbus::Variant{lastZoneSet - 1}; + m_pObject->emitSignal("ZonesChanged").onInterface(INTERFACE_NAME).withArguments(value->sessionHandle, options); + } +} + +bool CInputCapturePortal::SSession::zonesChanged() { + eis->resetPointer(); + return true; +} + +void CInputCapturePortal::disable(sdbus::ObjectPath sessionHandle) { + if (!sessionValid(sessionHandle)) + return; + + auto session = sessions[sessionHandle]; + + if (session->status == ACTIVATED) + deactivate(sessionHandle); + + if (!session->disable()) + return; + + std::unordered_map options; + m_pObject->emitSignal("Disabled").onInterface(INTERFACE_NAME).withArguments(sessionHandle, options); +} + +bool CInputCapturePortal::SSession::disable() { + status = CREATED; + barriers.clear(); + Debug::log(LOG, "[input-capture] Session {} disabled", sessionHandle.c_str()); + return true; +} + +void CInputCapturePortal::SSession::motion(double dx, double dy) { + if (status != ACTIVATED) + return; + + eis->sendMotion(dx, dy); +} + +void CInputCapturePortal::SSession::keymap(Keymap keymap) { + if (status == STOPPED) + return; + + eis->setKeymap(keymap); +} + +void CInputCapturePortal::SSession::key(uint32_t key, bool pressed) { + if (status != ACTIVATED) + return; + + eis->sendKey(key, pressed); +} + +void CInputCapturePortal::SSession::modifiers(uint32_t modsDepressed, uint32_t modsLatched, uint32_t modsLocked, uint32_t group) { + if (status != ACTIVATED) + return; + + eis->sendModifiers(modsDepressed, modsLatched, modsLocked, group); +} + +void CInputCapturePortal::SSession::button(uint32_t button, bool pressed) { + if (status != ACTIVATED) + return; + + eis->sendButton(button, pressed); +} + +void CInputCapturePortal::SSession::axis(bool axis, double value) { + if (status != ACTIVATED) + return; + + double x = 0; + double y = 0; + + if (axis) + x = value; + else + y = value; + + eis->sendScrollDelta(x, y); +} + +void CInputCapturePortal::SSession::axisValue120(bool axis, int32_t value) { + if (status != ACTIVATED) + return; + + int32_t x = 0; + int32_t y = 0; + + if (axis) + x = value; + else + y = value; + + eis->sendScrollDiscrete(x, y); +} + +void CInputCapturePortal::SSession::axisStop(bool axis) { + if (status != ACTIVATED) + return; + + eis->sendScrollStop(axis, !axis); +} + +void CInputCapturePortal::SSession::frame() { + if (status != ACTIVATED) + return; + + eis->sendPointerFrame(); +} diff --git a/src/portals/InputCapture.hpp b/src/portals/InputCapture.hpp new file mode 100644 index 0000000..ac66848 --- /dev/null +++ b/src/portals/InputCapture.hpp @@ -0,0 +1,102 @@ +#pragma once +#include "../dbusDefines.hpp" +#include "hyprland-input-capture-v1.hpp" +#include "../shared/Eis.hpp" +#include "../includes.hpp" +#include "../shared/Session.hpp" +#include + +enum ClientStatus { + CREATED, //Is ready to be activated + ENABLED, //Is ready for receiving inputs + ACTIVATED, //Currently receiving inputs + STOPPED //Can no longer be activated +}; + +struct SBarrier { + uint id; + int x1, y1, x2, y2; +}; + +class CInputCapturePortal { + public: + CInputCapturePortal(SP mgr); + + void zonesChanged(); + + private: + struct { + SP manager; + } m_sState; + + struct SSession { + std::string appid; + sdbus::ObjectPath requestHandle, sessionHandle; + std::string sessionId; + uint32_t capabilities = 0; + + std::unique_ptr request; + std::unique_ptr session; + std::unique_ptr eis; + + std::unordered_map barriers; + uint32_t activationId = 0; + ClientStatus status = CREATED; + + // + bool activate(double x, double y, uint32_t borderId); + bool deactivate(); + bool disable(); + bool zonesChanged(); + + void motion(double dx, double dy); + void key(uint32_t key, bool pressed); + void modifiers(uint32_t modsDepressed, uint32_t modsLatched, uint32_t modsLocked, uint32_t group); + void keymap(Keymap keymap); + void button(uint32_t button, bool pressed); + void axis(bool axis, double value); + void axisValue120(bool axis, int32_t value120); + void axisStop(bool axis); + void frame(); + + uint32_t isColliding(double px, double py, double nx, double ny); + }; + + std::unordered_map> sessions; + // + std::unique_ptr m_pObject; + uint sessionCounter = 0; + uint lastZoneSet = 0; + + Keymap keymap; //We store the active keymap ready to be sent when creating EIS + + const sdbus::InterfaceName INTERFACE_NAME = sdbus::InterfaceName{"org.freedesktop.impl.portal.InputCapture"}; + const sdbus::ObjectPath OBJECT_PATH = sdbus::ObjectPath{"/org/freedesktop/portal/desktop"}; + + dbUasv onCreateSession(sdbus::ObjectPath requestHandle, sdbus::ObjectPath sessionHandle, std::string appID, std::string parentWindow, + std::unordered_map options); + dbUasv onGetZones(sdbus::ObjectPath requestHandle, sdbus::ObjectPath sessionHandle, std::string appID, std::unordered_map opts); + dbUasv onSetPointerBarriers(sdbus::ObjectPath requestHandle, sdbus::ObjectPath sessionHandle, std::string appID, std::unordered_map opts, + std::vector> barriers, uint32_t zoneSet); + dbUasv onEnable(sdbus::ObjectPath sessionHandle, std::string appID, std::unordered_map opts); + dbUasv onDisable(sdbus::ObjectPath sessionHandle, std::string appID, std::unordered_map opts); + dbUasv onRelease(sdbus::ObjectPath sessionHandle, std::string appID, std::unordered_map opts); + sdbus::UnixFd onConnectToEIS(sdbus::ObjectPath sessionHandle, std::string appID, std::unordered_map opts); + + void onForceRelease(); + void onMotion(double x, double y, double dx, double dy); + void onKeymap(int32_t fd, uint32_t size); + void onKey(uint32_t key, bool pressed); + void onModifiers(uint32_t modsDepressed, uint32_t modsLatched, uint32_t modsLocked, uint32_t group); + void onButton(uint32_t button, bool pressed); + void onAxis(bool axis, double value); + void onAxisValue120(bool axis, int32_t value120); + void onAxisStop(bool axis); + void onFrame(); + + bool sessionValid(sdbus::ObjectPath sessionHandle); + + void activate(sdbus::ObjectPath sessionHandle, double x, double y, uint32_t borderId); + void deactivate(sdbus::ObjectPath sessionHandle); + void disable(sdbus::ObjectPath sessionHandle); +}; diff --git a/src/shared/Eis.cpp b/src/shared/Eis.cpp new file mode 100644 index 0000000..593cae5 --- /dev/null +++ b/src/shared/Eis.cpp @@ -0,0 +1,321 @@ +#include "Eis.hpp" +#include "../core/PortalManager.hpp" +#include "../helpers/MiscFunctions.hpp" +#include "src/helpers/Log.hpp" +#include +#include +#include +#include + +EmulatedInputServer::EmulatedInputServer(std::string socketName, Keymap _keymap) { + Debug::log(LOG, "[EIS] Init socket: {}", socketName); + + keymap = _keymap; + + const char* xdg = getenv("XDG_RUNTIME_DIR"); + if (xdg) + socketPath = std::string(xdg) + "/" + socketName; + + if (socketPath.empty()) { + Debug::log(ERR, "[EIS] Socket path is empty"); + return; + } + + eisCtx = eis_new(nullptr); + + if (eis_setup_backend_socket(eisCtx, socketPath.c_str())) { + Debug::log(ERR, "[EIS] Cannot init eis socket on {}", socketPath); + return; + } + Debug::log(LOG, "[EIS] Listening on {}", socketPath); + + g_pPortalManager->addFdToEventLoop(eis_get_fd(eisCtx), POLLIN, std::bind(&EmulatedInputServer::pollEvents, this)); +} + +void EmulatedInputServer::pollEvents() { + eis_dispatch(eisCtx); + + //Pull every availaible events + while (true) { + eis_event* e = eis_get_event(eisCtx); + + if (!e) { + eis_event_unref(e); + break; + } + + int rc = onEvent(e); + eis_event_unref(e); + if (rc != 0) + break; + } +} + +int EmulatedInputServer::onEvent(eis_event* e) { + eis_client* eisClient = nullptr; + eis_seat* seat = nullptr; + eis_device* device = nullptr; + + switch (eis_event_get_type(e)) { + case EIS_EVENT_CLIENT_CONNECT: + eisClient = eis_event_get_client(e); + Debug::log(LOG, "[EIS] {} client connected: {}", eis_client_is_sender(eisClient) ? "Sender" : "Receiver", eis_client_get_name(eisClient)); + + if (eis_client_is_sender(eisClient)) { + Debug::log(WARN, "[EIS] Unexpected sender client {} connected to input capture session", eis_client_get_name(eisClient)); + eis_client_disconnect(eisClient); + return 0; + } + + if (client.handle) { + Debug::log(WARN, "[EIS] Unexpected additional client {} connected to input capture session", eis_client_get_name(eisClient)); + eis_client_disconnect(eisClient); + return 0; + } + + client.handle = eisClient; + + eis_client_connect(eisClient); + Debug::log(LOG, "[EIS] Creating new default seat"); + seat = eis_client_new_seat(eisClient, "default"); + + eis_seat_configure_capability(seat, EIS_DEVICE_CAP_POINTER); + eis_seat_configure_capability(seat, EIS_DEVICE_CAP_BUTTON); + eis_seat_configure_capability(seat, EIS_DEVICE_CAP_SCROLL); + eis_seat_configure_capability(seat, EIS_DEVICE_CAP_KEYBOARD); + eis_seat_add(seat); + client.seat = seat; + break; + case EIS_EVENT_CLIENT_DISCONNECT: + eisClient = eis_event_get_client(e); + Debug::log(LOG, "[EIS] {} disconnected", eis_client_get_name(eisClient)); + eis_client_disconnect(eisClient); + + eis_seat_unref(client.seat); + clearPointer(); + clearKeyboard(); + client.handle = nullptr; + break; + case EIS_EVENT_SEAT_BIND: + Debug::log(LOG, "[EIS] Binding seats..."); + + if (eis_event_seat_has_capability(e, EIS_DEVICE_CAP_POINTER) && eis_event_seat_has_capability(e, EIS_DEVICE_CAP_BUTTON) && + eis_event_seat_has_capability(e, EIS_DEVICE_CAP_SCROLL)) + ensurePointer(); + else + clearPointer(); + + if (eis_event_seat_has_capability(e, EIS_DEVICE_CAP_KEYBOARD)) + ensureKeyboard(); + else + clearKeyboard(); + break; + case EIS_EVENT_DEVICE_CLOSED: + device = eis_event_get_device(e); + if (device == client.pointer) + clearPointer(); + else if (device == client.keyboard) { + Debug::log(LOG, "[EIS] Clearing keyboard"); + clearKeyboard(); + } else + Debug::log(WARN, "[EIS] Unknown device to close"); + break; + default: return 0; + } + return 0; +} + +void EmulatedInputServer::ensurePointer() { + if (client.pointer) + return; + + Debug::log(LOG, "[EIS] Creating pointer"); + eis_device* pointer = eis_seat_new_device(client.seat); + eis_device_configure_name(pointer, "captured relative pointer"); + eis_device_configure_capability(pointer, EIS_DEVICE_CAP_POINTER); + eis_device_configure_capability(pointer, EIS_DEVICE_CAP_BUTTON); + eis_device_configure_capability(pointer, EIS_DEVICE_CAP_SCROLL); + + for (auto& o : g_pPortalManager->getAllOutputs()) { + eis_region* r = eis_device_new_region(pointer); + + eis_region_set_offset(r, o->x, o->y); + eis_region_set_size(r, o->width, o->height); + eis_region_set_physical_scale(r, o->scale); + eis_region_add(r); + eis_region_unref(r); + } + + eis_device_add(pointer); + eis_device_resume(pointer); + + client.pointer = pointer; +} + +void EmulatedInputServer::ensureKeyboard() { + if (client.keyboard) + return; + + eis_device* keyboard = eis_seat_new_device(client.seat); + eis_device_configure_name(keyboard, "captured keyboard"); + eis_device_configure_capability(keyboard, EIS_DEVICE_CAP_KEYBOARD); + + if (keymap.fd != 0) { + Keymap _keymap = openKeymap(); + Debug::log(LOG, "Using keymap {}", _keymap.fd); + eis_keymap* eis_keymap = eis_device_new_keymap(keyboard, EIS_KEYMAP_TYPE_XKB, _keymap.fd, _keymap.size); + eis_keymap_add(eis_keymap); + eis_keymap_unref(eis_keymap); + close(_keymap.fd); + } + + eis_device_add(keyboard); + eis_device_resume(keyboard); + + client.keyboard = keyboard; +} + +Keymap EmulatedInputServer::openKeymap() { + Keymap _keymap; + + void* src = mmap(nullptr, keymap.size, PROT_READ, MAP_PRIVATE, keymap.fd, 0); + if (src == MAP_FAILED) { + Debug::log(ERR, "Failed to mmap the compositor keymap fd"); + return _keymap; + } + + int keymapFD = allocateSHMFile(keymap.size); + if (keymapFD < 0) { + Debug::log(ERR, "Failed to create a keymap file for keyboard grab"); + return _keymap; + } + + char* dst = (char*)mmap(nullptr, keymap.size, PROT_READ | PROT_WRITE, MAP_SHARED, keymapFD, 0); + if (dst == MAP_FAILED) { + Debug::log(ERR, "Failed to mmap a keymap file for keyboard grab"); + close(keymapFD); + return _keymap; + } + + memcpy(dst, src, keymap.size); + munmap(dst, keymap.size); + munmap(src, keymap.size); + + _keymap.fd = keymapFD; + _keymap.size = keymap.size; + + return _keymap; +} + +void EmulatedInputServer::clearPointer() { + if (!client.pointer) + return; + Debug::log(LOG, "[EIS] Clearing pointer"); + + eis_device_remove(client.pointer); + eis_device_unref(client.pointer); + client.pointer = nullptr; +} + +void EmulatedInputServer::clearKeyboard() { + if (!client.keyboard) + return; + Debug::log(LOG, "[EIS] Clearing keyboard"); + + eis_device_remove(client.keyboard); + eis_device_unref(client.keyboard); + client.keyboard = nullptr; +} + +int EmulatedInputServer::getFileDescriptor() { + return eis_backend_fd_add_client(eisCtx); +} + +void EmulatedInputServer::startEmulating(int sequence) { + Debug::log(LOG, "[EIS] Start Emulating"); + + if (client.pointer) + eis_device_start_emulating(client.pointer, sequence); + + if (client.keyboard) + eis_device_start_emulating(client.keyboard, sequence); +} + +void EmulatedInputServer::stopEmulating() { + Debug::log(LOG, "[EIS] Stop Emulating"); + + if (client.pointer) + eis_device_stop_emulating(client.pointer); + + if (client.keyboard) + eis_device_stop_emulating(client.keyboard); +} + +void EmulatedInputServer::setKeymap(Keymap _keymap) { + keymap = _keymap; +} + +void EmulatedInputServer::resetPointer() { + clearPointer(); + ensurePointer(); +} + +void EmulatedInputServer::sendMotion(double x, double y) { + if (!client.pointer) + return; + eis_device_pointer_motion(client.pointer, x, y); +} + +void EmulatedInputServer::sendKey(uint32_t key, bool pressed) { + if (!client.keyboard) + return; + uint64_t now = eis_now(eisCtx); + eis_device_keyboard_key(client.keyboard, key, pressed); + eis_device_frame(client.keyboard, now); +} + +void EmulatedInputServer::sendModifiers(uint32_t modsDepressed, uint32_t modsLatched, uint32_t modsLocked, uint32_t group) { + if (!client.keyboard) + return; + uint64_t now = eis_now(eisCtx); + eis_device_keyboard_send_xkb_modifiers(client.keyboard, modsDepressed, modsLatched, modsLocked, group); + eis_device_frame(client.keyboard, now); +} + +void EmulatedInputServer::sendButton(uint32_t button, bool pressed) { + if (!client.pointer) + return; + eis_device_button_button(client.pointer, button, pressed); +} + +void EmulatedInputServer::sendScrollDiscrete(int32_t x, int32_t y) { + if (!client.pointer) + return; + eis_device_scroll_discrete(client.pointer, x, y); +} + +void EmulatedInputServer::sendScrollDelta(double x, double y) { + if (!client.pointer) + return; + eis_device_scroll_delta(client.pointer, x, y); +} + +void EmulatedInputServer::sendScrollStop(bool x, bool y) { + if (!client.pointer) + return; + eis_device_scroll_stop(client.pointer, x, y); +} + +void EmulatedInputServer::sendPointerFrame() { + if (!client.pointer) + return; + uint64_t now = eis_now(eisCtx); + eis_device_frame(client.pointer, now); +} + +void EmulatedInputServer::stopServer() { + g_pPortalManager->removeFdFromEventLoop(eis_get_fd(eisCtx)); + Debug::log(LOG, "[EIS] Server fd {} destroyed", eis_get_fd(eisCtx)); + eis_unref(eisCtx); + eisCtx = nullptr; +} diff --git a/src/shared/Eis.hpp b/src/shared/Eis.hpp new file mode 100644 index 0000000..5688ed2 --- /dev/null +++ b/src/shared/Eis.hpp @@ -0,0 +1,59 @@ +#pragma once + +#include +#include + +struct Keymap { + int32_t fd = 0; + uint32_t size = 0; +}; + +/* + * Responsible to creating a socket for input communication + */ +class EmulatedInputServer { + public: + EmulatedInputServer(std::string socketPath, Keymap keymap); + std::string socketPath; + + void startEmulating(int activationId); + void stopEmulating(); + + void setKeymap(Keymap _keymap); + void resetPointer(); + + void sendMotion(double x, double y); + void sendKey(uint32_t key, bool pressed); + void sendModifiers(uint32_t modsDepressed, uint32_t modsLatched, uint32_t modsLocked, uint32_t group); + void sendButton(uint32_t button, bool pressed); + void sendScrollDelta(double x, double y); + void sendScrollDiscrete(int32_t x, int32_t y); + void sendScrollStop(bool stopX, bool stopY); + void sendPointerFrame(); + + int getFileDescriptor(); + + void stopServer(); + + private: + bool stop = false; + eis* eisCtx = nullptr; + + struct Client { + eis_client* handle = nullptr; + eis_seat* seat = nullptr; + + eis_device* pointer = nullptr; + eis_device* keyboard = nullptr; + } client; + + Keymap keymap; + + int onEvent(eis_event* e); + void pollEvents(); + void ensurePointer(); + void ensureKeyboard(); + Keymap openKeymap(); + void clearPointer(); + void clearKeyboard(); +}; diff --git a/subprojects/hyprland-protocols b/subprojects/hyprland-protocols index 4d29e48..5341ab4 160000 --- a/subprojects/hyprland-protocols +++ b/subprojects/hyprland-protocols @@ -1 +1 @@ -Subproject commit 4d29e48433270a2af06b8bc711ca1fe5109746cd +Subproject commit 5341ab4d644daf092a97ec1935ac72315a3aa19b