From e55c67fa2fefe5c5f18a2e920e6a155bfceb28a9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Thalheim?= Date: Tue, 3 Jun 2025 03:28:01 +0200 Subject: [PATCH] feat: add wayland clipboard support via wl-clipboard this is currently based on wl-copy/wl-paste. One could probably just implement wl-copy/wl-paste without adding much complexity and better performance. Co-authored-by: KoljaFrahm --- src/lib/deskflow/IClipboard.cpp | 2 +- src/lib/deskflow/IClipboard.h | 2 +- src/lib/platform/CMakeLists.txt | 4 + src/lib/platform/EiClipboard.cpp | 134 +++ src/lib/platform/EiClipboard.h | 61 ++ src/lib/platform/EiScreen.cpp | 73 +- src/lib/platform/EiScreen.h | 7 + src/lib/platform/WaylandClipboard.cpp | 792 ++++++++++++++++++ src/lib/platform/WaylandClipboard.h | 124 +++ src/unittests/platform/CMakeLists.txt | 11 + .../platform/WaylandClipboardTests.cpp | 376 +++++++++ .../platform/WaylandClipboardTests.h | 77 ++ 12 files changed, 1650 insertions(+), 13 deletions(-) create mode 100644 src/lib/platform/EiClipboard.cpp create mode 100644 src/lib/platform/EiClipboard.h create mode 100644 src/lib/platform/WaylandClipboard.cpp create mode 100644 src/lib/platform/WaylandClipboard.h create mode 100644 src/unittests/platform/WaylandClipboardTests.cpp create mode 100644 src/unittests/platform/WaylandClipboardTests.h diff --git a/src/lib/deskflow/IClipboard.cpp b/src/lib/deskflow/IClipboard.cpp index 73a3f84b7..4b732ad2f 100644 --- a/src/lib/deskflow/IClipboard.cpp +++ b/src/lib/deskflow/IClipboard.cpp @@ -40,7 +40,7 @@ void IClipboard::unmarshall(IClipboard *clipboard, const std::string_view &data, // save the data if it's a known format. if either the client // or server supports more clipboard formats than the other - // then one of them will get a format >= kNumFormats here. + // then one of them will get a format >= TotalFormats here. if (format < IClipboard::Format::TotalFormats) { clipboard->add(format, std::string(index, size)); } diff --git a/src/lib/deskflow/IClipboard.h b/src/lib/deskflow/IClipboard.h index a7d190734..ebec2ecb0 100644 --- a/src/lib/deskflow/IClipboard.h +++ b/src/lib/deskflow/IClipboard.h @@ -29,7 +29,7 @@ public: //! Clipboard formats /*! - The list of known clipboard formats. kNumFormats must be last and + The list of known clipboard formats. TotalFormats must be last and formats must be sequential starting from zero. Clipboard data set via add() and retrieved via get() must be in one of these formats. Platform dependent clipboard subclasses can and should present any diff --git a/src/lib/platform/CMakeLists.txt b/src/lib/platform/CMakeLists.txt index 0900a7ec6..6f2cf0e8c 100644 --- a/src/lib/platform/CMakeLists.txt +++ b/src/lib/platform/CMakeLists.txt @@ -131,12 +131,16 @@ elseif(UNIX) if(LIBEI_FOUND) list(APPEND PLATFORM_SOURCES + EiClipboard.cpp + EiClipboard.h EiEventQueueBuffer.cpp EiEventQueueBuffer.h EiKeyState.cpp EiKeyState.h EiScreen.cpp EiScreen.h + WaylandClipboard.cpp + WaylandClipboard.h ) # The Portal sources also require EI. if(LIBPORTAL_FOUND) diff --git a/src/lib/platform/EiClipboard.cpp b/src/lib/platform/EiClipboard.cpp new file mode 100644 index 000000000..6e8dadb72 --- /dev/null +++ b/src/lib/platform/EiClipboard.cpp @@ -0,0 +1,134 @@ +/* + * Deskflow -- mouse and keyboard sharing utility + * SPDX-FileCopyrightText: (C) 2025 Deskflow Developers + * SPDX-License-Identifier: GPL-2.0-only WITH LicenseRef-OpenSSL-Exception + */ + +#include "platform/EiClipboard.h" + +#include "base/Log.h" +#include "deskflow/ClipboardTypes.h" + +namespace deskflow { + +EiClipboard::EiClipboard() +{ + initialize(); +} + +EiClipboard::~EiClipboard() +{ + cleanup(); +} + +bool EiClipboard::isAvailable() const +{ + return m_available; +} + +IClipboard *EiClipboard::getClipboard(ClipboardID id) const +{ + if (!m_available || id >= m_clipboards.size()) { + return nullptr; + } + + return m_clipboards[id].get(); +} + +bool EiClipboard::hasChanged() const +{ + if (!m_available) { + return false; + } + + for (const auto &clipboard : m_clipboards) { + if (clipboard && clipboard->hasChanged()) { + return true; + } + } + + return false; +} + +void EiClipboard::startMonitoring() +{ + if (!m_available || m_monitoring) { + return; + } + + for (auto &clipboard : m_clipboards) { + if (clipboard) { + clipboard->startMonitoring(); + } + } + + m_monitoring = true; +} + +void EiClipboard::stopMonitoring() +{ + if (!m_available || !m_monitoring) { + return; + } + + for (auto &clipboard : m_clipboards) { + if (clipboard) { + clipboard->stopMonitoring(); + } + } + + m_monitoring = false; +} + +void EiClipboard::resetChanged() +{ + if (!m_available) { + return; + } + + for (auto &clipboard : m_clipboards) { + if (clipboard) { + clipboard->resetChanged(); + } + } +} + +void EiClipboard::initialize() +{ + // Check if Wayland clipboard is available + if (!WaylandClipboard::isAvailable()) { + LOG_WARN("wl-clipboard tools not found, clipboard functionality disabled"); + return; + } + + // Create clipboard instances for each clipboard type + m_clipboards.resize(kClipboardEnd); + + try { + // Primary clipboard (selection) + m_clipboards[kClipboardSelection] = std::make_unique(kClipboardSelection); + + // Standard clipboard + m_clipboards[kClipboardClipboard] = std::make_unique(kClipboardClipboard); + + m_available = true; + LOG_DEBUG1("initialized Wayland clipboard support"); + + } catch (const std::exception &e) { + LOG_ERR("failed to initialize clipboard: %s", e.what()); + cleanup(); + m_available = false; + } +} + +void EiClipboard::cleanup() +{ + if (m_monitoring) { + stopMonitoring(); + } + + m_clipboards.clear(); + m_available = false; +} + +} // namespace deskflow diff --git a/src/lib/platform/EiClipboard.h b/src/lib/platform/EiClipboard.h new file mode 100644 index 000000000..148e0a06a --- /dev/null +++ b/src/lib/platform/EiClipboard.h @@ -0,0 +1,61 @@ +/* + * Deskflow -- mouse and keyboard sharing utility + * SPDX-FileCopyrightText: (C) 2025 Deskflow Developers + * SPDX-License-Identifier: GPL-2.0-only WITH LicenseRef-OpenSSL-Exception + */ + +#pragma once + +#include "deskflow/ClipboardTypes.h" +#include "deskflow/IClipboard.h" +#include "platform/WaylandClipboard.h" + +#include +#include + +namespace deskflow { + +//! Clipboard manager for EiScreen +/*! +This class manages clipboard operations for the EiScreen implementation. +It automatically detects the best available clipboard backend and provides +a unified interface for clipboard operations. +*/ +class EiClipboard +{ +public: + EiClipboard(); + ~EiClipboard(); + + //! Check if clipboard functionality is available + bool isAvailable() const; + + //! Get clipboard for specific ID + IClipboard *getClipboard(ClipboardID id) const; + + //! Check if any clipboard has changed + bool hasChanged() const; + + //! Start monitoring clipboard changes + void startMonitoring(); + + //! Stop monitoring clipboard changes + void stopMonitoring(); + + //! Reset change detection + void resetChanged(); + +private: + //! Initialize clipboard backends + void initialize(); + + //! Cleanup clipboard backends + void cleanup(); + +private: + std::vector> m_clipboards; + bool m_available = false; + bool m_monitoring = false; +}; + +} // namespace deskflow diff --git a/src/lib/platform/EiScreen.cpp b/src/lib/platform/EiScreen.cpp index 3eb5f6828..9c5bcc33d 100644 --- a/src/lib/platform/EiScreen.cpp +++ b/src/lib/platform/EiScreen.cpp @@ -13,6 +13,8 @@ #include "common/Constants.h" #include "common/Settings.h" #include "deskflow/App.h" +#include "deskflow/IScreen.h" +#include "platform/EiClipboard.h" #include "platform/EiEventQueueBuffer.h" #include "platform/EiKeyState.h" #include "platform/PortalInputCapture.h" @@ -44,6 +46,7 @@ EiScreen::EiScreen(bool isPrimary, IEventQueue *events, bool usePortal, bool inv { initEi(); m_keyState = new EiKeyState(this, events); + m_clipboard = new EiClipboard(); // install event handlers m_events->addHandler(EventTypes::System, m_events->getSystemTarget(), [this](const auto &e) { handleSystemEvent(e); @@ -84,6 +87,7 @@ EiScreen::~EiScreen() cleanupEi(); delete m_keyState; + delete m_clipboard; delete m_portalRemoteDesktop; } @@ -159,9 +163,18 @@ void *EiScreen::getEventTarget() const return const_cast(static_cast(this)); } -bool EiScreen::getClipboard(ClipboardID, IClipboard *) const +bool EiScreen::getClipboard(ClipboardID id, IClipboard *clipboard) const { - return false; + if (!m_clipboard || !m_clipboard->isAvailable()) { + return false; + } + + IClipboard *sourceClipboard = m_clipboard->getClipboard(id); + if (!sourceClipboard) { + return false; + } + + return IClipboard::copy(clipboard, sourceClipboard); } void EiScreen::getShape(int32_t &x, int32_t &y, int32_t &w, int32_t &h) const @@ -328,12 +341,16 @@ void EiScreen::fakeKey(uint32_t keycode, bool isDown) const void EiScreen::enable() { // Nothing really to be done here + if (m_clipboard && m_clipboard->isAvailable()) { + m_clipboard->startMonitoring(); + } } void EiScreen::disable() { - // Nothing really to be done here, maybe cleanup in the future but ideally - // that's handled elsewhere + if (m_clipboard && m_clipboard->isAvailable()) { + m_clipboard->stopMonitoring(); + } } void EiScreen::enter() @@ -379,14 +396,34 @@ void EiScreen::leave() m_isOnScreen = false; } -bool EiScreen::setClipboard(ClipboardID, const IClipboard *) +bool EiScreen::setClipboard(ClipboardID id, const IClipboard *clipboard) { - return false; + if (!clipboard || !m_clipboard || !m_clipboard->isAvailable()) { + return false; + } + + IClipboard *targetClipboard = m_clipboard->getClipboard(id); + if (!targetClipboard) { + return false; + } + + return IClipboard::copy(targetClipboard, clipboard); } void EiScreen::checkClipboards() { // do nothing, we're always up to date + if (!m_clipboard || !m_clipboard->isAvailable()) { + return; + } + + if (m_clipboard->hasChanged()) { + // Send clipboard change events for all clipboard types + for (ClipboardID id = 0; id < kClipboardEnd; ++id) { + sendClipboardEvent(EventTypes::ClipboardChanged, id); + } + m_clipboard->resetChanged(); + } } void EiScreen::openScreensaver(bool notify) @@ -415,11 +452,6 @@ void EiScreen::setOptions(const OptionsList &options) // We don't have ei-specific options, nothing to do here } -void EiScreen::setSequenceNumber(uint32_t seqNum) -{ - // FIXME: what is this used for? -} - bool EiScreen::isPrimary() const { return m_isPrimary; @@ -523,6 +555,25 @@ void EiScreen::sendEvent(EventTypes type, void *data) m_events->addEvent(Event(type, getEventTarget(), data)); } +void EiScreen::sendClipboardEvent(EventTypes type, ClipboardID id) const +{ + auto *info = static_cast(malloc(sizeof(ClipboardInfo))); + if (info == nullptr) { + LOG_ERR("malloc failed for ClipboardInfo"); + return; + } + info->m_id = id; + info->m_sequenceNumber = m_sequenceNumber; + + // Use const_cast to call non-const sendEvent from const method + const_cast(this)->sendEvent(type, info); +} + +void EiScreen::setSequenceNumber(uint32_t seqNum) +{ + m_sequenceNumber = seqNum; +} + ButtonID EiScreen::mapButtonFromEvdev(ei_event *event) const { switch (ei_event_button_get_button(event)) { diff --git a/src/lib/platform/EiScreen.h b/src/lib/platform/EiScreen.h index 8de39351c..144622263 100644 --- a/src/lib/platform/EiScreen.h +++ b/src/lib/platform/EiScreen.h @@ -7,6 +7,7 @@ #pragma once +#include "deskflow/IScreen.h" #include "deskflow/PlatformScreen.h" #include "platform/XDGPowerManager.h" @@ -27,6 +28,8 @@ class EiKeyState; class PortalRemoteDesktop; class PortalInputCapture; +using ClipboardInfo = IScreen::ClipboardInfo; + //! Implementation of IPlatformScreen for X11 class EiScreen : public PlatformScreen { @@ -90,6 +93,7 @@ private: void initEi(); void cleanupEi(); void sendEvent(EventTypes type, void *data); + void sendClipboardEvent(EventTypes type, ClipboardID id) const; ButtonID mapButtonFromEvdev(ei_event *event) const; void onKeyEvent(ei_event *event); void onButtonEvent(ei_event *event); @@ -118,6 +122,9 @@ private: // keyboard stuff EiKeyState *m_keyState = nullptr; + // clipboard stuff + EiClipboard *m_clipboard = nullptr; + std::vector m_eiDevices; ei *m_ei = nullptr; diff --git a/src/lib/platform/WaylandClipboard.cpp b/src/lib/platform/WaylandClipboard.cpp new file mode 100644 index 000000000..413c1bd71 --- /dev/null +++ b/src/lib/platform/WaylandClipboard.cpp @@ -0,0 +1,792 @@ +/* + * Deskflow -- mouse and keyboard sharing utility + * SPDX-FileCopyrightText: (C) 2025 Deskflow Developers + * SPDX-License-Identifier: GPL-2.0-only WITH LicenseRef-OpenSSL-Exception + */ + +#include "platform/WaylandClipboard.h" + +#include "base/Log.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +// MIME types for different clipboard formats +const char *const kMimeTypeText = "text/plain;charset=utf-8"; +const char *const kMimeTypeHtml = "text/html"; +const char *const kMimeTypeBmp = "image/bmp"; + +// Additional HTML MIME type variants +const char *const kMimeTypeHtmlUtf8 = "text/html;charset=UTF-8"; +const char *const kMimeTypeHtmlWindows = "HTML Format"; + +// Command timeout (milliseconds) +const int kCommandTimeout = 5000; +const int kCacheValidityMs = 100; +const int kMonitorIntervalMs = 1000; +const int kMaxConsecutiveErrors = 5; + +// Helper function to wait for process with timeout and proper cleanup +bool waitpidWithTimeout(pid_t pid, int *status, int timeout_ms) +{ + auto start = std::chrono::steady_clock::now(); + + while (true) { + int result = waitpid(pid, status, WNOHANG); + if (result == pid) { + return true; // Process finished + } else if (result == -1) { + if (errno == EINTR) { + continue; // Interrupted, try again + } + return false; // Error + } + + // Check timeout + auto now = std::chrono::steady_clock::now(); + auto elapsed = std::chrono::duration_cast(now - start).count(); + if (elapsed >= timeout_ms) { + // Timeout - terminate the process + kill(pid, SIGTERM); + usleep(100000); // Give it 100ms to terminate gracefully + + result = waitpid(pid, status, WNOHANG); + if (result != pid) { + // Still not dead, force kill + kill(pid, SIGKILL); + waitpid(pid, status, 0); // This should not block + } + return false; // Timed out + } + + usleep(10000); // Sleep 10ms before checking again + } +} + +// RAII class for managing processes with exception safety +class ProcessGuard +{ +private: + pid_t m_pid; + bool m_released; + +public: + explicit ProcessGuard(pid_t pid) : m_pid(pid), m_released(false) + { + } + + ~ProcessGuard() + { + if (!m_released && m_pid > 0) { + // Force cleanup if not properly released + kill(m_pid, SIGTERM); + usleep(100000); + kill(m_pid, SIGKILL); + int status; + waitpid(m_pid, &status, 0); + } + } + + // Non-copyable, non-movable for simplicity + ProcessGuard(const ProcessGuard &) = delete; + ProcessGuard &operator=(const ProcessGuard &) = delete; + ProcessGuard(ProcessGuard &&) = delete; + ProcessGuard &operator=(ProcessGuard &&) = delete; + + pid_t get() const + { + return m_pid; + } + + void release() + { + m_released = true; + } +}; + +// RAII class for file descriptors +class FdGuard +{ +private: + int m_fd; + +public: + explicit FdGuard(int fd) : m_fd(fd) + { + } + + ~FdGuard() + { + if (m_fd >= 0) { + ::close(m_fd); + } + } + + // Non-copyable, non-movable for simplicity + FdGuard(const FdGuard &) = delete; + FdGuard &operator=(const FdGuard &) = delete; + FdGuard(FdGuard &&) = delete; + FdGuard &operator=(FdGuard &&) = delete; + + int get() const + { + return m_fd; + } + + void close() + { + if (m_fd >= 0) { + ::close(m_fd); + m_fd = -1; + } + } +}; + +} // namespace + +WaylandClipboard::WaylandClipboard(ClipboardID id) : m_id(id), m_useClipboard(id == kClipboardClipboard) +{ + // Initialize cached data + for (int i = 0; i < static_cast(Format::TotalFormats); ++i) { + m_cachedAvailable[i] = false; + } +} + +WaylandClipboard::~WaylandClipboard() +{ + stopMonitoring(); +} + +ClipboardID WaylandClipboard::getID() const +{ + return m_id; +} + +bool WaylandClipboard::isAvailable() +{ + return checkCommandExists("wl-paste") && checkCommandExists("wl-copy"); +} + +bool WaylandClipboard::checkCommandExists(const char *command) +{ + std::vector args = {command, "--help", nullptr}; + + // Set up file actions for posix_spawn + posix_spawn_file_actions_t fileActions; + posix_spawn_file_actions_init(&fileActions); + + // Redirect stdout and stderr to /dev/null + posix_spawn_file_actions_addopen(&fileActions, STDOUT_FILENO, "/dev/null", O_WRONLY, 0); + posix_spawn_file_actions_addopen(&fileActions, STDERR_FILENO, "/dev/null", O_WRONLY, 0); + + extern char **environ; + pid_t pid; + int spawnResult = posix_spawnp(&pid, command, &fileActions, nullptr, const_cast(args.data()), environ); + + posix_spawn_file_actions_destroy(&fileActions); + + if (spawnResult != 0) { + return false; + } + + ProcessGuard processGuard(pid); + + int status; + bool success = waitpidWithTimeout(pid, &status, kCommandTimeout); + if (success) { + processGuard.release(); + return WIFEXITED(status) && WEXITSTATUS(status) == 0; + } else { + return false; + } +} + +void WaylandClipboard::startMonitoring() +{ + if (m_monitoring) { + return; + } + m_stopMonitoring = false; + m_monitoring = true; + m_monitorThread = std::make_unique(&WaylandClipboard::monitorClipboard, this); +} + +void WaylandClipboard::stopMonitoring() +{ + if (!m_monitoring) { + return; + } + + m_stopMonitoring = true; + m_monitoring = false; + + if (m_monitorThread && m_monitorThread->joinable()) { + m_monitorThread->join(); + } + m_monitorThread.reset(); +} + +bool WaylandClipboard::hasChanged() const +{ + return m_hasChanged.load(); +} + +bool WaylandClipboard::empty() +{ + if (!m_open) { + return false; + } + + std::vector args; + if (m_useClipboard) { + args = {"wl-copy", nullptr}; + } else { + args = {"wl-copy", "-p", nullptr}; + } + + bool success = executeCommandWithInput(args, ""); + + if (success) { + // Update ownership and cache only if command succeeded + std::lock_guard lock(m_cacheMutex); + updateOwnership(true); + invalidateCache(); + } + + return success; +} + +void WaylandClipboard::add(Format format, const std::string &data) +{ + if (!m_open) { + return; + } + + if (format == Format::HTML) { + return; + } + + std::string mimeType = formatToMimeType(format); + if (mimeType.empty()) { + LOG_WARN("unsupported clipboard format: %d", format); + return; + } + + std::vector args; + if (m_useClipboard) { + args = {"wl-copy", "-t", mimeType.c_str(), nullptr}; + } else { + args = {"wl-copy", "-t", mimeType.c_str(), "-p", nullptr}; + } + + bool success = executeCommandWithInput(args, data); + if (success) { + std::lock_guard lock(m_cacheMutex); + updateOwnership(true); + invalidateCache(); + } else { + LOG_WARN("failed to set clipboard data"); + } +} + +bool WaylandClipboard::open(Time time) const +{ + if (m_open) { + LOG_DEBUG("failed to open clipboard: already opened"); + return false; + } + + m_open = true; + m_time = time; + + return true; +} + +void WaylandClipboard::close() const +{ + if (!m_open) { + return; + } + + LOG_DEBUG("close clipboard"); + + m_open = false; + const_cast(this)->invalidateCache(); +} + +IClipboard::Time WaylandClipboard::getTime() const +{ + return m_time; +} + +bool WaylandClipboard::has(Format format) const +{ + if (!m_open) { + return false; + } + + std::lock_guard lock(m_cacheMutex); + + // Check cache validity + Time currentTime = getCurrentTime(); + if (m_cached && (currentTime - m_cachedTime) < kCacheValidityMs) { + return m_cachedAvailable[static_cast(format)]; + } + + // Update cache by checking available MIME types + std::vector availableTypes = const_cast(this)->getAvailableMimeTypes(); + + if (availableTypes.empty()) { + // No types available - mark all formats as unavailable + for (int i = 0; i < static_cast(Format::TotalFormats); ++i) { + m_cachedAvailable[i] = false; + m_cachedData[i].clear(); + } + } else { + // Check each format against available types + for (int i = 0; i < static_cast(Format::TotalFormats); ++i) { + Format currentFormat = static_cast(i); + std::string mimeType = formatToMimeType(currentFormat); + + m_cachedAvailable[i] = false; + if (!mimeType.empty()) { + for (const std::string &available : availableTypes) { + if (available == mimeType || (currentFormat == Format::Text && available == "text/plain") || + (currentFormat == Format::HTML && available.find("text/html") == 0)) { + m_cachedAvailable[i] = true; + break; + } + } + } + } + } + + m_cached = true; + m_cachedTime = currentTime; + + return m_cachedAvailable[static_cast(format)]; +} + +std::string WaylandClipboard::get(Format format) const +{ + if (!m_open) { + return std::string(); + } + + std::lock_guard lock(m_cacheMutex); + + // Return cached data if available and valid + if (m_cached && m_cachedAvailable[static_cast(format)] && !m_cachedData[static_cast(format)].empty()) { + return m_cachedData[static_cast(format)]; + } + + std::string mimeType = formatToMimeType(format); + if (mimeType.empty()) { + return std::string(); + } + + std::vector args; + if (m_useClipboard) { + args = {"wl-paste", "-t", mimeType.c_str(), nullptr}; + } else { + args = {"wl-paste", "-t", mimeType.c_str(), "-p", nullptr}; + } + + std::string data = const_cast(this)->executeCommand(args); + + // Update cache + m_cachedData[static_cast(format)] = data; + m_cachedAvailable[static_cast(format)] = !data.empty(); + m_cached = true; + m_cachedTime = getCurrentTime(); + + return data; +} + +std::string WaylandClipboard::executeCommand(const std::vector &args) const +{ + int pipefd[2]; + if (pipe(pipefd) == -1) { + LOG_WARN("failed to create pipe"); + return std::string(); + } + + FdGuard readFd(pipefd[0]); + FdGuard writeFd(pipefd[1]); + + // Set FD_CLOEXEC on pipe file descriptors + fcntl(readFd.get(), F_SETFD, FD_CLOEXEC); + fcntl(writeFd.get(), F_SETFD, FD_CLOEXEC); + + // Set up file actions for posix_spawn + posix_spawn_file_actions_t fileActions; + posix_spawn_file_actions_init(&fileActions); + + // Redirect stdout to pipe write end + posix_spawn_file_actions_adddup2(&fileActions, writeFd.get(), STDOUT_FILENO); + + // Redirect stderr to /dev/null + posix_spawn_file_actions_addopen(&fileActions, STDERR_FILENO, "/dev/null", O_WRONLY, 0); + + // Close pipe file descriptors in child + posix_spawn_file_actions_addclose(&fileActions, readFd.get()); + posix_spawn_file_actions_addclose(&fileActions, writeFd.get()); + + extern char **environ; + pid_t pid; + int spawnResult = posix_spawnp(&pid, args[0], &fileActions, nullptr, const_cast(args.data()), environ); + + posix_spawn_file_actions_destroy(&fileActions); + + if (spawnResult != 0) { + LOG_WARN("failed to spawn process: %s", strerror(spawnResult)); + return std::string(); + } + + ProcessGuard processGuard(pid); + + // Parent process - close write end + writeFd.close(); + + std::string result; + char buffer[4096]; + + // Use poll to read with timeout + struct pollfd pfd; + pfd.fd = readFd.get(); + pfd.events = POLLIN; + + auto start = std::chrono::steady_clock::now(); + while (true) { + auto now = std::chrono::steady_clock::now(); + auto elapsed = std::chrono::duration_cast(now - start).count(); + int remainingTimeout = kCommandTimeout - elapsed; + + if (remainingTimeout <= 0) { + break; // Timeout + } + + int pollResult = poll(&pfd, 1, remainingTimeout); + if (pollResult == 0) { + break; // Timeout + } else if (pollResult < 0) { + if (errno == EINTR) { + continue; // Interrupted, try again + } + break; // Error + } + + if (pfd.revents & POLLIN) { + ssize_t bytesRead = read(readFd.get(), buffer, sizeof(buffer)); + if (bytesRead > 0) { + result.append(buffer, bytesRead); + } else if (bytesRead == 0) { + break; // EOF + } else if (errno != EINTR) { + break; // Error + } + } + + if (pfd.revents & (POLLHUP | POLLERR)) { + break; // Pipe closed or error + } + } + + readFd.close(); + + int status; + bool processFinished = waitpidWithTimeout(pid, &status, 1000); // Give 1 more second + if (processFinished) { + processGuard.release(); + + if (WIFEXITED(status) && WEXITSTATUS(status) == 0) { + // Remove trailing newline if present + if (!result.empty() && result.back() == '\n') { + result.pop_back(); + } + return result; + } else { + LOG_DEBUG1("command failed with status %d", WEXITSTATUS(status)); + return std::string(); + } + } else { + LOG_WARN("process did not terminate properly"); + return std::string(); + } +} + +bool WaylandClipboard::executeCommandWithInput(const std::vector &args, const std::string &input) const +{ + int pipefd[2]; + if (pipe(pipefd) == -1) { + LOG_WARN("failed to create pipe"); + return false; + } + + FdGuard readFd(pipefd[0]); + FdGuard writeFd(pipefd[1]); + + // Set FD_CLOEXEC on pipe file descriptors + fcntl(readFd.get(), F_SETFD, FD_CLOEXEC); + fcntl(writeFd.get(), F_SETFD, FD_CLOEXEC); + + // Set up file actions for posix_spawn + posix_spawn_file_actions_t fileActions; + posix_spawn_file_actions_init(&fileActions); + + // Redirect stdin from pipe read end + posix_spawn_file_actions_adddup2(&fileActions, readFd.get(), STDIN_FILENO); + + // Redirect stdout and stderr to /dev/null + posix_spawn_file_actions_addopen(&fileActions, STDOUT_FILENO, "/dev/null", O_WRONLY, 0); + posix_spawn_file_actions_addopen(&fileActions, STDERR_FILENO, "/dev/null", O_WRONLY, 0); + + // Close pipe file descriptors in child + posix_spawn_file_actions_addclose(&fileActions, readFd.get()); + posix_spawn_file_actions_addclose(&fileActions, writeFd.get()); + + extern char **environ; + pid_t pid; + int spawnResult = posix_spawnp(&pid, args[0], &fileActions, nullptr, const_cast(args.data()), environ); + + posix_spawn_file_actions_destroy(&fileActions); + + if (spawnResult != 0) { + LOG_WARN("failed to spawn process: %s", strerror(spawnResult)); + return false; + } + + ProcessGuard processGuard(pid); + + // Parent process - close read end + readFd.close(); + + bool writeSuccess = true; + if (!input.empty()) { + // Write with timeout using poll + struct pollfd pfd; + pfd.fd = writeFd.get(); + pfd.events = POLLOUT; + + const char *data = input.c_str(); + size_t totalWritten = 0; + size_t dataSize = input.length(); + auto start = std::chrono::steady_clock::now(); + + while (totalWritten < dataSize) { + auto now = std::chrono::steady_clock::now(); + auto elapsed = std::chrono::duration_cast(now - start).count(); + int remainingTimeout = kCommandTimeout - elapsed; + + if (remainingTimeout <= 0) { + writeSuccess = false; + break; + } + + int pollResult = poll(&pfd, 1, remainingTimeout); + if (pollResult == 0) { + writeSuccess = false; + break; // Timeout + } else if (pollResult < 0) { + if (errno == EINTR) { + continue; + } + writeSuccess = false; + break; + } + + if (pfd.revents & POLLOUT) { + ssize_t written = write(writeFd.get(), data + totalWritten, dataSize - totalWritten); + if (written > 0) { + totalWritten += written; + } else if (written < 0 && errno != EINTR && errno != EAGAIN) { + writeSuccess = false; + break; + } + } + + if (pfd.revents & (POLLHUP | POLLERR)) { + writeSuccess = false; + break; + } + } + + if (!writeSuccess) { + LOG_WARN("failed to write all input data"); + return false; + } + } + + writeFd.close(); + + int status; + bool processFinished = waitpidWithTimeout(pid, &status, kCommandTimeout); + if (processFinished) { + processGuard.release(); + return WIFEXITED(status) && WEXITSTATUS(status) == 0; + } else { + LOG_WARN("process did not terminate properly"); + return false; + } +} + +std::string WaylandClipboard::formatToMimeType(Format format) const +{ + switch (format) { + case Format::Text: + return kMimeTypeText; + case Format::HTML: + return kMimeTypeHtml; + case Format::Bitmap: + return kMimeTypeBmp; + default: + return std::string(); + } +} + +IClipboard::Format WaylandClipboard::mimeTypeToFormat(const std::string &mimeType) const +{ + if (mimeType == kMimeTypeText || mimeType == "text/plain") { + return Format::Text; + } + if (mimeType == kMimeTypeHtml || mimeType == kMimeTypeHtmlUtf8 || mimeType == kMimeTypeHtmlWindows || + mimeType.find("text/html") == 0) { + return Format::HTML; + } + if (mimeType == kMimeTypeBmp || mimeType == "image/bmp") { + return Format::Bitmap; + } + return Format::Text; // Default fallback +} + +std::vector WaylandClipboard::getAvailableMimeTypes() const +{ + std::vector args; + if (m_useClipboard) { + args = {"wl-paste", "--list-types", nullptr}; + } else { + args = {"wl-paste", "--list-types", "-p", nullptr}; + } + + std::string result = executeCommand(args); + std::vector types; + + if (!result.empty()) { + std::istringstream iss(result); + std::string type; + while (std::getline(iss, type)) { + if (!type.empty()) { + types.push_back(type); + } + } + } + + return types; +} + +std::string WaylandClipboard::getClipboardData(const std::string &mimeType) const +{ + std::vector args; + if (m_useClipboard) { + args = {"wl-paste", "-t", mimeType.c_str(), nullptr}; + } else { + args = {"wl-paste", "-t", mimeType.c_str(), "-p", nullptr}; + } + return executeCommand(args); +} + +bool WaylandClipboard::setClipboardData(const std::string &mimeType, const std::string &data) const +{ + std::vector args; + if (m_useClipboard) { + args = {"wl-copy", "-t", mimeType.c_str(), nullptr}; + } else { + args = {"wl-copy", "-t", mimeType.c_str(), "-p", nullptr}; + } + return executeCommandWithInput(args, data); +} + +void WaylandClipboard::monitorClipboard() +{ + std::vector lastTypes; + int consecutiveErrors = 0; + + while (!m_stopMonitoring) { + std::this_thread::sleep_for(std::chrono::milliseconds(kMonitorIntervalMs)); + try { + // Check if clipboard content has changed by comparing available types + std::vector currentTypes = getAvailableMimeTypes(); + + // Reset error counter on successful operation + consecutiveErrors = 0; + + if (currentTypes != lastTypes) { + m_hasChanged = true; + lastTypes = currentTypes; + + // Clear cache when clipboard changes + std::lock_guard lock(m_cacheMutex); + invalidateCache(); + const_cast(this)->updateOwnership(false); + } + } catch (const std::exception &e) { + LOG_WARN("clipboard monitoring error: %s", e.what()); + if (++consecutiveErrors >= kMaxConsecutiveErrors) { + LOG_ERR("too many consecutive errors in clipboard monitoring, stopping"); + break; + } + } catch (...) { + LOG_WARN("clipboard monitoring unknown error"); + if (++consecutiveErrors >= kMaxConsecutiveErrors) { + LOG_ERR("too many consecutive errors in clipboard monitoring, stopping"); + break; + } + } + } +} + +IClipboard::Time WaylandClipboard::getCurrentTime() const +{ + auto now = std::chrono::steady_clock::now(); + auto ms = std::chrono::duration_cast(now.time_since_epoch()); + return static_cast