From 1c789fd6a9f7e03ed5444fa2ebefd064841179e2 Mon Sep 17 00:00:00 2001 From: Peter Hutterer Date: Mon, 2 Feb 2026 11:10:24 +1000 Subject: [PATCH] feat: handle input capture session persistence Use the new libportal API to start a CreateSession2, then set the session persistence and read/write the tokens as required. This allows us to start a server without a permission dialog - provided the portal supports it and the user is ok with that. --- docs/user/configuration.md | 1 + src/lib/common/Settings.h | 2 + src/lib/platform/CMakeLists.txt | 12 +++ src/lib/platform/PortalInputCapture.cpp | 116 +++++++++++++++++++++--- src/lib/platform/PortalInputCapture.h | 3 + 5 files changed, 122 insertions(+), 12 deletions(-) diff --git a/docs/user/configuration.md b/docs/user/configuration.md index d0b369eb9..6a8825557 100644 --- a/docs/user/configuration.md +++ b/docs/user/configuration.md @@ -145,6 +145,7 @@ This section contains options used when in server mode it will begin with `[serv | externalConfig | `true` or `false` | When true use the external config path | | externalConfigFile | Filepath | Path the server config file if it does not exist the GUI will it generated based on the `internalConfig` section.| | protocol | `barrier` or `synergy` | The protocol to use when saying hello to clients. Can be set to barrier or synergy. If not set barrier is used as the default | +| xdpRestoreToken | UUID | Restore token provided by XDG portals | ### InternalConfig diff --git a/src/lib/common/Settings.h b/src/lib/common/Settings.h index 53060c3d6..c72d18471 100644 --- a/src/lib/common/Settings.h +++ b/src/lib/common/Settings.h @@ -106,6 +106,7 @@ public: inline static const auto ExternalConfig = QStringLiteral("server/externalConfig"); inline static const auto ExternalConfigFile = QStringLiteral("server/externalConfigFile"); inline static const auto Protocol = QStringLiteral("server/protocol"); + inline static const auto XdpRestoreToken = QStringLiteral("server/xdpRestoreToken"); }; // Enums types used in settings @@ -253,6 +254,7 @@ private: , Settings::Server::ExternalConfig , Settings::Server::ExternalConfigFile , Settings::Server::Protocol + , Settings::Server::XdpRestoreToken }; // When checking the default values this list contains the ones that default to false. diff --git a/src/lib/platform/CMakeLists.txt b/src/lib/platform/CMakeLists.txt index ea217ee27..d8af35a0e 100644 --- a/src/lib/platform/CMakeLists.txt +++ b/src/lib/platform/CMakeLists.txt @@ -9,6 +9,15 @@ if(UNIX AND NOT APPLE) pkg_check_modules(LIBPORTAL REQUIRED QUIET "libportal >= ${REQUIRED_LIBPORTAL_VERSION}") message(STATUS "libportal version: ${LIBPORTAL_VERSION}") + + # Check if we have the restore ability in the IC portal + include(CMakePushCheckState) + include(CheckSymbolExists) + cmake_push_check_state(RESET) + set(CMAKE_REQUIRED_INCLUDES "${CMAKE_REQUIRED_INCLUDES};${LIBPORTAL_INCLUDE_DIRS};${GLIB2_INCLUDE_DIRS}") + set(CMAKE_REQUIRED_LIBRARIES "${CMAKE_REQUIRED_LIBRARIES};${LIBPORTAL_LINK_LIBRARIES};${GLIB2_LINK_LIBRARIES}") + check_symbol_exists(xdp_input_capture_session_get_restore_token "libportal/inputcapture.h" HAVE_IC_RESTORE) + cmake_pop_check_state() endif() if(WIN32) @@ -178,6 +187,9 @@ if(UNIX) target_compile_definitions(platform PRIVATE HAVE_XKB_KEYMAP_MOD_GET_MASK=1) endif() target_compile_definitions(platform PUBLIC WINAPI_LIBEI WINAPI_LIBPORTAL) + if (HAVE_IC_RESTORE) + target_compile_definitions(platform PUBLIC HAVE_LIBPORTAL_INPUTCAPTURE_RESTORE) + endif() target_include_directories(platform PUBLIC ${LIBEI_INCLUDE_DIRS} ${LIBPORTAL_INCLUDE_DIRS}) target_link_libraries( platform diff --git a/src/lib/platform/PortalInputCapture.cpp b/src/lib/platform/PortalInputCapture.cpp index 9da3d9ba0..0a063cf40 100644 --- a/src/lib/platform/PortalInputCapture.cpp +++ b/src/lib/platform/PortalInputCapture.cpp @@ -12,6 +12,10 @@ #include "base/Log.h" #include "base/TMethodJob.h" +#ifdef HAVE_LIBPORTAL_INPUTCAPTURE_RESTORE +#include "common/Settings.h" +#endif + #include // for EIS fd hack, remove #include // for EIS fd hack, remove @@ -20,6 +24,7 @@ namespace deskflow { PortalInputCapture::PortalInputCapture(EiScreen *screen, IEventQueue *events) : m_screen{screen}, m_events{events}, + m_portalVersion(0), m_portal{xdp_portal_new()} { m_glibMainLoop = g_main_loop_new(nullptr, true); @@ -79,21 +84,10 @@ void PortalInputCapture::handleSessionClosed(XdpSession *session) m_signals.at(Signal::SessionClosed) = 0; } -void PortalInputCapture::handleInitSession(GObject *object, GAsyncResult *res) +void PortalInputCapture::setupSession(XdpInputCaptureSession *session) { - LOG_DEBUG("portal input capture session initialized"); g_autoptr(GError) error = nullptr; - auto session = xdp_portal_create_input_capture_session_finish(XDP_PORTAL(object), res, &error); - if (!session) { - LOG_ERR("failed to initialize input capture session, quitting: %s", error->message); - g_main_loop_quit(m_glibMainLoop); - m_events->addEvent(Event(EventTypes::Quit)); - return; - } - - m_session = session; - auto fd = xdp_input_capture_session_connect_to_eis(session, &error); if (fd < 0) { LOG_ERR("failed to connect to eis: %s", error->message); @@ -115,6 +109,46 @@ void PortalInputCapture::handleInitSession(GObject *object, GAsyncResult *res) handleZonesChanged(m_session, nullptr); } +void PortalInputCapture::handleInitSession(GObject *object, GAsyncResult *res) +{ + g_autoptr(GError) error = nullptr; + + LOG_DEBUG("portal input capture session initialized"); + + auto session = xdp_portal_create_input_capture_session_finish(XDP_PORTAL(object), res, &error); + if (!session) { + LOG_ERR("failed to initialize input capture session, quitting: %s", error->message); + g_main_loop_quit(m_glibMainLoop); + m_events->addEvent(Event(EventTypes::Quit)); + return; + } + + m_session = session; + + setupSession(session); +} + +void PortalInputCapture::handleStart(GObject *object, GAsyncResult *res) +{ + g_autoptr(GError) error = nullptr; + +#ifdef HAVE_LIBPORTAL_INPUTCAPTURE_RESTORE + LOG_DEBUG("portal input capture session initialized"); + if (!xdp_input_capture_session_start_finish(m_session, res, &error)) { + LOG_ERR("failed to start input capture session, quitting: %s", error->message); + g_main_loop_quit(m_glibMainLoop); + m_events->addEvent(Event(EventTypes::Quit)); + return; + } + + auto restoreToken = xdp_input_capture_session_get_restore_token(m_session); + if (restoreToken) { + Settings::setValue(Settings::Server::XdpRestoreToken, QString(restoreToken)); + } +#endif + setupSession(m_session); +} + void PortalInputCapture::handleSetPointerBarriers(const GObject *, GAsyncResult *res) { g_autoptr(GError) error = nullptr; @@ -151,6 +185,63 @@ void PortalInputCapture::handleSetPointerBarriers(const GObject *, GAsyncResult gboolean PortalInputCapture::initSession() { LOG_DEBUG("setting up input capture session"); + XdpInputCaptureSession *session; +#ifdef HAVE_LIBPORTAL_INPUTCAPTURE_RESTORE + g_autoptr(GError) error = nullptr; + + m_portalVersion = xdp_portal_get_input_capture_version_sync( + m_portal, + nullptr, // Cancellable + nullptr + ); + LOG_DEBUG("input capture version %d", m_portalVersion); + + switch (m_portalVersion) { + case 0: + LOG_WARN("portal bug: input capture version is %d", m_portalVersion); + // fallthrough + case 1: + xdp_portal_create_input_capture_session( + m_portal, + nullptr, // parent + static_cast(XDP_INPUT_CAPABILITY_KEYBOARD | XDP_INPUT_CAPABILITY_POINTER), + nullptr, // cancellable + [](GObject *obj, GAsyncResult *res, gpointer data) { + static_cast(data)->handleInitSession(obj, res); + }, + this + ); + break; + default: + session = xdp_portal_create_input_capture_session2_sync( + m_portal, + nullptr, // Cancellable + &error + ); + if (!session) { + LOG_ERR("failed to initialize input capture session, quitting: %s", error->message); + g_main_loop_quit(m_glibMainLoop); + m_events->addEvent(Event(EventTypes::Quit)); + return FALSE; + } + m_session = session; + xdp_input_capture_session_set_session_persistence(session, XDP_INPUT_CAPTURE_SESSION_PERSISTENCE_PERSISTENT); + if (auto sessionToken = Settings::value(Settings::Server::XdpRestoreToken).toByteArray(); !sessionToken.isEmpty()) { + xdp_input_capture_session_set_restore_token(session, strdup(sessionToken.data())); + } + xdp_input_capture_session_start( + m_session, + nullptr, // parent + static_cast(XDP_INPUT_CAPABILITY_KEYBOARD | XDP_INPUT_CAPABILITY_POINTER), + nullptr, // cancellable + [](GObject *obj, GAsyncResult *res, gpointer data) { + static_cast(data)->handleStart(obj, res); + }, + this + ); + break; + } +#else xdp_portal_create_input_capture_session( m_portal, nullptr, // parent @@ -161,6 +252,7 @@ gboolean PortalInputCapture::initSession() }, this ); +#endif return false; } diff --git a/src/lib/platform/PortalInputCapture.h b/src/lib/platform/PortalInputCapture.h index 48f5f763c..5237186ef 100644 --- a/src/lib/platform/PortalInputCapture.h +++ b/src/lib/platform/PortalInputCapture.h @@ -35,6 +35,8 @@ private: void glibThread(const void *); gboolean timeoutHandler() const; gboolean initSession(); + void setupSession(XdpInputCaptureSession *session); + void handleStart(GObject *object, GAsyncResult *res); void handleInitSession(GObject *object, GAsyncResult *res); void handleSetPointerBarriers(const GObject *object, GAsyncResult *res); void handleSessionClosed(XdpSession *session); @@ -83,6 +85,7 @@ private: EiScreen *m_screen = nullptr; IEventQueue *m_events = nullptr; + int m_portalVersion = 0; Thread *m_glibThread; GMainLoop *m_glibMainLoop = nullptr;