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:
2
.github/docker/archlinux/Dockerfile
vendored
2
.github/docker/archlinux/Dockerfile
vendored
@ -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
|
||||
|
||||
2
.github/docker/debian/Dockerfile
vendored
2
.github/docker/debian/Dockerfile
vendored
@ -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
|
||||
|
||||
2
.github/docker/fedora/Dockerfile
vendored
2
.github/docker/fedora/Dockerfile
vendored
@ -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
|
||||
|
||||
2
.github/docker/opensuse/Dockerfile
vendored
2
.github/docker/opensuse/Dockerfile
vendored
@ -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
|
||||
|
||||
6
.github/workflows/build-containers.yml
vendored
6
.github/workflows/build-containers.yml
vendored
@ -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
|
||||
|
||||
18
.github/workflows/ci.yml
vendored
18
.github/workflows/ci.yml
vendored
@ -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
|
||||
|
||||
5
.github/workflows/codeql-analysis.yml
vendored
5
.github/workflows/codeql-analysis.yml
vendored
@ -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
|
||||
|
||||
9
.github/workflows/sonarcloud-analysis.yml
vendored
9
.github/workflows/sonarcloud-analysis.yml
vendored
@ -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 }} \
|
||||
|
||||
5
.github/workflows/valgrind-analysis.yml
vendored
5
.github/workflows/valgrind-analysis.yml
vendored
@ -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
9
.gitmodules
vendored
@ -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
|
||||
@ -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
|
||||
|
||||
|
||||
@ -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).
|
||||
|
||||

|
||||
|
||||
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:*
|
||||
|
||||
@ -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 can’t 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()
|
||||
|
||||
@ -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
|
||||
|
||||
71
config.yaml
71
config.yaml
@ -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
|
||||
|
||||
@ -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
37
meson.build
Normal 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
2
meson_options.txt
Normal 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')
|
||||
@ -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}
|
||||
|
||||
2
res/dist/arch/PKGBUILD.in
vendored
2
res/dist/arch/PKGBUILD.in
vendored
@ -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')
|
||||
|
||||
@ -627,7 +627,7 @@ EXCLUDE_SYMLINKS = NO
|
||||
|
||||
EXCLUDE_PATTERNS = */.svn/* \
|
||||
*/.git/* \
|
||||
*/ext/* \
|
||||
*/subprojects/* \
|
||||
*/src/gui/* \
|
||||
*/src/test/*
|
||||
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -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(
|
||||
|
||||
@ -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)
|
||||
|
||||
|
||||
@ -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
25
scripts/lib/meson.py
Normal 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)
|
||||
@ -13,7 +13,7 @@ include_files = [
|
||||
"CMakeLists.txt",
|
||||
]
|
||||
|
||||
exclude_dirs = ["ext", "build", "deps"]
|
||||
exclude_dirs = ["subprojects", "build", "deps"]
|
||||
|
||||
|
||||
def main():
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -11,4 +11,5 @@ dependencies = [
|
||||
"dmgbuild; sys_platform == 'darwin'",
|
||||
"aqtinstall; sys_platform == 'win32' or sys_platform == 'darwin'",
|
||||
"colorama",
|
||||
"meson",
|
||||
]
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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();
|
||||
}
|
||||
@ -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();
|
||||
};
|
||||
@ -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>
|
||||
@ -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();
|
||||
|
||||
@ -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();
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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;
|
||||
};
|
||||
|
||||
@ -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;
|
||||
};
|
||||
|
||||
@ -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__))
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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()
|
||||
|
||||
|
||||
162
src/lib/platform/EiEventQueueBuffer.cpp
Normal file
162
src/lib/platform/EiEventQueueBuffer.cpp
Normal 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
|
||||
59
src/lib/platform/EiEventQueueBuffer.h
Normal file
59
src/lib/platform/EiEventQueueBuffer.h
Normal 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
|
||||
282
src/lib/platform/EiKeyState.cpp
Normal file
282
src/lib/platform/EiKeyState.cpp
Normal 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
|
||||
63
src/lib/platform/EiKeyState.h
Normal file
63
src/lib/platform/EiKeyState.h
Normal 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
|
||||
852
src/lib/platform/EiScreen.cpp
Normal file
852
src/lib/platform/EiScreen.cpp
Normal 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
201
src/lib/platform/EiScreen.h
Normal 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
|
||||
409
src/lib/platform/PortalInputCapture.cpp
Normal file
409
src/lib/platform/PortalInputCapture.cpp
Normal 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
|
||||
109
src/lib/platform/PortalInputCapture.h
Normal file
109
src/lib/platform/PortalInputCapture.h
Normal 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
|
||||
204
src/lib/platform/PortalRemoteDesktop.cpp
Normal file
204
src/lib/platform/PortalRemoteDesktop.cpp
Normal 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
|
||||
75
src/lib/platform/PortalRemoteDesktop.h
Normal file
75
src/lib/platform/PortalRemoteDesktop.h
Normal 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
|
||||
@ -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
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -70,6 +70,14 @@ public:
|
||||
UInt32 m_id;
|
||||
};
|
||||
|
||||
class EiConnectInfo {
|
||||
public:
|
||||
static EiConnectInfo *alloc(int fd);
|
||||
|
||||
public:
|
||||
int m_fd;
|
||||
};
|
||||
|
||||
//! @name manipulators
|
||||
//@{
|
||||
|
||||
|
||||
@ -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),
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -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
12
subprojects/.gitignore
vendored
Normal 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
16
subprojects/gtest.wrap
Normal 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
3
subprojects/libei.wrap
Normal file
@ -0,0 +1,3 @@
|
||||
[wrap-git]
|
||||
url = https://gitlab.freedesktop.org/libinput/libei.git
|
||||
revision = tags/1.3.0
|
||||
3
subprojects/libportal.wrap
Normal file
3
subprojects/libportal.wrap
Normal file
@ -0,0 +1,3 @@
|
||||
[wrap-git]
|
||||
url = https://github.com/flatpak/libportal.git
|
||||
revision = a1530a9
|
||||
5
subprojects/packagefiles/wintoast-patch/meson.build
Normal file
5
subprojects/packagefiles/wintoast-patch/meson.build
Normal file
@ -0,0 +1,5 @@
|
||||
project('wintoast', 'cpp', version : '1.3.0')
|
||||
|
||||
wintoast_dep = declare_dependency(
|
||||
include_directories : include_directories('.')
|
||||
)
|
||||
6
subprojects/wintoast.wrap
Normal file
6
subprojects/wintoast.wrap
Normal 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
|
||||
Reference in New Issue
Block a user