Wayland support (port Red Hat libei and libportal impl) (#7449)

* Add libei and libportal

* Port libei and libportal code by Peter Hutterer and Olivier Fourdan

* Add Peter Hutterer and Olivier Fourdan to important devs list

* Improve error handling for libei and libportal builds

* Checkout libportal  tags/0.7.1

* Hack out the gi-docgen clone

* Remove new submodules in favor of using ExternalProject_Add

* Remove submodule dirs

* Use ExternalProject_Add instead of submodules

* Fixed namespace

* Hack to work around type libportal causing type conflicts

* Add log helper functions

* Use original log functions

* Switch to FetchContent, use libportal a1530a9 (unreleased) and use camelCase member names for consistency

* Restore a few events (much more work required)

* Add TODOs for supporting multiple lib versions

* Revert "Switch to FetchContent, use libportal a1530a9 (unreleased) and use camelCase member names for consistency"

This reverts commit 610cebb5b6a08282eee68f4424fcdbe9eaab4bf9.

* Simplify cmake config by removing builds for libei and libportal (will do this in `install_deps.py` instead)

* Remove submodules

* Remove .gitmodules

* Use meson to build subprojects

* Update copyright with Peter Hutterer's guidance

* Use meson for installing deps

* Fixed typo in tag name

* Remove submodules

* Remove old submodules

* Fixed libei name

* Defaults for pugixml and gtest depending on whether source exists in subprojects

* Ignore some subproject dirs

* Make deps OS-specific

* Move python deps to pyproject

* Only compile and install on Linux with Meson

* Revert "Move python deps to pyproject"

This reverts commit 92c8a287b8376a4d166058c85f1d6081f6fdb423.

* Add ninja to brewfile

* Add python3-attr

* Restore original coverage config

* Add ninja for meson

* Include meson in same try-except

* Fixed ninja dep name

* Move libs to correct oS

* Fixed include for wintoast

* Disable docs for libportal

* More options for libei and libportal

* Fixed option for libei

* Use ninja directly to install

* Revert "Use ninja directly to install"

This reverts commit c926d78ba483406a55acd10fb157c39e13f90b71.

* Meson install verbose

* Prints stdout/stderr

* Remove `from None`

* Remove submodules that somehow crept back in?!

* Prepend sudo if exists

* Add libei deps for all distros

* Fixed Fedora package name

* Add more deps for other distros

* Add more libs (including pugixml)

* Fix lib name

* Drop -u from pacman

* Add vala to rhel

* Make libportal optional

* Make portal link optional

* Remove example code

* Always use system pugixml

* Disable interactive apt through install_deps.py

* Revert "Disable interactive apt through install_deps.py"

This reverts commit 5bbc8fd689182447c79b81db16c961b98361a292.

* Set DEBIAN_FRONTEND in workflows

* Set DEBIAN_FRONTEND in CodeQL workflow

* Add gtest dep

* Add bundled libei dep

* Add libei dep to Arch

* Use `googletest` on openSUSE

* Add gmock dep

* Remove gtest dep from openSUSE

* Add libei on Fedora

* Bundle libei for older Linux distros

* Disable libei dep for RPM

* Also bundle symlink to .so

* Use ${CMAKE_INSTALL_LIBDIR}

* Rename libei to fix openSUSE

* List installed files

* Add libei-devel to openSUSE

* Add googletest-devel to openSUSE

* Remove manual deps (probably resolved automatically)

* Remove googletest from openSUSE (doesn't provide google mock)

* Only add Portal* if libportal found

* WIP - Partial work on using old events system :'(

* Add deps install commands for subprojects

* Solved more compile issues related to events system, threads, etc

* Fixed bad config for adding x, ei, portal sources

* Remove redundant deps

* Remove (another) redundant dep

* Fixed pacman command

* Add Ubuntu and Linux Mint libei deps

* Fixed Ubuntu and Linux Mint libei deps aliases

* Only iterate subprojects if not None

* Add rhel, rocky, and alma for libei

* Make rhel-like deps same as fedora again

* Build libportal on rhel-like

* Re-enable meson rhel-like for libportal

* Remove dbus-python

* Make libportal optional (for rhel-like)

* Handle ei event queue results

* Re-introduce libportal

* Print libei and libportal versions

* Add ei/portal args and client screen

* Switch --use args to --no

* Don't build libei/libportal on older distros as it's pointless

* Make libei and libportal optional

* Add Debian 13 runner

* Make some packages optional

* Remove subprojects

* Improve comment

* Add comment for libportal

* Improve comment

* Add Debian 13 runner

* Make optional... optional

* Change continuation stripper to remove newline and continuation char

* Make command strip more uniform

* Fixed help var syntax

* Fixed libei linking

* Ensure `kHelpNoWayland` is always defined

* Improve help message

* Fixed Debian 13 runner name

* Include sstream and use const var

* Update ChangeLog

* Remove Wayland block

* Return new timer

* Make tray icon logging verbose

* Fixed arg parser for wayland args

* Fixed init of EI screen

* Fixed lint issues

* Update README to indicate Wayland support in GNOME 46

* Add missing word

* Fixed comment positions

* Automate CI env

* Tone down debug log messages

* Add missing comma

* Remove redundant log line
This commit is contained in:
Nick Bolton
2024-08-30 15:53:25 +01:00
committed by GitHub
parent 2b663a8dc9
commit 4e844bf307
74 changed files with 3331 additions and 510 deletions

View File

@ -13,5 +13,5 @@ RUN useradd -m build
WORKDIR /app
RUN --mount=type=bind,target=/app,rw \
./scripts/install_deps.py --ci-env && \
./scripts/install_deps.py && \
pacman -Scc --noconfirm

View File

@ -13,5 +13,5 @@ RUN apt update && \
WORKDIR /app
RUN --mount=type=bind,target=/app,rw \
./scripts/install_deps.py --ci-env && \
./scripts/install_deps.py && \
apt clean

View File

@ -12,5 +12,5 @@ RUN dnf upgrade -y && \
WORKDIR /app
RUN --mount=type=bind,target=/app,rw \
./scripts/install_deps.py --ci-env && \
./scripts/install_deps.py && \
dnf clean all

View File

@ -13,5 +13,5 @@ RUN zypper refresh && \
WORKDIR /app
RUN --mount=type=bind,target=/app,rw \
./scripts/install_deps.py --ci-env && \
./scripts/install_deps.py && \
zypper clean --all

View File

@ -26,6 +26,12 @@ jobs:
matrix:
os:
- name: debian-13-amd64
runs-on: ubuntu-latest
config-dir: debian
base-image: debian:trixie-slim
platform: amd64
- name: debian-12-amd64
runs-on: ubuntu-latest
config-dir: debian

View File

@ -79,7 +79,7 @@ jobs:
key: ${{ runner.os }}-deps-${{ hashFiles('config.yaml') }}
- name: Install dependencies
run: python ./scripts/install_deps.py --ci-env
run: python ./scripts/install_deps.py
- name: Setup VC++ environment
uses: ilammy/msvc-dev-cmd@v1
@ -156,7 +156,7 @@ jobs:
key: ${{ runner.os }}-deps-${{ hashFiles('config.yaml') }}
- name: Install dependencies
run: ./scripts/install_deps.py --ci-env
run: ./scripts/install_deps.py
- name: Configure
run: cmake -B build --preset=macos-release
@ -199,10 +199,6 @@ jobs:
container: ${{ matrix.distro.container }}
timeout-minutes: 20
env:
# Prevent apt prompting for input.
DEBIAN_FRONTEND: noninteractive
strategy:
# Normally, we want to fail fast, but in this case we shouldn't since one distro may
# fail due to transient issues unrelated to the build.
@ -210,6 +206,11 @@ jobs:
matrix:
distro:
- name: debian-13-amd64
container: symless/synergy-core:debian-13-amd64
runs-on: ubuntu-latest
extra-packages: true
- name: debian-12-arm64
container: symless/synergy-core:debian-12-arm64
runs-on: ubuntu-24.04-8-core-arm64
@ -272,7 +273,10 @@ jobs:
run: git config --global --add safe.directory $GITHUB_WORKSPACE
- name: Install dependencies
run: ./scripts/install_deps.py --ci-env
run: ./scripts/install_deps.py
env:
# Prevent apt prompting for input.
DEBIAN_FRONTEND: noninteractive
- name: Configure
run: cmake -B build --preset=linux-release

View File

@ -35,7 +35,10 @@ jobs:
run: git config --global --add safe.directory $GITHUB_WORKSPACE
- name: Install dependencies
run: ./scripts/install_deps.py --ci-env
run: ./scripts/install_deps.py
env:
# Prevent apt prompting for input.
DEBIAN_FRONTEND: noninteractive
- name: Initialize CodeQL
uses: github/codeql-action/init@v3

View File

@ -39,9 +39,12 @@ jobs:
- name: Install dependencies
run: |
./scripts/install_deps.py --ci-env &&
./scripts/install_deps.py &&
apt install curl unzip -y &&
pip install gcovr
env:
# Prevent apt prompting for input.
DEBIAN_FRONTEND: noninteractive
- name: Install SonarScanner
run: |
@ -102,8 +105,8 @@ jobs:
-Dsonar.projectKey=symless_synergy-core \
-Dsonar.sources=scripts,src/cmd,src/gui,src/lib \
-Dsonar.tests=src/test \
-Dsonar.exclusions=ext/**,build/** \
-Dsonar.coverage.exclusions=ext/**,scripts/**,src/test/** \
-Dsonar.exclusions=subprojects/**,build/** \
-Dsonar.coverage.exclusions=subprojects/**,scripts/**,src/test/** \
-Dsonar.cpd.exclusions=**/*Test*.cpp \
-Dsonar.host.url=https://sonarcloud.io \
-Dsonar.coverageReportPaths=${{ steps.coverage-paths.outputs.csv }} \

View File

@ -26,8 +26,11 @@ jobs:
- name: Install dependencies
run: |
./scripts/install_deps.py --ci-env &&
./scripts/install_deps.py &&
apt install valgrind -y
env:
# Prevent apt prompting for input.
DEBIAN_FRONTEND: noninteractive
- name: Configure
run: cmake -B build --preset=linux-release

9
.gitmodules vendored
View File

@ -1,9 +0,0 @@
[submodule "ext/googletest"]
path = ext/googletest
url = https://github.com/google/googletest.git
[submodule "ext/WinToast"]
path = ext/WinToast
url = https://github.com/mohabouje/WinToast
[submodule "ext/pugixml"]
path = ext/pugixml
url = https://github.com/zeux/pugixml

View File

@ -1,3 +1,4 @@
brew 'make'
brew 'cmake'
brew 'openssl'
brew 'ninja'

View File

@ -2,6 +2,7 @@
Enhancements:
- #7449 Wayland support (port Red Hat libei and libportal impl)
- #7448 Only add PR comments for internal PR
- #7445 Update `config.yaml` to support Linux Mint build target

View File

@ -10,6 +10,8 @@
This project contains the source code for _Synergy 1 Community Edition_ which is actively maintained, free to use, and does not require a license or serial key.
**Wayland support:** Wayland is supported (GNOME 46 is required).
![Synergy 1 Community Edition](https://github.com/user-attachments/assets/faf5bd69-336c-4bd0-ace3-e911f199d961)
To use the community edition, install the `synergy` package with your favorite package manager or build it yourself using the Developer Quick Start instructions below.
@ -95,7 +97,7 @@ sudo dnf install synergy
*Arch, Manjaro, etc:*
```
sudo pacman -Syu synergy
sudo pacman -S synergy
```
*Repology:*

View File

@ -1,3 +1,6 @@
set(LIBEI_MIN_VERSION 1.2.1)
set(LIBPORTAL_MIN_VERSION 0.6)
macro(configure_libs)
set(libs)
@ -9,8 +12,8 @@ macro(configure_libs)
config_qt()
configure_openssl()
update_submodules()
configure_test_libs()
configure_gtest()
configure_coverage()
endmacro()
@ -99,6 +102,7 @@ macro(configure_unix_libs)
configure_mac_libs()
else()
configure_xorg_libs()
configure_wayland_libs()
endif()
# For config.h, set some static values; it may be a good idea to make these
@ -155,11 +159,79 @@ macro(configure_mac_libs)
endmacro()
macro(configure_wayland_libs)
include(FindPkgConfig)
pkg_check_modules(LIBEI QUIET "libei-1.0 >= ${LIBEI_MIN_VERSION}")
if(LIBEI_FOUND)
message(STATUS "libei version: ${LIBEI_VERSION}")
add_definitions(-DWINAPI_LIBEI=1)
include_directories(${LIBEI_INCLUDE_DIRS})
else()
message(
WARNING
"libei >= ${LIBEI_MIN_VERSION} not found, Wayland support will be disabled."
)
endif()
pkg_check_modules(LIBPORTAL QUIET "libportal >= ${LIBPORTAL_MIN_VERSION}")
if(LIBPORTAL_FOUND)
message(STATUS "libportal version: ${LIBPORTAL_VERSION}")
add_definitions(-DWINAPI_LIBPORTAL=1)
include_directories(${LIBPORTAL_INCLUDE_DIRS})
check_libportal()
else()
message(
WARNING
"libportal >= ${LIBPORTAL_MIN_VERSION} not found, some Wayland features will be disabled."
)
endif()
pkg_check_modules(LIBXKBCOMMON REQUIRED xkbcommon)
pkg_check_modules(GLIB2 REQUIRED glib-2.0 gio-2.0)
find_library(LIBM m)
include_directories(${LIBXKBCOMMON_INCLUDE_DIRS} ${GLIB2_INCLUDE_DIRS}
${LIBM_INCLUDE_DIRS})
endmacro()
macro(check_libportal)
# libportal 0.7 has xdp_session_connect_to_eis but it doesn't have remote desktop session restore or
# the inputcapture code, so let's check for explicit functions that bits depending on what we have
include(CMakePushCheckState)
include(CheckCXXSourceCompiles)
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_session_connect_to_eis "libportal/portal.h"
HAVE_LIBPORTAL_SESSION_CONNECT_TO_EIS)
check_symbol_exists(
xdp_portal_create_remote_desktop_session_full "libportal/portal.h"
HAVE_LIBPORTAL_CREATE_REMOTE_DESKTOP_SESSION_FULL)
check_symbol_exists(xdp_input_capture_session_connect_to_eis
"libportal/inputcapture.h" HAVE_LIBPORTAL_INPUTCAPTURE)
# check_symbol_exists cant check for enum values
check_cxx_source_compiles(
"#include <libportal/portal.h>
int main() { XdpOutputType out = XDP_OUTPUT_NONE; }
" HAVE_LIBPORTAL_OUTPUT_NONE)
cmake_pop_check_state()
endmacro()
#
# X.org/X11 for Linux, BSD, etc
#
macro(configure_xorg_libs)
find_package(pugixml REQUIRED)
# Add include dir for BSD (posix uses /usr/include/)
set(CMAKE_INCLUDE_PATH "${CMAKE_INCLUDE_PATH}:/usr/local/include")
@ -256,6 +328,8 @@ endmacro()
#
macro(configure_windows_libs)
configure_wintoast()
set(CMAKE_CXX_FLAGS
"${CMAKE_CXX_FLAGS} /MP /D _BIND_TO_CURRENT_VCLIBS_VERSION=1")
set(CMAKE_CXX_FLAGS_RELEASE "${CMAKE_CXX_FLAGS_RELEASE} /MD /O2 /Ob2")
@ -306,18 +380,54 @@ macro(configure_openssl)
find_package(OpenSSL REQUIRED)
endmacro()
macro(configure_test_libs)
macro(configure_gtest)
if(BUILD_TESTS AND NOT EXISTS
"${PROJECT_SOURCE_DIR}/ext/googletest/CMakeLists.txt")
message(FATAL_ERROR "Git submodule for Google Test is missing")
file(GLOB gtest_base_dir ${PROJECT_SOURCE_DIR}/subprojects/googletest-*)
if(gtest_base_dir)
set(DEFAULT_SYSTEM_GTEST OFF)
else()
set(DEFAULT_SYSTEM_GTEST ON)
endif()
# Arch Linux package maintainers:
# We do care about not bundling libs and didn't mean to cause upset. We made some mistakes
# and we're trying to put that right.
# The comment "They BUNDLE a fucking zip for cryptopp" in synergy.git/PKGBUILD is only
# relevant to a very version of old the code, so the comment should probably be removed.
# If there are any problems like this in future, please do feel free send us a patch! :)
option(SYSTEM_GTEST "Use system GoogleTest" ${DEFAULT_SYSTEM_GTEST})
if(SYSTEM_GTEST)
message(STATUS "Using system GoogleTest")
find_package(GTest REQUIRED)
set(GTEST_LIBS GTest::GTest GTest::Main)
else()
message(STATUS "Building GoogleTest")
set(gtest_dir ${gtest_base_dir}/googletest)
set(gmock_dir ${gtest_base_dir}/googlemock)
include_directories(${gtest_dir} ${gmock_dir} ${gtest_dir}/include
${gmock_dir}/include)
add_library(gtest STATIC ${gtest_dir}/src/gtest-all.cc)
add_library(gmock STATIC ${gmock_dir}/src/gmock-all.cc)
if(UNIX)
# Ignore noisy GoogleTest warnings
set_target_properties(gtest PROPERTIES COMPILE_FLAGS "-w")
set_target_properties(gmock PROPERTIES COMPILE_FLAGS "-w")
endif()
set(GTEST_LIBS gtest gmock)
endif()
endmacro()
macro(configure_coverage)
if(ENABLE_COVERAGE)
message(STATUS "Enabling code coverage")
include(cmake/CodeCoverage.cmake)
append_coverage_compiler_flags()
set(test_exclude ext/* build/* src/test/*)
set(test_exclude subprojects/* build/* src/test/*)
set(test_src ${PROJECT_SOURCE_DIR}/src)
setup_target_for_coverage_gcovr_xml(
@ -343,35 +453,6 @@ macro(configure_test_libs)
else()
message(STATUS "Code coverage is disabled")
endif()
include_directories(BEFORE SYSTEM
${PROJECT_SOURCE_DIR}/ext/googletest/googletest/include)
endmacro()
macro(update_submodules)
find_package(Git QUIET)
if(GIT_FOUND AND EXISTS "${PROJECT_SOURCE_DIR}/.git")
option(GIT_SUBMODULE "Check submodules during build" ON)
if(GIT_SUBMODULE)
message(STATUS "Submodule update")
execute_process(
COMMAND ${GIT_EXECUTABLE} submodule update --init --recursive
WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}
RESULT_VARIABLE GIT_SUBMODULE_RESULT)
if(NOT GIT_SUBMODULE_RESULT EQUAL "0")
message(
FATAL_ERROR "Git submodule update failed: ${GIT_SUBMODULE_RESULT}")
endif()
endif()
endif()
endmacro()
macro(configure_python)
@ -414,3 +495,9 @@ function(find_openssl_dir_win32 result)
PARENT_SCOPE)
endfunction()
macro(configure_wintoast)
# WinToast is a pretty niche library, and there doesn't seem to be a package for it.
file(GLOB WINTOAST_DIR ${CMAKE_SOURCE_DIR}/subprojects/WinToast-*)
include_directories(${WINTOAST_DIR}/include)
endmacro()

View File

@ -91,14 +91,14 @@ macro(configure_linux_packaging)
set(CPACK_DEBIAN_PACKAGE_SECTION "utils")
set(CPACK_DEBIAN_PACKAGE_SHLIBDEPS ON)
set(CPACK_RPM_PACKAGE_LICENSE "GPLv2")
set(CPACK_RPM_PACKAGE_GROUP "Applications/System")
# HACK: The GUI depends on the Qt6 QPA plugins package, but that's not picked
# up by shlibdeps on Ubuntu 22 (though not a problem on Ubuntu 24 and Debian
# 12), so we must add it manually.
set(CPACK_DEBIAN_PACKAGE_DEPENDS "qt6-qpa-plugins")
set(CPACK_RPM_PACKAGE_LICENSE "GPLv2")
set(CPACK_RPM_PACKAGE_GROUP "Applications/System")
# The default for CMake seems to be /usr/local, which seems uncommon. While
# the default /usr/local prefix causes the app to appear on Debian and Fedora,
# it doesn't seem to appear on Arch Linux. Setting the prefix to /usr seems to

View File

@ -28,7 +28,9 @@ config:
sudo apt-get install -y \
cmake \
make \
ninja-build \
g++ \
file \
xorg-dev \
libx11-dev \
libxtst-dev \
@ -40,7 +42,12 @@ config:
qt6-base-dev \
qt6-tools-dev \
libgtk-3-dev \
file
libgtest-dev \
libgmock-dev \
libpugixml-dev \
libei-dev \
libportal-dev
optional: [libei-dev, libportal-dev]
linuxmint:
<<: *debian
@ -55,7 +62,9 @@ config:
sudo dnf install -y \
cmake \
make \
ninja-build \
gcc-c++ \
rpm-build \
openssl-devel \
glib2-devel \
gdk-pixbuf2-devel \
@ -65,7 +74,12 @@ config:
qt6-qtbase-devel \
qt6-qttools-devel \
gtk3-devel \
rpm-build
gtest-devel \
gmock-devel \
pugixml-devel \
libei-devel \
libportal-devel
optional: [libei-devel, libportal-devel]
# RHEL is not actually supported yet, since it doesn't have Qt6 libs.
# We simply use it as a base for Alma Linux and Rocky Linux.
@ -94,7 +108,9 @@ config:
command: sudo zypper install -y --force-resolution \
cmake \
make \
ninja \
gcc-c++ \
rpm-build \
libopenssl-devel \
glib2-devel \
gdk-pixbuf-devel \
@ -104,13 +120,16 @@ config:
qt6-base-devel \
qt6-tools-devel \
gtk3-devel \
rpm-build
pugixml-devel \
libei-devel \
libportal-devel
arch: &arch
dependencies:
command: sudo pacman -Syu --noconfirm \
command: sudo pacman -Sy --noconfirm \
base-devel \
cmake \
ninja \
gcc \
openssl \
glib2 \
@ -118,9 +137,53 @@ config:
libxtst \
libnotify \
libxkbfile \
gtest \
pugixml \
libei \
libportal \
qt6-base \
qt6-tools \
gtk3
manjaro:
<<: *arch
subprojects:
libei:
dependencies:
debian: &debian_libei |
sudo apt-get install -y \
python3-attr \
python3-jinja2 \
libsystemd-dev
ubuntu: *debian_libei
linuxmint: *debian_libei
fedora: &fedora_libei |
sudo dnf install -y \
python3-attrs \
python3-jinja2 \
systemd-devel
rhel: *fedora_libei
rocky: *fedora_libei
almalinux: *fedora_libei
libportal:
dependencies:
debian: &debian_libportal |
sudo apt-get install -y \
python3-dbusmock \
python3-pytest \
valac \
protobuf-c-compiler \
protobuf-compiler \
libglib2.0 \
libgtk-3-dev \
libprotobuf-c-dev \
libsystemd-dev \
libgirepository1.0-dev
ubuntu: *debian_libportal
linuxmint: *debian_libportal

View File

@ -17,16 +17,20 @@
"dotenv",
"Evenson",
"Feder",
"Fourdan",
"gdrive",
"Hadzhylov",
"Hetu",
"hotspots",
"Hutterer",
"ifdef",
"integtests",
"keychain",
"Keychains",
"Kutytska",
"Lanz",
"libei",
"libportal",
"LLDB",
"Lysytsia",
"macdeployqt",
@ -49,6 +53,8 @@
"Schoeneman",
"Serhii",
"Sorin",
"subproject",
"subprojects",
"synergyc",
"synergyd",
"synergys",

Submodule ext/WinToast deleted from 8abb85b44c

Submodule ext/googletest deleted from e397860881

Submodule ext/pugixml deleted from 9e382f9807

37
meson.build Normal file
View File

@ -0,0 +1,37 @@
# For now, we're only using Meson to resolve dependencies. CMake is called separately.
# In future, we may completely replace CMake with Meson.
project('synergy', 'cpp')
gtest = dependency('gtest', required: false)
if not gtest.found()
subproject('gtest')
endif
if host_machine.system() == 'windows'
wintoast = dependency('wintoast', required: false)
if not wintoast.found()
subproject('wintoast')
endif
endif
if host_machine.system() == 'linux'
system_libei = get_option('system_libei')
if system_libei
dependency('libei-1.0', required: false)
else
# Using the subproject is only useful for development; it's not intended for normal use.
# GNOME46 or above is required as this has the required bits for libei.
# Building on anything older is pointless as you won't be able to actually connect to anything.
subproject('libei', default_options: ['tests=disabled', 'liboeffis=disabled'])
endif
system_libportal = get_option('system_libportal')
if system_libportal
dependency('libportal', required: false)
else
# Using the subproject is only useful for development; it's not intended for normal use.
subproject('libportal', default_options: ['docs=false', 'backend-gtk3=enabled'])
endif
endif

2
meson_options.txt Normal file
View File

@ -0,0 +1,2 @@
option('system_libportal', type: 'boolean', value: true, description: 'Use system libportal')
option('system_libei', type: 'boolean', value: true, description: 'Use system libei')

View File

@ -168,3 +168,15 @@
/* Define to `unsigned int` if <sys/types.h> does not define. */
#cmakedefine size_t ${size_t}
/* Define if libportal has xdp_session_connect_to_eis */
#cmakedefine HAVE_LIBPORTAL_SESSION_CONNECT_TO_EIS ${HAVE_LIBPORTAL_SESSION_CONNECT_TO_EIS}
/* Define if libportal has xdp_portal_create_remote_desktop_session_full */
#cmakedefine HAVE_LIBPORTAL_CREATE_REMOTE_DESKTOP_SESSION_FULL ${HAVE_LIBPORTAL_CREATE_REMOTE_DESKTOP_SESSION_FULL}
/* Define if libportal has inputcapture support */
#cmakedefine HAVE_LIBPORTAL_INPUTCAPTURE ${HAVE_LIBPORTAL_INPUTCAPTURE}
/* Define if libei ei_device_start_emulating takes a sequence number */
#cmakedefine HAVE_LIBEI_SEQUENCE_NUMBER ${HAVE_LIBEI_SEQUENCE_NUMBER}

View File

@ -22,6 +22,8 @@ depends=(
'hicolor-icon-theme'
'qt6-base'
'qt6-tools'
'libei'
'libportal'
)
conflicts=('synergy-git' 'synergy1-bin' 'synergy2-bin' 'synergy3-bin')
options=('!debug')

View File

@ -627,7 +627,7 @@ EXCLUDE_SYMLINKS = NO
EXCLUDE_PATTERNS = */.svn/* \
*/.git/* \
*/ext/* \
*/subprojects/* \
*/src/gui/* \
*/src/test/*

View File

@ -5,25 +5,37 @@ import lib.env as env
import lib.cmd_utils as cmd_utils
import lib.qt_utils as qt_utils
import lib.github as github
import lib.meson as meson
path_env_var = "PATH"
cmake_prefix_env_var = "CMAKE_PREFIX_PATH"
def main():
is_ci = os.getenv("CI") is not None
if is_ci:
print("CI environment detected")
parser = argparse.ArgumentParser()
parser.add_argument(
"--pause-on-exit", action="store_true", help="Useful on Windows"
)
parser.add_argument(
"--only-python",
action="store_true",
help="Only install Python dependencies",
)
parser.add_argument(
"--ci-env",
action="store_true",
help="Set if running in CI environment",
help="Useful for faking CI env (defaults to true in CI env)",
default=is_ci,
)
parser.add_argument(
"--skip-system",
action="store_true",
help="Do not install system dependencies (apt, dnf, etc)",
)
parser.add_argument(
"--skip-meson", action="store_true", help="Do not setup and install with Meson"
)
parser.add_argument(
"--subproject", type=str, help="Sub-project to install dependencies for"
)
args = parser.parse_args()
@ -32,13 +44,11 @@ def main():
env.install_requirements()
error = False
if not args.only_python:
try:
deps = Dependencies(args.ci_env)
deps.install()
except Exception:
traceback.print_exc()
error = True
try:
run(args)
except Exception:
traceback.print_exc()
error = True
if args.pause_on_exit:
input("Press enter to continue...")
@ -47,6 +57,34 @@ def main():
sys.exit(1)
def run(args):
if args.subproject:
deps = SubprojectDependencies(args.subproject)
deps.install()
return
if not args.skip_system:
deps = Dependencies(args.ci_env)
deps.install()
if not args.skip_meson:
run_meson()
# It's a bit weird to use Meson just for installing deps, but it's a stopgap until
# we fully switch from CMake to Meson. For the meantime, Meson will install the deps
# so that CMake can find them easily. Once we switch to Meson, it might be possible for
# Meson handle the deps resolution, so that we won't need to install them on the system.
def run_meson():
meson.setup()
# Only compile and install on Linux for now, since we're only using Meson to fetch
# the deps on Windows and macOS.
if env.is_linux():
meson.compile()
meson.install()
class Dependencies:
def __init__(self, ci_env):
@ -55,9 +93,6 @@ class Dependencies:
self.config = Config()
self.ci_env = ci_env
if self.ci_env:
print("CI environment detected")
def install(self):
"""Installs dependencies for the current platform."""
@ -144,9 +179,49 @@ class Dependencies:
linux.run_command(command_pre, check)
command = self.config.get_os_deps_command(linux_distro=distro)
optional = self.config.get_os_deps_value(
"optional", linux_distro=distro, required=False
)
for optional_package in optional or []:
if not linux.is_package_available(optional_package):
print(f"Optional package not found, stripping: {optional_package}")
command = command.replace(optional_package, "")
print("Running dependencies command")
linux.run_command(command, check=True)
subprojects = self.config.get_os_subprojects()
if subprojects:
for subproject in subprojects:
deps = SubprojectDependencies(subproject)
deps.install()
class SubprojectDependencies:
def __init__(self, subproject):
from lib.config import Config
self.subproject = subproject
self.config = Config()
def install(self):
"""Installs dependencies for the current platform."""
print(f"Installing dependencies for sub-project: {self.subproject}")
if env.is_linux():
self.linux()
else:
raise RuntimeError(f"Unsupported platform: {os}")
def linux(self):
"""Installs dependencies on Linux."""
import lib.linux as linux
command = self.config.get_subproject_deps_command(self.subproject)
linux.run_command(command, check=True)
if __name__ == "__main__":
main()

View File

@ -27,23 +27,26 @@ def has_command(command):
return False
def strip_continuation_sequences(command):
def strip_continuation_sequences(command, strip_newlines=True):
"""
Remove the continuation sequences (\\) from a command.
To spread strings over multiple lines in YAML files, like in bash, a backslash is used at
the end of each line as continuation character.
When a YAML file is parsed, this becomes "\\ " (without a new line char), so this character
sequence must be removed before running the command.
This doesn't seem to be an issue on Windows, since the \\ path separator is rarely followed
by a space.
"""
cmd_continuation = "\\ "
if isinstance(command, list):
return [c.replace(cmd_continuation, "") for c in command]
else:
return command.replace(cmd_continuation, "")
raise ValueError("List commands are not supported")
cmd_continuation = " \\"
command = command.replace(cmd_continuation, "")
# Some versions of pyyaml will remove the newlines already, so always stripping
# makes the output more consistent.
if strip_newlines:
command = command.replace("\n", " ")
return command
def run(
@ -127,7 +130,7 @@ def run(
# Take control of how failed commands are printed:
# - if `print_cmd` is false, it will print `***` instead of the command
# - if the command was a list, the command is printed as a readable string
raise RuntimeError(f"Command failed: {command_str}") from None
raise RuntimeError(f"Command failed: {command_str}")
if result.returncode != 0:
print(

View File

@ -7,6 +7,7 @@ root_key = "config"
deps_key = "dependencies"
command_key = "command"
command_pre_key = "command-pre"
subprojects_key = "subprojects"
arrow = ""
@ -40,8 +41,8 @@ class Config:
self.os_name = env.get_os()
print("Config for OS:", self.os_name)
root = _get(data, root_key)
self.os = _get(root, self.os_name)
self.root = _get(data, root_key)
self.os = _get(self.root, self.os_name)
def get_os_value(self, key, required=True, linux_distro=None):
if linux_distro:
@ -78,6 +79,27 @@ class Config:
else:
return None
def get_os_subprojects(self):
distro, _distro_like, _distro_version = env.get_linux_distro()
return self.get_os_value(subprojects_key, linux_distro=distro, required=False)
def get_subproject_deps_command(self, subproject_name):
subprojects = _get(self.root, subprojects_key)
subproject = _get(subprojects, subproject_name, subprojects_key)
deps_parent = f"{subprojects_key}{arrow}{subproject_name}"
deps = _get(subproject, deps_key, deps_parent)
if env.is_linux():
distro, _distro_like, _distro_version = env.get_linux_distro()
if not distro:
raise RuntimeError("Unable to detect Linux distro")
command = _get(deps, distro, f"{deps_parent}{arrow}{deps_key}")
else:
command = _get(deps, self.os_name, f"{deps_parent}{arrow}{deps_key}")
return cmd_utils.strip_continuation_sequences(command)
def get_os_deps_command_pre(self, required=True, linux_distro=None):
return self.get_os_deps_command(command_pre_key, required, linux_distro)

View File

@ -1,4 +1,4 @@
import os, shutil, glob
import os, shutil, glob, sys
import lib.cmd_utils as cmd_utils
import lib.env as env
from enum import Enum, auto
@ -29,7 +29,7 @@ def run_command(command, check=True):
cmd_utils.run(command, check, shell=True, print_cmd=True)
def package(filename_base, package_type: PackageType):
def package(filename_base, package_type: PackageType, leave_test_installed=False):
extension, cmd = get_package_info(package_type)
run_package_cmd(cmd)
@ -38,7 +38,7 @@ def package(filename_base, package_type: PackageType):
target_path = copy_to_dist_dir(package_filename, target_file)
if package_type == PackageType.DISTRO:
test_install(target_path)
test_install(target_path, remove_test=not leave_test_installed)
def get_package_info(package_type: PackageType):
@ -113,26 +113,31 @@ def copy_to_dist_dir(source_file, target_file):
return target_path
def test_install(package_file):
def test_install(package_file, remove_test=True):
distro, distro_like, _distro_version = env.get_linux_distro()
if not distro_like:
distro_like = distro
install_pre = None
remove_pre = None
install_base = None
list_cmd = None
remove_base = None
if "debian" in distro_like:
install_pre = ["apt", "install", "-f", "-y"]
remove_pre = ["apt", "remove", "-y"]
install_base = ["apt", "install", "-f", "-y"]
remove_base = ["apt", "remove", "-y"]
list_cmd = ["dpkg", "-L", "synergy"]
elif "fedora" in distro_like:
install_pre = ["dnf", "install", "-y"]
remove_pre = ["dnf", "remove", "-y"]
install_base = ["dnf", "install", "-y"]
remove_base = ["dnf", "remove", "-y"]
list_cmd = ["rpm", "-ql", "synergy"]
elif "opensuse" in distro_like:
install_pre = ["zypper", "--no-gpg-checks", "install", "-y"]
remove_pre = ["zypper", "remove", "-y"]
install_base = ["zypper", "--no-gpg-checks", "install", "-y"]
remove_base = ["zypper", "remove", "-y"]
list_cmd = ["rpm", "-ql", "synergy"]
elif "arch" in distro_like:
install_pre = ["pacman", "-U", "--noconfirm"]
remove_pre = ["pacman", "-R", "--noconfirm"]
install_base = ["pacman", "-U", "--noconfirm"]
remove_base = ["pacman", "-R", "--noconfirm"]
list_cmd = ["pacman", "-Ql", "synergy"]
else:
raise RuntimeError(f"Linux distro not yet supported: {distro}")
@ -141,15 +146,46 @@ def test_install(package_file):
print("Testing installation...")
cmd_utils.run(
sudo + install_pre + [f"./{package_file}"],
sudo + install_base + [f"./{package_file}"],
check=True,
print_cmd=True,
)
print("Listing installed files...")
cmd_utils.run(sudo + list_cmd, check=True, print_cmd=True)
try:
cmd_utils.run(test_cmd, shell=True, check=True, print_cmd=True)
except Exception:
raise RuntimeError("Unable to verify version")
finally:
cmd_utils.run(sudo + remove_pre + [package_name], check=True, print_cmd=True)
if remove_test:
cmd_utils.run(
sudo + remove_base + [package_name], check=True, print_cmd=True
)
else:
print("Leaving test package installed")
print("Installation test passed")
def is_package_available(package):
distro, distro_like, _distro_version = env.get_linux_distro()
if not distro_like:
distro_like = distro
if "debian" in distro_like:
command = ["apt-cache", "show", package]
elif "fedora" in distro_like:
command = ["dnf", "info", package]
elif "opensuse" in distro_like:
command = ["zypper", "info", package]
elif "arch" in distro_like:
command = ["pacman", "-Si", package]
else:
raise RuntimeError(f"Linux distro not yet supported: {distro}")
result = cmd_utils.run(command, check=False, print_cmd=True, get_output=True)
if result.stderr:
print(result.stderr, file=sys.stderr)
return result.returncode == 0

25
scripts/lib/meson.py Normal file
View File

@ -0,0 +1,25 @@
import lib.cmd_utils as cmd_utils
import lib.env as env
import os
build_dir = "build/meson"
meson_bin = env.get_python_executable("meson")
def setup():
reconfigure = "--reconfigure" if os.path.exists(build_dir) else ""
cmd_utils.run([meson_bin, "setup", build_dir, reconfigure], print_cmd=True)
def compile():
cmd_utils.run([meson_bin, "compile", "-C", build_dir], print_cmd=True)
def install():
cmd = [meson_bin, "install", "-C", build_dir]
has_sudo = cmd_utils.has_command("sudo")
if has_sudo:
cmd.insert(0, "sudo")
cmd_utils.run(cmd, print_cmd=True)

View File

@ -13,7 +13,7 @@ include_files = [
"CMakeLists.txt",
]
exclude_dirs = ["ext", "build", "deps"]
exclude_dirs = ["subprojects", "build", "deps"]
def main():

View File

@ -4,6 +4,7 @@ import lib.env as env
env.ensure_in_venv(__file__)
import argparse
import platform
from lib.linux import PackageType
from dotenv import load_dotenv # type: ignore
@ -13,6 +14,13 @@ default_package_prefix = "synergy"
def main():
parser = argparse.ArgumentParser()
parser.add_argument(
"--leave-test-installed",
action="store_true",
help="Leave test package installed",
)
args = parser.parse_args()
load_dotenv(dotenv_path=env_file)
@ -25,7 +33,7 @@ def main():
elif env.is_mac():
mac_package(filename_base)
elif env.is_linux():
linux_package(filename_base, version)
linux_package(filename_base, version, args.leave_test_installed)
else:
raise RuntimeError(f"Unsupported platform: {env.get_os()}")
@ -68,12 +76,12 @@ def mac_package(filename_base):
mac.package(filename_base)
def linux_package(filename_base, version):
def linux_package(filename_base, version, leave_test_installed):
import lib.linux as linux
extra_packages = env.get_env_bool("LINUX_EXTRA_PACKAGES", False)
linux.package(filename_base, PackageType.DISTRO)
linux.package(filename_base, PackageType.DISTRO, leave_test_installed)
if extra_packages:
filename_base = get_filename_base(version, use_linux_distro=False)

View File

@ -11,4 +11,5 @@ dependencies = [
"dmgbuild; sys_platform == 'darwin'",
"aqtinstall; sys_platform == 'win32' or sys_platform == 'darwin'",
"colorama",
"meson",
]

View File

@ -66,39 +66,45 @@ void AboutDialog::updateLogo() const {
QString AboutDialog::importantDevelopers() const {
QStringList awesomePeople;
awesomePeople
// Chris is the ultimate creator, and the one who started it all in 2001.
awesomePeople << "Chris Schoeneman";
// Chris is the ultimate creator, and the one who started it all in 2001.
<< "Chris Schoeneman"
// Richard and Adam developed CosmoSynergy, the 90's predecessor project.
awesomePeople << "Richard Lee" << "Adam Feder";
// Richard and Adam developed CosmoSynergy, the 90's predecessor project.
<< "Richard Lee"
<< "Adam Feder"
// Nick continued the legacy in 2009 started by Chris.
awesomePeople << "Nick Bolton";
// Nick continued the legacy in 2009 started by Chris.
<< "Nick Bolton"
// Volker wrote the first version of the GUI (QSynergy) in 2008.
awesomePeople << "Volker Lanz";
// Volker wrote the first version of the GUI (QSynergy) in 2008.
<< "Volker Lanz"
// Re-ignited the project in 2008 and rebuilt the community.
awesomePeople << "Sorin Sbârnea";
// Re-ignited the project in 2008 and rebuilt the community.
<< "Sorin Sbârnea"
// Contributors of bug fixes in the early days.
awesomePeople << "Ryan Breen"
<< "Guido Poschta"
<< "Bertrand Landry Hetu"
<< "Tom Chadwick"
<< "Brent Priddy"
<< "Jason Axelson"
<< "Jake Petroules";
// Contributors of bug fixes in the early days.
<< "Ryan Breen"
<< "Guido Poschta"
<< "Bertrand Landry Hetu"
<< "Tom Chadwick"
<< "Brent Priddy"
<< "Jason Axelson"
<< "Jake Petroules"
// Symless employees (in order of joining).
awesomePeople << "Kyle Bloom"
<< "Daun Chung"
<< "Serhii Hadzhylov"
<< "Oleksandr Lysytsia"
<< "Olena Kutytska"
<< "Owen Phillips"
<< "Daniel Evenson";
// Implemented Wayland support (libei and libportal).
<< "Peter Hutterer"
<< "Olivier Fourdan"
// Symless employees (in order of joining).
<< "Kyle Bloom"
<< "Daun Chung"
<< "Serhii Hadzhylov"
<< "Oleksandr Lysytsia"
<< "Olena Kutytska"
<< "Owen Phillips"
<< "Daniel Evenson";
for (auto &person : awesomePeople) {
// prevent names from breaking on the space when wrapped

View File

@ -1,58 +0,0 @@
/*
* synergy -- mouse and keyboard sharing utility
* Copyright (C) 2021 Symless Ltd.
*
* This package is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* found in the file LICENSE that should have accompanied this file.
*
* This package is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#include "SetupWizardBlocker.h"
#include "MainWindow.h"
#include <QDesktopServices>
#include <QUrl>
static const std::vector<const char *> blockerTitels = {
"Wayland is not yet supported",
};
static const std::vector<const char *> blockerText = {
"We have detected your system is using Wayland which is not currently \n"
"supported, but we are working on it. It's top of our priority list. \n"
"\n"
"Please switch to Xorg if you wish to continue using Synergy today.",
};
SetupWizardBlocker::SetupWizardBlocker(BlockerType type) {
setupUi(this);
m_pLabelTitle->setText(blockerTitels[static_cast<int>(type)]);
m_pLabelInfo->setText(blockerText[static_cast<int>(type)]);
connect(
m_pButtonSupport, &QPushButton::released, this,
&SetupWizardBlocker::onlineSupport);
connect(
m_pButtonCancel, &QPushButton::released, this,
&SetupWizardBlocker::cancel);
}
void SetupWizardBlocker::onlineSupport() {
QDesktopServices::openUrl(QUrl(synergy::gui::kUrlHelp));
cancel();
}
void SetupWizardBlocker::cancel() {
QDialog::reject();
QCoreApplication::quit();
}

View File

@ -1,36 +0,0 @@
/*
* synergy -- mouse and keyboard sharing utility
* Copyright (C) 2021 Symless Ltd.
*
* This package is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* found in the file LICENSE that should have accompanied this file.
*
* This package is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#pragma once
#include "ui_SetupWizardBlocker.h"
#include <QDialog>
class MainWindow;
class SetupWizardBlocker : public QDialog, public Ui::SetupWizardBlocker {
Q_OBJECT
public:
enum class BlockerType { Wayland };
explicit SetupWizardBlocker(BlockerType type);
protected:
void onlineSupport();
void cancel();
};

View File

@ -1,129 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>SetupWizardBlocker</class>
<widget class="QDialog" name="SetupWizardBlocker">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>750</width>
<height>600</height>
</rect>
</property>
<property name="contextMenuPolicy">
<enum>Qt::NoContextMenu</enum>
</property>
<property name="windowTitle">
<string>Setup Synergy</string>
</property>
<property name="windowOpacity">
<double>1.000000000000000</double>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<property name="spacing">
<number>20</number>
</property>
<property name="bottomMargin">
<number>40</number>
</property>
<item>
<spacer name="verticalSpacer">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>40</height>
</size>
</property>
</spacer>
</item>
<item alignment="Qt::AlignHCenter">
<widget class="QLabel" name="m_pLabelImage">
<property name="sizePolicy">
<sizepolicy hsizetype="Fixed" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string/>
</property>
<property name="pixmap">
<pixmap resource="../res/Synergy.qrc">:/res/image/setupBlocker.png</pixmap>
</property>
<property name="scaledContents">
<bool>true</bool>
</property>
<property name="margin">
<number>30</number>
</property>
</widget>
</item>
<item alignment="Qt::AlignHCenter">
<widget class="QLabel" name="m_pLabelTitle">
<property name="font">
<font>
<pointsize>18</pointsize>
<bold>true</bold>
</font>
</property>
<property name="text">
<string notr="true">m_pLabelTitle</string>
</property>
</widget>
</item>
<item alignment="Qt::AlignHCenter">
<widget class="QLabel" name="m_pLabelInfo">
<property name="text">
<string>m_pLabelInfo</string>
</property>
</widget>
</item>
<item>
<spacer name="verticalSpacer_2">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>40</height>
</size>
</property>
</spacer>
</item>
<item alignment="Qt::AlignHCenter">
<widget class="QPushButton" name="m_pButtonSupport">
<property name="sizePolicy">
<sizepolicy hsizetype="Fixed" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>Get technical support</string>
</property>
</widget>
</item>
<item alignment="Qt::AlignHCenter">
<widget class="QPushButton" name="m_pButtonCancel">
<property name="sizePolicy">
<sizepolicy hsizetype="Fixed" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>Exit</string>
</property>
</widget>
</item>
</layout>
</widget>
<resources>
<include location="../res/Synergy.qrc"/>
</resources>
<connections/>
</ui>

View File

@ -19,7 +19,6 @@
#include "MainWindow.h"
#include "QSynergyApplication.h"
#include "SetupWizard.h"
#include "SetupWizardBlocker.h"
#include "common/constants.h"
#include "common/version.h"
#include "gui/Logger.h"
@ -117,14 +116,6 @@ int main(int argc, char *argv[]) {
&configScopes, &ConfigScopes::saving, &appConfig,
[&appConfig]() { appConfig.commit(); }, Qt::DirectConnection);
std::unique_ptr<SetupWizardBlocker> setupBlocker;
if (qgetenv("XDG_SESSION_TYPE") == "wayland") {
SetupWizardBlocker blocked(SetupWizardBlocker::BlockerType::Wayland);
blocked.exec();
qInfo("wayland detected, exiting");
return 0;
}
if (appConfig.wizardShouldRun()) {
SetupWizard wizard(appConfig);
auto result = wizard.exec();

View File

@ -48,6 +48,7 @@ EVENT_TYPE_ACCESSOR(IPrimaryScreen)
EVENT_TYPE_ACCESSOR(IScreen)
EVENT_TYPE_ACCESSOR(Clipboard)
EVENT_TYPE_ACCESSOR(File)
EVENT_TYPE_ACCESSOR(Ei)
// interrupt handler. this just adds a quit event to the queue.
static void interrupt(Arch::ESignal, void *data) {
@ -82,6 +83,7 @@ EventQueue::EventQueue()
m_typesForIScreen(NULL),
m_typesForClipboard(NULL),
m_typesForFile(NULL),
m_typesForEi(NULL),
m_readyMutex(new Mutex),
m_readyCondVar(new CondVar<bool>(m_readyMutex, false)) {
m_mutex = ARCH->newMutex();

View File

@ -20,6 +20,7 @@
#include "arch/IArchMultithread.h"
#include "base/Event.h"
#include "base/EventTypes.h"
#include "base/IEventQueue.h"
#include "base/PriorityQueue.h"
#include "base/Stopwatch.h"
@ -158,6 +159,7 @@ public:
IScreenEvents &forIScreen();
ClipboardEvents &forClipboard();
FileEvents &forFile();
EiEvents &forEi();
private:
ClientEvents *m_typesForClient;
@ -180,6 +182,7 @@ private:
IScreenEvents *m_typesForIScreen;
ClipboardEvents *m_typesForClipboard;
FileEvents *m_typesForFile;
EiEvents *m_typesForEi;
Mutex *m_readyMutex;
CondVar<bool> *m_readyCondVar;
std::queue<Event> m_pending;

View File

@ -193,3 +193,10 @@ REGISTER_EVENT(Clipboard, clipboardSending)
REGISTER_EVENT(File, fileChunkSending)
REGISTER_EVENT(File, fileRecieveCompleted)
REGISTER_EVENT(File, keepAlive)
//
// Ei
//
REGISTER_EVENT(Ei, connected)
REGISTER_EVENT(Ei, sessionClosed)

View File

@ -750,3 +750,31 @@ private:
Event::Type m_fileRecieveCompleted;
Event::Type m_keepAlive;
};
class EiEvents : public EventTypes {
public:
EiEvents() : m_connected(Event::kUnknown), m_sessionClosed(Event::kUnknown) {}
//! @name accessors
//@{
//! Get connected event type
/*!
This event is sent whenever connection to EIS is established and a file
descriptor for reading events is available.
*/
Event::Type connected();
//! Get session closed event type
/*!
This event is sent whenever the portal session managing our EIS connection
is closed.
*/
Event::Type sessionClosed();
//@}
private:
Event::Type m_connected;
Event::Type m_sessionClosed;
};

View File

@ -50,6 +50,7 @@ class IPrimaryScreenEvents;
class IScreenEvents;
class ClipboardEvents;
class FileEvents;
class EiEvents;
//! Event queue interface
/*!
@ -243,4 +244,5 @@ public:
virtual IScreenEvents &forIScreen() = 0;
virtual ClipboardEvents &forClipboard() = 0;
virtual FileEvents &forFile() = 0;
virtual EiEvents &forEi() = 0;
};

View File

@ -216,3 +216,16 @@ otherwise it expands to a call that doesn't.
#define CLOG_DEBUG3 CLOG_TRACE "%z\070"
#define CLOG_DEBUG4 CLOG_TRACE "%z\071" // char is '9'
#define CLOG_DEBUG5 CLOG_TRACE "%z\072" // char is ':'
#define LOG_PRINT(...) LOG((CLOG_PRINT __VA_ARGS__))
#define LOG_CRIT(...) LOG((CLOG_CRIT __VA_ARGS__))
#define LOG_ERR(...) LOG((CLOG_ERR __VA_ARGS__))
#define LOG_WARN(...) LOG((CLOG_WARN __VA_ARGS__))
#define LOG_NOTE(...) LOG((CLOG_NOTE __VA_ARGS__))
#define LOG_INFO(...) LOG((CLOG_INFO __VA_ARGS__))
#define LOG_DEBUG(...) LOG((CLOG_DEBUG __VA_ARGS__))
#define LOG_DEBUG1(...) LOG((CLOG_DEBUG1 __VA_ARGS__))
#define LOG_DEBUG2(...) LOG((CLOG_DEBUG2 __VA_ARGS__))
#define LOG_DEBUG3(...) LOG((CLOG_DEBUG3 __VA_ARGS__))
#define LOG_DEBUG4(...) LOG((CLOG_DEBUG4 __VA_ARGS__))
#define LOG_DEBUG5(...) LOG((CLOG_DEBUG5 __VA_ARGS__))

View File

@ -17,6 +17,7 @@
#include "TrayIcon.h"
#include "Logger.h"
#include "common/constants.h"
namespace synergy::gui {
@ -43,7 +44,8 @@ void TrayIcon::showRetryLoop() {
} else {
// on some platforms, it's not always possible to create the tray when the
// app starts, so keep trying until it is possible.
qDebug("system tray not ready yet, retrying in %d ms", kShowRetryInterval);
logVerbose(QString("system tray not ready yet, retrying in %1 ms")
.arg(kShowRetryInterval));
QTimer::singleShot(kShowRetryInterval, this, &TrayIcon::showRetryLoop);
}
}

View File

@ -27,8 +27,32 @@ elseif(APPLE)
"OSX*.m"
"OSX*.mm")
elseif(UNIX)
file(GLOB headers "XWindows*.h")
file(GLOB sources "XWindows*.cpp")
file(GLOB x_headers "XWindows*.h")
file(GLOB x_sources "XWindows*.cpp")
if(LIBEI_FOUND)
file(GLOB ei_headers "Ei*.h")
file(GLOB ei_sources "Ei*.cpp")
# The Portal sources also require EI.
if(LIBPORTAL_FOUND)
file(GLOB portal_headers "Portal*.h")
file(GLOB portal_sources "Portal*.cpp")
endif()
endif()
list(
APPEND
headers
${x_headers}
${ei_headers}
${portal_headers})
list(
APPEND
sources
${x_sources}
${ei_sources}
${portal_sources})
endif()
if(ADD_HEADERS_TO_SOURCES)
@ -43,6 +67,19 @@ include_directories(${inc})
add_library(platform STATIC ${sources})
target_link_libraries(platform client ${libs})
macro(link_wayland_libs)
target_link_libraries(platform ${LIBXKBCOMMON_LINK_LIBRARIES}
${GLIB2_LINK_LIBRARIES} ${LIBM_LIBRARIES})
if(LIBEI_FOUND)
target_link_libraries(platform ${LIBEI_LINK_LIBRARIES})
endif()
if(LIBPORTAL_FOUND)
target_link_libraries(platform ${LIBPORTAL_LINK_LIBRARIES})
endif()
endmacro()
if(UNIX)
target_link_libraries(
platform
@ -56,6 +93,8 @@ if(UNIX)
if(NOT APPLE)
find_package(Qt6 COMPONENTS DBus)
target_link_libraries(platform Qt6::DBus)
link_wayland_libs()
endif()
endif()

View File

@ -0,0 +1,162 @@
/*
* synergy -- mouse and keyboard sharing utility
* Copyright (C) 2022 Red Hat, Inc.
* Copyright (C) 2024 Symless Ltd.
*
* This package is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* found in the file LICENSE that should have accompanied this file.
*
* This package is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#include "platform/EiEventQueueBuffer.h"
#include "base/Event.h"
#include "base/EventTypes.h"
#include "base/IEventQueue.h"
#include "base/Log.h"
#include "mt/Thread.h"
#include <cassert>
#include <cstdio>
#include <fcntl.h>
#include <poll.h>
#include <unistd.h>
class EventQueueTimer {};
namespace synergy {
EiEventQueueBuffer::EiEventQueueBuffer(
EiScreen *screen, ei *ei, IEventQueue *events)
: ei_(ei_ref(ei)),
events_(events) {
// We need a pipe to signal ourselves when addEvent() is called
int pipefd[2];
int result = pipe(pipefd);
assert(result == 0);
int pipeflags;
pipeflags = fcntl(pipefd[0], F_GETFL);
fcntl(pipefd[0], F_SETFL, pipeflags | O_NONBLOCK);
pipeflags = fcntl(pipefd[1], F_GETFL);
fcntl(pipefd[1], F_SETFL, pipeflags | O_NONBLOCK);
pipe_r_ = pipefd[0];
pipe_w_ = pipefd[1];
}
EiEventQueueBuffer::~EiEventQueueBuffer() {
ei_unref(ei_);
close(pipe_r_);
close(pipe_w_);
}
void EiEventQueueBuffer::waitForEvent(double timeout_in_ms) {
Thread::testCancel();
enum {
EIFD,
PIPEFD,
POLLFD_COUNT, // Last element
};
struct pollfd pfds[POLLFD_COUNT];
pfds[EIFD].fd = ei_get_fd(ei_);
pfds[EIFD].events = POLLIN;
pfds[PIPEFD].fd = pipe_r_;
pfds[PIPEFD].events = POLLIN;
int timeout =
(timeout_in_ms < 0.0) ? -1 : static_cast<int>(1000.0 * timeout_in_ms);
int retval = poll(pfds, POLLFD_COUNT, timeout);
if (retval > 0) {
if (pfds[EIFD].revents & POLLIN) {
std::lock_guard<std::mutex> lock(mutex_);
// libei doesn't allow ei_event_ref() because events are
// supposed to be short-lived only. So instead, we create an NULL-data
// kSystemEvent whenever there's data on the fd, shove that event
// into our event queue and once we process the event (see
// getEvent()), the EiScreen will call ei_dispatch() and process
// all actual pending ei events. In theory this means that a
// flood of ei events could starve the events added with
// addEvents() but let's hope it doesn't come to that.
queue_.push({true, 0U});
}
// the pipefd data doesn't matter, it only exists to wake up the thread
// and potentially testCancel
if (pfds[PIPEFD].revents & POLLIN) {
char buf[64];
auto result = read(pipe_r_, buf, sizeof(buf)); // discard
LOG_DEBUG("event queue read result: %d", result);
}
}
Thread::testCancel();
}
IEventQueueBuffer::Type
EiEventQueueBuffer::getEvent(Event &event, uint32_t &dataID) {
// the addEvent/getEvent pair is a bit awkward for libei.
//
// it assumes that there's a nice queue of events sitting there that we can
// just append to and get everything back out in the same order. We *could*
// emulate that by taking the libei events immediately out of the event
// queue after dispatch (see above) and putting it into the event queue,
// intermixed with whatever addEvents() did.
//
// But this makes locking more awkward and libei isn't really designed to
// keep calling ei_dispatch() while we hold a bunch of event refs. So instead
// we just have a "something happened" event on the ei fd and the rest is
// handled by the EiScreen.
//
std::lock_guard<std::mutex> lock(mutex_);
auto pair = queue_.front();
queue_.pop();
// if this an injected special event, just return the data and exit
if (pair.first == false) {
dataID = pair.second;
return kUser;
}
event = Event(Event::kSystem, events_->getSystemTarget());
return kSystem;
}
bool EiEventQueueBuffer::addEvent(uint32_t dataID) {
std::lock_guard<std::mutex> lock(mutex_);
queue_.push({false, dataID});
// tickle the pipe so our read thread wakes up
auto result = write(pipe_w_, "!", 1);
LOG_DEBUG("event queue write result: %d", result);
return true;
}
bool EiEventQueueBuffer::isEmpty() const {
std::lock_guard<std::mutex> lock(mutex_);
return queue_.empty();
}
EventQueueTimer *
EiEventQueueBuffer::newTimer(double duration, bool oneShot) const {
return new EventQueueTimer;
}
void EiEventQueueBuffer::deleteTimer(EventQueueTimer *timer) const {
delete timer;
}
} // namespace synergy

View File

@ -0,0 +1,59 @@
/*
* synergy -- mouse and keyboard sharing utility
* Copyright (C) 2022 Red Hat, Inc.
* Copyright (C) 2024 Symless Ltd.
*
* This package is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* found in the file LICENSE that should have accompanied this file.
*
* This package is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#pragma once
#include "config.h"
#include "base/IEventQueueBuffer.h"
#include "mt/Thread.h"
#include "platform/EiScreen.h"
#include "synergy/IScreen.h"
#include <libei.h>
#include <memory>
#include <mutex>
#include <queue>
namespace synergy {
//! Event queue buffer for Ei
class EiEventQueueBuffer : public IEventQueueBuffer {
public:
EiEventQueueBuffer(EiScreen *screen, ei *ei, IEventQueue *events);
~EiEventQueueBuffer();
// IEventQueueBuffer overrides
void init() override {}
void waitForEvent(double timeout_in_ms) override;
Type getEvent(Event &event, uint32_t &dataID) override;
bool addEvent(uint32_t dataID) override;
bool isEmpty() const override;
EventQueueTimer *newTimer(double duration, bool oneShot) const override;
void deleteTimer(EventQueueTimer *) const override;
private:
ei *ei_;
IEventQueue *events_;
std::queue<std::pair<bool, uint32_t>> queue_;
int pipe_w_, pipe_r_;
mutable std::mutex mutex_;
};
} // namespace synergy

View File

@ -0,0 +1,282 @@
/*
* synergy -- mouse and keyboard sharing utility
* Copyright (C) 2022 Red Hat, Inc.
* Copyright (C) 2024 Symless Ltd.
*
* This package is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* found in the file LICENSE that should have accompanied this file.
*
* This package is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#include "platform/EiKeyState.h"
#include "base/Log.h"
#include "platform/XWindowsUtil.h"
#include "synergy/AppUtil.h"
#include "synergy/ClientApp.h"
#include <algorithm>
#include <cstddef>
#include <memory>
#include <unistd.h>
#include <xkbcommon/xkbcommon.h>
namespace synergy {
EiKeyState::EiKeyState(EiScreen *screen, IEventQueue *events)
: KeyState(
events, AppUtil::instance().getKeyboardLayoutList(),
ClientApp::instance().args().m_enableLangSync),
screen_{screen} {
xkb_ = xkb_context_new(XKB_CONTEXT_NO_FLAGS);
// FIXME: PrimaryClient->enable() calls into our keymap, so we must have
// one during initial startup - even before we know what our actual keymap is.
// Once we get the actual keymap from EIS, we swap it out so hopefully that's
// enough.
init_default_keymap();
}
void EiKeyState::init_default_keymap() {
if (xkb_keymap_) {
xkb_keymap_unref(xkb_keymap_);
}
xkb_keymap_ =
xkb_keymap_new_from_names(xkb_, nullptr, XKB_KEYMAP_COMPILE_NO_FLAGS);
if (xkb_state_) {
xkb_state_unref(xkb_state_);
}
xkb_state_ = xkb_state_new(xkb_keymap_);
}
void EiKeyState::init(int fd, size_t len) {
auto buffer = std::make_unique<char[]>(len + 1);
auto sz = read(fd, buffer.get(), len);
if ((size_t)sz < len) {
LOG_NOTE("failed to create xkb context: %s", strerror(errno));
return;
}
// See xkbcommon/libxkbcommon issue #307, xkb_keymap_new_from_buffer fails if
// we have a terminating null byte. Since we can't control whether the other
// end sends that byte, enforce null-termination in our buffer and pass the
// whole thing as string.
buffer[len] = '\0'; // guarantee null-termination
auto keymap = xkb_keymap_new_from_string(
xkb_, buffer.get(), XKB_KEYMAP_FORMAT_TEXT_V1,
XKB_KEYMAP_COMPILE_NO_FLAGS);
if (!keymap) {
LOG_NOTE("failed to compile keymap, falling back to defaults");
// Falling back to layout "us" is a lot more useful than segfaulting
init_default_keymap();
return;
}
if (xkb_keymap_) {
xkb_keymap_unref(xkb_keymap_);
}
xkb_keymap_ = keymap;
if (xkb_state_) {
xkb_state_unref(xkb_state_);
}
xkb_state_ = xkb_state_new(xkb_keymap_);
}
EiKeyState::~EiKeyState() {
xkb_context_unref(xkb_);
xkb_keymap_unref(xkb_keymap_);
xkb_state_unref(xkb_state_);
}
bool EiKeyState::fakeCtrlAltDel() {
// pass keys through unchanged
return false;
}
KeyModifierMask EiKeyState::pollActiveModifiers() const {
std::uint32_t xkb_mask =
xkb_state_serialize_mods(xkb_state_, XKB_STATE_MODS_EFFECTIVE);
return convert_mod_mask(xkb_mask);
}
std::int32_t EiKeyState::pollActiveGroup() const {
return xkb_state_serialize_layout(xkb_state_, XKB_STATE_LAYOUT_EFFECTIVE);
}
void EiKeyState::pollPressedKeys(KeyButtonSet &pressedKeys) const {
// FIXME
return;
}
std::uint32_t EiKeyState::convert_mod_mask(std::uint32_t xkb_mask) const {
std::uint32_t barrier_mask = 0;
for (xkb_mod_index_t xkbmod = 0; xkbmod < xkb_keymap_num_mods(xkb_keymap_);
xkbmod++) {
if ((xkb_mask & (1 << xkbmod)) == 0)
continue;
const char *name = xkb_keymap_mod_get_name(xkb_keymap_, xkbmod);
if (strcmp(XKB_MOD_NAME_SHIFT, name) == 0)
barrier_mask |= (1 << kKeyModifierBitShift);
else if (strcmp(XKB_MOD_NAME_CAPS, name) == 0)
barrier_mask |= (1 << kKeyModifierBitCapsLock);
else if (strcmp(XKB_MOD_NAME_CTRL, name) == 0)
barrier_mask |= (1 << kKeyModifierBitControl);
else if (strcmp(XKB_MOD_NAME_ALT, name) == 0)
barrier_mask |= (1 << kKeyModifierBitAlt);
else if (strcmp(XKB_MOD_NAME_LOGO, name) == 0)
barrier_mask |= (1 << kKeyModifierBitSuper);
}
return barrier_mask;
}
// Only way to figure out whether a key is a modifier key is to press it,
// check if a modifier changed state and then release it again.
// Luckily xkbcommon allows us to do this in a separate state.
void EiKeyState::assign_generated_modifiers(
std::uint32_t keycode, synergy::KeyMap::KeyItem &item) {
std::uint32_t mods_generates = 0;
auto state = xkb_state_new(xkb_keymap_);
enum xkb_state_component changed =
xkb_state_update_key(state, keycode, XKB_KEY_DOWN);
if (changed) {
for (xkb_mod_index_t m = 0; m < xkb_keymap_num_mods(xkb_keymap_); m++) {
if (xkb_state_mod_index_is_active(state, m, XKB_STATE_MODS_LOCKED))
item.m_lock = true;
if (xkb_state_mod_index_is_active(state, m, XKB_STATE_MODS_EFFECTIVE)) {
mods_generates |= (1 << m);
}
}
}
xkb_state_update_key(state, keycode, XKB_KEY_UP);
xkb_state_unref(state);
item.m_generates = convert_mod_mask(mods_generates);
}
void EiKeyState::getKeyMap(synergy::KeyMap &keyMap) {
auto min_keycode = xkb_keymap_min_keycode(xkb_keymap_);
auto max_keycode = xkb_keymap_max_keycode(xkb_keymap_);
// X keycodes are evdev keycodes + 8 (libei gives us evdev keycodes)
for (auto keycode = min_keycode; keycode <= max_keycode; keycode++) {
// skip keys with no groups (they generate no symbols)
if (xkb_keymap_num_layouts_for_key(xkb_keymap_, keycode) == 0)
continue;
for (auto group = 0U; group < xkb_keymap_num_layouts(xkb_keymap_);
group++) {
for (auto level = 0U;
level < xkb_keymap_num_levels_for_key(xkb_keymap_, keycode, group);
level++) {
const xkb_keysym_t *syms;
xkb_mod_mask_t masks[64];
auto nmasks = xkb_keymap_key_get_mods_for_level(
xkb_keymap_, keycode, group, level, masks, 64);
auto nsyms = xkb_keymap_key_get_syms_by_level(
xkb_keymap_, keycode, group, level, &syms);
if (nsyms == 0)
continue;
if (nsyms > 1)
LOG_WARN(
"multiple keysyms per keycode are not supported, keycode %d",
keycode);
synergy::KeyMap::KeyItem item{};
xkb_keysym_t keysym = syms[0];
KeySym sym = static_cast<KeyID>(keysym);
item.m_id = XWindowsUtil::mapKeySymToKeyID(sym);
item.m_button = static_cast<KeyButton>(keycode) - 8; // X keycode offset
item.m_group = group;
// For debugging only
char keysym_name[128] = {0};
xkb_keysym_get_name(keysym, keysym_name, sizeof(keysym_name));
// Set to all modifiers this key may be affected by
uint32_t mods_sensitive = 0;
for (auto n = 0U; n < nmasks; n++) {
mods_sensitive |= masks[n];
}
item.m_sensitive = convert_mod_mask(mods_sensitive);
uint32_t mods_required = 0;
for (std::size_t m = 0; m < nmasks; m++) {
mods_required |= masks[m];
}
item.m_required = convert_mod_mask(mods_required);
assign_generated_modifiers(keycode, item);
// add capslock version of key is sensitive to capslock
if (item.m_sensitive & KeyModifierShift &&
item.m_sensitive & KeyModifierCapsLock) {
item.m_required &= ~KeyModifierShift;
item.m_required |= KeyModifierCapsLock;
keyMap.addKeyEntry(item);
item.m_required |= KeyModifierShift;
item.m_required &= ~KeyModifierCapsLock;
}
keyMap.addKeyEntry(item);
}
}
}
// allow composition across groups
keyMap.allowGroupSwitchDuringCompose();
}
void EiKeyState::fakeKey(const Keystroke &keystroke) {
switch (keystroke.m_type) {
case Keystroke::kButton:
LOG_DEBUG1(
"fake key: %03x (%08x) %s", keystroke.m_data.m_button.m_button,
keystroke.m_data.m_button.m_client,
keystroke.m_data.m_button.m_press ? "down" : "up");
screen_->fakeKey(
keystroke.m_data.m_button.m_button, keystroke.m_data.m_button.m_press);
break;
default:
break;
}
}
KeyID EiKeyState::map_key_from_keyval(uint32_t keyval) const {
// FIXME: That might be a bit crude...?
xkb_keysym_t xkb_keysym = xkb_state_key_get_one_sym(xkb_state_, keyval);
KeySym keysym = static_cast<KeySym>(xkb_keysym);
KeyID keyid = XWindowsUtil::mapKeySymToKeyID(keysym);
LOG_DEBUG1(
"mapped key: code=%d keysym=0x%04lx to keyID=%d", keyval, keysym, keyid);
return keyid;
}
void EiKeyState::update_xkb_state(uint32_t keyval, bool is_pressed) {
LOG_DEBUG1("update key state: keyval=%d pressed=%i", keyval, is_pressed);
xkb_state_update_key(
xkb_state_, keyval, is_pressed ? XKB_KEY_DOWN : XKB_KEY_UP);
}
} // namespace synergy

View File

@ -0,0 +1,63 @@
/*
* synergy -- mouse and keyboard sharing utility
* Copyright (C) 2022 Red Hat, Inc.
* Copyright (C) 2024 Symless Ltd.
*
* This package is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* found in the file LICENSE that should have accompanied this file.
*
* This package is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#pragma once
#include "platform/EiScreen.h"
#include "synergy/KeyState.h"
struct xkb_context;
struct xkb_keymap;
struct xkb_state;
namespace synergy {
/// A key state for Ei
class EiKeyState : public KeyState {
public:
EiKeyState(EiScreen *screen, IEventQueue *events);
~EiKeyState();
void init(int fd, std::size_t len);
void init_default_keymap();
// IKeyState overrides
bool fakeCtrlAltDel() override;
KeyModifierMask pollActiveModifiers() const override;
std::int32_t pollActiveGroup() const override;
void pollPressedKeys(KeyButtonSet &pressedKeys) const override;
KeyID map_key_from_keyval(std::uint32_t keyval) const;
void update_xkb_state(std::uint32_t keyval, bool is_pressed);
protected:
// KeyState overrides
void getKeyMap(KeyMap &keyMap) override;
void fakeKey(const Keystroke &keystroke) override;
private:
std::uint32_t convert_mod_mask(std::uint32_t xkb_mask) const;
void assign_generated_modifiers(std::uint32_t keycode, KeyMap::KeyItem &item);
EiScreen *screen_ = nullptr;
xkb_context *xkb_ = nullptr;
xkb_keymap *xkb_keymap_ = nullptr;
xkb_state *xkb_state_ = nullptr;
};
} // namespace synergy

View File

@ -0,0 +1,852 @@
/*
* synergy -- mouse and keyboard sharing utility
* Copyright (C) 2022 Red Hat, Inc.
* Copyright (C) 2024 Symless Ltd.
*
* This package is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* found in the file LICENSE that should have accompanied this file.
*
* This package is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#include "platform/EiScreen.h"
#include "arch/Arch.h"
#include "arch/XArch.h"
#include "base/IEventQueue.h"
#include "base/Log.h"
#include "base/Stopwatch.h"
#include "base/TMethodEventJob.h"
#include "platform/EiEventQueueBuffer.h"
#include "platform/EiKeyState.h"
#include "synergy/Clipboard.h"
#include "synergy/KeyMap.h"
#include "synergy/XScreen.h"
#if WINAPI_LIBPORTAL
#include "platform/PortalInputCapture.h"
#include "platform/PortalRemoteDesktop.h"
#endif
#include <algorithm>
#include <cmath>
#include <cstdlib>
#include <cstring>
#include <unistd.h>
#include <vector>
struct ScrollRemainder {
double x, y; // scroll remainder in pixels
};
namespace synergy {
EiScreen::EiScreen(bool is_primary, IEventQueue *events, bool use_portal)
: PlatformScreen(events),
is_primary_(is_primary),
events_(events),
w_(1),
h_(1),
is_on_screen_(is_primary) {
init_ei();
key_state_ = new EiKeyState(this, events);
// install event handlers
events_->adoptHandler(
Event::kSystem, events_->getSystemTarget(),
new TMethodEventJob<EiScreen>(this, &EiScreen::handleSystemEvent));
if (use_portal) {
events_->adoptHandler(
events_->forEi().connected(), getEventTarget(),
new TMethodEventJob<EiScreen>(
this, &EiScreen::handle_connected_to_eis_event));
if (is_primary) {
#if HAVE_LIBPORTAL_INPUTCAPTURE
portal_input_capture_ = new PortalInputCapture(this, events_);
#else
throw std::invalid_argument(
"Missing libportal InputCapture portal support");
#endif // HAVE_LIBPORTAL_INPUTCAPTURE
} else {
#if WINAPI_LIBPORTAL
events_->adoptHandler(
events_->forEi().sessionClosed(), getEventTarget(),
new TMethodEventJob<EiScreen>(
this, &EiScreen::handle_portal_session_closed));
portal_remote_desktop_ = new PortalRemoteDesktop(this, events_);
#else
throw std::invalid_argument(
"Missing libportal RemoteDesktop portal support");
#endif // WINAPI_LIBPORTAL
}
} else {
// Note: socket backend does not support reconnections
auto rc = ei_setup_backend_socket(ei_, nullptr);
if (rc != 0) {
LOG_DEBUG("ei init error: %s", strerror(-rc));
throw std::runtime_error("Failed to init ei context");
}
}
}
EiScreen::~EiScreen() {
events_->adoptBuffer(nullptr);
events_->removeHandler(Event::kSystem, events_->getSystemTarget());
cleanup_ei();
delete key_state_;
#if WINAPI_LIBPORTAL
delete portal_remote_desktop_;
#endif
#if HAVE_LIBPORTAL_INPUTCAPTURE
delete portal_input_capture_;
#endif
}
void EiScreen::handle_ei_log_event(
ei *ei, ei_log_priority priority, const char *message,
ei_log_context *context) {
switch (priority) {
case EI_LOG_PRIORITY_DEBUG:
LOG_DEBUG("ei: %s", message);
break;
case EI_LOG_PRIORITY_INFO:
LOG_INFO("ei: %s", message);
break;
case EI_LOG_PRIORITY_WARNING:
LOG_WARN("ei: %s", message);
break;
case EI_LOG_PRIORITY_ERROR:
LOG_ERR("ei: %s", message);
break;
default:
LOG_PRINT("ei: %s", message);
break;
}
}
void EiScreen::init_ei() {
if (is_primary_) {
ei_ = ei_new_receiver(nullptr); // we receive from the display server
} else {
ei_ = ei_new_sender(nullptr); // we send to the display server
}
ei_set_user_data(ei_, this);
ei_log_set_priority(ei_, EI_LOG_PRIORITY_DEBUG);
ei_log_set_handler(ei_, cb_handle_ei_log_event);
ei_configure_name(ei_, "synergy client");
// install the platform event queue
events_->adoptBuffer(nullptr);
events_->adoptBuffer(new EiEventQueueBuffer(this, ei_, events_));
}
void EiScreen::cleanup_ei() {
if (ei_pointer_) {
free(ei_device_get_user_data(ei_pointer_));
ei_device_set_user_data(ei_pointer_, nullptr);
ei_pointer_ = ei_device_unref(ei_pointer_);
}
if (ei_keyboard_) {
free(ei_device_get_user_data(ei_keyboard_));
ei_device_set_user_data(ei_keyboard_, nullptr);
ei_keyboard_ = ei_device_unref(ei_keyboard_);
}
if (ei_abs_) {
free(ei_device_get_user_data(ei_abs_));
ei_device_set_user_data(ei_abs_, nullptr);
ei_abs_ = ei_device_unref(ei_abs_);
}
ei_seat_unref(ei_seat_);
for (auto it = ei_devices_.begin(); it != ei_devices_.end(); it++) {
free(ei_device_get_user_data(*it));
ei_device_set_user_data(*it, nullptr);
ei_device_unref(*it);
}
ei_devices_.clear();
ei_ = ei_unref(ei_);
}
void *EiScreen::getEventTarget() const {
return const_cast<void *>(static_cast<const void *>(this));
}
bool EiScreen::getClipboard(ClipboardID id, IClipboard *clipboard) const {
return false;
}
void EiScreen::getShape(int32_t &x, int32_t &y, int32_t &w, int32_t &h) const {
x = x_;
y = y_;
w = w_;
h = h_;
}
void EiScreen::getCursorPos(int32_t &x, int32_t &y) const {
x = cursor_x_;
y = cursor_y_;
}
void EiScreen::reconfigure(uint32_t) {
// do nothing
}
void EiScreen::warpCursor(int32_t x, int32_t y) {
cursor_x_ = x;
cursor_y_ = y;
}
std::uint32_t EiScreen::registerHotKey(KeyID key, KeyModifierMask mask) {
static std::uint32_t next_id;
std::uint32_t id = std::min(++next_id, 1u);
// Bug: id rollover means duplicate hotkey ids. Oh well.
auto set = hotkeys_.find(key);
if (set == hotkeys_.end()) {
hotkeys_.emplace(key, HotKeySet{key});
set = hotkeys_.find(key);
}
set->second.add_item(HotKeyItem(mask, id));
return id;
}
void EiScreen::unregisterHotKey(uint32_t id) {
for (auto &set : hotkeys_) {
if (set.second.remove_by_id(id)) {
break;
}
}
}
void EiScreen::fakeInputBegin() {
// FIXME -- not implemented
}
void EiScreen::fakeInputEnd() {
// FIXME -- not implemented
}
std::int32_t EiScreen::getJumpZoneSize() const { return 1; }
bool EiScreen::isAnyMouseButtonDown(uint32_t &buttonID) const { return false; }
void EiScreen::getCursorCenter(int32_t &x, int32_t &y) const {
x = x_ + w_ / 2;
y = y_ + h_ / 2;
}
void EiScreen::fakeMouseButton(ButtonID button, bool press) {
uint32_t code;
if (!ei_pointer_)
return;
switch (button) {
case kButtonLeft:
code = 0x110; // BTN_LEFT
break;
case kButtonMiddle:
code = 0x112; // BTN_MIDDLE
break;
case kButtonRight:
code = 0x111; // BTN_RIGHT
break;
default:
code = 0x110 + (button - 1);
break;
}
ei_device_button_button(ei_pointer_, code, press);
ei_device_frame(ei_pointer_, ei_now(ei_));
}
void EiScreen::fakeMouseMove(int32_t x, int32_t y) {
// We get one motion event before enter() with the target position
if (!is_on_screen_) {
cursor_x_ = x;
cursor_y_ = y;
return;
}
if (!ei_abs_)
return;
ei_device_pointer_motion_absolute(ei_abs_, x, y);
ei_device_frame(ei_abs_, ei_now(ei_));
}
void EiScreen::fakeMouseRelativeMove(int32_t dx, int32_t dy) const {
if (!ei_pointer_)
return;
ei_device_pointer_motion(ei_pointer_, dx, dy);
ei_device_frame(ei_pointer_, ei_now(ei_));
}
void EiScreen::fakeMouseWheel(int32_t xDelta, int32_t yDelta) const {
if (!ei_pointer_)
return;
// libei and synergy seem to use opposite directions, so we have
// to send EI the opposite of the value received if we want to remain
// compatible with other platforms (including X11).
ei_device_scroll_discrete(ei_pointer_, -xDelta, -yDelta);
ei_device_frame(ei_pointer_, ei_now(ei_));
}
void EiScreen::fakeKey(uint32_t keycode, bool is_down) const {
if (!ei_keyboard_)
return;
auto xkb_keycode = keycode + 8;
key_state_->update_xkb_state(xkb_keycode, is_down);
ei_device_keyboard_key(ei_keyboard_, keycode, is_down);
ei_device_frame(ei_keyboard_, ei_now(ei_));
}
void EiScreen::enable() {
// Nothing really to be done here
}
void EiScreen::disable() {
// Nothing really to be done here, maybe cleanup in the future but ideally
// that's handled elsewhere
}
void EiScreen::enter() {
is_on_screen_ = true;
if (!is_primary_) {
++sequence_number_;
if (ei_pointer_) {
ei_device_start_emulating(ei_pointer_, sequence_number_);
}
if (ei_keyboard_) {
ei_device_start_emulating(ei_keyboard_, sequence_number_);
}
if (ei_abs_) {
ei_device_start_emulating(ei_abs_, sequence_number_);
fakeMouseMove(cursor_x_, cursor_y_);
}
}
#if HAVE_LIBPORTAL_INPUTCAPTURE
else {
LOG_DEBUG("releasing input capture at x=%i y=%i", cursor_x_, cursor_y_);
portal_input_capture_->release(cursor_x_, cursor_y_);
}
#endif
}
bool EiScreen::leave() {
if (!is_primary_) {
if (ei_pointer_) {
ei_device_stop_emulating(ei_pointer_);
}
if (ei_keyboard_) {
ei_device_stop_emulating(ei_keyboard_);
}
if (ei_abs_) {
ei_device_stop_emulating(ei_abs_);
}
}
is_on_screen_ = false;
return true;
}
bool EiScreen::setClipboard(ClipboardID id, const IClipboard *clipboard) {
return false;
}
void EiScreen::checkClipboards() {
// do nothing, we're always up to date
}
void EiScreen::openScreensaver(bool notify) {
// FIXME
}
void EiScreen::closeScreensaver() {
// FIXME
}
void EiScreen::screensaver(bool activate) {
// FIXME
}
void EiScreen::resetOptions() {
// Should reset options to neutral, see setOptions().
// We don't have ei-specific options, nothing to do here
}
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 is_primary_; }
void EiScreen::update_shape() {
for (auto it = ei_devices_.begin(); it != ei_devices_.end(); it++) {
auto idx = 0;
struct ei_region *r;
while ((r = ei_device_get_region(*it, idx++)) != nullptr) {
x_ = std::min(ei_region_get_x(r), x_);
y_ = std::min(ei_region_get_y(r), y_);
w_ = std::max(ei_region_get_x(r) + ei_region_get_width(r), w_);
h_ = std::max(ei_region_get_y(r) + ei_region_get_height(r), h_);
}
}
LOG_NOTE("logical output size: %dx%d@%d.%d", w_, h_, x_, y_);
cursor_x_ = x_ + w_ / 2;
cursor_y_ = y_ + h_ / 2;
sendEvent(events_->forIScreen().shapeChanged(), nullptr);
}
void EiScreen::add_device(struct ei_device *device) {
LOG_DEBUG("adding device %s", ei_device_get_name(device));
// Noteworthy: EI in principle supports multiple devices with multiple
// capabilities, so there may be more than one logical pointer (or even
// multiple seats). Supporting this is ... tricky so for now we go the easy
// route: one device for each capability. Note this may be the same device
// if the first device comes with multiple capabilities.
if (!ei_pointer_ && ei_device_has_capability(device, EI_DEVICE_CAP_POINTER) &&
ei_device_has_capability(device, EI_DEVICE_CAP_BUTTON) &&
ei_device_has_capability(device, EI_DEVICE_CAP_SCROLL)) {
ei_pointer_ = ei_device_ref(device);
}
if (!ei_keyboard_ &&
ei_device_has_capability(device, EI_DEVICE_CAP_KEYBOARD)) {
ei_keyboard_ = ei_device_ref(device);
struct ei_keymap *keymap = ei_device_keyboard_get_keymap(device);
if (keymap && ei_keymap_get_type(keymap) == EI_KEYMAP_TYPE_XKB) {
int fd = ei_keymap_get_fd(keymap);
size_t len = ei_keymap_get_size(keymap);
key_state_->init(fd, len);
} else {
// We rely on the EIS implementation to give us a keymap, otherwise we
// really have no idea what a keycode means (other than it's linux/input.h
// code) Where the EIS implementation does not tell us, we just default to
// whatever libxkbcommon thinks is default. At least this way we can
// influence with env vars what we get
LOG_WARN(
"keyboard device %s does not have a keymap, we are guessing",
ei_device_get_name(device));
key_state_->init_default_keymap();
}
key_state_->updateKeyMap();
}
if (!ei_abs_ &&
ei_device_has_capability(device, EI_DEVICE_CAP_POINTER_ABSOLUTE) &&
ei_device_has_capability(device, EI_DEVICE_CAP_BUTTON) &&
ei_device_has_capability(device, EI_DEVICE_CAP_SCROLL)) {
ei_abs_ = ei_device_ref(device);
}
ei_devices_.emplace_back(ei_device_ref(device));
update_shape();
}
void EiScreen::remove_device(struct ei_device *device) {
LOG_DEBUG("removing device %s", ei_device_get_name(device));
if (device == ei_pointer_)
ei_pointer_ = ei_device_unref(ei_pointer_);
if (device == ei_keyboard_)
ei_keyboard_ = ei_device_unref(ei_keyboard_);
if (device == ei_abs_)
ei_abs_ = ei_device_unref(ei_abs_);
for (auto it = ei_devices_.begin(); it != ei_devices_.end(); it++) {
if (*it == device) {
ei_devices_.erase(it);
ei_device_unref(device);
break;
}
}
update_shape();
}
void EiScreen::sendEvent(Event::Type type, void *data) {
events_->addEvent(Event(type, getEventTarget(), data));
}
ButtonID EiScreen::map_button_from_evdev(ei_event *event) const {
uint32_t button = ei_event_button_get_button(event);
switch (button) {
case 0x110:
return kButtonLeft;
case 0x111:
return kButtonRight;
case 0x112:
return kButtonMiddle;
case 0x113:
return kButtonExtra0;
case 0x114:
return kButtonExtra1;
default:
return kButtonNone;
}
return kButtonNone;
}
bool EiScreen::on_hotkey(KeyID keyid, bool is_pressed, KeyModifierMask mask) {
auto it = hotkeys_.find(keyid);
if (it == hotkeys_.end()) {
return false;
}
// Note: our mask (see on_key_event) only contains some modifiers
// but we don't put a limitation on modifiers in the hotkeys. So some
// key combinations may not work correctly, more effort is needed here.
auto id = it->second.find_by_mask(mask);
if (id != 0) {
Event::Type type = is_pressed ? events_->forIPrimaryScreen().hotKeyDown()
: events_->forIPrimaryScreen().hotKeyUp();
sendEvent(type, HotKeyInfo::alloc(id));
return true;
}
return false;
}
void EiScreen::on_key_event(ei_event *event) {
uint32_t keycode = ei_event_keyboard_get_key(event);
uint32_t keyval = keycode + 8;
bool pressed = ei_event_keyboard_get_key_is_press(event);
KeyID keyid = key_state_->map_key_from_keyval(keyval);
KeyButton keybutton = static_cast<KeyButton>(keyval);
key_state_->update_xkb_state(keyval, pressed);
KeyModifierMask mask = key_state_->pollActiveModifiers();
LOG_DEBUG1(
"event: key %s keycode=%d keyid=%d mask=0x%x",
pressed ? "press" : "release", keycode, keyid, mask);
if (is_primary_ && on_hotkey(keyid, pressed, mask)) {
return;
}
if (keyid != kKeyNone) {
key_state_->sendKeyEvent(
getEventTarget(), pressed, false, keyid, mask, 1, keybutton);
}
}
void EiScreen::on_button_event(ei_event *event) {
assert(is_primary_);
ButtonID button = map_button_from_evdev(event);
bool pressed = ei_event_button_get_is_press(event);
KeyModifierMask mask = key_state_->pollActiveModifiers();
LOG_DEBUG1(
"event: button %s button=%d mask=0x%x", pressed ? "press" : "release",
button, mask);
if (button == kButtonNone) {
LOG_DEBUG("event: button not recognized");
return;
}
auto eventType = pressed ? events_->forIPrimaryScreen().buttonDown()
: events_->forIPrimaryScreen().buttonUp();
sendEvent(eventType, ButtonInfo::alloc(button, mask));
}
void EiScreen::on_pointer_scroll_event(ei_event *event) {
// Ratio of 10 pixels == one wheel click because that's what mutter/gtk
// use (for historical reasons).
const int PIXELS_PER_WHEEL_CLICK = 10;
// Our logical wheel clicks are multiples 120, so we
// convert between the two and keep the remainders because
// we will very likely get subpixel scroll events.
// This means a single pixel is 120/PIXEL_TO_WHEEL_RATIO in wheel values.
const int PIXEL_TO_WHEEL_RATIO = 120 / PIXELS_PER_WHEEL_CLICK;
assert(is_primary_);
double dx = ei_event_scroll_get_dx(event);
double dy = ei_event_scroll_get_dy(event);
struct ei_device *device = ei_event_get_device(event);
LOG_DEBUG1("event: scroll (%.2f, %.2f)", dx, dy);
struct ScrollRemainder *remainder =
static_cast<struct ScrollRemainder *>(ei_device_get_user_data(device));
if (!remainder) {
remainder = new ScrollRemainder();
ei_device_set_user_data(device, remainder);
}
dx += remainder->x;
dy += remainder->y;
double x, y;
double rx = modf(dx, &x);
double ry = modf(dy, &y);
assert(!std::isnan(x) && !std::isinf(x));
assert(!std::isnan(y) && !std::isinf(y));
// libei and synergy seem to use opposite directions, so we have
// to send the opposite of the value reported by EI if we want to
// remain compatible with other platforms (including X11).
if (x != 0 || y != 0)
sendEvent(
events_->forIPrimaryScreen().wheel(),
WheelInfo::alloc(
(int32_t)-x * PIXEL_TO_WHEEL_RATIO,
(int32_t)-y * PIXEL_TO_WHEEL_RATIO));
remainder->x = rx;
remainder->y = ry;
}
void EiScreen::on_pointer_scroll_discrete_event(ei_event *event) {
// both libei and synergy use multiples of 120 to represent
// one scroll wheel click event so we can just forward things
// as-is.
assert(is_primary_);
std::int32_t dx = ei_event_scroll_get_discrete_dx(event);
std::int32_t dy = ei_event_scroll_get_discrete_dy(event);
LOG_DEBUG1("event: scroll discrete (%d, %d)", dx, dy);
// libei and synergy seem to use opposite directions, so we have
// to send the opposite of the value reported by EI if we want to
// remain compatible with other platforms (including X11).
sendEvent(events_->forIPrimaryScreen().wheel(), WheelInfo::alloc(-dx, -dy));
}
void EiScreen::on_motion_event(ei_event *event) {
assert(is_primary_);
double dx = ei_event_pointer_get_dx(event);
double dy = ei_event_pointer_get_dy(event);
if (is_on_screen_) {
LOG_DEBUG("event: motion on primary x=%i y=%i)", cursor_x_, cursor_y_);
sendEvent(
events_->forIPrimaryScreen().motionOnPrimary(),
MotionInfo::alloc(cursor_x_, cursor_y_));
#if HAVE_LIBPORTAL_INPUTCAPTURE
if (portal_input_capture_->is_active()) {
portal_input_capture_->release();
}
#endif
} else {
buffer_dx += dx;
buffer_dy += dy;
auto pixel_dx = static_cast<std::int32_t>(buffer_dx);
auto pixel_dy = static_cast<std::int32_t>(buffer_dy);
if (pixel_dx || pixel_dy) {
LOG_DEBUG("event: motion on secondary x=%d y=%d", pixel_dx, pixel_dy);
sendEvent(
events_->forIPrimaryScreen().motionOnSecondary(),
MotionInfo::alloc(pixel_dx, pixel_dy));
buffer_dx -= pixel_dx;
buffer_dy -= pixel_dy;
}
}
}
void EiScreen::on_abs_motion_event(ei_event *event) { assert(is_primary_); }
void EiScreen::handle_connected_to_eis_event(const Event &event, void *) {
int fd = static_cast<EiConnectInfo *>(event.getData())->m_fd;
LOG_DEBUG("eis connection established, fd=%d", fd);
auto rc = ei_setup_backend_fd(ei_, fd);
if (rc != 0) {
LOG_NOTE("failed to set up ei: %s", strerror(-rc));
}
}
void EiScreen::handle_portal_session_closed(const Event &event, void *) {
// Portal may or may EI_EVENT_DISCONNECT us before sending the DBus Closed
// signal Let's clean up either way.
cleanup_ei();
init_ei();
}
void EiScreen::handleSystemEvent(const Event &sysevent, void *) {
std::lock_guard<std::mutex> lock(mutex_);
bool disconnected = false;
// Only one ei_dispatch per system event, see the comment in
// EiEventQueueBuffer::addEvent
ei_dispatch(ei_);
struct ei_event *event;
while ((event = ei_get_event(ei_)) != nullptr) {
auto type = ei_event_get_type(event);
auto seat = ei_event_get_seat(event);
auto device = ei_event_get_device(event);
switch (type) {
case EI_EVENT_CONNECT:
LOG_DEBUG("connected to eis");
break;
case EI_EVENT_SEAT_ADDED:
if (!ei_seat_) {
ei_seat_ = ei_seat_ref(seat);
ei_seat_bind_capabilities(
ei_seat_, EI_DEVICE_CAP_POINTER, EI_DEVICE_CAP_POINTER_ABSOLUTE,
EI_DEVICE_CAP_KEYBOARD, EI_DEVICE_CAP_BUTTON, EI_DEVICE_CAP_SCROLL,
nullptr);
LOG_DEBUG("ei: using seat %s", ei_seat_get_name(ei_seat_));
// we don't care about touch
}
break;
case EI_EVENT_DEVICE_ADDED:
if (seat == ei_seat_) {
add_device(device);
} else {
LOG_INFO("seat %s is ignored", ei_seat_get_name(ei_seat_));
}
break;
case EI_EVENT_DEVICE_REMOVED:
remove_device(device);
break;
case EI_EVENT_SEAT_REMOVED:
if (seat == ei_seat_) {
ei_seat_ = ei_seat_unref(ei_seat_);
}
break;
case EI_EVENT_DISCONNECT:
// We're using libei which emulates the various seat/device remove events
// so by the time we get here our EiScreen should be in a neutral state.
//
// We don't do anything here, we let the portal's Session.Closed signal
// handle the rest.
LOG_WARN("disconnected from eis");
disconnected = true;
break;
case EI_EVENT_DEVICE_PAUSED:
LOG_DEBUG("device %s is paused", ei_device_get_name(device));
break;
case EI_EVENT_DEVICE_RESUMED:
LOG_DEBUG("device %s is resumed", ei_device_get_name(device));
if (!is_primary_ && is_on_screen_) {
ei_device_start_emulating(device, ++sequence_number_);
}
break;
case EI_EVENT_KEYBOARD_MODIFIERS:
// FIXME
break;
// events below are for a receiver context (barriers)
case EI_EVENT_FRAME:
break;
case EI_EVENT_DEVICE_START_EMULATING:
LOG_DEBUG("device %s starts emulating", ei_device_get_name(device));
break;
case EI_EVENT_DEVICE_STOP_EMULATING:
LOG_DEBUG("device %s stops emulating", ei_device_get_name(device));
break;
case EI_EVENT_KEYBOARD_KEY:
on_key_event(event);
break;
case EI_EVENT_BUTTON_BUTTON:
on_button_event(event);
break;
case EI_EVENT_POINTER_MOTION:
on_motion_event(event);
break;
case EI_EVENT_POINTER_MOTION_ABSOLUTE:
on_abs_motion_event(event);
break;
case EI_EVENT_TOUCH_UP:
break;
case EI_EVENT_TOUCH_MOTION:
break;
case EI_EVENT_TOUCH_DOWN:
break;
case EI_EVENT_SCROLL_DELTA:
on_pointer_scroll_event(event);
break;
case EI_EVENT_SCROLL_DISCRETE:
on_pointer_scroll_discrete_event(event);
break;
case EI_EVENT_SCROLL_STOP:
case EI_EVENT_SCROLL_CANCEL:
break;
}
ei_event_unref(event);
}
if (disconnected)
ei_ = ei_unref(ei_);
}
void EiScreen::updateButtons() {
// libei relies on the EIS implementation to keep our button count correct,
// so there's not much we need to/can do here.
}
IKeyState *EiScreen::getKeyState() const { return key_state_; }
String EiScreen::getSecureInputApp() const {
throw std::runtime_error("Not implemented");
}
EiScreen::HotKeyItem::HotKeyItem(std::uint32_t mask, std::uint32_t id)
: mask_(mask),
id_(id) {}
EiScreen::HotKeySet::HotKeySet(KeyID key) : id_(key) {}
bool EiScreen::HotKeySet::remove_by_id(std::uint32_t id) {
for (auto it = set_.begin(); it != set_.end(); ++it) {
if (it->id_ == id) {
set_.erase(it);
return true;
}
}
return false;
}
void EiScreen::HotKeySet::add_item(HotKeyItem item) { set_.push_back(item); }
std::uint32_t EiScreen::HotKeySet::find_by_mask(std::uint32_t mask) const {
for (const auto &item : set_) {
if (item.mask_ == mask) {
return item.id_;
}
}
return 0;
}
} // namespace synergy

201
src/lib/platform/EiScreen.h Normal file
View File

@ -0,0 +1,201 @@
/*
* synergy -- mouse and keyboard sharing utility
* Copyright (C) 2022 Red Hat, Inc.
* Copyright (C) 2024 Symless Ltd.
*
* This package is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* found in the file LICENSE that should have accompanied this file.
*
* This package is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#pragma once
#include "config.h"
#include "synergy/KeyMap.h"
#include "synergy/PlatformScreen.h"
#include <libei.h>
#include <mutex>
#include <set>
#include <vector>
struct ei;
struct ei_event;
struct ei_seat;
struct ei_device;
namespace synergy {
class EiClipboard;
class EiKeyState;
class PortalRemoteDesktop;
#if HAVE_LIBPORTAL_INPUTCAPTURE
class PortalInputCapture;
#endif
//! Implementation of IPlatformScreen for X11
class EiScreen : public PlatformScreen {
public:
EiScreen(bool is_primary, IEventQueue *events, bool use_portal);
~EiScreen();
// IScreen overrides
void *getEventTarget() const override;
bool getClipboard(ClipboardID id, IClipboard *) const override;
void getShape(
std::int32_t &x, std::int32_t &y, std::int32_t &width,
std::int32_t &height) const override;
void getCursorPos(std::int32_t &x, std::int32_t &y) const override;
// IPrimaryScreen overrides
void reconfigure(std::uint32_t activeSides) override;
void warpCursor(std::int32_t x, std::int32_t y) override;
std::uint32_t registerHotKey(KeyID key, KeyModifierMask mask) override;
void unregisterHotKey(std::uint32_t id) override;
void fakeInputBegin() override;
void fakeInputEnd() override;
std::int32_t getJumpZoneSize() const override;
bool isAnyMouseButtonDown(std::uint32_t &buttonID) const override;
void getCursorCenter(std::int32_t &x, std::int32_t &y) const override;
// ISecondaryScreen overrides
void fakeMouseButton(ButtonID id, bool press) override;
void fakeMouseMove(std::int32_t x, std::int32_t y) override;
void fakeMouseRelativeMove(std::int32_t dx, std::int32_t dy) const override;
void fakeMouseWheel(std::int32_t xDelta, std::int32_t yDelta) const override;
void fakeKey(std::uint32_t keycode, bool is_down) const;
// IPlatformScreen overrides
void enable() override;
void disable() override;
void enter() override;
bool leave() override;
bool setClipboard(ClipboardID, const IClipboard *) override;
void checkClipboards() override;
void openScreensaver(bool notify) override;
void closeScreensaver() override;
void screensaver(bool activate) override;
void resetOptions() override;
void setOptions(const OptionsList &options) override;
void setSequenceNumber(std::uint32_t) override;
bool isPrimary() const override;
protected:
// IPlatformScreen overrides
void handleSystemEvent(const Event &event, void *) override;
void updateButtons() override;
IKeyState *getKeyState() const override;
String getSecureInputApp() const override;
void update_shape();
void add_device(ei_device *device);
void remove_device(ei_device *device);
private:
void init_ei();
void cleanup_ei();
void sendEvent(Event::Type type, void *data);
ButtonID map_button_from_evdev(ei_event *event) const;
void on_key_event(ei_event *event);
void on_button_event(ei_event *event);
void send_wheel_events(
ei_device *device, const int threshold, double dx, double dy,
bool is_discrete);
void on_pointer_scroll_event(ei_event *event);
void on_pointer_scroll_discrete_event(ei_event *event);
void on_motion_event(ei_event *event);
void on_abs_motion_event(ei_event *event);
bool on_hotkey(KeyID key, bool is_press, KeyModifierMask mask);
void handle_ei_log_event(
ei *ei, ei_log_priority priority, const char *message,
ei_log_context *context);
void handle_connected_to_eis_event(const Event &event, void *);
void handle_portal_session_closed(const Event &event, void *);
static void cb_handle_ei_log_event(
ei *ei, ei_log_priority priority, const char *message,
ei_log_context *context) {
auto screen = reinterpret_cast<EiScreen *>(ei_get_user_data(ei));
screen->handle_ei_log_event(ei, priority, message, context);
}
private:
// true if screen is being used as a primary screen, false otherwise
bool is_primary_ = false;
IEventQueue *events_ = nullptr;
// keyboard stuff
EiKeyState *key_state_ = nullptr;
std::vector<ei_device *> ei_devices_;
ei *ei_ = nullptr;
ei_seat *ei_seat_ = nullptr;
ei_device *ei_pointer_ = nullptr;
ei_device *ei_keyboard_ = nullptr;
ei_device *ei_abs_ = nullptr;
std::uint32_t sequence_number_ = 0;
std::uint32_t x_ = 0;
std::uint32_t y_ = 0;
std::uint32_t w_ = 0;
std::uint32_t h_ = 0;
// true if mouse has entered the screen
bool is_on_screen_;
// server: last pointer position
// client: position sent before enter()
std::int32_t cursor_x_ = 0;
std::int32_t cursor_y_ = 0;
double buffer_dx = 0;
double buffer_dy = 0;
mutable std::mutex mutex_;
PortalRemoteDesktop *portal_remote_desktop_ = nullptr;
#if HAVE_LIBPORTAL_INPUTCAPTURE
PortalInputCapture *portal_input_capture_ = nullptr;
#endif
struct HotKeyItem {
public:
HotKeyItem(std::uint32_t mask, std::uint32_t id);
bool operator<(const HotKeyItem &other) const {
return mask_ < other.mask_;
};
public:
std::uint32_t mask_ = 0;
std::uint32_t id_ = 0; // for registering the hotkey
};
class HotKeySet {
public:
HotKeySet(KeyID keyid);
KeyID keyid() const { return id_; };
bool remove_by_id(std::uint32_t id);
void add_item(HotKeyItem item);
std::uint32_t find_by_mask(std::uint32_t mask) const;
private:
KeyID id_ = 0;
std::vector<HotKeyItem> set_;
};
using HotKeyMap = std::map<KeyID, HotKeySet>;
HotKeyMap hotkeys_;
};
} // namespace synergy

View File

@ -0,0 +1,409 @@
/*
* synergy -- mouse and keyboard sharing utility
* Copyright (C) 2022 Red Hat, Inc.
* Copyright (C) 2024 Symless Ltd.
*
* This package is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* found in the file LICENSE that should have accompanied this file.
*
* This package is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#include "config.h"
#if HAVE_LIBPORTAL_INPUTCAPTURE
#include "base/Event.h"
#include "base/Log.h"
#include "base/TMethodJob.h"
#include "platform/PortalInputCapture.h"
#include <sys/socket.h> // for EIS fd hack, remove
#include <sys/un.h> // for EIS fd hack, remove
namespace synergy {
enum signals {
SESSION_CLOSED,
DISABLED,
ACTIVATED,
DEACTIVATED,
ZONES_CHANGED,
_N_SIGNALS,
};
PortalInputCapture::PortalInputCapture(EiScreen *screen, IEventQueue *events)
: screen_(screen),
events_(events),
portal_(xdp_portal_new()),
signals_(_N_SIGNALS) {
glib_main_loop_ = g_main_loop_new(nullptr, true);
glib_thread_ = new Thread(new TMethodJob<PortalInputCapture>(
this, &PortalInputCapture::glib_thread));
auto init_capture_cb = [](gpointer data) -> gboolean {
return reinterpret_cast<PortalInputCapture *>(data)
->init_input_capture_session();
};
g_idle_add(init_capture_cb, this);
}
PortalInputCapture::~PortalInputCapture() {
if (g_main_loop_is_running(glib_main_loop_))
g_main_loop_quit(glib_main_loop_);
if (glib_thread_) {
glib_thread_->cancel();
glib_thread_->wait();
glib_thread_ = nullptr;
g_main_loop_unref(glib_main_loop_);
glib_main_loop_ = nullptr;
}
if (session_) {
XdpSession *parent_session =
xdp_input_capture_session_get_session(session_);
g_signal_handler_disconnect(
G_OBJECT(parent_session), signals_[SESSION_CLOSED]);
g_signal_handler_disconnect(session_, signals_[DISABLED]);
g_signal_handler_disconnect(session_, signals_[ACTIVATED]);
g_signal_handler_disconnect(session_, signals_[DEACTIVATED]);
g_signal_handler_disconnect(session_, signals_[ZONES_CHANGED]);
g_object_unref(session_);
}
for (auto b : barriers_) {
g_object_unref(b);
}
barriers_.clear();
g_object_unref(portal_);
}
gboolean PortalInputCapture::timeout_handler() {
return true; // keep re-triggering
}
int PortalInputCapture::fake_eis_fd() {
auto path = std::getenv("LIBEI_SOCKET");
if (!path) {
LOG_DEBUG("cannot fake eis socket, env var not set: LIBEI_SOCKET");
return -1;
}
auto sock = socket(AF_UNIX, SOCK_STREAM | SOCK_NONBLOCK, 0);
// Dealing with the socket directly because nothing in lib/... supports
// AF_UNIX and I'm too lazy to fix all this for a temporary hack
int fd = sock;
struct sockaddr_un addr = {
.sun_family = AF_UNIX,
.sun_path = {0},
};
std::snprintf(addr.sun_path, sizeof(addr.sun_path), "%s", path);
auto result = connect(fd, (struct sockaddr *)&addr, sizeof(addr));
if (result != 0) {
LOG_DEBUG("faked eis fd failed: %s", strerror(errno));
}
return sock;
}
void PortalInputCapture::cb_session_closed(XdpSession *session) {
LOG_ERR("portal input capture session was closed, exiting");
g_main_loop_quit(glib_main_loop_);
events_->addEvent(Event::kQuit);
g_signal_handler_disconnect(session, signals_[SESSION_CLOSED]);
signals_[SESSION_CLOSED] = 0;
}
void PortalInputCapture::cb_init_input_capture_session(
GObject *object, GAsyncResult *res) {
LOG_DEBUG("portal session ready");
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(glib_main_loop_);
events_->addEvent(Event::kQuit);
return;
}
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);
// FIXME: Development hack to avoid having to assemble all parts just for
// testing this code.
fd = fake_eis_fd();
if (fd < 0) {
g_main_loop_quit(glib_main_loop_);
events_->addEvent(Event::kQuit);
return;
}
}
// Socket ownership is transferred to the EiScreen
events_->addEvent(Event(
events_->forEi().connected(), screen_->getEventTarget(),
EiScreen::EiConnectInfo::alloc(fd)));
// FIXME: the lambda trick doesn't work here for unknown reasons, we need
// the static function
signals_[DISABLED] = g_signal_connect(
G_OBJECT(session), "disabled", G_CALLBACK(cb_disabled_cb), this);
signals_[ACTIVATED] = g_signal_connect(
G_OBJECT(session_), "activated", G_CALLBACK(cb_activated_cb), this);
signals_[DEACTIVATED] = g_signal_connect(
G_OBJECT(session_), "deactivated", G_CALLBACK(cb_deactivated_cb), this);
signals_[ZONES_CHANGED] = g_signal_connect(
G_OBJECT(session_), "zones-changed", G_CALLBACK(cb_zones_changed_cb),
this);
XdpSession *parent_session = xdp_input_capture_session_get_session(session);
signals_[SESSION_CLOSED] = g_signal_connect(
G_OBJECT(parent_session), "closed", G_CALLBACK(cb_session_closed_cb),
this);
cb_zones_changed(session_, nullptr);
}
void PortalInputCapture::cb_set_pointer_barriers(
GObject *object, GAsyncResult *res) {
g_autoptr(GError) error = nullptr;
auto failed_list = xdp_input_capture_session_set_pointer_barriers_finish(
session_, res, &error);
if (failed_list) {
auto it = failed_list;
while (it) {
guint id;
g_object_get(it->data, "id", &id, nullptr);
for (auto elem = barriers_.begin(); elem != barriers_.end(); elem++) {
if (*elem == it->data) {
int x1, x2, y1, y2;
g_object_get(
G_OBJECT(*elem), "x1", &x1, "x2", &x2, "y1", &y1, "y2", &y2,
nullptr);
LOG_WARN(
"failed to apply barrier %d (%d/%d-%d/%d)", id, x1, y1, x2, y2);
g_object_unref(*elem);
barriers_.erase(elem);
break;
}
}
it = it->next;
}
}
g_list_free_full(failed_list, g_object_unref);
enable();
}
gboolean PortalInputCapture::init_input_capture_session() {
LOG_DEBUG("setting up input capture session");
xdp_portal_create_input_capture_session(
portal_,
nullptr, // parent
static_cast<XdpInputCapability>(
XDP_INPUT_CAPABILITY_KEYBOARD | XDP_INPUT_CAPABILITY_POINTER),
nullptr, // cancellable
[](GObject *obj, GAsyncResult *res, gpointer data) {
reinterpret_cast<PortalInputCapture *>(data)
->cb_init_input_capture_session(obj, res);
},
this);
return false;
}
void PortalInputCapture::enable() {
if (!enabled_) {
LOG_DEBUG("enabling the portal input capture session");
xdp_input_capture_session_enable(session_);
enabled_ = true;
}
}
void PortalInputCapture::disable() {
if (enabled_) {
LOG_DEBUG("disabling the portal input capture session");
xdp_input_capture_session_disable(session_);
enabled_ = false;
}
}
void PortalInputCapture::release() {
LOG_DEBUG("releasing input capture session, id=%d", activation_id_);
xdp_input_capture_session_release(session_, activation_id_);
is_active_ = false;
}
void PortalInputCapture::release(double x, double y) {
LOG_DEBUG(
"releasing input capture session, id=%d x=%.1f y=%.1f", activation_id_, x,
y);
xdp_input_capture_session_release_at(session_, activation_id_, x, y);
is_active_ = false;
}
void PortalInputCapture::cb_disabled(XdpInputCaptureSession *session) {
LOG_DEBUG("portal cb disabled");
if (!enabled_)
return; // Nothing to do
enabled_ = false;
is_active_ = false;
// FIXME: need some better heuristics here of when we want to enable again
// But we don't know *why* we got disabled (and it's doubtfull we ever
// will), so we just assume that the zones will change or something and we
// can re-enable again
// ... very soon
g_timeout_add(
1000,
[](gpointer data) -> gboolean {
reinterpret_cast<PortalInputCapture *>(data)->enable();
return false;
},
this);
}
void PortalInputCapture::cb_activated(
XdpInputCaptureSession *session, std::uint32_t activation_id,
GVariant *options) {
LOG_DEBUG("portal cb activated, id=%d", activation_id);
if (options) {
gdouble x, y;
if (g_variant_lookup(options, "cursor_position", "(dd)", &x, &y)) {
screen_->warpCursor((int)x, (int)y);
} else {
LOG_WARN("failed to get cursor position");
}
} else {
LOG_WARN("activation has no options");
}
activation_id_ = activation_id;
is_active_ = true;
}
void PortalInputCapture::cb_deactivated(
XdpInputCaptureSession *session, std::uint32_t activation_id,
GVariant *options) {
LOG_DEBUG("cb deactivated, id=%i", activation_id);
is_active_ = false;
}
void PortalInputCapture::cb_zones_changed(
XdpInputCaptureSession *session, GVariant *options) {
for (auto b : barriers_)
g_object_unref(b);
barriers_.clear();
auto zones = xdp_input_capture_session_get_zones(session);
while (zones != nullptr) {
guint w, h;
gint x, y;
g_object_get(
zones->data, "width", &w, "height", &h, "x", &x, "y", &y, nullptr);
LOG_DEBUG("input capture zone, %dx%d@%d,%d", w, h, x, y);
int x1, x2, y1, y2;
// Hardcoded behaviour: our pointer barriers are always at the edges of
// all zones. Since the implementation is supposed to reject the ones in
// the wrong place, we can just install barriers everywhere and let EIS
// figure it out. Also a lot easier to implement for now though it doesn't
// cover differently-sized screens...
auto id = barriers_.size() + 1;
x1 = x;
y1 = y;
x2 = x + w - 1;
y2 = y;
LOG_DEBUG("barrier (top) %zd at %d,%d-%d,%d", id, x1, y1, x2, y2);
barriers_.push_back(XDP_INPUT_CAPTURE_POINTER_BARRIER(g_object_new(
XDP_TYPE_INPUT_CAPTURE_POINTER_BARRIER, "id", id, "x1", x1, "y1", y1,
"x2", x2, "y2", y2, nullptr)));
id = barriers_.size() + 1;
x1 = x + w;
y1 = y;
x2 = x + w;
y2 = y + h - 1;
LOG_DEBUG("barrier (right) %zd at %d,%d-%d,%d", id, x1, y1, x2, y2);
barriers_.push_back(XDP_INPUT_CAPTURE_POINTER_BARRIER(g_object_new(
XDP_TYPE_INPUT_CAPTURE_POINTER_BARRIER, "id", id, "x1", x1, "y1", y1,
"x2", x2, "y2", y2, nullptr)));
id = barriers_.size() + 1;
x1 = x;
y1 = y;
x2 = x;
y2 = y + h - 1;
LOG_DEBUG("barrier (left) %zd at %d,%d-%d,%d", id, x1, y1, x2, y2);
barriers_.push_back(XDP_INPUT_CAPTURE_POINTER_BARRIER(g_object_new(
XDP_TYPE_INPUT_CAPTURE_POINTER_BARRIER, "id", id, "x1", x1, "y1", y1,
"x2", x2, "y2", y2, nullptr)));
id = barriers_.size() + 1;
x1 = x;
y1 = y + h;
x2 = x + w - 1;
y2 = y + h;
LOG_DEBUG("barrier (bottom) %zd at %d,%d-%d,%d", id, x1, y1, x2, y2);
barriers_.push_back(XDP_INPUT_CAPTURE_POINTER_BARRIER(g_object_new(
XDP_TYPE_INPUT_CAPTURE_POINTER_BARRIER, "id", id, "x1", x1, "y1", y1,
"x2", x2, "y2", y2, nullptr)));
zones = zones->next;
}
GList *list = nullptr;
for (auto const &b : barriers_) {
list = g_list_append(list, b);
}
xdp_input_capture_session_set_pointer_barriers(
session_, list,
nullptr, // cancellable
[](GObject *obj, GAsyncResult *res, gpointer data) {
reinterpret_cast<PortalInputCapture *>(data)->cb_set_pointer_barriers(
obj, res);
},
this);
}
void PortalInputCapture::glib_thread(void *) {
auto context = g_main_loop_get_context(glib_main_loop_);
LOG_DEBUG("glib thread running");
while (g_main_loop_is_running(glib_main_loop_)) {
Thread::testCancel();
g_main_context_iteration(context, true);
}
LOG_DEBUG("shutting down glib thread");
}
} // namespace synergy
#endif

View File

@ -0,0 +1,109 @@
/*
* synergy -- mouse and keyboard sharing utility
* Copyright (C) 2022 Red Hat, Inc.
* Copyright (C) 2024 Symless Ltd.
*
* This package is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* found in the file LICENSE that should have accompanied this file.
*
* This package is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#pragma once
#include "config.h"
#if HAVE_LIBPORTAL_INPUTCAPTURE
#include "mt/Thread.h"
#include "platform/EiScreen.h"
#include <glib.h>
#include <libportal/inputcapture.h>
#include <libportal/portal.h>
#include <memory>
namespace synergy {
class PortalInputCapture {
public:
PortalInputCapture(EiScreen *screen, IEventQueue *events);
~PortalInputCapture();
void enable();
void disable();
void release();
void release(double x, double y);
bool is_active() const { return is_active_; }
private:
void glib_thread(void *);
gboolean timeout_handler();
gboolean init_input_capture_session();
void cb_init_input_capture_session(GObject *object, GAsyncResult *res);
void cb_set_pointer_barriers(GObject *object, GAsyncResult *res);
void cb_session_closed(XdpSession *session);
void cb_disabled(XdpInputCaptureSession *session);
void cb_activated(
XdpInputCaptureSession *session, std::uint32_t activation_id,
GVariant *options);
void cb_deactivated(
XdpInputCaptureSession *session, std::uint32_t activation_id,
GVariant *options);
void cb_zones_changed(XdpInputCaptureSession *session, GVariant *options);
/// g_signal_connect callback wrapper
static void cb_session_closed_cb(XdpSession *session, gpointer data) {
reinterpret_cast<PortalInputCapture *>(data)->cb_session_closed(session);
}
static void cb_disabled_cb(XdpInputCaptureSession *session, gpointer data) {
reinterpret_cast<PortalInputCapture *>(data)->cb_disabled(session);
}
static void cb_activated_cb(
XdpInputCaptureSession *session, std::uint32_t activation_id,
GVariant *options, gpointer data) {
reinterpret_cast<PortalInputCapture *>(data)->cb_activated(
session, activation_id, options);
}
static void cb_deactivated_cb(
XdpInputCaptureSession *session, std::uint32_t activation_id,
GVariant *options, gpointer data) {
reinterpret_cast<PortalInputCapture *>(data)->cb_deactivated(
session, activation_id, options);
}
static void cb_zones_changed_cb(
XdpInputCaptureSession *session, GVariant *options, gpointer data) {
reinterpret_cast<PortalInputCapture *>(data)->cb_zones_changed(
session, options);
}
int fake_eis_fd();
private:
EiScreen *screen_ = nullptr;
IEventQueue *events_ = nullptr;
Thread *glib_thread_;
GMainLoop *glib_main_loop_ = nullptr;
XdpPortal *portal_ = nullptr;
XdpInputCaptureSession *session_ = nullptr;
std::vector<guint> signals_;
bool enabled_ = false;
bool is_active_ = false;
std::uint32_t activation_id_ = 0;
std::vector<XdpInputCapturePointerBarrier *> barriers_;
};
} // namespace synergy
#endif // HAVE_LIBPORTAL_INPUTCAPTURE

View File

@ -0,0 +1,204 @@
/*
* synergy -- mouse and keyboard sharing utility
* Copyright (C) 2022 Red Hat, Inc.
* Copyright (C) 2024 Symless Ltd.
*
* This package is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* found in the file LICENSE that should have accompanied this file.
*
* This package is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#include "platform/PortalRemoteDesktop.h"
#include "base/Log.h"
#include "base/TMethodJob.h"
#include <sys/socket.h> // for EIS fd hack, remove
#include <sys/un.h> // for EIS fd hack, remove
namespace synergy {
PortalRemoteDesktop::PortalRemoteDesktop(EiScreen *screen, IEventQueue *events)
: screen_(screen),
events_(events),
portal_(xdp_portal_new()) {
glib_main_loop_ = g_main_loop_new(nullptr, true);
glib_thread_ = new Thread(new TMethodJob<PortalRemoteDesktop>(
this, &PortalRemoteDesktop::glib_thread));
reconnect(0);
}
PortalRemoteDesktop::~PortalRemoteDesktop() {
if (g_main_loop_is_running(glib_main_loop_))
g_main_loop_quit(glib_main_loop_);
if (glib_thread_ != nullptr) {
glib_thread_->cancel();
glib_thread_->wait();
delete glib_thread_;
glib_thread_ = nullptr;
g_main_loop_unref(glib_main_loop_);
glib_main_loop_ = nullptr;
}
if (session_signal_id_)
g_signal_handler_disconnect(session_, session_signal_id_);
if (session_ != nullptr)
g_object_unref(session_);
g_object_unref(portal_);
free(session_restore_token_);
}
gboolean PortalRemoteDesktop::timeout_handler() {
return true; // keep re-triggering
}
void PortalRemoteDesktop::reconnect(unsigned int timeout) {
auto init_cb = [](gpointer data) -> gboolean {
return reinterpret_cast<PortalRemoteDesktop *>(data)
->init_remote_desktop_session();
};
if (timeout > 0)
g_timeout_add(timeout, init_cb, this);
else
g_idle_add(init_cb, this);
}
void PortalRemoteDesktop::cb_session_closed(XdpSession *session) {
LOG_ERR("portal remote desktop session was closed, reconnecting");
g_signal_handler_disconnect(session, session_signal_id_);
session_signal_id_ = 0;
events_->addEvent(
Event(events_->forEi().sessionClosed(), screen_->getEventTarget()));
// gcc warning "Suspicious usage of 'sizeof(A*)'" can be ignored
g_clear_object(&session_);
reconnect(1000);
}
void PortalRemoteDesktop::cb_session_started(
GObject *object, GAsyncResult *res) {
g_autoptr(GError) error = nullptr;
auto session = XDP_SESSION(object);
auto success = xdp_session_start_finish(session, res, &error);
if (!success) {
LOG_ERR("failed to start portal remote desktop session");
g_main_loop_quit(glib_main_loop_);
events_->addEvent(Event::kQuit);
return;
}
session_restore_token_ = xdp_session_get_restore_token(session);
// ConnectToEIS requires version 2 of the xdg-desktop-portal (and the same
// version in the impl.portal), i.e. you'll need an updated compositor on
// top of everything...
auto fd = -1;
#if HAVE_LIBPORTAL_SESSION_CONNECT_TO_EIS
fd = xdp_session_connect_to_eis(session, &error);
#endif
if (fd < 0) {
g_main_loop_quit(glib_main_loop_);
events_->addEvent(Event::kQuit);
return;
}
// Socket ownership is transferred to the EiScreen
events_->addEvent(Event(
events_->forEi().connected(), screen_->getEventTarget(),
EiScreen::EiConnectInfo::alloc(fd)));
}
void PortalRemoteDesktop::cb_init_remote_desktop_session(
GObject *object, GAsyncResult *res) {
LOG_DEBUG("remote desktop session ready");
g_autoptr(GError) error = nullptr;
auto session = xdp_portal_create_remote_desktop_session_finish(
XDP_PORTAL(object), res, &error);
if (!session) {
LOG_ERR("failed to initialize remote desktop session: %s", error->message);
// This was the first attempt to connect to the RD portal - quit if that
// fails.
if (session_iteration_ == 0) {
g_main_loop_quit(glib_main_loop_);
events_->addEvent(Event::kQuit);
} else {
this->reconnect(1000);
}
return;
}
session_ = session;
++session_iteration_;
// FIXME: the lambda trick doesn't work here for unknown reasons, we need
// the static function
session_signal_id_ = g_signal_connect(
G_OBJECT(session), "closed", G_CALLBACK(cb_session_closed_cb), this);
LOG_DEBUG("Session ready, starting");
xdp_session_start(
session,
nullptr, // parent
nullptr, // cancellable
[](GObject *obj, GAsyncResult *res, gpointer data) {
reinterpret_cast<PortalRemoteDesktop *>(data)->cb_session_started(
obj, res);
},
this);
}
#if !defined(HAVE_LIBPORTAL_CREATE_REMOTE_DESKTOP_SESSION_FULL)
static inline void xdp_portal_create_remote_desktop_session_full(
XdpPortal *portal, XdpDeviceType devices, XdpOutputType outputs,
XdpRemoteDesktopFlags flags, XdpCursorMode cursor_mode,
XdpPersistMode _unused1, const char *_unused2, GCancellable *cancellable,
GAsyncReadyCallback callback, gpointer data) {
xdp_portal_create_remote_desktop_session(
portal, devices, outputs, flags, cursor_mode, cancellable, callback,
data);
}
#endif
gboolean PortalRemoteDesktop::init_remote_desktop_session() {
LOG_DEBUG(
"setting up remote desktop session with restore token %s",
session_restore_token_);
xdp_portal_create_remote_desktop_session_full(
portal_,
static_cast<XdpDeviceType>(XDP_DEVICE_POINTER | XDP_DEVICE_KEYBOARD),
XDP_OUTPUT_NONE, XDP_REMOTE_DESKTOP_FLAG_NONE, XDP_CURSOR_MODE_HIDDEN,
XDP_PERSIST_MODE_TRANSIENT, session_restore_token_,
nullptr, // cancellable
[](GObject *obj, GAsyncResult *res, gpointer data) {
reinterpret_cast<PortalRemoteDesktop *>(data)
->cb_init_remote_desktop_session(obj, res);
},
this);
return false; // don't reschedule
}
void PortalRemoteDesktop::glib_thread(void *) {
auto context = g_main_loop_get_context(glib_main_loop_);
while (g_main_loop_is_running(glib_main_loop_)) {
Thread::testCancel();
g_main_context_iteration(context, true);
}
}
} // namespace synergy

View File

@ -0,0 +1,75 @@
/*
* synergy -- mouse and keyboard sharing utility
* Copyright (C) 2022 Red Hat, Inc.
* Copyright (C) 2024 Symless Ltd.
*
* This package is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* found in the file LICENSE that should have accompanied this file.
*
* This package is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#pragma once
#include "config.h"
#include "mt/Thread.h"
#include "platform/EiScreen.h"
#include <glib.h>
#include <libportal/portal.h>
#if !HAVE_LIBPORTAL_OUTPUT_NONE
// Added in libportal ad82a74 Jun 2022, not yet released in libportal 0.6
// should be used as a patch on ≤ 0.6, and non-git
#define XDP_OUTPUT_NONE (XdpOutputType)0
#endif
namespace synergy {
class PortalRemoteDesktop {
public:
PortalRemoteDesktop(EiScreen *screen, IEventQueue *events);
~PortalRemoteDesktop();
private:
void glib_thread(void *);
gboolean timeout_handler();
gboolean init_remote_desktop_session();
void cb_init_remote_desktop_session(GObject *object, GAsyncResult *res);
void cb_session_started(GObject *object, GAsyncResult *res);
void cb_session_closed(XdpSession *session);
void reconnect(unsigned int timeout = 1000);
/// g_signal_connect callback wrapper
static void cb_session_closed_cb(XdpSession *session, gpointer data) {
reinterpret_cast<PortalRemoteDesktop *>(data)->cb_session_closed(session);
}
int fake_eis_fd();
private:
EiScreen *screen_;
IEventQueue *events_;
Thread *glib_thread_;
GMainLoop *glib_main_loop_ = nullptr;
XdpPortal *portal_ = nullptr;
XdpSession *session_ = nullptr;
char *session_restore_token_ = nullptr;
guint session_signal_id_ = 0;
/// The number of successful sessions we've had already
guint session_iteration_ = 0;
};
} // namespace synergy

View File

@ -183,8 +183,8 @@ private:
" -l --log <file> write log messages to file.\n" \
" --no-tray disable the system tray icon.\n" \
" --enable-drag-drop enable file drag & drop.\n" \
" --enable-crypto enable the crypto (ssl) plugin.\n" \
" --tls-cert specify the path to the tls certificate file.\n"
" --enable-crypto enable TLS encryption.\n" \
" --tls-cert specify the path to the TLS certificate file.\n"
#define HELP_COMMON_INFO_2 \
" -h, --help display this help and exit.\n" \
@ -218,3 +218,25 @@ private:
" --exit-pause wait for key press on exit, can be useful for\n" \
" reading error messages that occur on exit.\n"
#endif
#if WINAPI_LIBEI
const auto kNoWaylandEiArg =
" --no-wayland-ei do not use EI (emulated input) for\n"
" Wayland and instead use the legacy\n"
" X Window System.\n";
const auto kHelpNoWayland = "";
#elif WINAPI_XWINDOWS
const auto kNoWaylandEiArg = "";
const auto kHelpNoWayland =
"\n"
"Your Linux distribution does not support Wayland EI (emulated input)\n"
"which is required for Wayland support. Please use a Linux distribution\n"
"that supports Wayland EI.\n";
#else
const auto kNoWaylandEiArg = "";
const auto kHelpNoWayland = "";
#endif

View File

@ -42,7 +42,7 @@ bool ArgParser::parseServerArgs(
updateCommonArgs(argv);
int i = 1;
while (i < argc) {
if (parsePlatformArg(args, argc, argv, i)) {
if (parsePlatformArgs(args, argc, argv, i, true)) {
++i;
continue;
} else if (parseGenericArgs(argc, argv, i)) {
@ -82,7 +82,7 @@ bool ArgParser::parseClientArgs(
int i{1};
while (i < argc) {
if (parsePlatformArg(args, argc, argv, i)) {
if (parsePlatformArgs(args, argc, argv, i, false)) {
++i;
continue;
} else if (parseGenericArgs(argc, argv, i)) {
@ -137,9 +137,9 @@ bool ArgParser::parseClientArgs(
return true;
}
bool ArgParser::parsePlatformArg(
bool ArgParser::parsePlatformArgs(
synergy::ArgsBase &argsBase, const int &argc, const char *const *argv,
int &i) {
int &i, bool isServer) {
#if WINAPI_MSWINDOWS
if (isArg(i, argc, argv, nullptr, "--service")) {
LOG((CLOG_WARN "obsolete argument --service, use synergyd instead."));
@ -155,6 +155,7 @@ bool ArgParser::parsePlatformArg(
return true;
#elif WINAPI_XWINDOWS
if (isArg(i, argc, argv, "-display", "--display", 1)) {
// use alternative display
argsBase.m_display = argv[++i];
@ -164,6 +165,16 @@ bool ArgParser::parsePlatformArg(
argsBase.m_disableXInitThreads = true;
}
#if WINAPI_LIBEI
else if (isArg(i, argc, argv, nullptr, "--no-wayland-ei")) {
argsBase.m_disableWaylandEi = true;
}
else if (!isServer && isArg(i, argc, argv, nullptr, "--no-wayland-portal")) {
argsBase.m_disableWaylandPortal = true;
}
#endif
else {
// option not supported here
return false;

View File

@ -38,9 +38,9 @@ public:
parseServerArgs(synergy::ServerArgs &args, int argc, const char *const *argv);
bool
parseClientArgs(synergy::ClientArgs &args, int argc, const char *const *argv);
bool parsePlatformArg(
bool parsePlatformArgs(
synergy::ArgsBase &argsBase, const int &argc, const char *const *argv,
int &i);
int &i, bool isServer);
bool parseToolArgs(ToolArgs &args, int argc, const char *const *argv);
bool parseGenericArgs(int argc, const char *const *argv, int &i);
bool parseDeprecatedArgs(int argc, const char *const *argv, int &i);

View File

@ -1,7 +1,6 @@
/*
* synergy -- mouse and keyboard sharing utility
* Copyright (C) 2012-2020 Symless Ltd.
* Copyright (C) 2012 Nick Bolton
* Copyright (C) 2012 Symless Ltd.
*
* This package is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
@ -16,12 +15,24 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#ifndef SYNERGY_CORE_ARGSBASE_H
#define SYNERGY_CORE_ARGSBASE_H
#pragma once
#include "base/String.h"
namespace synergy {
#if WINAPI_LIBEI
const auto kDisableEiDefault = false;
#else
const auto kDisableEiDefault = true;
#endif
#if WINAPI_LIBPORTAL
const auto kDisablePortalDefault = false;
#else
const auto kDisablePortalDefault = true;
#endif
/**
* @brief This is the base Argument class that will store the generic
* arguments passed into the applications this will be derived
@ -95,11 +106,18 @@ public:
/// @brief Stop this computer from sleeping
bool m_preventSleep = false;
/// @brief Disable EI (Emulated Input) for Wayland support
bool m_disableWaylandEi = kDisableEiDefault;
/// @brief Disable Portal for Wayland support (use EI sockets instead)
bool m_disableWaylandPortal = kDisablePortalDefault;
#if SYSAPI_WIN32
bool m_debugServiceWait = false;
bool m_pauseOnExit = false;
bool m_stopOnDeskSwitch = false;
#endif
#if WINAPI_XWINDOWS
bool m_disableXInitThreads = false;
#endif
@ -112,5 +130,3 @@ protected:
}
};
} // namespace synergy
#endif // SYNERGY_CORE_ARGSBASE_H

View File

@ -21,21 +21,12 @@ file(GLOB sources "*.cpp" "languages/*.cpp")
if(WIN32)
file(GLOB arch_headers "win32/*.h")
file(GLOB arch_sources "win32/*.cpp")
include_directories("../../../ext/WinToast/src")
list(APPEND arch_sources "../../../ext/WinToast/src/wintoastlib.cpp")
list(APPEND arch_sources ${WINTOAST_DIR}/src/wintoastlib.cpp)
elseif(UNIX)
file(GLOB arch_headers "unix/*.h")
file(GLOB arch_sources "unix/*.cpp")
endif()
option(SYSTEM_PUGIXML "Use system pugixml instead of vendored one" OFF)
if(SYSTEM_PUGIXML)
find_package(pugixml REQUIRED)
else()
include_directories("../../../ext/pugixml/src")
list(APPEND arch_sources "../../../ext/pugixml/src/pugixml.cpp")
endif()
list(APPEND sources ${arch_sources})
list(APPEND headers ${arch_headers})
@ -56,18 +47,18 @@ if(UNIX)
platform
mt
server)
if(NOT APPLE)
target_link_libraries(synlib pugixml)
find_package(PkgConfig REQUIRED)
pkg_check_modules(lib_glib REQUIRED IMPORTED_TARGET glib-2.0)
pkg_search_module(PC_GDKPIXBUF gdk-pixbuf-2.0)
include_directories(${PC_GDKPIXBUF_INCLUDE_DIRS})
pkg_check_modules(lib_gdkpixbuf REQUIRED IMPORTED_TARGET gdk-pixbuf-2.0)
pkg_check_modules(lib_notify REQUIRED IMPORTED_TARGET libnotify)
target_link_libraries(synlib PkgConfig::lib_glib PkgConfig::lib_gdkpixbuf
PkgConfig::lib_notify)
endif()
endif()
if(SYSTEM_PUGIXML)
target_link_libraries(synlib pugixml)
endif()

View File

@ -48,9 +48,14 @@
#if WINAPI_MSWINDOWS
#include "platform/MSWindowsScreen.h"
#elif WINAPI_XWINDOWS
#endif
#if WINAPI_XWINDOWS
#include "platform/XWindowsScreen.h"
#elif WINAPI_CARBON
#endif
#if WINAPI_LIBEI
#include "platform/EiScreen.h"
#endif
#if WINAPI_CARBON
#include "platform/OSXScreen.h"
#endif
@ -106,44 +111,53 @@ void ClientApp::parseArgs(int argc, const char *const *argv) {
}
void ClientApp::help() {
#if WINAPI_XWINDOWS
#define WINAPI_ARG " [--display <display>] [--no-xinitthreads]"
#define WINAPI_INFO \
" --display <display> connect to the X server at <display>\n" \
" --no-xinitthreads do not call XInitThreads()\n"
#else
#define WINAPI_ARG ""
#define WINAPI_INFO ""
#endif
std::stringstream help;
help << "Usage: " << args().m_pname << " [--address <address>]"
<< " [--yscroll <delta>]"
<< " [--sync-language]"
<< " [--invert-scroll]"
<< " [--host]" << WINAPI_ARG << HELP_SYS_ARGS << HELP_COMMON_ARGS
<< " <server-address>"
<< "\n\n"
<< "Connect to a synergy mouse/keyboard sharing server.\n"
<< "\n"
<< " -a, --address <address> local network interface address.\n"
<< HELP_COMMON_INFO_1 << WINAPI_INFO << HELP_SYS_INFO
<< " --yscroll <delta> defines the vertical scrolling delta, "
"which is\n"
<< " 120 by default.\n"
<< " --sync-language set this parameter to enable language "
"synchronization.\n"
<< " --invert-scroll invert scroll direction on this "
"computer.\n"
<< " --host client starts a listener and waits for a "
"server connection.\n"
<< HELP_COMMON_INFO_2 << "\n"
<< "* marks defaults.\n"
<< "\n"
<< "The server address is of the form: [<hostname>][:<port>]. The "
"hostname\n"
<< "must be the address or hostname of the server. The port overrides "
"the\n"
<< "default port, " << kDefaultPort << ".\n";
help
<< "Usage: " << args().m_pname << " [--address <address>]"
<< " [--yscroll <delta>]"
<< " [--sync-language]"
<< " [--invert-scroll]"
<< " [--host]"
#ifdef WINAPI_XWINDOWS
<< " [--display <display>]"
<< " [--no-xinitthreads]"
#endif
#ifdef WINAPI_LIBEI
<< " [--use-x-window]"
#endif
<< HELP_SYS_ARGS << HELP_COMMON_ARGS << " <server-address>"
<< "\n\n"
<< "Connect to a synergy mouse/keyboard sharing server.\n"
<< "\n"
<< " -a, --address <address> local network interface address.\n"
<< HELP_COMMON_INFO_1 << HELP_SYS_INFO
<< " --yscroll <delta> defines the vertical scrolling delta,\n"
<< " which is 120 by default.\n"
<< " --sync-language enable language synchronization.\n"
<< " --invert-scroll invert scroll direction on this\n"
<< " computer.\n"
<< " --host act as a host; invert server/client mode\n"
<< " and listen instead of connecting.\n"
#if WINAPI_XWINDOWS
<< " --display <display> connect to the X server at <display>\n"
<< " --no-xinitthreads do not call XInitThreads()\n"
#endif
#if defined(WINAPI_XWINDOWS) && defined(WINAPI_LIBEI)
<< kNoWaylandEiArg
#endif
#if defined(WINAPI_LIBPORTAL) && defined(WINAPI_LIBEI)
<< " --no-wayland-portal do not use Portal for Wayland and \n"
<< " connect to EI socket instead.\n"
#endif
<< HELP_COMMON_INFO_2 << "\n"
<< "* marks defaults.\n"
<< kHelpNoWayland
<< "\n"
<< "The server address is of the form: [<hostname>][:<port>].\n"
<< "The hostname must be the address or hostname of the server.\n"
<< "The port overrides the default port, " << kDefaultPort << ".\n";
LOG((CLOG_PRINT "%s", help.str().c_str()));
}
@ -172,13 +186,23 @@ synergy::Screen *ClientApp::createScreen() {
false, args().m_noHooks, args().m_stopOnDeskSwitch, m_events,
args().m_enableLangSync, args().m_clientScrollDirection),
m_events);
#elif WINAPI_XWINDOWS
#endif
#if WINAPI_LIBEI
if (!args().m_disableWaylandEi) {
return new synergy::Screen(
new synergy::EiScreen(false, m_events, !args().m_disableWaylandPortal),
m_events);
}
#endif
#if WINAPI_XWINDOWS
return new synergy::Screen(
new XWindowsScreen(
args().m_display, false, args().m_disableXInitThreads,
args().m_yscroll, m_events, args().m_clientScrollDirection),
m_events);
#elif WINAPI_CARBON
#endif
#if WINAPI_CARBON
return new synergy::Screen(
new OSXScreen(
m_events, false, args().m_enableLangSync,

View File

@ -79,3 +79,13 @@ IPrimaryScreen::HotKeyInfo *IPrimaryScreen::HotKeyInfo::alloc(UInt32 id) {
info->m_id = id;
return info;
}
//
// IPrimaryScreen::EiConnectInfo
//
IPrimaryScreen::EiConnectInfo *IPrimaryScreen::EiConnectInfo::alloc(int fd) {
EiConnectInfo *info = (EiConnectInfo *)malloc(sizeof(EiConnectInfo));
info->m_fd = fd;
return info;
}

View File

@ -70,6 +70,14 @@ public:
UInt32 m_id;
};
class EiConnectInfo {
public:
static EiConnectInfo *alloc(int fd);
public:
int m_fd;
};
//! @name manipulators
//@{

View File

@ -36,6 +36,7 @@
#include "server/ClientProxy.h"
#include "server/PrimaryClient.h"
#include "server/Server.h"
#include "synergy/App.h"
#include "synergy/ArgParser.h"
#include "synergy/Screen.h"
#include "synergy/ServerArgs.h"
@ -48,9 +49,14 @@
#if WINAPI_MSWINDOWS
#include "platform/MSWindowsScreen.h"
#elif WINAPI_XWINDOWS
#endif
#if WINAPI_XWINDOWS
#include "platform/XWindowsScreen.h"
#elif WINAPI_CARBON
#endif
#if WINAPI_LIBEI
#include "platform/EiScreen.h"
#endif
#if WINAPI_CARBON
#include "platform/OSXScreen.h"
#endif
@ -60,6 +66,7 @@
#include <fstream>
#include <iostream>
#include <sstream>
#include <stdio.h>
//
@ -104,46 +111,61 @@ void ServerApp::parseArgs(int argc, const char *const *argv) {
}
void ServerApp::help() {
// window api args (windows/x-windows/carbon)
#if WINAPI_XWINDOWS
#define WINAPI_ARGS " [--display <display>] [--no-xinitthreads]"
#define WINAPI_INFO \
" --display <display> connect to the X server at <display>\n" \
" --no-xinitthreads do not call XInitThreads()\n"
#else
#define WINAPI_ARGS
#define WINAPI_INFO
#endif
static const int buffer_size = 3000;
char buffer[buffer_size];
snprintf(
buffer, buffer_size,
"Usage: %s"
" [--address <address>]"
" [--config <pathname>]" WINAPI_ARGS HELP_SYS_ARGS HELP_COMMON_ARGS "\n\n"
"Start the synergy mouse/keyboard sharing server.\n"
"\n"
" -a, --address <address> listen for clients on the given address.\n"
" -c, --config <pathname> use the named configuration file "
"instead.\n" HELP_COMMON_INFO_1 WINAPI_INFO
HELP_SYS_INFO HELP_COMMON_INFO_2 "\n"
"* marks defaults.\n"
"\n"
"The argument for --address is of the form: [<hostname>][:<port>]. The\n"
"hostname must be the address or hostname of an interface on the "
"system.\n"
"The default is to listen on all interfaces. The port overrides the\n"
"default port, %d.\n"
"\n"
"If no configuration file pathname is provided then the first of the\n"
"following to load successfully sets the configuration:\n"
" %s\n"
" %s\n",
args().m_pname, kDefaultPort,
ARCH->concatPath(ARCH->getUserDirectory(), USR_CONFIG_NAME).c_str(),
ARCH->concatPath(ARCH->getSystemDirectory(), SYS_CONFIG_NAME).c_str());
const auto userConfig =
ARCH->concatPath(ARCH->getUserDirectory(), USR_CONFIG_NAME);
const auto sysConfig =
ARCH->concatPath(ARCH->getSystemDirectory(), SYS_CONFIG_NAME);
LOG((CLOG_PRINT "%s", buffer));
std::stringstream help;
help
<< "Usage: " << args().m_pname
<< " [--address <address>]"
<< " [--config <pathname>]"
#if WINAPI_XWINDOWS
<< " [--display <display>] [--no-xinitthreads]"
#endif
#ifdef WINAPI_LIBEI
<< " [--no-wayland-ei]"
#endif
<< HELP_SYS_ARGS HELP_COMMON_ARGS "\n\n"
<< "Start the synergy mouse/keyboard sharing server.\n"
<< "\n"
<< " -a, --address <address> listen for clients on the given address.\n"
<< " -c, --config <pathname> use the named configuration file "
<< "instead.\n" HELP_COMMON_INFO_1
#if WINAPI_XWINDOWS
<< " --display <display> connect to the X server at <display>\n"
<< " --no-xinitthreads do not call XInitThreads()\n"
#endif
#if defined(WINAPI_XWINDOWS) && defined(WINAPI_LIBEI)
<< kNoWaylandEiArg
#endif
<< HELP_SYS_INFO HELP_COMMON_INFO_2 "\n"
<< "* marks defaults.\n"
<< kHelpNoWayland
<< "\n"
<< "The argument for --address is of the form: [<hostname>][:<port>]. "
"The\n"
<< "hostname must be the address or hostname of an interface on the "
<< "system.\n"
<< "The default is to listen on all interfaces. The port overrides the\n"
<< "default port, " << kDefaultPort << ".\n"
<< "\n"
<< "If no configuration file pathname is provided then the first of the\n"
<< "following to load successfully sets the configuration:\n"
<< " " << userConfig << "\n"
<< " " << sysConfig << "\n";
LOG((CLOG_PRINT "%s", help.str().c_str()));
}
void ServerApp::reloadSignalHandler(Arch::ESignal, void *) {
@ -527,7 +549,17 @@ synergy::Screen *ServerApp::createScreen() {
new MSWindowsScreen(
true, args().m_noHooks, args().m_stopOnDeskSwitch, m_events),
m_events);
#elif WINAPI_XWINDOWS
#endif
#if WINAPI_LIBEI
if (!args().m_disableWaylandEi) {
return new synergy::Screen(
new synergy::EiScreen(true, m_events, !args().m_disableWaylandPortal),
m_events);
}
#endif
#if WINAPI_XWINDOWS
return new synergy::Screen(
new XWindowsScreen(
args().m_display, true, args().m_disableXInitThreads, 0, m_events),

View File

@ -33,6 +33,7 @@ static const ButtonID kButtonLeft = 1;
static const ButtonID kButtonMiddle = 2;
static const ButtonID kButtonRight = 3;
static const ButtonID kButtonExtra0 = 4;
static const ButtonID kButtonExtra1 = 5;
static const ButtonID kMacButtonRight = 2;
static const ButtonID kMacButtonMiddle = 3;

View File

@ -19,8 +19,6 @@ macro(config_all_tests)
set(base_dir ${CMAKE_SOURCE_DIR})
set(src_dir ${base_dir}/src)
set(test_base_dir ${src_dir}/test)
set(ext_dir ${base_dir}/ext)
set(gtest_base_dir ${ext_dir}/googletest)
set(gui_dir ${src_dir}/gui/src)
config_test_deps()
@ -126,25 +124,10 @@ endmacro()
macro(config_test_deps)
set(gtest_dir ${gtest_base_dir}/googletest)
set(gmock_dir ${gtest_base_dir}/googlemock)
include_directories(${gtest_dir} ${gmock_dir} ${gtest_dir}/include
${gmock_dir}/include)
# gui library autogen headers:
# qt doesn't seem to auto include the autogen headers for libraries.
include_directories(${CMAKE_BINARY_DIR}/src/lib/gui/gui_autogen/include)
add_library(gtest STATIC ${gtest_dir}/src/gtest-all.cc)
add_library(gmock STATIC ${gmock_dir}/src/gmock-all.cc)
if(UNIX)
# ignore warnings in gtest and gmock
set_target_properties(gtest PROPERTIES COMPILE_FLAGS "-w")
set_target_properties(gmock PROPERTIES COMPILE_FLAGS "-w")
endif()
set(test_libs
gtest
gmock
@ -161,6 +144,7 @@ macro(config_test_deps)
ipc
license
gui
${GTEST_LIBS}
${libs})
endmacro()

View File

@ -67,5 +67,6 @@ public:
MOCK_METHOD(IScreenEvents &, forIScreen, (), (override));
MOCK_METHOD(ClipboardEvents &, forClipboard, (), (override));
MOCK_METHOD(FileEvents &, forFile, (), (override));
MOCK_METHOD(EiEvents &, forEi, (), (override));
MOCK_METHOD(void, waitForReady, (), (const, override));
};

12
subprojects/.gitignore vendored Normal file
View File

@ -0,0 +1,12 @@
# Fetched dependencies.
/packagecache
/googletest-*
/WinToast-*
/libei
/libportal
/gi-docgen
/munit
# Added by dependencies.
/gi-docgen.wrap
/munit.wrap

16
subprojects/gtest.wrap Normal file
View File

@ -0,0 +1,16 @@
[wrap-file]
directory = googletest-1.15.0
source_url = https://github.com/google/googletest/archive/refs/tags/v1.15.0.tar.gz
source_filename = gtest-1.15.0.tar.gz
source_hash = 7315acb6bf10e99f332c8a43f00d5fbb1ee6ca48c52f6b936991b216c586aaad
patch_filename = gtest_1.15.0-1_patch.zip
patch_url = https://wrapdb.mesonbuild.com/v2/gtest_1.15.0-1/get_patch
patch_hash = 5f8e484c48fdc1029c7fd08807bd2615f8c9d16f90df6d81984f4f292752c925
source_fallback_url = https://github.com/mesonbuild/wrapdb/releases/download/gtest_1.15.0-1/gtest-1.15.0.tar.gz
wrapdb_version = 1.15.0-1
[provide]
gtest = gtest_dep
gtest_main = gtest_main_dep
gmock = gmock_dep
gmock_main = gmock_main_dep

3
subprojects/libei.wrap Normal file
View File

@ -0,0 +1,3 @@
[wrap-git]
url = https://gitlab.freedesktop.org/libinput/libei.git
revision = tags/1.3.0

View File

@ -0,0 +1,3 @@
[wrap-git]
url = https://github.com/flatpak/libportal.git
revision = a1530a9

View File

@ -0,0 +1,5 @@
project('wintoast', 'cpp', version : '1.3.0')
wintoast_dep = declare_dependency(
include_directories : include_directories('.')
)

View File

@ -0,0 +1,6 @@
[wrap-file]
directory = WinToast-1.3.0
source_url = https://github.com/mohabouje/WinToast/archive/refs/tags/v1.3.0.tar.gz
source_filename = wintoast-1.3.0.tar.gz
source_hash = 998bd82fb2f49ee4b0df98774424d72c2bc18225188f251a9242af28bb80e6d4
patch_directory = wintoast-patch