Compare commits

..
Author SHA1 Message Date
dijunkun e6c26511cd [fix] capture Windows keyboard input with Raw Input, refs #94 2026-08-03 17:38:46 +08:00
dijunkun c0b880e1eb [fix] pass Windows resource compiler to Slint build 2026-08-01 02:08:05 +08:00
dijunkun c10f1499f2 [fix] encode web audio before RTP packetization, fixes #93 2026-07-31 23:58:56 +08:00
dijunkun d7a9c26ff9 [fix] scale local ID text to fit 2026-07-31 23:57:09 +08:00
dijunkun f809ad173c [fix] restore Linux keyboard injection 2026-07-31 23:57:09 +08:00
dijunkun 34155d0266 [fix] let SDL select available Linux audio driver 2026-07-31 23:57:09 +08:00
dijunkun 9d544788b0 [fix] use custom titlebar only with X11 2026-07-31 23:57:08 +08:00
dijunkun dd16e65b68 [fix] stabilize third-party dependency builds 2026-07-31 23:57:08 +08:00
dijunkun 91eac61a90 [docs] use GCC 10 toolchain for Linux builds 2026-07-31 23:57:08 +08:00
dijunkun 941cc79af5 [fix] use xmake-managed CMake for builds 2026-07-31 23:57:08 +08:00
dijunkun 0d6ecfc730 [ci] build Linux releases on Ubuntu 20.04 for compatibility 2026-07-31 23:57:08 +08:00
dijunkun d32974dc18 ci: add Linux capture build dependencies 2026-07-28 17:58:39 +08:00
dijunkun efd8b7c0ff fix: let ICE drive connection failure state 2026-07-28 17:43:25 +08:00
dijunkun 383452ed3b ci: enable Wayland for Linux release builds 2026-07-28 17:25:49 +08:00
dijunkun 2570e57958 ci: use macOS 26 SDK for release artifacts 2026-07-28 16:29:32 +08:00
dijunkun eed90dad34 revert: keep macOS main window titlebar opaque 2026-07-28 16:29:32 +08:00
dijunkun c53ce2a581 fix: prevent video callback crash during transport teardown 2026-07-28 15:26:13 +08:00
dijunkun 708c39bb99 fix: keep macOS main window titlebar opaque 2026-07-28 14:42:06 +08:00
dijunkun cee249a25b fix: align DTLS handling with SRTP negotiation 2026-07-28 14:11:17 +08:00
dijunkun 7c3b13409c fix: restore remote keyboard control in Slint client, refs #91 2026-07-28 14:11:16 +08:00
dijunkun e3d15f8f9a fix: include platform header for Linux build 2026-07-27 10:18:48 +08:00
dijunkun a6e8038286 fix: block remote mouse input over control bar 2026-07-27 00:30:48 +08:00
dijunkun 765452e3c9 fix: close server window after controller disconnects 2026-07-27 00:22:33 +08:00
dijunkun 7cd0d6a351 fix: support SDL keyboard auto-repeat 2026-07-26 18:11:41 +08:00
dijunkun c01b52eac5 fix: refine settings dialog scrolling layout 2026-07-26 17:52:23 +08:00
dijunkun 737cf9b558 fix: prioritize online recent connections 2026-07-26 16:49:03 +08:00
dijunkun 6f5faa7111 fix: refine Wayland window behavior and titlebars 2026-07-23 20:40:13 +08:00
dijunkun 3a72e4849e Merge branch 'slint-client' of https://github.com/kunkundi/crossdesk into slint-client 2026-07-23 16:29:21 +08:00
dijunkun 43e44af730 fix: copy Slint runtime beside Windows binaries 2026-07-23 16:27:19 +08:00
dijunkun 97e592861c fix: remove main window maximize button 2026-07-23 16:27:11 +08:00
dijunkun 31b8983328 fix: gray out unavailable hardware codec setting 2026-07-23 11:11:08 +08:00
dijunkun 522f519d38 fix: include X11 headers for Wayland keyboard 2026-07-22 21:56:41 +08:00
dijunkun 37c4fe2e8a build: install Rust toolchain for Slint 2026-07-22 17:23:35 +08:00
dijunkun 2c20599800 fix: improve update notes display 2026-07-22 03:08:22 +08:00
dijunkun f5c4e2ba4a ci: add Linux build image workflow 2026-07-22 00:06:18 +08:00
57 changed files with 3360 additions and 1231 deletions
+6 -4
View File
@@ -26,8 +26,9 @@ concurrency:
cancel-in-progress: true
env:
IMAGE_NAME: crossdesk/ubuntu22.04
STACK_TAG: buildenv-cuda12.6.3-xmake3.0.9-rust1.92.0
IMAGE_NAME: crossdesk/ubuntu20.04
STACK_TAG: buildenv-cuda12.6.3-gcc10-cmake3.31.6-pipewire0.3.48-xmake3.0.9-rust1.92.0
CMAKE_VERSION: 3.31.6
XMAKE_VERSION: 3.0.9
RUST_VERSION: 1.92.0
RUSTUP_VERSION: 1.28.2
@@ -44,11 +45,11 @@ jobs:
- arch: amd64
platform: linux/amd64
runner: ubuntu-24.04
base_image: nvidia/cuda:12.6.3-devel-ubuntu22.04
base_image: nvidia/cuda:12.6.3-devel-ubuntu20.04
- arch: arm64
platform: linux/arm64
runner: ubuntu-24.04-arm
base_image: ubuntu:22.04
base_image: ubuntu:20.04
steps:
- name: Set candidate tag
id: image
@@ -86,6 +87,7 @@ jobs:
sbom: false
build-args: |
BASE_IMAGE=${{ matrix.base_image }}
CMAKE_VERSION=${{ env.CMAKE_VERSION }}
XMAKE_VERSION=${{ env.XMAKE_VERSION }}
RUST_VERSION=${{ env.RUST_VERSION }}
RUSTUP_VERSION=${{ env.RUSTUP_VERSION }}
+70 -12
View File
@@ -29,12 +29,12 @@ jobs:
include:
- arch: amd64
runner: ubuntu-24.04
image: crossdesk/ubuntu22.04:buildenv-cuda12.6.3-xmake3.0.9-rust1.92.0
image: crossdesk/ubuntu20.04:buildenv-cuda12.6.3-gcc10-cmake3.31.6-pipewire0.3.48-xmake3.0.9-rust1.92.0
package_script: ./scripts/linux/pkg_amd64.sh
- arch: arm64
runner: ubuntu-24.04-arm
image: crossdesk/ubuntu22.04:buildenv-cuda12.6.3-xmake3.0.9-rust1.92.0
image: crossdesk/ubuntu20.04:buildenv-cuda12.6.3-gcc10-cmake3.31.6-pipewire0.3.48-xmake3.0.9-rust1.92.0
package_script: ./scripts/linux/pkg_arm64.sh
container:
@@ -103,12 +103,33 @@ jobs:
- name: Verify Linux build image
run: verify-crossdesk-build-image
- name: Ensure Wayland build dependencies
shell: bash
run: |
if pkg-config --exists dbus-1 libdrm && \
test -f /opt/crossdesk-pipewire-sdk/include/pipewire-0.3/pipewire/pipewire.h && \
test -f /opt/crossdesk-pipewire-sdk/include/spa-0.2/spa/param/video/format-utils.h; then
exit 0
fi
apt-get update
apt-get install -y --no-install-recommends \
libdbus-1-dev \
libdrm-dev
rm -rf /var/lib/apt/lists/*
install-crossdesk-pipewire-sdk
pkg-config --exists dbus-1 libdrm
test -f /opt/crossdesk-pipewire-sdk/include/pipewire-0.3/pipewire/pipewire.h
test -f /opt/crossdesk-pipewire-sdk/include/spa-0.2/spa/param/video/format-utils.h
- name: Build CrossDesk
env:
CUDA_PATH: /usr/local/cuda
XMAKE_GLOBALDIR: /data
run: |
xmake f --CROSSDESK_VERSION=${LEGAL_VERSION} --USE_CUDA=true --root -y
xmake f --CROSSDESK_VERSION=${LEGAL_VERSION} --USE_CUDA=true --USE_WAYLAND=true \
--cc=gcc-10 --cxx=g++-10 --root -y
xmake b -vy --root crossdesk
- name: Package
@@ -116,13 +137,19 @@ jobs:
chmod +x ${{ matrix.package_script }}
${{ matrix.package_script }} ${LEGAL_VERSION}
- name: Verify packaged Ubuntu 22.04 glibc baseline
- name: Verify Ubuntu 20.04 package dependency resolution
run: |
PACKAGE_FILE="${GITHUB_WORKSPACE}/crossdesk-linux-${{ matrix.arch }}-${LEGAL_VERSION}.deb"
apt-get update
apt-get install --simulate "${PACKAGE_FILE}"
- name: Verify packaged Ubuntu 20.04 glibc baseline
run: |
PACKAGE_FILE="${GITHUB_WORKSPACE}/crossdesk-linux-${{ matrix.arch }}-${LEGAL_VERSION}.deb"
VERIFY_DIR="${RUNNER_TEMP}/crossdesk-glibc-${{ matrix.arch }}"
install -d "${VERIFY_DIR}"
dpkg-deb --extract "${PACKAGE_FILE}" "${VERIFY_DIR}"
verify-crossdesk-glibc-baseline "${VERIFY_DIR}" 2.35
verify-crossdesk-glibc-baseline "${VERIFY_DIR}" 2.31
- name: Upload artifact
uses: actions/upload-artifact@v6
@@ -134,17 +161,22 @@ jobs:
build-macos:
name: Build on macOS
runs-on: ${{ matrix.runner }}
env:
MACOSX_DEPLOYMENT_TARGET: "14.0"
EXPECTED_MACOS_SDK: "26.5"
strategy:
matrix:
include:
- arch: x64
runner: macos-15-intel
cache-key: intel
binary_arch: x86_64
runner: macos-26-intel
cache-key: intel-macos26-xcode26.6-min14
out-dir: ./build/macosx/x86_64/release/crossdesk
package_script: ./scripts/macosx/pkg_x64.sh
- arch: arm64
runner: macos-14
cache-key: arm
binary_arch: arm64
runner: macos-26
cache-key: arm-macos26-xcode26.6-min14
out-dir: ./build/macosx/arm64/release/crossdesk
package_script: ./scripts/macosx/pkg_arm64.sh
@@ -189,6 +221,17 @@ jobs:
echo "BUILD_DATE=${BUILD_DATE}" >> $GITHUB_ENV
echo "PATCH_NUMBER=${PATCH_NUMBER}" >> $GITHUB_ENV
- name: Select Xcode 26.6
shell: bash
run: |
sudo xcode-select -s /Applications/Xcode_26.6.app/Contents/Developer
xcodebuild -version
SDK_VERSION="$(xcrun --sdk macosx --show-sdk-version)"
if [[ "${SDK_VERSION}" != "${EXPECTED_MACOS_SDK}" ]]; then
echo "Unexpected macOS SDK: expected ${EXPECTED_MACOS_SDK}, got ${SDK_VERSION}" >&2
exit 1
fi
- name: Cache xmake dependencies
uses: actions/cache@v5
with:
@@ -211,7 +254,7 @@ jobs:
- name: Build CrossDesk
run: |
xmake f --CROSSDESK_VERSION=${VERSION_NUM} --USE_CUDA=true -y
xmake f --target_minver=${MACOSX_DEPLOYMENT_TARGET} --CROSSDESK_VERSION=${VERSION_NUM} --USE_CUDA=true -y
xmake b -vy crossdesk
- name: Package CrossDesk app
@@ -219,6 +262,22 @@ jobs:
chmod +x ${{ matrix.package_script }}
${{ matrix.package_script }} ${VERSION_NUM}
- name: Verify packaged app
shell: bash
run: |
PACKAGE_FILE="${GITHUB_WORKSPACE}/crossdesk-macos-${{ matrix.arch }}-${VERSION_NUM}.pkg"
VERIFY_DIR="$(mktemp -d "${RUNNER_TEMP}/crossdesk-macos-pkg.XXXXXX")"
trap 'rm -rf "${VERIFY_DIR}"' EXIT
pkgutil --expand-full "${PACKAGE_FILE}" "${VERIFY_DIR}/expanded"
APP_PATH="$(find "${VERIFY_DIR}/expanded" -type d -name 'CrossDesk.app' -print -quit)"
if [[ -z "${APP_PATH}" ]]; then
echo "CrossDesk.app is missing from ${PACKAGE_FILE}" >&2
exit 1
fi
./scripts/macosx/verify_app.sh "${APP_PATH}" "${{ matrix.binary_arch }}" "${EXPECTED_MACOS_SDK}"
- name: Upload build artifacts
uses: actions/upload-artifact@v6
with:
@@ -404,8 +463,7 @@ jobs:
release:
name: Publish Release
if: startsWith(github.ref, 'refs/tags/v')
needs:
[build-linux, build-macos, build-windows-x64]
needs: [build-linux, build-macos, build-windows-x64]
runs-on: ubuntu-latest
steps:
+30 -8
View File
@@ -32,7 +32,7 @@ CrossDesk 是 [MiniRTC](https://github.com/kunkundi/minirtc.git) 实时音视频
|----------------|---------------------------|
| **Windows** | Windows 10 及以上 (64 位) |
| **macOS** | macOS Intel 15.0 及以上 ( 大于 14.0 小于 15.0 的版本可自行编译实现兼容 )<br> macOS Apple Silicon 14.0 及以上 |
| **Linux** | Ubuntu 22.04 及以上 ( 低版本可自行编译实现兼容 ) |
| **Linux** | Ubuntu 20.04 及以上 |
## 使用
@@ -80,16 +80,18 @@ Windows 安装包会自动打包 `crossdesk_service.exe` 和 `crossdesk_session_
依赖:
- [xmake](https://xmake.io/#/guide/installation)
- [cmake](https://cmake.org/download/)
- [cmake](https://cmake.org/download/) 3.21 及以上(系统版本过低时由 xmake 自动安装)
### Linux
Linux 构建目前支持 Ubuntu 22.04 及以上版本的 amd64 和 arm64。先安装基础编译依赖:
Linux 构建支持 Ubuntu 20.04 及以上版本的 amd64 和 arm64。发布安装包以
Ubuntu 20.04/glibc 2.31 为兼容基线。先安装基础编译依赖:
```bash
sudo apt-get update
sudo apt-get install -y \
git curl unzip build-essential cmake pkg-config binutils dpkg-dev \
git curl unzip build-essential gcc-10 g++-10 python3-pip \
pkg-config binutils dpkg-dev \
libx11-dev libxext-dev libxrender-dev libxft-dev libxrandr-dev \
libxinerama-dev libxcursor-dev libxi-dev libxfixes-dev libxv-dev \
libxtst-dev libxcb-randr0-dev libxcb-xtest0-dev libxcb-xinerama0-dev \
@@ -97,10 +99,19 @@ sudo apt-get install -y \
libasound2-dev libsndio-dev libpulse-dev
```
Ubuntu 20.04 自带的 CMake 3.16 不满足要求时,xmake 会自动下载并使用
CMake 3.21 或更高版本,无需手动替换系统 CMake。
Ubuntu 20.04 默认的 GCC 9 缺少 Slint C++ API 所需的部分 C++20 标准库,
因此上述命令同时安装并在后续配置中指定 GCC 10。
需要启用 Wayland 捕获或 DRM 捕获时,再安装对应依赖:
```bash
sudo apt-get install -y libdbus-1-dev libpipewire-0.3-dev libdrm-dev
sudo apt-get install -y libdbus-1-dev libdrm-dev
# Ubuntu 22.04 及以上也可以直接安装系统开发包
# sudo apt-get install -y libpipewire-0.3-dev libspa-0.2-dev
```
下载子模块并编译 Release 版本:
@@ -112,16 +123,25 @@ cd crossdesk
# 已经克隆过仓库时执行这一行
git submodule update --init --recursive
xmake f -m release --USE_CUDA=false -y
# Ubuntu 20.04 没有 PipeWire 0.3 开发包,安装仅用于编译的头文件 SDK。
# 已安装发行版 PipeWire 0.3 开发包时可跳过此步骤。
sudo ./docker/linux-build/install-pipewire-sdk.sh
xmake f -c -m release --USE_CUDA=false --toolchain=gcc-10 -y
xmake b -vy crossdesk
```
这里应使用 `--toolchain=gcc-10`,以确保 CrossDesk 及 xmake 在配置阶段构建的
libyuv 等第三方包都使用 GCC 10。只指定 `--cc`/`--cxx` 时,依赖安装可能仍回退到
系统默认的 `/bin/cc``/bin/c++``-c` 会清除旧配置,切换编译器时必须保留。
amd64 构建结果位于 `build/linux/x86_64/release/crossdesk`arm64 构建结果位于 `build/linux/arm64/release/crossdesk`
启用 Wayland 和 DRM 的配置示例:
```bash
xmake f -m release --USE_WAYLAND=true --USE_DRM=true --USE_CUDA=false -y
xmake f -c -m release --USE_WAYLAND=true --USE_DRM=true --USE_CUDA=false \
--toolchain=gcc-10 -y
xmake b -vy crossdesk
```
@@ -136,6 +156,8 @@ xmake b -vy crossdesk
```
打包脚本会将 Slint 共享运行库安装到软件包私有目录 `/usr/lib/crossdesk`,无需用户另外安装 `libslint_cpp.so`
PipeWire 不属于强制运行时依赖:程序在运行时检测宿主系统的 PipeWire 0.3
没有该运行库时仍可使用 X11(以及构建时启用的 DRM)捕获。
### 通用编译选项
@@ -158,7 +180,7 @@ xmake r crossdesk
### 无 CUDA 环境下的开发支持
对于**未安装 CUDA 环境的 Linux 开发者,如果希望编译后的成果物拥有硬件编解码能力**,这里提供了预配置的 [Ubuntu 22.04 Docker 镜像](https://hub.docker.com/r/crossdesk/ubuntu22.04)。该镜像内置必要的构建依赖,可在容器中开箱即用,无需额外配置即可直接编译项目
对于**未安装 CUDA 环境的 Linux 开发者,如果希望编译后的成果物拥有硬件编解码能力**,这里提供了预配置的 [Ubuntu 20.04 兼容构建镜像](https://hub.docker.com/r/crossdesk/ubuntu20.04)。该镜像内置必要的构建依赖,可生成兼容 glibc 2.31 的单一 Linux 安装包
进入容器,下载工程后执行:
```
+38 -9
View File
@@ -32,7 +32,7 @@ CrossDesk is an experimental application of [MiniRTC](https://github.com/kunkund
|-----------|-----------------|
| **Windows** | Windows 10 or later (64-bit) |
| **macOS** | macOS Intel 15.0 or later *(versions between 14.0 and 15.0 can be built manually for compatibility)*<br>macOS Apple Silicon 14.0 or later |
| **Linux** | Ubuntu 22.04 or later *(older versions can be built manually for compatibility)* |
| **Linux** | Ubuntu 20.04 or later |
## Usage
@@ -85,16 +85,19 @@ If the remote Windows service is not installed, not running, or temporarily unav
Requirements:
- [xmake](https://xmake.io/#/guide/installation)
- [cmake](https://cmake.org/download/)
- [cmake](https://cmake.org/download/) 3.21 or later (installed automatically by xmake when the system version is too old)
### Linux
Linux builds currently support Ubuntu 22.04 or later on amd64 and arm64. Install the base build dependencies first:
Linux builds support Ubuntu 20.04 or later on amd64 and arm64. Release packages
use Ubuntu 20.04/glibc 2.31 as their compatibility baseline. Install the base
build dependencies first:
```bash
sudo apt-get update
sudo apt-get install -y \
git curl unzip build-essential cmake pkg-config binutils dpkg-dev \
git curl unzip build-essential gcc-10 g++-10 python3-pip \
pkg-config binutils dpkg-dev \
libx11-dev libxext-dev libxrender-dev libxft-dev libxrandr-dev \
libxinerama-dev libxcursor-dev libxi-dev libxfixes-dev libxv-dev \
libxtst-dev libxcb-randr0-dev libxcb-xtest0-dev libxcb-xinerama0-dev \
@@ -102,10 +105,21 @@ sudo apt-get install -y \
libasound2-dev libsndio-dev libpulse-dev
```
When Ubuntu 20.04's bundled CMake 3.16 is too old, xmake automatically
downloads and uses CMake 3.21 or later; replacing the system CMake manually is
not required.
Ubuntu 20.04's default GCC 9 lacks parts of the C++20 standard library needed
by Slint's C++ API, so the commands above install GCC 10 and the configuration
below selects it explicitly.
Install the optional dependencies when Wayland capture or DRM capture is enabled:
```bash
sudo apt-get install -y libdbus-1-dev libpipewire-0.3-dev libdrm-dev
sudo apt-get install -y libdbus-1-dev libdrm-dev
# Ubuntu 22.04 and later can alternatively use the distribution packages.
# sudo apt-get install -y libpipewire-0.3-dev libspa-0.2-dev
```
Clone the submodules and build a release binary:
@@ -117,16 +131,28 @@ cd crossdesk
# Run this when the repository has already been cloned
git submodule update --init --recursive
xmake f -m release --USE_CUDA=false -y
# Ubuntu 20.04 has no PipeWire 0.3 development package. Install the
# header-only SDK used during compilation. Skip this when the distribution's
# PipeWire 0.3 development packages are already installed.
sudo ./docker/linux-build/install-pipewire-sdk.sh
xmake f -c -m release --USE_CUDA=false --toolchain=gcc-10 -y
xmake b -vy crossdesk
```
Use `--toolchain=gcc-10` so that both CrossDesk and third-party packages such
as libyuv, which xmake builds during configuration, use GCC 10. Specifying only
`--cc`/`--cxx` may still let dependency builds fall back to the system
`/bin/cc` and `/bin/c++`. Keep `-c` when switching compilers to clear the old
configuration.
The amd64 binary is written to `build/linux/x86_64/release/crossdesk`; the arm64 binary is written to `build/linux/arm64/release/crossdesk`.
Example configuration with Wayland and DRM capture enabled:
```bash
xmake f -m release --USE_WAYLAND=true --USE_DRM=true --USE_CUDA=false -y
xmake f -c -m release --USE_WAYLAND=true --USE_DRM=true --USE_CUDA=false \
--toolchain=gcc-10 -y
xmake b -vy crossdesk
```
@@ -141,6 +167,9 @@ Build a Debian package after compiling a release binary on the matching architec
```
The package scripts install the shared Slint runtime in the private `/usr/lib/crossdesk` directory, so users do not need to install `libslint_cpp.so` separately.
PipeWire is not a mandatory runtime dependency. CrossDesk detects the host's
PipeWire 0.3 runtime dynamically and can still use X11 capture (and DRM when
enabled at build time) when it is unavailable.
### Common build options
@@ -163,8 +192,8 @@ xmake r crossdesk
#### Development Without CUDA Environment
For **Linux developers who do not have a CUDA environment installed and want to enable hardware codec feature**, a preconfigured [Ubuntu 22.04 Docker image](https://hub.docker.com/r/crossdesk/ubuntu22.04) is provided.
This image comes with all required build dependencies and allows you to build the project directly inside the container without any additional setup.
For **Linux developers who do not have a CUDA environment installed and want to enable hardware codec feature**, a preconfigured [Ubuntu 20.04 compatibility build image](https://hub.docker.com/r/crossdesk/ubuntu20.04) is provided.
This image contains the required build dependencies and produces a single Linux package compatible with the glibc 2.31 baseline.
After entering the container, download the project and run:
```
+25 -4
View File
@@ -1,15 +1,16 @@
# syntax=docker/dockerfile:1.7
ARG BASE_IMAGE=nvidia/cuda:12.6.3-devel-ubuntu22.04
ARG BASE_IMAGE=nvidia/cuda:12.6.3-devel-ubuntu20.04
FROM ${BASE_IMAGE}
ARG TARGETARCH
ARG CMAKE_VERSION=3.31.6
ARG XMAKE_VERSION=3.0.9
ARG RUST_VERSION=1.92.0
ARG RUSTUP_VERSION=1.28.2
LABEL org.opencontainers.image.title="CrossDesk Linux build environment" \
org.opencontainers.image.description="Ubuntu 22.04 build environment for CrossDesk" \
org.opencontainers.image.description="Ubuntu 20.04 compatibility build environment for CrossDesk" \
org.opencontainers.image.source="https://github.com/kunkundi/crossdesk" \
org.opencontainers.image.licenses="GPL-3.0-only"
@@ -22,6 +23,8 @@ ENV DEBIAN_FRONTEND=noninteractive \
XMAKE_GLOBALDIR=/data \
XMAKE_ROOT=y \
CUDA_PATH=/usr/local/cuda \
CC=/usr/bin/gcc-10 \
CXX=/usr/bin/g++-10 \
PATH=/root/.local/bin:/opt/cargo/bin:/usr/local/bin:${PATH}
SHELL ["/bin/bash", "-o", "pipefail", "-c"]
@@ -35,13 +38,14 @@ RUN apt-get update \
build-essential \
ca-certificates \
ccache \
cmake \
curl \
dbus \
dpkg-dev \
file \
flex \
gettext \
gcc-10 \
g++-10 \
git \
libasound2-dev \
libdbus-1-dev \
@@ -93,6 +97,22 @@ RUN apt-get update \
zlib1g-dev \
&& rm -rf /var/lib/apt/lists/*
# Slint 1.17 requires CMake 3.21 or newer, while Ubuntu 20.04 provides 3.16.
# PyPI publishes pinned manylinux2014 wheels for both amd64 and arm64.
RUN python3 -m pip install --no-cache-dir "cmake==${CMAKE_VERSION}" \
&& cmake --version | grep -F "cmake version ${CMAKE_VERSION}"
# Ubuntu 20.04 only ships PipeWire 0.2. Install the Ubuntu 22.04-era 0.3.48
# headers as a build-only SDK. CrossDesk loads the host's PipeWire 0.3 runtime
# with dlopen(), so no PipeWire shared library is copied into release packages.
COPY docker/linux-build/install-pipewire-sdk.sh \
/usr/local/bin/install-crossdesk-pipewire-sdk
RUN chmod 0755 /usr/local/bin/install-crossdesk-pipewire-sdk \
&& install-crossdesk-pipewire-sdk \
&& grep -F "#define PW_MICRO 48" \
/opt/crossdesk-pipewire-sdk/include/pipewire-0.3/pipewire/version.h
RUN export GITHUB_ACTIONS=true \
&& curl -fsSL https://xmake.io/shget.text \
| bash -s -- "v${XMAKE_VERSION}" \
@@ -134,7 +154,8 @@ COPY submodules/minirtc/thirdparty ./submodules/minirtc/thirdparty
# CrossDesk's exact dependency graph. Source files are intentionally excluded
# from the Docker context; source-only changes therefore reuse this layer.
RUN xmake repo -u --root \
&& xmake f -m release --USE_CUDA=true --root -y \
&& xmake f -m release --USE_CUDA=true --USE_WAYLAND=true \
--cc=gcc-10 --cxx=g++-10 --root -y \
&& rm -rf /data/.xmake/cache /tmp/crossdesk-dependencies \
&& mkdir -p /workspace
+15 -8
View File
@@ -1,7 +1,9 @@
# CrossDesk Linux 构建镜像
这个目录维护 CrossDesk 专用的 Ubuntu 22.04 构建环境。镜像包含 xmake
Rust、Linux 开发库以及根据项目 `xmake.lua` 提前编译好的依赖。
这个目录维护 CrossDesk 专用的 Ubuntu 20.04 兼容构建环境。镜像包含 GCC 10
CMake 3.31、xmake、Rust、Linux 开发库(包括 D-Bus、DRM,以及仅用于编译的
PipeWire 0.3/SPA 头文件 SDK)以及根据项目
`xmake.lua` 提前编译好的依赖。
## 发布方式
@@ -10,14 +12,19 @@ Rust、Linux 开发库以及根据项目 `xmake.lua` 提前编译好的依赖。
全部通过后再合并为同一个 multi-arch 镜像:
```text
crossdesk/ubuntu22.04:buildenv-cuda12.6.3-xmake3.0.9-rust1.92.0
crossdesk/ubuntu20.04:buildenv-cuda12.6.3-gcc10-cmake3.31.6-pipewire0.3.48-xmake3.0.9-rust1.92.0
```
Docker 会根据运行机器自动选择正确架构。amd64 变体以
`nvidia/cuda:12.6.3-devel-ubuntu22.04` 为基础;arm64 变体不包含当前构建
用不到的 CUDA SDK。两个变体都使用 Ubuntu 22.04/glibc 2.35。
因此该镜像生成的 Linux 安装包最低支持 Ubuntu 22.04,不再保证兼容
Ubuntu 20.04。
`nvidia/cuda:12.6.3-devel-ubuntu20.04` 为基础;arm64 变体不包含当前构建
用不到的 CUDA SDK。两个变体都使用 Ubuntu 20.04/glibc 2.31,因此生成的
单一 Linux 安装包可运行在 Ubuntu 20.04 及更高版本。
Ubuntu 20.04 仓库只提供 PipeWire 0.2。镜像通过
`install-pipewire-sdk.sh` 安装固定版本的 PipeWire 0.3.48/SPA 头文件,但不
安装或打包 PipeWire 动态库。CrossDesk 在运行时通过 `dlopen()` 使用宿主系统
提供的 PipeWire 0.3Ubuntu 20.04 没有该运行库时仍可使用 X11(以及构建时
启用的 DRM),Ubuntu 22.04 及更高版本则可使用 Wayland 捕获。
每次成功发布还会生成 `sha-<完整提交 SHA>`,用于精确回滚;`latest` 和上面的
工具链版本 tag 只会在两个架构都验证成功后更新。
@@ -26,7 +33,7 @@ Ubuntu 20.04。
在仓库的 Actions secrets 中配置:
- `DOCKERHUB_USERNAME`:有权推送 `crossdesk/ubuntu22.04` 的 Docker Hub 用户名
- `DOCKERHUB_USERNAME`:有权推送 `crossdesk/ubuntu20.04` 的 Docker Hub 用户名
- `DOCKERHUB_TOKEN`:该用户的 Docker Hub access token
工作流文件进入默认分支后,也可以从 Actions 页面手动运行。为了避免首次发布
+55
View File
@@ -0,0 +1,55 @@
#!/usr/bin/env bash
set -euo pipefail
PIPEWIRE_VERSION="0.3.48"
PIPEWIRE_ARCHIVE_SHA256="0d218be86b3d2b548c06259c47ad8d110ee1c09f071e17c4393eeef5c880fa6f"
INSTALL_PREFIX="${1:-/opt/crossdesk-pipewire-sdk}"
IFS='.' read -r PIPEWIRE_VERSION_MAJOR PIPEWIRE_VERSION_MINOR \
PIPEWIRE_VERSION_MICRO <<< "${PIPEWIRE_VERSION}"
if [[ "${INSTALL_PREFIX}" != /* ]]; then
echo "Install prefix must be an absolute path: ${INSTALL_PREFIX}" >&2
exit 2
fi
WORK_DIR="$(mktemp -d "${TMPDIR:-/tmp}/crossdesk-pipewire-sdk.XXXXXX")"
trap 'rm -rf "${WORK_DIR}"' EXIT
ARCHIVE_PATH="${WORK_DIR}/pipewire-${PIPEWIRE_VERSION}.tar.gz"
SOURCE_DIR="${WORK_DIR}/pipewire-${PIPEWIRE_VERSION}"
ARCHIVE_URL="https://gitlab.freedesktop.org/pipewire/pipewire/-/archive/${PIPEWIRE_VERSION}/pipewire-${PIPEWIRE_VERSION}.tar.gz"
curl --proto '=https' --tlsv1.2 --fail --silent --show-error --location \
--retry 3 "${ARCHIVE_URL}" -o "${ARCHIVE_PATH}"
echo "${PIPEWIRE_ARCHIVE_SHA256} ${ARCHIVE_PATH}" | sha256sum --check -
tar -xzf "${ARCHIVE_PATH}" -C "${WORK_DIR}"
PIPEWIRE_INCLUDE_DIR="${INSTALL_PREFIX}/include/pipewire-0.3/pipewire"
SPA_INCLUDE_DIR="${INSTALL_PREFIX}/include/spa-0.2/spa"
while IFS= read -r -d '' header_path; do
relative_path="${header_path#${SOURCE_DIR}/src/pipewire/}"
install -D -m 0644 "${header_path}" \
"${PIPEWIRE_INCLUDE_DIR}/${relative_path}"
done < <(find "${SOURCE_DIR}/src/pipewire" -type f -name '*.h' -print0)
while IFS= read -r -d '' header_path; do
relative_path="${header_path#${SOURCE_DIR}/spa/include/spa/}"
install -D -m 0644 "${header_path}" \
"${SPA_INCLUDE_DIR}/${relative_path}"
done < <(find "${SOURCE_DIR}/spa/include/spa" -type f -name '*.h' -print0)
sed \
-e "s/@PIPEWIRE_VERSION_MAJOR@/${PIPEWIRE_VERSION_MAJOR}/g" \
-e "s/@PIPEWIRE_VERSION_MINOR@/${PIPEWIRE_VERSION_MINOR}/g" \
-e "s/@PIPEWIRE_VERSION_MICRO@/${PIPEWIRE_VERSION_MICRO}/g" \
-e 's/@PIPEWIRE_API_VERSION@/"0.3"/g' \
"${SOURCE_DIR}/src/pipewire/version.h.in" \
> "${PIPEWIRE_INCLUDE_DIR}/version.h"
chmod 0644 "${PIPEWIRE_INCLUDE_DIR}/version.h"
test -f "${PIPEWIRE_INCLUDE_DIR}/pipewire.h"
test -f "${PIPEWIRE_INCLUDE_DIR}/version.h"
test -f "${SPA_INCLUDE_DIR}/param/video/format-utils.h"
echo "Installed PipeWire ${PIPEWIRE_VERSION} header SDK under ${INSTALL_PREFIX}"
+49 -7
View File
@@ -2,19 +2,22 @@
set -euo pipefail
EXPECTED_ARCH="${EXPECTED_ARCH:-}"
EXPECTED_CMAKE_VERSION="${EXPECTED_CMAKE_VERSION:-3.31.6}"
EXPECTED_XMAKE_VERSION="${EXPECTED_XMAKE_VERSION:-3.0.9}"
EXPECTED_RUST_VERSION="${EXPECTED_RUST_VERSION:-1.92.0}"
BUILD_CC="${CC:-/usr/bin/gcc-10}"
BUILD_CXX="${CXX:-/usr/bin/g++-10}"
# shellcheck disable=SC1091
source /etc/os-release
if [[ "${ID}" != "ubuntu" || "${VERSION_ID}" != "22.04" ]]; then
echo "Expected Ubuntu 22.04, found ${ID:-unknown} ${VERSION_ID:-unknown}" >&2
if [[ "${ID}" != "ubuntu" || "${VERSION_ID}" != "20.04" ]]; then
echo "Expected Ubuntu 20.04, found ${ID:-unknown} ${VERSION_ID:-unknown}" >&2
exit 1
fi
GLIBC_VERSION="$(getconf GNU_LIBC_VERSION | awk '{print $2}')"
if [[ "${GLIBC_VERSION}" != "2.35" ]]; then
echo "Expected glibc 2.35, found ${GLIBC_VERSION}" >&2
if [[ "${GLIBC_VERSION}" != "2.31" ]]; then
echo "Expected glibc 2.31, found ${GLIBC_VERSION}" >&2
exit 1
fi
@@ -28,9 +31,48 @@ XMAKE_VERSION_OUTPUT="$(xmake --version 2>&1)"
grep -F "v${EXPECTED_XMAKE_VERSION}" <<<"${XMAKE_VERSION_OUTPUT}" >/dev/null
rustc --version | grep -F "rustc ${EXPECTED_RUST_VERSION}" >/dev/null
cargo --version >/dev/null
cmake --version >/dev/null
g++ --version >/dev/null
pkg-config --exists xft
cmake --version | grep -F "cmake version ${EXPECTED_CMAKE_VERSION}" >/dev/null
"${BUILD_CC}" --version | grep -F 'gcc-10' >/dev/null
"${BUILD_CXX}" --version | grep -F 'g++-10' >/dev/null
printf '%s\n' \
'#include <concepts>' \
'#include <span>' \
'template <typename T> concept TestConcept = true;' \
'int main() {' \
' int value = 0;' \
' std::span<int> values(&value, 1);' \
' static_assert(TestConcept<int>);' \
' return values.empty();' \
'}' \
| "${BUILD_CXX}" -std=c++20 -fsyntax-only -x c++ -
for pkg_config_package in \
dbus-1 \
libdrm \
xft; do
if ! pkg-config --exists "${pkg_config_package}"; then
echo "Required pkg-config package is missing: ${pkg_config_package}" >&2
exit 1
fi
done
for sdk_header in \
/opt/crossdesk-pipewire-sdk/include/pipewire-0.3/pipewire/pipewire.h \
/opt/crossdesk-pipewire-sdk/include/pipewire-0.3/pipewire/version.h \
/opt/crossdesk-pipewire-sdk/include/spa-0.2/spa/param/video/format-utils.h; do
if [[ ! -f "${sdk_header}" ]]; then
echo "Required PipeWire build SDK header is missing: ${sdk_header}" >&2
exit 1
fi
done
printf '%s\n' \
'#include <pipewire/pipewire.h>' \
'#include <spa/param/video/format-utils.h>' \
'static_assert(PW_CHECK_VERSION(0, 3, 48));' \
'int main() { return 0; }' \
| "${BUILD_CXX}" -std=c++17 -fsyntax-only -x c++ \
-I/opt/crossdesk-pipewire-sdk/include/pipewire-0.3 \
-I/opt/crossdesk-pipewire-sdk/include/spa-0.2 -
for package_path in \
"s/slint" \
+1 -1
View File
@@ -7,7 +7,7 @@ if [[ $# -lt 1 || $# -gt 2 ]]; then
fi
INPUT_PATH="$1"
MAXIMUM_VERSION="${2:-2.35}"
MAXIMUM_VERSION="${2:-2.31}"
verify_elf() {
local elf_file="$1"
+10 -1
View File
@@ -104,6 +104,15 @@ OUTPUT_FILE="$PROJECT_ROOT/${PKG_NAME}-linux-${DEBIAN_ARCH}-${APP_VERSION}.deb"
install -d "$DEBIAN_DIR" "$BIN_DIR" "$PRIVATE_DIR" "$DESKTOP_DIR"
install -m 0755 "$BUILD_BINARY" "$PRIVATE_DIR/crossdesk-bin"
# PipeWire is deliberately optional. The Wayland capturer resolves PipeWire
# 0.3 with dlopen() so the same package can run on Ubuntu 20.04 without the
# runtime while using the host runtime on newer Ubuntu releases.
if readelf -d "$BUILD_BINARY" \
| grep -qE 'Shared library: \[libpipewire-0\.3\.so'; then
echo "CrossDesk must not link directly to the optional PipeWire runtime" >&2
exit 1
fi
# Slint is intentionally built as a shared library. Keep that runtime private
# to CrossDesk instead of relying on a non-existent distribution package.
SLINT_NEEDED="$(readelf -d "$BUILD_BINARY" | sed -n \
@@ -147,7 +156,7 @@ Version: $DEB_VERSION
Architecture: $DEBIAN_ARCH
Maintainer: $MAINTAINER
Description: $DESCRIPTION
Depends: libc6 (>= 2.35), libstdc++6 (>= 9), libx11-6, libxext6,
Depends: libc6 (>= 2.31), libstdc++6 (>= 10), libx11-6, libxext6,
libxrender1, libxft2, libxrandr2, libxfixes3, libxcb1, libxcb-randr0,
libxcb-xtest0, libxcb-xinerama0, libxcb-shape0, libxcb-xkb1,
libxcb-xfixes0, libxv1, libxtst6, $ALSA_RUNTIME_DEP, libsndio7.0,
+131
View File
@@ -0,0 +1,131 @@
#!/usr/bin/env bash
set -euo pipefail
if [[ $# -ne 2 ]]; then
echo "Usage: $0 <app-bundle> <expected-architecture>" >&2
exit 2
fi
APP_BUNDLE="$1"
EXPECTED_ARCH="$2"
APP_EXECUTABLE="$APP_BUNDLE/Contents/MacOS/CrossDesk"
FRAMEWORKS_DIR="$APP_BUNDLE/Contents/Frameworks"
for command_name in codesign install_name_tool lipo otool; do
if ! command -v "$command_name" >/dev/null 2>&1; then
echo "Required bundling command is missing: $command_name" >&2
exit 1
fi
done
if [[ ! -x "$APP_EXECUTABLE" ]]; then
echo "Missing app executable: $APP_EXECUTABLE" >&2
exit 1
fi
if ! lipo "$APP_EXECUTABLE" -verify_arch "$EXPECTED_ARCH" >/dev/null 2>&1; then
echo "App executable does not contain the expected architecture: $EXPECTED_ARCH" >&2
lipo -archs "$APP_EXECUTABLE" >&2 || true
exit 1
fi
SLINT_REFERENCE="$(otool -L "$APP_EXECUTABLE" | awk \
'$1 ~ /(^|\/)libslint_cpp[^\/[:space:]]*\.dylib$/ { print $1; exit }')"
if [[ -z "$SLINT_REFERENCE" ]]; then
echo "The app executable does not reference the Slint runtime" >&2
exit 1
fi
SLINT_NAME="${SLINT_REFERENCE##*/}"
SLINT_CURRENT_VERSION="$(otool -L "$APP_EXECUTABLE" | awk -v dependency="$SLINT_REFERENCE" '
$1 == dependency {
for (i = 1; i <= NF; ++i) {
if ($i == "current") {
version = $(i + 2)
gsub(/[),]/, "", version)
print version
exit
}
}
}')"
slint_runtime_version() {
local library="$1"
otool -L "$library" | awk -v name="$SLINT_NAME" '
index($1, "/" name) || $1 == "@rpath/" name {
for (i = 1; i <= NF; ++i) {
if ($i == "current") {
version = $(i + 2)
gsub(/[),]/, "", version)
print version
exit
}
}
}'
}
find_slint_runtime() {
local candidate=""
local candidate_version=""
local package_root=""
local package_roots=()
if [[ "$SLINT_REFERENCE" == /* && -f "$SLINT_REFERENCE" ]]; then
if lipo "$SLINT_REFERENCE" -verify_arch "$EXPECTED_ARCH" >/dev/null 2>&1; then
echo "$SLINT_REFERENCE"
return 0
fi
fi
if [[ -n "${XMAKE_GLOBALDIR:-}" ]]; then
package_roots+=("$XMAKE_GLOBALDIR/.xmake/packages")
fi
package_roots+=("$HOME/.xmake/packages" "/data/.xmake/packages")
for package_root in "${package_roots[@]}"; do
[[ -d "$package_root/s/slint" ]] || continue
while IFS= read -r candidate; do
if ! lipo "$candidate" -verify_arch "$EXPECTED_ARCH" >/dev/null 2>&1; then
continue
fi
candidate_version="$(slint_runtime_version "$candidate")"
if [[ -n "$SLINT_CURRENT_VERSION" && "$candidate_version" != "$SLINT_CURRENT_VERSION" ]]; then
continue
fi
echo "$candidate"
return 0
done < <(find "$package_root/s/slint" -type f -path "*/lib/$SLINT_NAME" -print 2>/dev/null)
done
return 1
}
SLINT_LIBRARY="$(find_slint_runtime || true)"
if [[ -z "$SLINT_LIBRARY" ]]; then
echo "Unable to locate $SLINT_NAME for $EXPECTED_ARCH (current version $SLINT_CURRENT_VERSION)" >&2
exit 1
fi
install -d "$FRAMEWORKS_DIR"
install -m 0755 "$SLINT_LIBRARY" "$FRAMEWORKS_DIR/$SLINT_NAME"
install_name_tool -id "@rpath/$SLINT_NAME" "$FRAMEWORKS_DIR/$SLINT_NAME"
if [[ "$SLINT_REFERENCE" != "@rpath/$SLINT_NAME" ]]; then
install_name_tool -change "$SLINT_REFERENCE" "@rpath/$SLINT_NAME" "$APP_EXECUTABLE"
fi
if ! otool -l "$APP_EXECUTABLE" | awk '
$1 == "cmd" { in_rpath = ($2 == "LC_RPATH"); next }
in_rpath && $1 == "path" && $2 == "@executable_path/../Frameworks" { found = 1 }
END { exit(found ? 0 : 1) }'; then
install_name_tool -add_rpath "@executable_path/../Frameworks" "$APP_EXECUTABLE"
fi
# install_name_tool invalidates the linker's ad-hoc signature. Sign nested code
# first, then seal the complete app bundle so local packages remain launchable.
codesign --force --sign - "$FRAMEWORKS_DIR/$SLINT_NAME"
codesign --force --deep --sign - "$APP_BUNDLE"
echo "Bundled Slint runtime: $SLINT_LIBRARY"
+8 -3
View File
@@ -1,14 +1,16 @@
#!/bin/bash
set -e
#!/usr/bin/env bash
set -euo pipefail
APP_NAME="crossdesk"
APP_NAME_UPPER="CrossDesk"
EXECUTABLE_PATH="./build/macosx/arm64/release/crossdesk"
PLATFORM="macos"
ARCH="arm64"
BINARY_ARCH="arm64"
IDENTIFIER="cn.crossdesk.app"
ICON_PATH="icons/macos/crossdesk.icns"
MACOS_MIN_VERSION="10.12"
MACOS_MIN_VERSION="14.0"
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
normalize_app_version() {
local input="$1"
@@ -95,6 +97,9 @@ EOF
xattr -cr "${APP_BUNDLE}" 2>/dev/null || true
find "${APP_BUNDLE}" -name '._*' -delete
"${SCRIPT_DIR}/bundle_app.sh" "${APP_BUNDLE}" "${BINARY_ARCH}"
"${SCRIPT_DIR}/verify_app.sh" "${APP_BUNDLE}" "${BINARY_ARCH}"
echo ".app created successfully."
mkdir -p build_pkg_scripts
+8 -3
View File
@@ -1,14 +1,16 @@
#!/bin/bash
set -e
#!/usr/bin/env bash
set -euo pipefail
APP_NAME="crossdesk"
APP_NAME_UPPER="CrossDesk"
EXECUTABLE_PATH="build/macosx/x86_64/release/crossdesk"
PLATFORM="macos"
ARCH="x64"
BINARY_ARCH="x86_64"
IDENTIFIER="cn.crossdesk.app"
ICON_PATH="icons/macos/crossdesk.icns"
MACOS_MIN_VERSION="10.12"
MACOS_MIN_VERSION="14.0"
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
normalize_app_version() {
local input="$1"
@@ -95,6 +97,9 @@ EOF
xattr -cr "${APP_BUNDLE}" 2>/dev/null || true
find "${APP_BUNDLE}" -name '._*' -delete
"${SCRIPT_DIR}/bundle_app.sh" "${APP_BUNDLE}" "${BINARY_ARCH}"
"${SCRIPT_DIR}/verify_app.sh" "${APP_BUNDLE}" "${BINARY_ARCH}"
echo ".app created successfully."
mkdir -p build_pkg_scripts
+117
View File
@@ -0,0 +1,117 @@
#!/usr/bin/env bash
set -euo pipefail
if [[ $# -lt 2 || $# -gt 3 ]]; then
echo "Usage: $0 <app-bundle> <expected-architecture> [expected-sdk-version]" >&2
exit 2
fi
APP_BUNDLE="$1"
EXPECTED_ARCH="$2"
EXPECTED_SDK_VERSION="${3:-}"
APP_EXECUTABLE="$APP_BUNDLE/Contents/MacOS/CrossDesk"
FRAMEWORKS_DIR="$APP_BUNDLE/Contents/Frameworks"
INFO_PLIST="$APP_BUNDLE/Contents/Info.plist"
if [[ ! -x "$APP_EXECUTABLE" ]]; then
echo "Missing app executable: $APP_EXECUTABLE" >&2
exit 1
fi
if ! lipo "$APP_EXECUTABLE" -verify_arch "$EXPECTED_ARCH" >/dev/null 2>&1; then
echo "Unexpected app architecture; expected $EXPECTED_ARCH" >&2
lipo -archs "$APP_EXECUTABLE" >&2 || true
exit 1
fi
if ! otool -l "$APP_EXECUTABLE" | awk '
$1 == "cmd" { in_rpath = ($2 == "LC_RPATH"); next }
in_rpath && $1 == "path" && $2 == "@executable_path/../Frameworks" { found = 1 }
END { exit(found ? 0 : 1) }'; then
echo "Missing app rpath: @executable_path/../Frameworks" >&2
exit 1
fi
verify_dependencies() {
local macho="$1"
local dependency=""
local relative_path=""
while IFS= read -r dependency; do
case "$dependency" in
/System/Library/*|/usr/lib/*)
;;
@rpath/*)
relative_path="${dependency#@rpath/}"
if [[ ! -f "$FRAMEWORKS_DIR/$relative_path" ]]; then
echo "Unbundled dependency in $macho: $dependency" >&2
return 1
fi
;;
@executable_path/../Frameworks/*)
relative_path="${dependency#@executable_path/../Frameworks/}"
if [[ ! -f "$FRAMEWORKS_DIR/$relative_path" ]]; then
echo "Unbundled dependency in $macho: $dependency" >&2
return 1
fi
;;
@loader_path/*)
relative_path="${dependency#@loader_path/}"
if [[ ! -f "$(dirname "$macho")/$relative_path" ]]; then
echo "Unbundled dependency in $macho: $dependency" >&2
return 1
fi
;;
*)
echo "External non-system dependency in $macho: $dependency" >&2
return 1
;;
esac
done < <(otool -L "$macho" | tail -n +2 | awk '{ print $1 }')
}
verify_dependencies "$APP_EXECUTABLE"
SLINT_LIBRARY="$(find "$FRAMEWORKS_DIR" -maxdepth 1 -type f -name 'libslint_cpp*.dylib' -print -quit 2>/dev/null || true)"
if [[ -z "$SLINT_LIBRARY" ]]; then
echo "The packaged app does not contain libslint_cpp.dylib" >&2
exit 1
fi
if ! lipo "$SLINT_LIBRARY" -verify_arch "$EXPECTED_ARCH" >/dev/null 2>&1; then
echo "Unexpected Slint runtime architecture; expected $EXPECTED_ARCH" >&2
lipo -archs "$SLINT_LIBRARY" >&2 || true
exit 1
fi
verify_dependencies "$SLINT_LIBRARY"
PLIST_MIN_VERSION="$(/usr/libexec/PlistBuddy -c 'Print :LSMinimumSystemVersion' "$INFO_PLIST")"
BINARY_MIN_VERSION="$(otool -l "$APP_EXECUTABLE" | awk '
$1 == "cmd" { build_version = ($2 == "LC_BUILD_VERSION"); version_min = ($2 == "LC_VERSION_MIN_MACOSX"); next }
build_version && $1 == "minos" { print $2; exit }
version_min && $1 == "version" { print $2; exit }')"
if [[ -z "$BINARY_MIN_VERSION" || "$PLIST_MIN_VERSION" != "$BINARY_MIN_VERSION" ]]; then
echo "Minimum macOS version mismatch: plist=$PLIST_MIN_VERSION binary=${BINARY_MIN_VERSION:-unknown}" >&2
exit 1
fi
if [[ -n "$EXPECTED_SDK_VERSION" ]]; then
verify_sdk_version() {
local macho="$1"
local actual_sdk=""
actual_sdk="$(otool -l "$macho" | awk '
$1 == "cmd" { build_version = ($2 == "LC_BUILD_VERSION"); next }
build_version && $1 == "sdk" { print $2; exit }')"
if [[ "$actual_sdk" != "$EXPECTED_SDK_VERSION" ]]; then
echo "Unexpected macOS SDK in $macho: expected $EXPECTED_SDK_VERSION, got ${actual_sdk:-unknown}" >&2
return 1
fi
}
verify_sdk_version "$APP_EXECUTABLE"
verify_sdk_version "$SLINT_LIBRARY"
fi
codesign --verify --deep --strict --verbose=2 "$APP_BUNDLE"
echo "Verified $APP_BUNDLE ($EXPECTED_ARCH, macOS $BINARY_MIN_VERSION+)"
+15
View File
@@ -0,0 +1,15 @@
#ifndef CROSSDESK_COMMON_WINDOWS_INPUT_MARKER_H_
#define CROSSDESK_COMMON_WINDOWS_INPUT_MARKER_H_
#include <cstdint>
namespace crossdesk {
// SendInput copies dwExtraInfo into KBDLLHOOKSTRUCT. Tag CrossDesk-generated
// keyboard input so the controller hook can ignore only its own injections
// while still accepting input from accessibility tools or remote sessions.
inline constexpr std::uintptr_t kInjectedKeyboardInputMarker = 0x4353444B;
} // namespace crossdesk
#endif // CROSSDESK_COMMON_WINDOWS_INPUT_MARKER_H_
@@ -1,9 +1,14 @@
#include "keyboard_capturer.h"
#include <X11/Xlib.h>
#include <X11/extensions/XTest.h>
#include <X11/keysym.h>
#include <errno.h>
#include <poll.h>
#include "keyboard_converter.h"
#include "linux_evdev_keycode.h"
#include "platform.h"
#include "rd_log.h"
#include "windows_key_metadata.h"
@@ -13,6 +18,40 @@ namespace crossdesk {
static OnKeyAction g_on_key_action = nullptr;
static void* g_user_ptr = nullptr;
static KeyCode ResolveX11Keycode(Display* display, int key_code,
uint32_t scan_code, bool extended) {
if (!display) {
return 0;
}
const auto key_it = vkCodeToX11KeySym.find(key_code);
if (key_it != vkCodeToX11KeySym.end()) {
const KeyCode x11_keycode =
XKeysymToKeycode(display, static_cast<KeySym>(key_it->second));
if (x11_keycode != 0) {
return x11_keycode;
}
}
// Some controllers can preserve the physical Windows scan code even when
// they cannot resolve a Windows virtual-key value. Xorg's evdev keycodes use
// the Linux input code plus the protocol-reserved offset of 8.
const int evdev_keycode =
ResolveLinuxEvdevKeycodeFromWindowsKey(key_code, scan_code, extended);
if (evdev_keycode < 0) {
return 0;
}
int min_keycode = 0;
int max_keycode = 0;
XDisplayKeycodes(display, &min_keycode, &max_keycode);
const int x11_keycode = evdev_keycode + 8;
if (x11_keycode < min_keycode || x11_keycode > max_keycode) {
return 0;
}
return static_cast<KeyCode>(x11_keycode);
}
static KeySym NormalizeKeySym(KeySym key_sym) {
if (key_sym >= XK_a && key_sym <= XK_z) {
return key_sym - XK_a + XK_A;
@@ -58,7 +97,16 @@ KeyboardCapturer::KeyboardCapturer()
display_ = XOpenDisplay(nullptr);
if (!display_) {
LOG_ERROR("Failed to open X display.");
return;
}
int event_base = 0;
int error_base = 0;
int major_version = 0;
int minor_version = 0;
x11_xtest_available_ =
XTestQueryExtension(display_, &event_base, &error_base, &major_version,
&minor_version) != 0;
}
KeyboardCapturer::~KeyboardCapturer() {
@@ -66,8 +114,10 @@ KeyboardCapturer::~KeyboardCapturer() {
CleanupWaylandPortal();
if (display_) {
std::lock_guard<std::mutex> lock(x11_injection_mutex_);
XCloseDisplay(display_);
display_ = nullptr;
x11_xtest_available_ = false;
}
}
@@ -152,8 +202,6 @@ int KeyboardCapturer::Unhook() {
int KeyboardCapturer::SendKeyboardCommand(int key_code, bool is_down,
uint32_t scan_code, bool extended) {
(void)scan_code;
(void)extended;
if (IsWaylandSession()) {
if (!use_wayland_portal_ && !wayland_init_attempted_) {
wayland_init_attempted_ = true;
@@ -177,12 +225,33 @@ int KeyboardCapturer::SendKeyboardCommand(int key_code, bool is_down,
return -1;
}
if (vkCodeToX11KeySym.find(key_code) != vkCodeToX11KeySym.end()) {
int x11_key_code = vkCodeToX11KeySym[key_code];
KeyCode keycode = XKeysymToKeycode(display_, x11_key_code);
XTestFakeKeyEvent(display_, keycode, is_down, CurrentTime);
XFlush(display_);
std::lock_guard<std::mutex> lock(x11_injection_mutex_);
if (!x11_xtest_available_) {
LOG_ERROR("XTest extension not available for keyboard injection");
return -2;
}
const KeyCode x11_keycode =
ResolveX11Keycode(display_, key_code, scan_code, extended);
if (x11_keycode == 0) {
LOG_WARN(
"Cannot map remote keyboard event to X11 keycode, vk_code={}, "
"scan_code={}, extended={}",
key_code, scan_code, extended);
return -3;
}
if (!XTestFakeKeyEvent(display_, x11_keycode, is_down, CurrentTime)) {
LOG_ERROR(
"XTest keyboard injection failed, vk_code={}, scan_code={}, "
"extended={}, x11_keycode={}, is_down={}",
key_code, scan_code, extended, static_cast<int>(x11_keycode), is_down);
return -4;
}
// Complete the request before reporting success to the keyboard-state
// reconciler. This also makes X11 protocol errors observable immediately.
XSync(display_, False);
return 0;
}
} // namespace crossdesk
@@ -7,13 +7,10 @@
#ifndef _KEYBOARD_CAPTURER_H_
#define _KEYBOARD_CAPTURER_H_
#include <X11/Xlib.h>
#include <X11/extensions/XTest.h>
#include <X11/keysym.h>
#include <atomic>
#include <cstdint>
#include <functional>
#include <mutex>
#include <string>
#include <thread>
@@ -21,6 +18,7 @@
struct DBusConnection;
struct DBusMessageIter;
struct _XDisplay;
namespace crossdesk {
@@ -48,10 +46,12 @@ class KeyboardCapturer : public DeviceController {
append_args);
private:
Display* display_;
Window root_;
_XDisplay* display_;
unsigned long root_;
std::atomic<bool> running_;
std::thread event_thread_;
std::mutex x11_injection_mutex_;
bool x11_xtest_available_ = false;
bool use_wayland_portal_ = false;
bool wayland_init_attempted_ = false;
DBusConnection* dbus_connection_ = nullptr;
@@ -5,6 +5,9 @@
#include <map>
#if defined(CROSSDESK_HAS_WAYLAND_CAPTURER) && CROSSDESK_HAS_WAYLAND_CAPTURER
#include <X11/Xlib.h>
#include <X11/keysym.h>
#include <dbus/dbus.h>
#endif
@@ -608,6 +611,7 @@ int KeyboardCapturer::SendWaylandKeyboardCommand(int key_code, bool is_down,
// Prefer keycode injection to preserve physical-key semantics and avoid
// implicit Shift interpretation for uppercase keysyms.
if (display_) {
std::lock_guard<std::mutex> lock(x11_injection_mutex_);
const int keysym = key_it->second;
const KeyCode x11_keycode =
XKeysymToKeycode(display_, static_cast<KeySym>(keysym));
@@ -1,5 +1,6 @@
#include "keyboard_capturer.h"
#include <cstdint>
#include <unordered_map>
#include "keyboard_converter.h"
@@ -10,6 +11,7 @@ namespace crossdesk {
static OnKeyAction g_on_key_action = nullptr;
static void* g_user_ptr = nullptr;
static std::unordered_map<int, int> g_unmapped_keycode_to_vk;
constexpr int64_t kCrossDeskInjectedKeyboardEvent = 0x43524f5353444553;
static int VkCodeFromUnicode(UniChar ch) {
if (ch >= 'a' && ch <= 'z') {
@@ -103,6 +105,10 @@ static int ResolveVkCodeFromMacEvent(CGEventRef event, CGKeyCode key_code,
CGEventRef eventCallback(CGEventTapProxy proxy, CGEventType type,
CGEventRef event, void* userInfo) {
(void)proxy;
if (CGEventGetIntegerValueField(event, kCGEventSourceUserData) ==
kCrossDeskInjectedKeyboardEvent) {
return event;
}
if (!g_on_key_action) {
return event;
}
@@ -302,6 +308,8 @@ int KeyboardCapturer::SendKeyboardCommand(int key_code, bool is_down,
}
CGEventSetFlags(event, ToCGEventFlags(injected_flags));
CGEventSetIntegerValueField(event, kCGEventSourceUserData,
kCrossDeskInjectedKeyboardEvent);
CGEventPost(kCGHIDEventTap, event);
CFRelease(event);
@@ -314,6 +322,8 @@ int KeyboardCapturer::SendKeyboardCommand(int key_code, bool is_down,
LOG_ERROR("CGEventCreateKeyboardEvent failed for fn release");
return -1;
}
CGEventSetIntegerValueField(fn_release_event, kCGEventSourceUserData,
kCrossDeskInjectedKeyboardEvent);
CGEventPost(kCGHIDEventTap, fn_release_event);
CFRelease(fn_release_event);
}
@@ -1,36 +1,17 @@
#include "keyboard_capturer.h"
#include <hidusage.h>
#include "rd_log.h"
#include "windows_input_marker.h"
namespace crossdesk {
namespace {
static OnKeyAction g_on_key_action = nullptr;
static void* g_user_ptr = nullptr;
constexpr wchar_t kRawInputWindowClassName[] =
L"CrossDeskKeyboardRawInputWindow";
static int NormalizeModifierVkCode(const KBDLLHOOKSTRUCT* kb_data) {
if (kb_data == nullptr) {
return -1;
}
if (kb_data->vkCode != VK_SHIFT && kb_data->vkCode != VK_CONTROL &&
kb_data->vkCode != VK_MENU) {
return static_cast<int>(kb_data->vkCode);
}
UINT scan_code = static_cast<UINT>(kb_data->scanCode & 0xFF);
if ((kb_data->flags & LLKHF_EXTENDED) != 0) {
scan_code |= 0xE000;
}
const UINT normalized_vk = MapVirtualKeyW(scan_code, MAPVK_VSC_TO_VK_EX);
if (normalized_vk != 0) {
return static_cast<int>(normalized_vk);
}
return static_cast<int>(kb_data->vkCode);
}
static bool PreferSideSpecificVkInjection(int key_code) {
bool PreferSideSpecificVkInjection(int key_code) {
switch (key_code) {
case VK_LSHIFT:
case VK_RSHIFT:
@@ -46,55 +27,247 @@ static bool PreferSideSpecificVkInjection(int key_code) {
}
}
LRESULT CALLBACK KeyboardProc(int nCode, WPARAM wParam, LPARAM lParam) {
if (nCode == HC_ACTION && g_on_key_action) {
KBDLLHOOKSTRUCT* kbData = reinterpret_cast<KBDLLHOOKSTRUCT*>(lParam);
const int key_code = NormalizeModifierVkCode(kbData);
if (wParam == WM_KEYDOWN || wParam == WM_SYSKEYDOWN) {
g_on_key_action(key_code, true, kbData->scanCode,
(kbData->flags & LLKHF_EXTENDED) != 0, g_user_ptr);
} else if (wParam == WM_KEYUP || wParam == WM_SYSKEYUP) {
g_on_key_action(key_code, false, kbData->scanCode,
(kbData->flags & LLKHF_EXTENDED) != 0, g_user_ptr);
}
return 1;
}
return CallNextHookEx(NULL, nCode, wParam, lParam);
}
} // namespace
KeyboardCapturer::KeyboardCapturer() {}
KeyboardCapturer::~KeyboardCapturer() {}
KeyboardCapturer::~KeyboardCapturer() { Unhook(); }
int KeyboardCapturer::Hook(OnKeyAction on_key_action, void* user_ptr) {
g_on_key_action = on_key_action;
g_user_ptr = user_ptr;
if (capture_thread_.joinable()) {
return 0;
}
keyboard_hook_ = SetWindowsHookEx(WH_KEYBOARD_LL, KeyboardProc, NULL, 0);
if (!keyboard_hook_) {
LOG_ERROR("Failed to install keyboard hook");
on_key_action_ = on_key_action;
user_ptr_ = user_ptr;
{
std::lock_guard<std::mutex> lock(capture_state_mutex_);
capture_thread_id_ = 0;
capture_start_complete_ = false;
capture_start_succeeded_ = false;
}
capture_thread_ = std::thread(&KeyboardCapturer::RawInputThreadMain, this);
std::unique_lock<std::mutex> lock(capture_state_mutex_);
capture_start_condition_.wait(
lock, [this] { return capture_start_complete_; });
const bool capture_started = capture_start_succeeded_;
lock.unlock();
if (!capture_started) {
if (capture_thread_.joinable()) {
capture_thread_.join();
}
on_key_action_ = nullptr;
user_ptr_ = nullptr;
return -1;
}
return 0;
}
int KeyboardCapturer::Unhook() {
if (keyboard_hook_) {
g_on_key_action = nullptr;
g_user_ptr = nullptr;
UnhookWindowsHookEx(keyboard_hook_);
keyboard_hook_ = nullptr;
DWORD capture_thread_id = 0;
{
std::lock_guard<std::mutex> lock(capture_state_mutex_);
capture_thread_id = capture_thread_id_;
}
if (capture_thread_id != 0 &&
!PostThreadMessageW(capture_thread_id, WM_QUIT, 0, 0)) {
LOG_WARN("Failed to stop keyboard raw input thread, thread_id={}, error={}",
capture_thread_id, GetLastError());
}
if (capture_thread_.joinable()) {
capture_thread_.join();
}
on_key_action_ = nullptr;
user_ptr_ = nullptr;
return 0;
}
// apply remote keyboard commands to the local machine
void KeyboardCapturer::RawInputThreadMain() {
const DWORD thread_id = GetCurrentThreadId();
MSG message{};
PeekMessageW(&message, nullptr, WM_USER, WM_USER, PM_NOREMOVE);
const bool capture_started = CreateRawInputWindow();
{
std::lock_guard<std::mutex> lock(capture_state_mutex_);
capture_thread_id_ = thread_id;
capture_start_succeeded_ = capture_started;
capture_start_complete_ = true;
}
capture_start_condition_.notify_one();
if (!capture_started) {
std::lock_guard<std::mutex> lock(capture_state_mutex_);
capture_thread_id_ = 0;
return;
}
LOG_INFO("Keyboard raw input capture started, thread_id={}", thread_id);
while (true) {
const BOOL get_message_result = GetMessageW(&message, nullptr, 0, 0);
if (get_message_result <= 0) {
if (get_message_result < 0) {
LOG_WARN("Keyboard raw input message loop failed, thread_id={}, "
"error={}",
thread_id, GetLastError());
}
break;
}
TranslateMessage(&message);
DispatchMessageW(&message);
}
DestroyRawInputWindow();
{
std::lock_guard<std::mutex> lock(capture_state_mutex_);
capture_thread_id_ = 0;
}
LOG_INFO("Keyboard raw input capture stopped, thread_id={}", thread_id);
}
bool KeyboardCapturer::CreateRawInputWindow() {
const HINSTANCE instance = GetModuleHandleW(nullptr);
WNDCLASSEXW window_class{};
window_class.cbSize = sizeof(window_class);
window_class.lpfnWndProc = &KeyboardCapturer::RawInputWindowProc;
window_class.hInstance = instance;
window_class.lpszClassName = kRawInputWindowClassName;
if (RegisterClassExW(&window_class) == 0) {
const DWORD error = GetLastError();
if (error != ERROR_CLASS_ALREADY_EXISTS) {
LOG_WARN("Failed to register keyboard raw input window class, error={}",
error);
return false;
}
}
raw_input_window_ = CreateWindowExW(
0, kRawInputWindowClassName, L"", 0, 0, 0, 0, 0, HWND_MESSAGE, nullptr,
instance, this);
if (!raw_input_window_) {
LOG_WARN("Failed to create keyboard raw input window, error={}",
GetLastError());
return false;
}
RAWINPUTDEVICE keyboard_device{};
keyboard_device.usUsagePage = HID_USAGE_PAGE_GENERIC;
keyboard_device.usUsage = HID_USAGE_GENERIC_KEYBOARD;
keyboard_device.dwFlags = RIDEV_DEVNOTIFY | RIDEV_INPUTSINK;
keyboard_device.hwndTarget = raw_input_window_;
if (!RegisterRawInputDevices(&keyboard_device, 1,
sizeof(keyboard_device))) {
LOG_WARN("Failed to register keyboard raw input, error={}", GetLastError());
DestroyWindow(raw_input_window_);
raw_input_window_ = nullptr;
return false;
}
raw_input_registered_ = true;
return true;
}
void KeyboardCapturer::DestroyRawInputWindow() {
if (raw_input_registered_) {
RAWINPUTDEVICE keyboard_device{};
keyboard_device.usUsagePage = HID_USAGE_PAGE_GENERIC;
keyboard_device.usUsage = HID_USAGE_GENERIC_KEYBOARD;
keyboard_device.dwFlags = RIDEV_REMOVE;
keyboard_device.hwndTarget = nullptr;
if (!RegisterRawInputDevices(&keyboard_device, 1,
sizeof(keyboard_device))) {
LOG_WARN("Failed to unregister keyboard raw input, error={}",
GetLastError());
}
raw_input_registered_ = false;
}
if (raw_input_window_) {
DestroyWindow(raw_input_window_);
raw_input_window_ = nullptr;
}
}
LRESULT CALLBACK KeyboardCapturer::RawInputWindowProc(
HWND window, UINT message, WPARAM w_param, LPARAM l_param) {
KeyboardCapturer* capturer = reinterpret_cast<KeyboardCapturer*>(
GetWindowLongPtrW(window, GWLP_USERDATA));
if (message == WM_NCCREATE) {
auto* create = reinterpret_cast<CREATESTRUCTW*>(l_param);
capturer = static_cast<KeyboardCapturer*>(create->lpCreateParams);
SetWindowLongPtrW(window, GWLP_USERDATA,
reinterpret_cast<LONG_PTR>(capturer));
} else if (message == WM_INPUT && capturer) {
capturer->HandleRawInput(reinterpret_cast<HRAWINPUT>(l_param));
} else if (message == WM_NCDESTROY) {
SetWindowLongPtrW(window, GWLP_USERDATA, 0);
}
return DefWindowProcW(window, message, w_param, l_param);
}
void KeyboardCapturer::HandleRawInput(HRAWINPUT raw_input_handle) {
RAWINPUT input{};
UINT input_size = sizeof(input);
const UINT bytes_read =
GetRawInputData(raw_input_handle, RID_INPUT, &input, &input_size,
sizeof(RAWINPUTHEADER));
if (bytes_read == static_cast<UINT>(-1) ||
bytes_read < sizeof(RAWINPUTHEADER) ||
input.header.dwType != RIM_TYPEKEYBOARD) {
return;
}
const RAWKEYBOARD& keyboard = input.data.keyboard;
if (keyboard.VKey == 0xFF ||
keyboard.ExtraInformation ==
static_cast<ULONG>(kInjectedKeyboardInputMarker)) {
return;
}
bool is_down = false;
if (keyboard.Message == WM_KEYDOWN || keyboard.Message == WM_SYSKEYDOWN) {
is_down = true;
} else if (keyboard.Message != WM_KEYUP &&
keyboard.Message != WM_SYSKEYUP) {
return;
}
const bool extended = (keyboard.Flags & (RI_KEY_E0 | RI_KEY_E1)) != 0;
UINT mapped_scan_code = keyboard.MakeCode;
if ((keyboard.Flags & RI_KEY_E0) != 0) {
mapped_scan_code |= 0xE000;
} else if ((keyboard.Flags & RI_KEY_E1) != 0) {
mapped_scan_code |= 0xE100;
}
if (mapped_scan_code == 0xE11D || mapped_scan_code == 0xE02A) {
return;
}
int key_code = static_cast<int>(keyboard.VKey);
if (key_code == VK_SHIFT || key_code == VK_CONTROL || key_code == VK_MENU) {
const UINT normalized =
MapVirtualKeyW(mapped_scan_code, MAPVK_VSC_TO_VK_EX);
if (normalized != 0) {
key_code = static_cast<int>(normalized);
}
}
if (on_key_action_) {
on_key_action_(key_code, is_down, keyboard.MakeCode, extended, user_ptr_);
}
}
// Apply remote keyboard commands to the local machine.
int KeyboardCapturer::SendKeyboardCommand(int key_code, bool is_down,
uint32_t scan_code, bool extended) {
INPUT input = {0};
input.type = INPUT_KEYBOARD;
input.ki.dwExtraInfo =
static_cast<ULONG_PTR>(kInjectedKeyboardInputMarker);
const bool prefer_vk = PreferSideSpecificVkInjection(key_code);
const UINT resolved_scan_code =
@@ -110,7 +283,7 @@ int KeyboardCapturer::SendKeyboardCommand(int key_code, bool is_down,
input.ki.dwFlags |= KEYEVENTF_EXTENDEDKEY;
}
} else {
input.ki.wVk = (WORD)key_code;
input.ki.wVk = static_cast<WORD>(key_code);
if (prefer_vk && resolved_scan_code != 0) {
input.ki.wScan = static_cast<WORD>(resolved_scan_code & 0xFF);
@@ -131,7 +304,7 @@ int KeyboardCapturer::SendKeyboardCommand(int key_code, bool is_down,
input.ki.dwFlags |= KEYEVENTF_KEYUP;
}
UINT sent = SendInput(1, &input, sizeof(INPUT));
const UINT sent = SendInput(1, &input, sizeof(INPUT));
if (sent != 1) {
LOG_WARN("SendInput failed for key_code={}, is_down={}, err={}", key_code,
is_down, GetLastError());
@@ -9,6 +9,10 @@
#include <Windows.h>
#include <condition_variable>
#include <mutex>
#include <thread>
#include "device_controller.h"
namespace crossdesk {
@@ -26,8 +30,25 @@ class KeyboardCapturer : public DeviceController {
bool extended = false);
private:
HHOOK keyboard_hook_ = nullptr;
static LRESULT CALLBACK RawInputWindowProc(HWND window, UINT message,
WPARAM w_param, LPARAM l_param);
void RawInputThreadMain();
bool CreateRawInputWindow();
void DestroyRawInputWindow();
void HandleRawInput(HRAWINPUT raw_input_handle);
OnKeyAction on_key_action_ = nullptr;
void* user_ptr_ = nullptr;
HWND raw_input_window_ = nullptr;
bool raw_input_registered_ = false;
std::thread capture_thread_;
DWORD capture_thread_id_ = 0;
bool capture_start_complete_ = false;
bool capture_start_succeeded_ = false;
std::mutex capture_state_mutex_;
std::condition_variable capture_start_condition_;
};
} // namespace crossdesk
#endif
#endif
@@ -1,6 +1,9 @@
#include "mouse_controller.h"
#include <X11/Xlib.h>
#include <X11/Xutil.h>
#include <X11/extensions/XTest.h>
#include <unistd.h>
#include "platform.h"
#include "rd_log.h"
@@ -7,10 +7,6 @@
#ifndef _MOUSE_CONTROLLER_H_
#define _MOUSE_CONTROLLER_H_
#include <X11/Xlib.h>
#include <X11/Xutil.h>
#include <unistd.h>
#include <cstdint>
#include <functional>
#include <string>
@@ -20,6 +16,7 @@
struct DBusConnection;
struct DBusMessageIter;
struct _XDisplay;
namespace crossdesk {
@@ -53,8 +50,8 @@ class MouseController : public DeviceController {
enum class WaylandAbsoluteMode { kUnknown, kPixels, kNormalized, kDisabled };
Display* display_ = nullptr;
Window root_ = 0;
_XDisplay* display_ = nullptr;
unsigned long root_ = 0;
std::vector<DisplayInfo> display_info_list_;
int screen_width_ = 0;
int screen_height_ = 0;
+9 -5
View File
@@ -1,6 +1,7 @@
#ifndef CROSSDESK_GUI_APPLICATION_STATE_H_
#define CROSSDESK_GUI_APPLICATION_STATE_H_
#include <atomic>
#include <chrono>
#include <cstdint>
#include <memory>
@@ -64,7 +65,7 @@ struct InteractionState {
bool start_keyboard_capturer_ = false;
bool show_cursor_ = false;
bool keyboard_capturer_is_started_ = false;
bool keyboard_capturer_uses_sdl_events_ = false;
bool keyboard_capturer_uses_window_events_ = false;
bool foucs_on_main_window_ = false;
bool focus_on_stream_window_ = false;
bool audio_capture_ = false;
@@ -119,8 +120,11 @@ struct StreamWindowState {
};
struct ServerWindowState {
bool need_to_create_server_window_ = false;
bool need_to_destroy_server_window_ = false;
// Transport callbacks run outside the UI thread. Keep the two lifecycle
// requests atomic so a disconnect cannot be lost while the Slint timer is
// deciding whether to create or destroy the controlled-side window.
std::atomic<bool> need_to_create_server_window_{false};
std::atomic<bool> need_to_destroy_server_window_{false};
bool server_window_created_ = false;
bool server_window_inited_ = false;
int server_window_width_default_ = 250;
@@ -186,6 +190,6 @@ struct ApplicationState : MainWindowState,
ServerWindowState,
UiState {};
} // namespace crossdesk::gui_detail
} // namespace crossdesk::gui_detail
#endif // CROSSDESK_GUI_APPLICATION_STATE_H_
#endif // CROSSDESK_GUI_APPLICATION_STATE_H_
File diff suppressed because it is too large Load Diff
+9
View File
@@ -35,7 +35,12 @@ private:
void SyncConnectionDialog();
void SyncPlatformDialogs();
void SyncStreamWindow();
void SyncStreamKeyboardFocus();
void SetStreamKeyboardFocus(bool focused);
void SyncServerWindow();
#if defined(__linux__) && !defined(__APPLE__)
void SyncXWaylandWindowActivation();
#endif
void UpdateLocalization();
void ResetSettingsUi();
void SaveSettingsFromUi();
@@ -59,6 +64,10 @@ private:
bool OpenUrl(const std::string &url);
std::unique_ptr<SlintUi> ui_;
#if defined(__linux__) && !defined(__APPLE__)
bool use_xwayland_gui_ = false;
bool use_x11_custom_titlebar_ = false;
#endif
};
} // namespace crossdesk
+2 -1
View File
@@ -200,7 +200,8 @@ void GuiApplication::ProcessSdlEvent(const SDL_Event &event) {
case SDL_EVENT_KEY_DOWN:
case SDL_EVENT_KEY_UP:
if (keyboard_capturer_is_started_ && keyboard_capturer_uses_sdl_events_ &&
if (keyboard_capturer_is_started_ &&
keyboard_capturer_uses_window_events_ &&
focus_on_stream_window_ && stream_window_ &&
SDL_GetWindowID(stream_window_) == event.key.windowID) {
ProcessKeyboardEvent(event);
+3 -5
View File
@@ -220,10 +220,8 @@ int GuiApplication::ProcessKeyboardEvent(const SDL_Event &event) {
return -1;
}
if (event.type == SDL_EVENT_KEY_DOWN && event.key.repeat) {
return 0;
}
// SDL represents key auto-repeat as additional key-down events. Forward
// them so the fallback path has the same long-press behavior as native hooks.
const int key_code = TranslateSdlKeyboardEventToVk(event.key);
if (key_code < 0) {
return 0;
@@ -412,4 +410,4 @@ int GuiApplication::ProcessMouseEvent(const SDL_Event &event) {
return 0;
}
} // namespace crossdesk
} // namespace crossdesk
@@ -179,6 +179,7 @@ struct TranslationRow {
X(notification, u8"通知", "Notification", u8"Уведомление") \
X(new_version_available, u8"新版本可用", "New Version Available", \
u8"Доступна новая версия") \
X(release_notes, u8"更新内容", "Release Notes", u8"Содержание обновления") \
X(version, u8"版本", "Version", u8"Версия") \
X(release_date, u8"发布日期: ", "Release Date: ", u8"Дата релиза: ") \
X(access_website, u8"访问官网: ", \
@@ -11,6 +11,7 @@ namespace crossdesk {
namespace {
constexpr uint64_t kCaptureResumeKeyFrameGapMs = 500;
constexpr size_t kMaxCapturedKeyboardInputs = 512;
} // namespace
@@ -228,27 +229,27 @@ int SessionDeviceManager::StopMouseController() {
}
int SessionDeviceManager::StartKeyboardCapturer() {
owner_.keyboard_capturer_uses_sdl_events_ = false;
owner_.keyboard_capturer_uses_window_events_ = false;
#ifdef __APPLE__
if (!owner_.EnsureMacAccessibilityPermission()) {
owner_.keyboard_capturer_uses_sdl_events_ = true;
owner_.keyboard_capturer_uses_window_events_ = true;
return 0;
}
#endif
#if defined(__linux__) && !defined(__APPLE__)
if (IsWaylandSession()) {
owner_.keyboard_capturer_uses_sdl_events_ = true;
LOG_INFO("Start keyboard capturer with SDL Wayland backend");
owner_.keyboard_capturer_uses_window_events_ = true;
LOG_INFO("Start keyboard capturer with Slint Wayland backend");
return 0;
}
#endif
if (!keyboard_capturer_) {
owner_.keyboard_capturer_uses_sdl_events_ = true;
owner_.keyboard_capturer_uses_window_events_ = true;
LOG_WARN(
"keyboard capturer is nullptr, falling back to SDL keyboard events");
"keyboard capturer is nullptr, falling back to Slint keyboard events");
return 0;
}
@@ -256,31 +257,32 @@ int SessionDeviceManager::StartKeyboardCapturer() {
[](int key_code, bool is_down, uint32_t scan_code, bool extended,
void *user_ptr) {
if (user_ptr) {
auto *runtime = static_cast<GuiRuntime *>(user_ptr);
runtime->keyboard_.SendKeyCommand(key_code, is_down, scan_code,
extended);
auto *devices = static_cast<SessionDeviceManager *>(user_ptr);
devices->QueueCapturedKeyboardInput(key_code, is_down, scan_code,
extended);
}
},
&owner_);
this);
if (hook_ret != 0) {
owner_.keyboard_capturer_uses_sdl_events_ = true;
owner_.keyboard_capturer_uses_window_events_ = true;
LOG_WARN(
"Start keyboard capturer failed, falling back to SDL keyboard events");
"Start keyboard capturer failed, falling back to Slint keyboard events");
} else {
LOG_INFO("Start keyboard capturer with native hook");
LOG_INFO("Start keyboard capturer with native input");
}
return 0;
}
int SessionDeviceManager::StopKeyboardCapturer() {
if (owner_.keyboard_capturer_uses_sdl_events_) {
owner_.keyboard_capturer_uses_sdl_events_ = false;
LOG_INFO("Stop keyboard capturer with SDL keyboard backend");
if (owner_.keyboard_capturer_uses_window_events_) {
owner_.keyboard_capturer_uses_window_events_ = false;
LOG_INFO("Stop keyboard capturer with Slint keyboard backend");
return 0;
}
if (keyboard_capturer_) {
keyboard_capturer_->Unhook();
ClearCapturedKeyboardInput();
LOG_INFO("Stop keyboard capturer");
}
return 0;
@@ -410,6 +412,7 @@ void SessionDeviceManager::UpdateInteractions() {
owner_.keyboard_capturer_is_started_ = true;
}
if (owner_.keyboard_capturer_is_started_) {
DrainCapturedKeyboardInput();
owner_.keyboard_.SendHeartbeat(false);
}
} else if (owner_.keyboard_capturer_is_started_) {
@@ -421,6 +424,36 @@ void SessionDeviceManager::UpdateInteractions() {
owner_.keyboard_.CheckRemoteTimeouts();
}
void SessionDeviceManager::QueueCapturedKeyboardInput(int key_code,
bool is_down,
uint32_t scan_code,
bool extended) {
std::lock_guard<std::mutex> lock(captured_keyboard_inputs_mutex_);
if (captured_keyboard_inputs_.size() >= kMaxCapturedKeyboardInputs) {
captured_keyboard_inputs_.pop_front();
LOG_WARN("Captured keyboard input queue overflow, dropping oldest event");
}
captured_keyboard_inputs_.push_back(
CapturedKeyboardInput{key_code, is_down, scan_code, extended});
}
void SessionDeviceManager::DrainCapturedKeyboardInput() {
std::deque<CapturedKeyboardInput> inputs;
{
std::lock_guard<std::mutex> lock(captured_keyboard_inputs_mutex_);
inputs.swap(captured_keyboard_inputs_);
}
for (const CapturedKeyboardInput &input : inputs) {
owner_.keyboard_.SendKeyCommand(input.key_code, input.is_down,
input.scan_code, input.extended);
}
}
void SessionDeviceManager::ClearCapturedKeyboardInput() {
std::lock_guard<std::mutex> lock(captured_keyboard_inputs_mutex_);
captured_keyboard_inputs_.clear();
}
bool SessionDeviceManager::SendKeyboardCommand(int key_code, bool is_down,
uint32_t scan_code,
bool extended) {
@@ -5,6 +5,8 @@
#include <cstddef>
#include <cstdint>
#include <deque>
#include <mutex>
#include <string>
#include <vector>
@@ -53,6 +55,18 @@ public:
const std::vector<DisplayInfo> &display_info_list() const;
private:
struct CapturedKeyboardInput {
int key_code = 0;
bool is_down = false;
uint32_t scan_code = 0;
bool extended = false;
};
void QueueCapturedKeyboardInput(int key_code, bool is_down,
uint32_t scan_code, bool extended);
void DrainCapturedKeyboardInput();
void ClearCapturedKeyboardInput();
GuiRuntime &owner_;
SDL_AudioStream *output_stream_ = nullptr;
ScreenCapturerFactory *screen_capturer_factory_ = nullptr;
@@ -63,6 +77,8 @@ private:
MouseController *mouse_controller_ = nullptr;
KeyboardCapturer *keyboard_capturer_ = nullptr;
std::vector<DisplayInfo> display_info_list_;
std::deque<CapturedKeyboardInput> captured_keyboard_inputs_;
std::mutex captured_keyboard_inputs_mutex_;
uint64_t last_frame_time_ = 0;
std::string last_video_frame_stream_id_;
};
+35 -34
View File
@@ -1,14 +1,14 @@
#include "features/input/keyboard_controller.h"
#include <SDL3/SDL.h>
#include <cstdint>
#include <mutex>
#include <nlohmann/json.hpp>
#include <string>
#include <unordered_map>
#include <vector>
#include <SDL3/SDL.h>
#include <nlohmann/json.hpp>
#include "minirtc.h"
#include "rd_log.h"
#include "runtime/gui_runtime.h"
@@ -45,8 +45,8 @@ int NormalizeWindowsModifierVk(int key_code, uint32_t scan_code,
#endif
}
void PopulateWindowsKeyMetadataFromVk(int key_code, uint32_t *scan_code_out,
bool *extended_out) {
void PopulateWindowsKeyMetadataFromVk(int key_code, uint32_t* scan_code_out,
bool* extended_out) {
if (!scan_code_out || !extended_out) {
return;
}
@@ -65,7 +65,7 @@ void PopulateWindowsKeyMetadataFromVk(int key_code, uint32_t *scan_code_out,
#if _WIN32
constexpr uint32_t kSecureDesktopInputLogIntervalMs = 2000;
void LogSecureDesktopInputBlocked(uint32_t *last_tick, const char *stage) {
void LogSecureDesktopInputBlocked(uint32_t* last_tick, const char* stage) {
const uint32_t now = static_cast<uint32_t>(SDL_GetTicks());
if (*last_tick != 0 && now - *last_tick < kSecureDesktopInputLogIntervalMs) {
return;
@@ -77,8 +77,8 @@ void LogSecureDesktopInputBlocked(uint32_t *last_tick, const char *stage) {
stage ? stage : "");
}
bool IsTransientSecureDesktopInputFailure(const nlohmann::json &response,
const RemoteAction &action) {
bool IsTransientSecureDesktopInputFailure(const nlohmann::json& response,
const RemoteAction& action) {
return response.is_object() &&
response.value("error", std::string()) == "send_input_failed" &&
response.value("code", 0u) == ERROR_ACCESS_DENIED &&
@@ -87,9 +87,9 @@ bool IsTransientSecureDesktopInputFailure(const nlohmann::json &response,
}
#endif
} // namespace
} // namespace
KeyboardController::KeyboardController(GuiRuntime &owner) : owner_(owner) {}
KeyboardController::KeyboardController(GuiRuntime& owner) : owner_(owner) {}
void KeyboardController::TrackPressedKey(int key_code, bool is_down,
uint32_t scan_code, bool extended) {
@@ -106,13 +106,13 @@ void KeyboardController::ForceReleasePressedKeys() {
{
std::lock_guard<std::mutex> lock(pressed_keys_mutex_);
pressed_keys.reserve(pressed_keys_.size());
for (const auto &[_, key] : pressed_keys_) {
for (const auto& [_, key] : pressed_keys_) {
pressed_keys.push_back(key);
}
pressed_keys_.clear();
}
for (const PressedKey &key : pressed_keys) {
for (const PressedKey& key : pressed_keys) {
SendKeyCommand(key.key_code, false, key.scan_code, key.extended);
}
SendHeartbeat(true);
@@ -130,7 +130,7 @@ void KeyboardController::SendHeartbeat(bool force) {
{
std::lock_guard<std::mutex> lock(pressed_keys_mutex_);
size_t index = 0;
for (const auto &[_, key] : pressed_keys_) {
for (const auto& [_, key] : pressed_keys_) {
if (index >= kMaxKeyboardStateKeys) {
LOG_WARN("Keyboard heartbeat truncated, pressed_keys={}",
pressed_keys_.size());
@@ -223,9 +223,10 @@ bool KeyboardController::InjectRemoteKey(int key_code, bool is_down,
action.k.flag = is_down ? KeyFlag::key_down : KeyFlag::key_up;
if (!json.is_discarded() &&
IsTransientSecureDesktopInputFailure(json, action)) {
LOG_INFO("Secure desktop keyboard injection transient failure, "
"key_code={}, is_down={}, response={}",
key_code, is_down, response);
LOG_INFO(
"Secure desktop keyboard injection transient failure, "
"key_code={}, is_down={}, response={}",
key_code, is_down, response);
return true;
}
@@ -245,15 +246,15 @@ bool KeyboardController::InjectRemoteKey(int key_code, bool is_down,
extended);
}
void KeyboardController::ApplyRemoteEvent(const std::string &remote_id,
const RemoteAction &action) {
void KeyboardController::ApplyRemoteEvent(const std::string& remote_id,
const RemoteAction& action) {
const int key_code = static_cast<int>(action.k.key_value);
const bool is_down = action.k.flag == KeyFlag::key_down;
const bool injected =
InjectRemoteKey(key_code, is_down, action.k.scan_code, action.k.extended);
std::lock_guard<std::mutex> lock(remote_states_mutex_);
auto &state = remote_states_[remote_id];
auto& state = remote_states_[remote_id];
state.last_seen_tick = static_cast<uint32_t>(SDL_GetTicks());
if (is_down && injected) {
state.pressed_keys[key_code] =
@@ -263,13 +264,13 @@ void KeyboardController::ApplyRemoteEvent(const std::string &remote_id,
}
}
void KeyboardController::ApplyRemoteState(const std::string &remote_id,
const RemoteAction &action) {
void KeyboardController::ApplyRemoteState(const std::string& remote_id,
const RemoteAction& action) {
std::vector<PressedKey> keys_to_release;
std::vector<PressedKey> keys_to_press;
{
std::lock_guard<std::mutex> lock(remote_states_mutex_);
auto &state = remote_states_[remote_id];
auto& state = remote_states_[remote_id];
if (action.ks.seq != 0 && state.last_seq != 0 &&
static_cast<int32_t>(action.ks.seq - state.last_seq) <= 0) {
return;
@@ -283,24 +284,24 @@ void KeyboardController::ApplyRemoteState(const std::string &remote_id,
const size_t count =
(std::min)(action.ks.pressed_count, kMaxKeyboardStateKeys);
for (size_t index = 0; index < count; ++index) {
const auto &key = action.ks.pressed_keys[index];
const auto& key = action.ks.pressed_keys[index];
const int key_code = static_cast<int>(key.key_value);
desired_keys[key_code] =
PressedKey{key_code, key.scan_code, key.extended};
}
for (const auto &[key_code, key] : state.pressed_keys) {
for (const auto& [key_code, key] : state.pressed_keys) {
if (desired_keys.find(key_code) == desired_keys.end()) {
keys_to_release.push_back(key);
}
}
for (const auto &[key_code, key] : desired_keys) {
for (const auto& [key_code, key] : desired_keys) {
if (state.pressed_keys.find(key_code) == state.pressed_keys.end()) {
keys_to_press.push_back(key);
}
}
}
for (const PressedKey &key : keys_to_release) {
for (const PressedKey& key : keys_to_release) {
if (InjectRemoteKey(key.key_code, false, key.scan_code, key.extended)) {
std::lock_guard<std::mutex> lock(remote_states_mutex_);
const auto state_it = remote_states_.find(remote_id);
@@ -309,7 +310,7 @@ void KeyboardController::ApplyRemoteState(const std::string &remote_id,
}
}
}
for (const PressedKey &key : keys_to_press) {
for (const PressedKey& key : keys_to_press) {
if (InjectRemoteKey(key.key_code, true, key.scan_code, key.extended)) {
std::lock_guard<std::mutex> lock(remote_states_mutex_);
remote_states_[remote_id].pressed_keys[key.key_code] = key;
@@ -317,8 +318,8 @@ void KeyboardController::ApplyRemoteState(const std::string &remote_id,
}
}
void KeyboardController::ReleaseRemotePressedKeys(const std::string &remote_id,
const char *reason) {
void KeyboardController::ReleaseRemotePressedKeys(const std::string& remote_id,
const char* reason) {
std::vector<PressedKey> keys_to_release;
{
std::lock_guard<std::mutex> lock(remote_states_mutex_);
@@ -326,7 +327,7 @@ void KeyboardController::ReleaseRemotePressedKeys(const std::string &remote_id,
if (state_it == remote_states_.end()) {
return;
}
for (const auto &[_, key] : state_it->second.pressed_keys) {
for (const auto& [_, key] : state_it->second.pressed_keys) {
keys_to_release.push_back(key);
}
remote_states_.erase(state_it);
@@ -336,7 +337,7 @@ void KeyboardController::ReleaseRemotePressedKeys(const std::string &remote_id,
LOG_WARN("Releasing {} remote keyboard keys for remote_id={}, reason={}",
keys_to_release.size(), remote_id, reason ? reason : "unknown");
}
for (const PressedKey &key : keys_to_release) {
for (const PressedKey& key : keys_to_release) {
InjectRemoteKey(key.key_code, false, key.scan_code, key.extended);
}
}
@@ -346,7 +347,7 @@ void KeyboardController::CheckRemoteTimeouts() {
std::vector<std::string> timed_out_remotes;
{
std::lock_guard<std::mutex> lock(remote_states_mutex_);
for (const auto &[remote_id, state] : remote_states_) {
for (const auto& [remote_id, state] : remote_states_) {
if (state.keyboard_state_seen && !state.pressed_keys.empty() &&
state.last_seen_tick != 0 &&
now - state.last_seen_tick > kRemoteReleaseTimeoutMs) {
@@ -354,9 +355,9 @@ void KeyboardController::CheckRemoteTimeouts() {
}
}
}
for (const std::string &remote_id : timed_out_remotes) {
for (const std::string& remote_id : timed_out_remotes) {
ReleaseRemotePressedKeys(remote_id, "keyboard_heartbeat_timeout");
}
}
} // namespace crossdesk
} // namespace crossdesk
+4
View File
@@ -16,6 +16,10 @@ bool HideDisabledMainWindowZoomButton();
// fullscreen action with an immediate, single-window fullscreen transition.
bool ConfigureStreamWindowLiveResize();
// Returns whether the configured stream window is currently the active AppKit
// key window.
bool IsStreamWindowActive();
// Enters or leaves the stream window's animation-free fullscreen mode.
bool SetStreamWindowFullscreen(bool fullscreen);
bool IsStreamWindowFullscreen();
+7
View File
@@ -146,6 +146,13 @@ bool ConfigureStreamWindowLiveResize() {
}
}
bool IsStreamWindowActive() {
@autoreleasepool {
return stream_window != nil && [NSApp isActive] &&
[stream_window isKeyWindow];
}
}
bool SetStreamWindowFullscreen(bool fullscreen) {
@autoreleasepool {
if (stream_window == nil || stream_fullscreen == fullscreen) {
+70 -62
View File
@@ -1,5 +1,3 @@
#include "runtime/gui_runtime.h"
#include <algorithm>
#include <chrono>
#include <cstring>
@@ -11,18 +9,15 @@
#include <vector>
#include "localization.h"
#include "platform.h"
#include "rd_log.h"
#include "runtime/gui_runtime.h"
namespace crossdesk {
namespace {
constexpr auto kPresenceProbeTimeout = std::chrono::seconds(5);
constexpr auto kConnectionAttemptTimeout = std::chrono::seconds(20);
bool IsConnectionAttemptPending(ConnectionStatus status) {
return status == ConnectionStatus::Connecting ||
status == ConnectionStatus::Gathering;
}
} // namespace
} // namespace
void GuiRuntime::HandleConnectionStatusChange() {
if (signal_connected_ && peer_ && need_to_send_recent_connections_) {
@@ -31,7 +26,7 @@ void GuiRuntime::HandleConnectionStatusChange() {
j["type"] = "recent_connections_presence";
j["user_id"] = client_id_;
j["devices"] = nlohmann::json::array();
for (const auto &id : recent_connection_ids_) {
for (const auto& id : recent_connection_ids_) {
std::string pure_id = id;
size_t pos_y = pure_id.find('Y');
size_t pos_n = pure_id.find('N');
@@ -95,7 +90,7 @@ void GuiRuntime::HandlePendingPresenceProbe() {
show_offline_warning_window_ = true;
}
void GuiRuntime::HandleConnectionTimeouts() {
void GuiRuntime::HandlePresenceProbeTimeout() {
const auto now = std::chrono::steady_clock::now();
bool presence_probe_timed_out = false;
@@ -121,48 +116,67 @@ void GuiRuntime::HandleConnectionTimeouts() {
show_offline_warning_window_ = true;
LOG_WARN("Presence probe timed out for [{}]", presence_remote_id);
}
}
bool rejoin_state_changed = false;
for (auto &[_, props] : remote_sessions_) {
if (!props || !props->connection_attempt_active_.load()) {
continue;
}
void GuiRuntime::HandleServerControllerDisconnected(
const std::string& remote_id, const char* reason) {
keyboard_.ReleaseRemotePressedKeys(remote_id, reason);
const ConnectionStatus status = props->connection_status_.load();
if (!IsConnectionAttemptPending(status)) {
props->connection_attempt_active_.store(false);
continue;
}
if (now - props->connection_attempt_started_at_ <
kConnectionAttemptTimeout) {
continue;
}
LOG_WARN("Connection to [{}] timed out, status={}", props->remote_id_,
static_cast<int>(status));
props->connection_attempt_active_.store(false);
props->connection_established_ = false;
props->rejoin_ = false;
props->connection_status_.store(ConnectionStatus::Failed);
focused_remote_id_ = props->remote_id_;
show_connection_status_window_ = true;
rejoin_state_changed = true;
}
if (rejoin_state_changed) {
need_to_rejoin_ = false;
for (const auto &[_, props] : remote_sessions_) {
if (props && props->rejoin_) {
need_to_rejoin_ = true;
break;
bool has_connected_controller = false;
bool has_web_controller = false;
std::string remaining_controller_id;
{
std::unique_lock lock(connection_status_mutex_);
connection_status_.erase(remote_id);
connection_host_names_.erase(remote_id);
for (const auto& [id, status] : connection_status_) {
if (status != ConnectionStatus::Connected) {
continue;
}
has_connected_controller = true;
has_web_controller =
has_web_controller || id.find("web") != std::string::npos;
if (remaining_controller_id.empty()) {
remaining_controller_id = id;
}
}
}
show_cursor_ = has_web_controller;
if (has_connected_controller) {
remote_client_id_ = remaining_controller_id;
return;
}
need_to_create_server_window_.store(false, std::memory_order_release);
need_to_destroy_server_window_.store(true, std::memory_order_release);
is_server_mode_ = false;
#if defined(__linux__) && !defined(__APPLE__)
if (IsWaylandSession()) {
// Keep Wayland capture session warm to avoid black screen on subsequent
// reconnects.
start_screen_capturer_ = true;
LOG_INFO(
"Keeping Wayland screen capturer running after disconnect to "
"preserve reconnect stability");
} else {
start_screen_capturer_ = false;
}
#else
start_screen_capturer_ = false;
#endif
start_speaker_capturer_ = false;
start_mouse_controller_ = false;
start_keyboard_capturer_ = false;
remote_client_id_.clear();
if (audio_capture_) {
devices_.StopSpeakerCapturer();
audio_capture_ = false;
}
devices_.ResetToInitialDisplay();
}
int GuiRuntime::RequestSingleDevicePresence(const std::string &remote_id,
const char *password,
int GuiRuntime::RequestSingleDevicePresence(const std::string& remote_id,
const char* password,
bool remember_password) {
if (!signal_connected_ || !peer_) {
return -1;
@@ -221,7 +235,7 @@ void GuiRuntime::CloseRemoteSession(std::shared_ptr<RemoteSession> props) {
std::thread save_thread([buffer_copy, video_width, video_height, remote_id,
remote_host_name, password,
thumbnail = thumbnail_]() {
thumbnail->SaveToThumbnail((char *)buffer_copy.data(), video_width,
thumbnail->SaveToThumbnail((char*)buffer_copy.data(), video_width,
video_height, remote_id, remote_host_name,
password);
});
@@ -255,7 +269,7 @@ void GuiRuntime::CloseAllRemoteSessions() {
{
// std::shared_lock lock(remote_sessions_mutex_);
for (auto &it : remote_sessions_) {
for (auto& it : remote_sessions_) {
auto props = it.second;
CloseRemoteSession(props);
}
@@ -279,7 +293,7 @@ void GuiRuntime::WaitForThumbnailSaveTasks() {
return;
}
for (auto &thread : threads_to_join) {
for (auto& thread : threads_to_join) {
if (thread.joinable()) {
thread.join();
}
@@ -300,9 +314,8 @@ void GuiRuntime::ResetRemoteSessionResources(
}
}
std::shared_ptr<GuiRuntime::RemoteSession>
GuiRuntime::FindRemoteSession(
const std::string &remote_id) {
std::shared_ptr<GuiRuntime::RemoteSession> GuiRuntime::FindRemoteSession(
const std::string& remote_id) {
if (remote_id.empty()) {
return nullptr;
}
@@ -315,8 +328,7 @@ GuiRuntime::FindRemoteSession(
return it->second;
}
int GuiRuntime::ConnectTo(const std::string &remote_id, const char *password,
int GuiRuntime::ConnectTo(const std::string& remote_id, const char* password,
bool remember_password, bool bypass_presence_check) {
if (!bypass_presence_check && !device_presence_cache_.IsOnline(remote_id)) {
int ret =
@@ -336,19 +348,17 @@ int GuiRuntime::ConnectTo(const std::string &remote_id, const char *password,
focused_remote_id_ = remote_id;
// std::shared_lock shared_lock(remote_sessions_mutex_);
bool exists =
(remote_sessions_.find(remote_id) != remote_sessions_.end());
bool exists = (remote_sessions_.find(remote_id) != remote_sessions_.end());
// shared_lock.unlock();
if (!exists) {
PeerPtr *peer_to_init = nullptr;
PeerPtr* peer_to_init = nullptr;
std::string local_id;
{
// std::unique_lock unique_lock(remote_sessions_mutex_);
if (remote_sessions_.find(remote_id) == remote_sessions_.end()) {
remote_sessions_[remote_id] =
std::make_shared<RemoteSession>();
remote_sessions_[remote_id] = std::make_shared<RemoteSession>();
auto props = remote_sessions_[remote_id];
props->local_id_ = "C-" + std::string(client_id_);
props->remote_id_ = remote_id;
@@ -371,7 +381,7 @@ int GuiRuntime::ConnectTo(const std::string &remote_id, const char *password,
return -1;
}
for (const auto &display_info : devices_.display_info_list()) {
for (const auto& display_info : devices_.display_info_list()) {
AddVideoStream(props->peer_, display_info.name.c_str());
}
AddAudioStream(props->peer_, props->audio_label_.c_str());
@@ -402,8 +412,6 @@ int GuiRuntime::ConnectTo(const std::string &remote_id, const char *password,
auto props = remote_sessions_[remote_id];
if (!props->connection_established_) {
props->connection_status_.store(ConnectionStatus::Connecting);
props->connection_attempt_active_.store(true);
props->connection_attempt_started_at_ = std::chrono::steady_clock::now();
show_connection_status_window_ = true;
props->remember_password_ = remember_password;
@@ -429,4 +437,4 @@ int GuiRuntime::ConnectTo(const std::string &remote_id, const char *password,
return 0;
}
} // namespace crossdesk
} // namespace crossdesk
+20 -20
View File
@@ -8,16 +8,16 @@
#include "features/devices/session_device_manager.h"
#include "features/file_transfer/file_transfer_manager.h"
#include "features/input/keyboard_controller.h"
#include "features/settings/settings_manager.h"
#include "runtime/gui_state.h"
#include "runtime/peer_event_handler.h"
#include "features/settings/settings_manager.h"
namespace crossdesk {
// Shared GUI runtime. It owns subsystem controllers and cross-cutting session
// state, but no window lifecycle, ImGui view, or transport callback methods.
class GuiRuntime : protected gui_detail::GuiState {
protected:
protected:
using FileTransferState = gui_detail::FileTransferState;
using RemoteSession = gui_detail::RemoteSession;
@@ -32,35 +32,35 @@ protected:
GuiRuntime();
~GuiRuntime();
static void SdlCaptureAudioIn(void *userdata, Uint8 *stream, int len);
static void SdlCaptureAudioOut(void *userdata, Uint8 *stream, int len);
static void SdlCaptureAudioIn(void* userdata, Uint8* stream, int len);
static void SdlCaptureAudioOut(void* userdata, Uint8* stream, int len);
int CreateConnectionPeer();
int ConnectTo(const std::string &remote_id, const char *password,
int ConnectTo(const std::string& remote_id, const char* password,
bool remember_password, bool bypass_presence_check = false);
int RequestSingleDevicePresence(const std::string &remote_id,
const char *password, bool remember_password);
int RequestSingleDevicePresence(const std::string& remote_id,
const char* password, bool remember_password);
void UpdateLabels();
void HandleRecentConnections();
void HandleConnectionStatusChange();
void HandlePendingPresenceProbe();
void HandleConnectionTimeouts();
void HandlePresenceProbeTimeout();
void HandleServerControllerDisconnected(const std::string& remote_id,
const char* reason);
void HandleWindowsServiceIntegration();
void CloseRemoteSession(std::shared_ptr<RemoteSession> props);
void CloseAllRemoteSessions();
void ResetRemoteSessionResources(
std::shared_ptr<RemoteSession> props);
void ResetRemoteSessionResources(std::shared_ptr<RemoteSession> props);
void WaitForThumbnailSaveTasks();
std::shared_ptr<RemoteSession>
FindRemoteSession(const std::string &remote_id);
std::shared_ptr<RemoteSession> FindRemoteSession(
const std::string& remote_id);
void ResetRemoteServiceStatus(RemoteSession &props);
void ApplyRemoteServiceStatus(RemoteSession &props,
const ServiceStatus &status);
RemoteUnlockState
GetRemoteUnlockState(const RemoteSession &props) const;
void ResetRemoteServiceStatus(RemoteSession& props);
void ApplyRemoteServiceStatus(RemoteSession& props,
const ServiceStatus& status);
RemoteUnlockState GetRemoteUnlockState(const RemoteSession& props) const;
#if _WIN32
void ResetLocalWindowsServiceState(bool clear_pending_sas);
#endif
@@ -82,7 +82,7 @@ protected:
KeyboardController keyboard_;
PeerEventHandler peer_events_;
private:
private:
friend class ClipboardController;
friend class SessionDeviceManager;
friend class FileTransferManager;
@@ -91,6 +91,6 @@ private:
friend class PeerEventHandler;
};
} // namespace crossdesk
} // namespace crossdesk
#endif // CROSSDESK_GUI_RUNTIME_H_
#endif // CROSSDESK_GUI_RUNTIME_H_
+199 -250
View File
@@ -26,12 +26,12 @@
namespace crossdesk {
PeerEventHandler::PeerEventHandler(GuiRuntime &owner) : owner_(owner) {}
PeerEventHandler::PeerEventHandler(GuiRuntime& owner) : owner_(owner) {}
void PeerEventHandler::OnSignalMessage(const char *message, size_t size,
void *user_data) {
auto *handler = static_cast<PeerEventHandler *>(user_data);
GuiRuntime *runtime = handler ? &handler->owner_ : nullptr;
void PeerEventHandler::OnSignalMessage(const char* message, size_t size,
void* user_data) {
auto* handler = static_cast<PeerEventHandler*>(user_data);
GuiRuntime* runtime = handler ? &handler->owner_ : nullptr;
if (!runtime || !message || size == 0) {
return;
}
@@ -43,7 +43,7 @@ void PeerEventHandler::OnSignalMessage(const char *message, size_t size,
std::string type = j["type"].get<std::string>();
if (type == "presence") {
if (j.contains("devices") && j["devices"].is_array()) {
for (auto &dev : j["devices"]) {
for (auto& dev : j["devices"]) {
if (!dev.is_object()) {
continue;
}
@@ -88,11 +88,10 @@ void PeerEventHandler::OnSignalMessage(const char *message, size_t size,
}
}
void PeerEventHandler::OnSignalStatus(SignalStatus status, const char *user_id,
size_t user_id_size, void *user_data) {
auto *handler = static_cast<PeerEventHandler *>(user_data);
GuiRuntime *runtime = handler ? &handler->owner_ : nullptr;
void PeerEventHandler::OnSignalStatus(SignalStatus status, const char* user_id,
size_t user_id_size, void* user_data) {
auto* handler = static_cast<PeerEventHandler*>(user_data);
GuiRuntime* runtime = handler ? &handler->owner_ : nullptr;
if (!runtime) {
return;
}
@@ -150,13 +149,12 @@ void PeerEventHandler::OnSignalStatus(SignalStatus status, const char *user_id,
}
void PeerEventHandler::OnConnectionStatus(ConnectionStatus status,
const char *user_id,
const char* user_id,
const size_t user_id_size,
void *user_data) {
auto *handler = static_cast<PeerEventHandler *>(user_data);
GuiRuntime *runtime = handler ? &handler->owner_ : nullptr;
if (!runtime)
return;
void* user_data) {
auto* handler = static_cast<PeerEventHandler*>(user_data);
GuiRuntime* runtime = handler ? &handler->owner_ : nullptr;
if (!runtime) return;
std::string remote_id(user_id, user_id_size);
std::shared_ptr<GuiRuntime::RemoteSession> props;
@@ -172,113 +170,112 @@ void PeerEventHandler::OnConnectionStatus(ConnectionStatus status,
runtime->is_client_mode_ = true;
runtime->show_connection_status_window_ = true;
props->connection_status_.store(status);
if (status != ConnectionStatus::Connecting &&
status != ConnectionStatus::Gathering) {
props->connection_attempt_active_.store(false);
}
switch (status) {
case ConnectionStatus::Connected: {
runtime->ResetRemoteServiceStatus(*props);
{
RemoteAction remote_action;
remote_action.i.display_num =
runtime->devices_.display_info_list().size();
remote_action.i.display_list =
(char **)malloc(remote_action.i.display_num * sizeof(char *));
remote_action.i.left =
(int *)malloc(remote_action.i.display_num * sizeof(int));
remote_action.i.top =
(int *)malloc(remote_action.i.display_num * sizeof(int));
remote_action.i.right =
(int *)malloc(remote_action.i.display_num * sizeof(int));
remote_action.i.bottom =
(int *)malloc(remote_action.i.display_num * sizeof(int));
for (int i = 0; i < remote_action.i.display_num; i++) {
LOG_INFO("Local display [{}:{}]", i + 1,
runtime->devices_.display_info_list()[i].name);
remote_action.i.display_list[i] = (char *)malloc(
runtime->devices_.display_info_list()[i].name.length() + 1);
strncpy(remote_action.i.display_list[i],
runtime->devices_.display_info_list()[i].name.c_str(),
runtime->devices_.display_info_list()[i].name.length());
remote_action.i.display_list
[i][runtime->devices_.display_info_list()[i].name.length()] = '\0';
remote_action.i.left[i] =
runtime->devices_.display_info_list()[i].left;
remote_action.i.top[i] = runtime->devices_.display_info_list()[i].top;
remote_action.i.right[i] =
runtime->devices_.display_info_list()[i].right;
remote_action.i.bottom[i] =
runtime->devices_.display_info_list()[i].bottom;
case ConnectionStatus::Connected: {
runtime->ResetRemoteServiceStatus(*props);
{
RemoteAction remote_action;
remote_action.i.display_num =
runtime->devices_.display_info_list().size();
remote_action.i.display_list =
(char**)malloc(remote_action.i.display_num * sizeof(char*));
remote_action.i.left =
(int*)malloc(remote_action.i.display_num * sizeof(int));
remote_action.i.top =
(int*)malloc(remote_action.i.display_num * sizeof(int));
remote_action.i.right =
(int*)malloc(remote_action.i.display_num * sizeof(int));
remote_action.i.bottom =
(int*)malloc(remote_action.i.display_num * sizeof(int));
for (int i = 0; i < remote_action.i.display_num; i++) {
LOG_INFO("Local display [{}:{}]", i + 1,
runtime->devices_.display_info_list()[i].name);
remote_action.i.display_list[i] = (char*)malloc(
runtime->devices_.display_info_list()[i].name.length() + 1);
strncpy(remote_action.i.display_list[i],
runtime->devices_.display_info_list()[i].name.c_str(),
runtime->devices_.display_info_list()[i].name.length());
remote_action.i
.display_list[i][runtime->devices_.display_info_list()[i]
.name.length()] = '\0';
remote_action.i.left[i] =
runtime->devices_.display_info_list()[i].left;
remote_action.i.top[i] =
runtime->devices_.display_info_list()[i].top;
remote_action.i.right[i] =
runtime->devices_.display_info_list()[i].right;
remote_action.i.bottom[i] =
runtime->devices_.display_info_list()[i].bottom;
}
std::string host_name = GetHostName();
remote_action.type = ControlType::host_infomation;
memcpy(&remote_action.i.host_name, host_name.data(),
host_name.size());
remote_action.i.host_name[host_name.size()] = '\0';
remote_action.i.host_name_size = host_name.size();
std::string msg = remote_action.to_json();
int ret = SendReliableDataFrame(props->peer_, msg.data(), msg.size(),
runtime->control_data_label_.c_str());
remote_action_codec::Free(remote_action);
}
std::string host_name = GetHostName();
remote_action.type = ControlType::host_infomation;
memcpy(&remote_action.i.host_name, host_name.data(), host_name.size());
remote_action.i.host_name[host_name.size()] = '\0';
remote_action.i.host_name_size = host_name.size();
std::string msg = remote_action.to_json();
int ret = SendReliableDataFrame(props->peer_, msg.data(), msg.size(),
runtime->control_data_label_.c_str());
remote_action_codec::Free(remote_action);
if (!runtime->need_to_create_stream_window_ &&
!runtime->remote_sessions_.empty()) {
runtime->need_to_create_stream_window_ = true;
}
props->connection_established_ = true;
runtime->start_keyboard_capturer_ = true;
break;
}
if (!runtime->need_to_create_stream_window_ &&
!runtime->remote_sessions_.empty()) {
runtime->need_to_create_stream_window_ = true;
}
props->connection_established_ = true;
runtime->start_keyboard_capturer_ = true;
break;
}
case ConnectionStatus::Disconnected:
case ConnectionStatus::Failed:
case ConnectionStatus::Closed: {
runtime->keyboard_.ReleaseRemotePressedKeys(remote_id,
"connection_closed");
props->connection_established_ = false;
props->enable_mouse_control_ = false;
runtime->ResetRemoteServiceStatus(*props);
{
std::lock_guard<std::mutex> lock(props->video_frame_mutex_);
props->front_frame_.reset();
props->back_frame_.reset();
props->video_width_ = 0;
props->video_height_ = 0;
props->video_size_ = 0;
props->render_rect_dirty_ = true;
props->stream_cleanup_pending_ = true;
}
runtime->focus_on_stream_window_ = false;
break;
}
case ConnectionStatus::IncorrectPassword: {
runtime->password_validating_ = false;
runtime->password_validating_time_++;
if (runtime->connect_button_pressed_) {
runtime->connect_button_pressed_ = false;
case ConnectionStatus::Disconnected:
case ConnectionStatus::Failed:
case ConnectionStatus::Closed: {
runtime->keyboard_.ReleaseRemotePressedKeys(remote_id,
"connection_closed");
props->connection_established_ = false;
runtime->connect_button_label_ =
localization::connect[runtime->localization_language_index_];
props->enable_mouse_control_ = false;
runtime->ResetRemoteServiceStatus(*props);
{
std::lock_guard<std::mutex> lock(props->video_frame_mutex_);
props->front_frame_.reset();
props->back_frame_.reset();
props->video_width_ = 0;
props->video_height_ = 0;
props->video_size_ = 0;
props->render_rect_dirty_ = true;
props->stream_cleanup_pending_ = true;
}
runtime->focus_on_stream_window_ = false;
break;
}
break;
}
case ConnectionStatus::NoSuchTransmissionId:
case ConnectionStatus::RemoteUnavailable: {
if (runtime->connect_button_pressed_) {
props->connection_established_ = false;
runtime->connect_button_label_ =
localization::connect[runtime->localization_language_index_];
case ConnectionStatus::IncorrectPassword: {
runtime->password_validating_ = false;
runtime->password_validating_time_++;
if (runtime->connect_button_pressed_) {
runtime->connect_button_pressed_ = false;
props->connection_established_ = false;
runtime->connect_button_label_ =
localization::connect[runtime->localization_language_index_];
}
break;
}
break;
}
default:
break;
case ConnectionStatus::NoSuchTransmissionId:
case ConnectionStatus::RemoteUnavailable: {
if (runtime->connect_button_pressed_) {
props->connection_established_ = false;
runtime->connect_button_label_ =
localization::connect[runtime->localization_language_index_];
}
break;
}
default:
break;
}
} else {
runtime->is_client_mode_ = false;
@@ -289,155 +286,107 @@ void PeerEventHandler::OnConnectionStatus(ConnectionStatus status,
}
switch (status) {
case ConnectionStatus::Connected: {
case ConnectionStatus::Connected: {
#if _WIN32
runtime->last_windows_service_status_tick_ = 0;
runtime->last_windows_service_status_tick_ = 0;
#endif
{
RemoteAction remote_action;
remote_action.i.display_num =
runtime->devices_.display_info_list().size();
remote_action.i.display_list =
(char **)malloc(remote_action.i.display_num * sizeof(char *));
remote_action.i.left =
(int *)malloc(remote_action.i.display_num * sizeof(int));
remote_action.i.top =
(int *)malloc(remote_action.i.display_num * sizeof(int));
remote_action.i.right =
(int *)malloc(remote_action.i.display_num * sizeof(int));
remote_action.i.bottom =
(int *)malloc(remote_action.i.display_num * sizeof(int));
for (int i = 0; i < remote_action.i.display_num; i++) {
LOG_INFO("Local display [{}:{}]", i + 1,
runtime->devices_.display_info_list()[i].name);
remote_action.i.display_list[i] = (char *)malloc(
runtime->devices_.display_info_list()[i].name.length() + 1);
strncpy(remote_action.i.display_list[i],
runtime->devices_.display_info_list()[i].name.c_str(),
runtime->devices_.display_info_list()[i].name.length());
remote_action.i.display_list
[i][runtime->devices_.display_info_list()[i].name.length()] = '\0';
remote_action.i.left[i] =
runtime->devices_.display_info_list()[i].left;
remote_action.i.top[i] = runtime->devices_.display_info_list()[i].top;
remote_action.i.right[i] =
runtime->devices_.display_info_list()[i].right;
remote_action.i.bottom[i] =
runtime->devices_.display_info_list()[i].bottom;
}
std::string host_name = GetHostName();
remote_action.type = ControlType::host_infomation;
memcpy(&remote_action.i.host_name, host_name.data(), host_name.size());
remote_action.i.host_name[host_name.size()] = '\0';
remote_action.i.host_name_size = host_name.size();
std::string msg = remote_action.to_json();
int ret = SendReliableDataFrame(runtime->peer_, msg.data(), msg.size(),
runtime->control_data_label_.c_str());
remote_action_codec::Free(remote_action);
}
runtime->need_to_create_server_window_ = true;
runtime->is_server_mode_ = true;
runtime->start_screen_capturer_ = true;
runtime->start_speaker_capturer_ = true;
runtime->remote_client_id_ = remote_id;
runtime->start_mouse_controller_ = true;
{
std::shared_lock lock(runtime->connection_status_mutex_);
if (std::all_of(runtime->connection_status_.begin(),
runtime->connection_status_.end(), [](const auto &kv) {
return kv.first.find("web") != std::string::npos;
})) {
runtime->show_cursor_ = true;
}
}
break;
}
case ConnectionStatus::Disconnected:
case ConnectionStatus::Failed:
case ConnectionStatus::Closed: {
runtime->keyboard_.ReleaseRemotePressedKeys(remote_id,
"connection_closed");
bool all_disconnected = false;
{
std::shared_lock lock(runtime->connection_status_mutex_);
all_disconnected =
std::all_of(runtime->connection_status_.begin(),
runtime->connection_status_.end(), [](const auto &kv) {
return kv.second == ConnectionStatus::Closed ||
kv.second == ConnectionStatus::Failed ||
kv.second == ConnectionStatus::Disconnected;
});
}
if (all_disconnected) {
runtime->need_to_destroy_server_window_ = true;
runtime->is_server_mode_ = false;
#if defined(__linux__) && !defined(__APPLE__)
if (IsWaylandSession()) {
// Keep Wayland capture session warm to avoid black screen on
// subsequent reconnects.
runtime->start_screen_capturer_ = true;
LOG_INFO("Keeping Wayland screen capturer running after "
"disconnect to preserve reconnect stability");
} else {
runtime->start_screen_capturer_ = false;
}
#else
runtime->start_screen_capturer_ = false;
#endif
runtime->start_speaker_capturer_ = false;
runtime->start_mouse_controller_ = false;
runtime->start_keyboard_capturer_ = false;
runtime->remote_client_id_ = "";
if (props)
props->connection_established_ = false;
if (runtime->audio_capture_) {
runtime->devices_.StopSpeakerCapturer();
runtime->audio_capture_ = false;
}
{
std::unique_lock lock(runtime->connection_status_mutex_);
runtime->connection_status_.erase(remote_id);
runtime->connection_host_names_.erase(remote_id);
}
runtime->devices_.ResetToInitialDisplay();
}
RemoteAction remote_action;
remote_action.i.display_num =
runtime->devices_.display_info_list().size();
remote_action.i.display_list =
(char**)malloc(remote_action.i.display_num * sizeof(char*));
remote_action.i.left =
(int*)malloc(remote_action.i.display_num * sizeof(int));
remote_action.i.top =
(int*)malloc(remote_action.i.display_num * sizeof(int));
remote_action.i.right =
(int*)malloc(remote_action.i.display_num * sizeof(int));
remote_action.i.bottom =
(int*)malloc(remote_action.i.display_num * sizeof(int));
for (int i = 0; i < remote_action.i.display_num; i++) {
LOG_INFO("Local display [{}:{}]", i + 1,
runtime->devices_.display_info_list()[i].name);
remote_action.i.display_list[i] = (char*)malloc(
runtime->devices_.display_info_list()[i].name.length() + 1);
strncpy(remote_action.i.display_list[i],
runtime->devices_.display_info_list()[i].name.c_str(),
runtime->devices_.display_info_list()[i].name.length());
remote_action.i
.display_list[i][runtime->devices_.display_info_list()[i]
.name.length()] = '\0';
remote_action.i.left[i] =
runtime->devices_.display_info_list()[i].left;
remote_action.i.top[i] =
runtime->devices_.display_info_list()[i].top;
remote_action.i.right[i] =
runtime->devices_.display_info_list()[i].right;
remote_action.i.bottom[i] =
runtime->devices_.display_info_list()[i].bottom;
}
{
std::shared_lock lock(runtime->connection_status_mutex_);
if (std::all_of(runtime->connection_status_.begin(),
runtime->connection_status_.end(), [](const auto &kv) {
return kv.first.find("web") == std::string::npos;
})) {
runtime->show_cursor_ = false;
}
}
std::string host_name = GetHostName();
remote_action.type = ControlType::host_infomation;
memcpy(&remote_action.i.host_name, host_name.data(),
host_name.size());
remote_action.i.host_name[host_name.size()] = '\0';
remote_action.i.host_name_size = host_name.size();
break;
}
default:
break;
std::string msg = remote_action.to_json();
int ret =
SendReliableDataFrame(runtime->peer_, msg.data(), msg.size(),
runtime->control_data_label_.c_str());
remote_action_codec::Free(remote_action);
}
runtime->need_to_destroy_server_window_.store(
false, std::memory_order_release);
runtime->need_to_create_server_window_.store(true,
std::memory_order_release);
runtime->is_server_mode_ = true;
runtime->start_screen_capturer_ = true;
runtime->start_speaker_capturer_ = true;
runtime->remote_client_id_ = remote_id;
runtime->start_mouse_controller_ = true;
{
std::shared_lock lock(runtime->connection_status_mutex_);
if (std::all_of(runtime->connection_status_.begin(),
runtime->connection_status_.end(),
[](const auto& kv) {
return kv.first.find("web") != std::string::npos;
})) {
runtime->show_cursor_ = true;
}
}
break;
}
case ConnectionStatus::Disconnected:
case ConnectionStatus::Failed:
case ConnectionStatus::Closed: {
runtime->HandleServerControllerDisconnected(remote_id,
"connection_closed");
break;
}
default:
break;
}
}
}
void PeerEventHandler::OnNetStatusReport(
const char *client_id, size_t client_id_size, TraversalMode mode,
const XNetTrafficStats *net_traffic_stats, const char *user_id,
const size_t user_id_size, void *user_data) {
auto *handler = static_cast<PeerEventHandler *>(user_data);
GuiRuntime *runtime = handler ? &handler->owner_ : nullptr;
const char* client_id, size_t client_id_size, TraversalMode mode,
const XNetTrafficStats* net_traffic_stats, const char* user_id,
const size_t user_id_size, void* user_data) {
auto* handler = static_cast<PeerEventHandler*>(user_data);
GuiRuntime* runtime = handler ? &handler->owner_ : nullptr;
if (!runtime) {
return;
}
if (strchr(client_id, '@') != nullptr && strchr(user_id, '-') == nullptr) {
std::string id, password;
const char *at_pos = strchr(client_id, '@');
const char* at_pos = strchr(client_id, '@');
if (at_pos == nullptr) {
id = client_id;
password.clear();
@@ -510,4 +459,4 @@ void PeerEventHandler::OnNetStatusReport(
props->net_traffic_stats_ = *net_traffic_stats;
}
}
} // namespace crossdesk
} // namespace crossdesk
+3 -5
View File
@@ -58,7 +58,7 @@ struct FileTransferState {
// connection, media, input, window and transfer data that share one lifetime.
struct RemoteSession {
Params params_;
PeerPtr *peer_ = nullptr;
PeerPtr* peer_ = nullptr;
std::string audio_label_ = "control_audio";
std::string data_label_ = "data";
std::string mouse_label_ = "mouse";
@@ -74,8 +74,6 @@ struct RemoteSession {
SignalStatus signal_status_ = SignalStatus::SignalClosed;
bool connection_established_ = false;
bool rejoin_ = false;
std::atomic<bool> connection_attempt_active_ = false;
std::chrono::steady_clock::time_point connection_attempt_started_at_;
bool net_traffic_stats_button_pressed_ = false;
bool enable_mouse_control_ = true;
bool mouse_controller_is_started_ = false;
@@ -160,6 +158,6 @@ struct RemoteSession {
using RemoteSessionPtr = std::shared_ptr<RemoteSession>;
} // namespace crossdesk::gui_detail
} // namespace crossdesk::gui_detail
#endif // CROSSDESK_GUI_REMOTE_SESSION_H_
#endif // CROSSDESK_GUI_REMOTE_SESSION_H_
+1 -1
View File
@@ -295,7 +295,7 @@ export component ImGuiCheckBox inherits Rectangle {
border-radius: 4px;
if root.checked: Text {
text: FontAwesomeIcons.check;
color: #2e86de;
color: root.enabled ? #2e86de : #8b8f95;
font-family: "Font Awesome 6 Free";
font-weight: 900;
font-size: 16px;
+381 -87
View File
@@ -9,6 +9,11 @@ export struct RecentConnection {
thumbnail: image,
}
export struct ReleaseNoteBlock {
content: styled-text,
section-gap: bool,
}
export global UiStrings {
in-out property <string> local-desktop: "Local Desktop";
in-out property <string> remote-desktop: "Remote Desktop";
@@ -48,8 +53,8 @@ export global UiStrings {
in-out property <string> server-port: "Signal port";
in-out property <string> coturn-port: "TURN port";
in-out property <string> version: "Version";
in-out property <string> copyright: "Copyright © CrossDesk contributors";
in-out property <string> license: "Licensed under GPL-3.0-only";
in-out property <string> copyright: "© 2026 by JUNKUN DI. All right reserved.";
in-out property <string> license: "Licensed under GNU GPL v3.";
in-out property <string> signal-connected: "Signal server connected";
in-out property <string> signal-disconnected: "Signal server disconnected";
in-out property <string> tls-error: "Signal server TLS certificate error";
@@ -179,15 +184,21 @@ component ConnectionDialogButton inherits Rectangle {
export component MainWindow inherits Window {
title: "CrossDesk";
preferred-width: 640px;
preferred-height: 450px;
preferred-height: 450px + root.titlebar-height;
min-width: 640px;
max-width: 640px;
min-height: 450px;
max-height: 450px;
no-frame: false;
background: white;
min-height: 450px + root.titlebar-height;
max-height: 450px + root.titlebar-height;
no-frame: root.custom-titlebar;
background: root.wayland-titlebar ? transparent : white;
default-font-size: ImGuiFontStyle.base;
in property <bool> custom-titlebar: false;
in property <bool> wayland-titlebar: false;
in property <bool> window-active: true;
private property <length> titlebar-height:
root.custom-titlebar ? (root.wayland-titlebar ? 32px : 31px) : 0px;
private property <length> wayland-corner-radius: 12px;
in property <string> local-id: "";
in property <string> local-password: "";
in-out property <string> remote-id-input: "";
@@ -199,7 +210,7 @@ export component MainWindow inherits Window {
in property <string> current-version: "";
in property <string> latest-version: "";
in property <string> release-name: "";
in property <string> release-notes: "";
in property <[ReleaseNoteBlock]> release-note-blocks;
in property <string> release-date: "";
in-out property <string> offline-warning: "";
in property <bool> connection-dialog-open: false;
@@ -237,7 +248,9 @@ export component MainWindow inherits Window {
in-out property <string> server-port: "";
in-out property <string> coturn-port: "";
in property <bool> settings-session-active: false;
in property <bool> hardware-codec-visible: true;
// Keep the setting visible on every platform. Unsupported builds expose it
// as disabled so the fixed settings layout never turns into a blank row.
in property <bool> hardware-codec-available: true;
callback copy-local-id;
callback toggle-password-visibility;
@@ -263,9 +276,13 @@ export component MainWindow inherits Window {
callback cancel-portable-service;
callback acknowledge-portable-service;
callback acknowledge-portable-service-suppressed;
callback main-title-drag(int, length, length);
callback minimize-main-window;
callback close-main-window;
private property <bool> menu-open: false;
in-out property <bool> settings-open: false;
private property <length> settings-scroll-y: 0px;
in-out property <bool> self-host-settings-open: false;
in-out property <bool> about-open: false;
in-out property <bool> update-open: root.update-available;
@@ -299,17 +316,216 @@ export component MainWindow inherits Window {
? root.recent-tooltip-pointer-x - root.recent-tooltip-width - 12px
: root.recent-tooltip-pointer-x + 12px));
private property <length> recent-tooltip-y: min(
root.height - root.recent-tooltip-height - 8px,
root.height - root.titlebar-height - root.recent-tooltip-height - 8px,
max(8px, root.recent-tooltip-pointer-y - root.recent-tooltip-height - 8px));
changed settings-open => {
if root.settings-open {
root.settings-scroll-y = 0px;
focus-sink.focus();
}
}
reset-remote-id => {
root.remote-id-input = "";
remote-id-editor.focus();
}
Rectangle {
window-surface := Rectangle {
width: root.width;
height: root.height;
background: root.wayland-titlebar ? #f6f5f4 : transparent;
border-radius:
root.wayland-titlebar ? root.wayland-corner-radius : 0px;
clip: root.wayland-titlebar;
if root.custom-titlebar && root.wayland-titlebar: Rectangle {
x: 0px;
y: 0px;
width: parent.width;
height: root.titlebar-height;
background: #f6f5f4;
z: 100;
Text {
x: 80px;
width: parent.width - 160px;
height: parent.height;
text: "CrossDesk";
color: root.window-active ? #3d3d3d : #747474;
font-size: 12px;
font-weight: 600;
horizontal-alignment: center;
vertical-alignment: center;
overflow: elide;
}
wayland-title-drag := TouchArea {
width: parent.width - 80px;
property <int> phase: self.pressed ? 1 : 0;
changed phase => {
root.main-title-drag(phase, self.mouse-x, self.mouse-y);
}
moved => {
if (self.pressed) {
root.main-title-drag(2, self.mouse-x, self.mouse-y);
}
}
}
wayland-minimize-button := Rectangle {
x: parent.width - 74px;
y: 4px;
width: 24px;
height: 24px;
border-radius: 12px;
background: wayland-minimize-touch.pressed ? #c0bfbc
: wayland-minimize-touch.has-hover ? #deddda
: root.window-active ? #e6e6e6
: transparent;
accessible-role: button;
accessible-label: "Minimize";
accessible-action-default => { wayland-minimize-touch.clicked(); }
Rectangle {
x: 8px;
y: 13px;
width: 8px;
height: 0.75px;
background: root.window-active
|| wayland-minimize-touch.has-hover
|| wayland-minimize-touch.pressed ? #454545 : #898989;
}
wayland-minimize-touch := TouchArea {
clicked => { root.minimize-main-window(); }
}
}
wayland-close-button := Rectangle {
x: parent.width - 34px;
y: 4px;
width: 24px;
height: 24px;
border-radius: 12px;
background: wayland-close-touch.pressed ? #b2161d
: wayland-close-touch.has-hover ? #e01b24
: root.window-active ? #e6e6e6
: transparent;
accessible-role: button;
accessible-label: "Close";
accessible-action-default => { wayland-close-touch.clicked(); }
Path {
width: parent.width;
height: parent.height;
viewbox-width: 24;
viewbox-height: 24;
commands: "M 14.75 14.75 L 8.25 8.25 M 14.75 8.25 L 8.25 14.75";
stroke: wayland-close-touch.has-hover
|| wayland-close-touch.pressed ? white
: root.window-active ? #454545 : #898989;
stroke-width: 1px;
}
wayland-close-touch := TouchArea {
clicked => { root.close-main-window(); }
}
}
Rectangle {
y: parent.height - 1px;
height: 1px;
background: #d5d3d0;
}
}
if root.custom-titlebar && !root.wayland-titlebar: Rectangle {
x: 0px;
y: 0px;
width: root.width;
height: 31px;
background: #f9f9f9;
z: 100;
Image {
x: 8px;
y: 7px;
width: 16px;
height: 16px;
source: @image-url("../../../icons/linux/crossdesk_16x16.png");
image-fit: contain;
}
Text {
x: 30px;
width: parent.width - 122px;
height: parent.height;
text: "CrossDesk";
color: #202124;
font-size: 12px;
vertical-alignment: center;
overflow: elide;
}
title-drag := TouchArea {
x: 0px;
width: parent.width - 92px;
property <int> phase: self.pressed ? 1 : 0;
changed phase => { root.main-title-drag(phase, self.mouse-x, self.mouse-y); }
moved => { if (self.pressed) { root.main-title-drag(2, self.mouse-x, self.mouse-y); } }
}
minimize-button := Rectangle {
x: parent.width - 92px;
width: 46px;
height: parent.height;
background: minimize-touch.pressed ? #c7c7c7
: minimize-touch.has-hover ? #e5e5e5
: transparent;
accessible-role: button;
accessible-label: "Minimize";
accessible-action-default => { minimize-touch.clicked(); }
Text {
text: FontAwesomeIcons.minus;
color: #202124;
font-family: "Font Awesome 6 Free";
font-weight: 900;
font-size: 10px;
horizontal-alignment: center;
vertical-alignment: center;
accessible-role: none;
}
minimize-touch := TouchArea { clicked => { root.minimize-main-window(); } }
}
close-button := Rectangle {
x: parent.width - 46px;
width: 46px;
height: parent.height;
background: close-touch.pressed ? #b3251b
: close-touch.has-hover ? #c42b1c
: transparent;
accessible-role: button;
accessible-label: "Close";
accessible-action-default => { close-touch.clicked(); }
Text {
text: FontAwesomeIcons.xmark;
color: close-touch.has-hover || close-touch.pressed ? white : #202124;
font-family: "Font Awesome 6 Free";
font-weight: 900;
font-size: 12px;
horizontal-alignment: center;
vertical-alignment: center;
accessible-role: none;
}
close-touch := TouchArea { clicked => { root.close-main-window(); } }
}
Rectangle {
y: parent.height - 1px;
height: 1px;
background: #d4d4d4;
}
}
content-layer := Rectangle {
x: 0px;
y: root.titlebar-height;
width: root.width;
height: root.height - self.y;
clip: true;
Rectangle {
width: parent.width;
height: parent.height;
background: white;
border-width: 0px;
border-radius: 0px;
@@ -329,8 +545,8 @@ export component MainWindow inherits Window {
z: 7;
}
// App actions live in the content header; native window controls and
// dragging are provided by the system title bar.
// App actions live in the content header; window controls and dragging
// remain in the native or Windows-specific title bar above it.
menu-button := IconButton {
x: parent.width - 40px;
y: 4px;
@@ -385,6 +601,15 @@ export component MainWindow inherits Window {
border-width: 1px;
border-color: ImGuiLineStyle.border;
border-radius: 4px;
// Measure the ID using the preferred field size.
// The visible text can then scale only on systems
// whose font metrics would otherwise clip it.
local-id-width-probe := Text {
visible: false;
text: root.local-id;
font-size: ImGuiFontStyle.field-value;
letter-spacing: 1px;
}
Text {
x: 8px;
// Compensate for the font's ascender-heavy
@@ -394,7 +619,9 @@ export component MainWindow inherits Window {
height: parent.height - 4px;
text: root.local-id;
color: #20242a;
font-size: ImGuiFontStyle.field-value;
font-size: max(18px,
ImGuiFontStyle.field-value *
(self.width / max(self.width, local-id-width-probe.preferred-width)));
letter-spacing: 1px;
horizontal-alignment: left;
vertical-alignment: center;
@@ -775,8 +1002,8 @@ export component MainWindow inherits Window {
if root.menu-open: TouchArea {
x: 0px;
y: 0px;
width: root.width;
height: root.height;
width: parent.width;
height: parent.height;
z: 9;
clicked => {
root.menu-open = false;
@@ -1180,9 +1407,9 @@ export component MainWindow inherits Window {
TouchArea { clicked => { focus-sink.focus(); } }
DialogSurface {
x: root.compact-language ? 219px : 190px;
y: 9px;
y: 60px;
width: root.compact-language ? 202px : 269px;
height: 432px;
height: 330px;
border-radius: 7px;
Rectangle {
@@ -1197,55 +1424,86 @@ export component MainWindow inherits Window {
Text { x: 6px; text: UiStrings.settings; font-size: ImGuiFontStyle.base; font-weight: 600; color: #202124; vertical-alignment: center; }
}
Text { x: 8px; y: 31px; width: root.compact-language ? 108px : 176px; height: 24px; text: UiStrings.language; font-size: ImGuiFontStyle.body; overflow: elide; vertical-alignment: center; }
CompactComboBox { x: root.compact-language ? 120px : 188px; y: 30px; model: ["中文", "English", "Русский"]; current-index <=> root.language-index; }
Rectangle { x: 7px; y: 57px; width: parent.width - 14px; height: 1px; background: ImGuiLineStyle.separator; }
settings-scroll := ScrollView {
x: 0px;
y: 21px;
width: parent.width;
height: 265px;
viewport-y <=> root.settings-scroll-y;
Text { x: 8px; y: 61px; width: root.compact-language ? 108px : 176px; height: 24px; text: UiStrings.video-quality; font-size: ImGuiFontStyle.body; overflow: elide; vertical-alignment: center; }
CompactComboBox { x: root.compact-language ? 120px : 188px; y: 60px; enabled: !root.settings-session-active; model: [UiStrings.quality-low, UiStrings.quality-medium, UiStrings.quality-high]; current-index <=> root.video-quality-index; }
Rectangle { x: 7px; y: 87px; width: parent.width - 14px; height: 1px; background: ImGuiLineStyle.separator; }
settings-content := VerticalLayout {
// Reserve space for the overlaid vertical scrollbar.
width: settings-scroll.width;
padding-top: 8px;
padding-bottom: 8px;
spacing: 0px;
Text { x: 8px; y: 91px; width: root.compact-language ? 108px : 176px; height: 24px; text: UiStrings.frame-rate; font-size: ImGuiFontStyle.body; overflow: elide; vertical-alignment: center; }
CompactComboBox { x: root.compact-language ? 120px : 188px; y: 90px; enabled: !root.settings-session-active; model: ["30 fps", "60 fps"]; current-index <=> root.frame-rate-index; }
Rectangle { x: 7px; y: 117px; width: parent.width - 14px; height: 1px; background: ImGuiLineStyle.separator; }
Rectangle {
height: 30px;
Text { x: 8px; y: 3px; width: root.compact-language ? 94px : 162px; height: 24px; text: UiStrings.language; font-size: ImGuiFontStyle.body; overflow: elide; vertical-alignment: center; }
CompactComboBox { x: root.compact-language ? 106px : 174px; y: 2px; model: ["中文", "English", "Русский"]; current-index <=> root.language-index; }
}
Rectangle {
height: 30px;
Text { x: 8px; y: 3px; width: root.compact-language ? 94px : 162px; height: 24px; text: UiStrings.video-quality; font-size: ImGuiFontStyle.body; overflow: elide; vertical-alignment: center; }
CompactComboBox { x: root.compact-language ? 106px : 174px; y: 2px; enabled: !root.settings-session-active; model: [UiStrings.quality-low, UiStrings.quality-medium, UiStrings.quality-high]; current-index <=> root.video-quality-index; }
}
Rectangle {
height: 30px;
Text { x: 8px; y: 3px; width: root.compact-language ? 94px : 162px; height: 24px; text: UiStrings.frame-rate; font-size: ImGuiFontStyle.body; overflow: elide; vertical-alignment: center; }
CompactComboBox { x: root.compact-language ? 106px : 174px; y: 2px; enabled: !root.settings-session-active; model: ["30 fps", "60 fps"]; current-index <=> root.frame-rate-index; }
}
Rectangle {
height: 30px;
Text { x: 8px; y: 3px; width: root.compact-language ? 94px : 162px; height: 24px; text: UiStrings.codec; font-size: ImGuiFontStyle.body; overflow: elide; vertical-alignment: center; }
CompactComboBox { x: root.compact-language ? 106px : 174px; y: 2px; enabled: !root.settings-session-active; model: [UiStrings.codec-h264, UiStrings.codec-av1]; current-index <=> root.codec-index; }
}
Rectangle {
height: 30px;
Text { x: 8px; y: 3px; width: root.compact-language ? 145px : 212px; height: 24px; text: UiStrings.hardware-codec; color: root.hardware-codec-available ? #202124 : #8b8f95; font-size: ImGuiFontStyle.body; overflow: elide; vertical-alignment: center; }
ImGuiCheckBox { x: root.compact-language ? 157px : 224px; y: 3px; checked <=> root.hardware-codec-enabled; enabled: root.hardware-codec-available && !root.settings-session-active; }
}
Rectangle {
height: 30px;
Text { x: 8px; y: 3px; width: root.compact-language ? 145px : 212px; height: 24px; text: UiStrings.turn-relay; font-size: ImGuiFontStyle.body; overflow: elide; vertical-alignment: center; }
ImGuiCheckBox { x: root.compact-language ? 157px : 224px; y: 3px; checked <=> root.turn-enabled; enabled: !root.settings-session-active; }
}
Rectangle {
height: 30px;
Text { x: 8px; y: 3px; width: root.compact-language ? 145px : 212px; height: 24px; text: UiStrings.srtp; font-size: ImGuiFontStyle.body; overflow: elide; vertical-alignment: center; }
ImGuiCheckBox { x: root.compact-language ? 157px : 224px; y: 3px; checked <=> root.srtp-enabled; enabled: !root.settings-session-active; }
}
Rectangle {
height: 30px;
CompactButton { x: 8px; y: 3px; width: root.compact-language ? 70px : 206px; text: UiStrings.self-hosted; enabled: !root.settings-session-active; clicked => { root.self-host-settings-open = true; } }
ImGuiCheckBox { x: root.compact-language ? 157px : 224px; y: 3px; checked <=> root.self-hosted-enabled; enabled: !root.settings-session-active; }
}
Rectangle {
height: 30px;
Text { x: 8px; y: 3px; width: root.compact-language ? 145px : 212px; height: 24px; text: UiStrings.autostart; font-size: ImGuiFontStyle.body; overflow: elide; vertical-alignment: center; }
ImGuiCheckBox { x: root.compact-language ? 157px : 224px; y: 3px; checked <=> root.autostart-enabled; enabled: !root.settings-session-active; }
}
Rectangle {
height: 30px;
Text { x: 8px; y: 3px; width: root.compact-language ? 145px : 212px; height: 24px; text: UiStrings.daemon; font-size: ImGuiFontStyle.body; overflow: elide; vertical-alignment: center; }
ImGuiCheckBox { x: root.compact-language ? 157px : 224px; y: 3px; checked <=> root.daemon-enabled; enabled: !root.settings-session-active; }
}
Rectangle {
height: 30px;
Text { x: 8px; y: 3px; width: root.compact-language ? 145px : 212px; height: 24px; text: UiStrings.minimize-to-tray; font-size: ImGuiFontStyle.body; overflow: elide; vertical-alignment: center; }
ImGuiCheckBox { x: root.compact-language ? 157px : 224px; y: 3px; checked <=> root.minimize-to-tray-enabled; enabled: !root.settings-session-active; }
}
Rectangle {
height: 30px;
Text { x: 8px; y: 3px; width: root.compact-language ? 94px : 154px; height: 24px; text: UiStrings.file-save-path; font-size: ImGuiFontStyle.body; overflow: elide; vertical-alignment: center; }
CompactButton { x: root.compact-language ? 106px : 166px; y: 3px; width: root.compact-language ? 73px : 80px; text: root.file-save-path == "" ? UiStrings.default-desktop : root.file-save-path; enabled: !root.settings-session-active; clicked => { root.browse-save-path(); } }
}
}
}
Text { x: 8px; y: 121px; width: root.compact-language ? 108px : 176px; height: 24px; text: UiStrings.codec; font-size: ImGuiFontStyle.body; overflow: elide; vertical-alignment: center; }
CompactComboBox { x: root.compact-language ? 120px : 188px; y: 120px; enabled: !root.settings-session-active; model: [UiStrings.codec-h264, UiStrings.codec-av1]; current-index <=> root.codec-index; }
Rectangle { x: 7px; y: 147px; width: parent.width - 14px; height: 1px; background: ImGuiLineStyle.separator; }
if root.hardware-codec-visible: Text { x: 8px; y: 151px; width: root.compact-language ? 155px : 225px; height: 24px; text: UiStrings.hardware-codec; font-size: ImGuiFontStyle.body; overflow: elide; vertical-alignment: center; }
if root.hardware-codec-visible: ImGuiCheckBox { x: root.compact-language ? 171px : 238px; y: 151px; checked <=> root.hardware-codec-enabled; enabled: !root.settings-session-active; }
Rectangle { x: 7px; y: 177px; width: parent.width - 14px; height: 1px; background: ImGuiLineStyle.separator; }
Text { x: 8px; y: 181px; width: root.compact-language ? 155px : 225px; height: 24px; text: UiStrings.turn-relay; font-size: ImGuiFontStyle.body; overflow: elide; vertical-alignment: center; }
ImGuiCheckBox { x: root.compact-language ? 171px : 238px; y: 181px; checked <=> root.turn-enabled; enabled: !root.settings-session-active; }
Rectangle { x: 7px; y: 207px; width: parent.width - 14px; height: 1px; background: ImGuiLineStyle.separator; }
Text { x: 8px; y: 211px; width: root.compact-language ? 155px : 225px; height: 24px; text: UiStrings.srtp; font-size: ImGuiFontStyle.body; overflow: elide; vertical-alignment: center; }
ImGuiCheckBox { x: root.compact-language ? 171px : 238px; y: 211px; checked <=> root.srtp-enabled; enabled: !root.settings-session-active; }
Rectangle { x: 7px; y: 237px; width: parent.width - 14px; height: 1px; background: ImGuiLineStyle.separator; }
CompactButton { x: 8px; y: 241px; width: root.compact-language ? 70px : 220px; text: UiStrings.self-hosted; enabled: !root.settings-session-active; clicked => { root.self-host-settings-open = true; } }
ImGuiCheckBox { x: root.compact-language ? 171px : 238px; y: 241px; checked <=> root.self-hosted-enabled; enabled: !root.settings-session-active; }
Rectangle { x: 7px; y: 267px; width: parent.width - 14px; height: 1px; background: ImGuiLineStyle.separator; }
Text { x: 8px; y: 271px; width: root.compact-language ? 155px : 225px; height: 24px; text: UiStrings.autostart; font-size: ImGuiFontStyle.body; overflow: elide; vertical-alignment: center; }
ImGuiCheckBox { x: root.compact-language ? 171px : 238px; y: 271px; checked <=> root.autostart-enabled; enabled: !root.settings-session-active; }
Rectangle { x: 7px; y: 297px; width: parent.width - 14px; height: 1px; background: ImGuiLineStyle.separator; }
Text { x: 8px; y: 301px; width: root.compact-language ? 155px : 225px; height: 24px; text: UiStrings.daemon; font-size: ImGuiFontStyle.body; overflow: elide; vertical-alignment: center; }
ImGuiCheckBox { x: root.compact-language ? 171px : 238px; y: 301px; checked <=> root.daemon-enabled; enabled: !root.settings-session-active; }
Rectangle { x: 7px; y: 327px; width: parent.width - 14px; height: 1px; background: ImGuiLineStyle.separator; }
Text { x: 8px; y: 331px; width: root.compact-language ? 155px : 225px; height: 24px; text: UiStrings.minimize-to-tray; font-size: ImGuiFontStyle.body; overflow: elide; vertical-alignment: center; }
ImGuiCheckBox { x: root.compact-language ? 171px : 238px; y: 331px; checked <=> root.minimize-to-tray-enabled; enabled: !root.settings-session-active; }
Rectangle { x: 7px; y: 357px; width: parent.width - 14px; height: 1px; background: ImGuiLineStyle.separator; }
Text { x: 8px; y: 361px; width: root.compact-language ? 108px : 168px; height: 24px; text: UiStrings.file-save-path; font-size: ImGuiFontStyle.body; overflow: elide; vertical-alignment: center; }
CompactButton { x: root.compact-language ? 120px : 180px; y: 361px; width: root.compact-language ? 73px : 80px; text: root.file-save-path == "" ? UiStrings.default-desktop : root.file-save-path; enabled: !root.settings-session-active; clicked => { root.browse-save-path(); } }
CompactButton { x: root.compact-language ? 63px : 89px; y: 396px; primary: true; text: UiStrings.ok; clicked => { root.save-settings(); root.settings-open = false; root.self-host-settings-open = false; } }
CompactButton { x: root.compact-language ? 104px : 130px; y: 396px; width: root.compact-language ? 34px : 56px; text: UiStrings.cancel; clicked => { root.cancel-settings(); root.settings-open = false; root.self-host-settings-open = false; } }
Rectangle { x: 7px; y: 286px; width: parent.width - 14px; height: 1px; background: ImGuiLineStyle.separator; }
CompactButton { x: root.compact-language ? 63px : 89px; y: 297px; primary: true; text: UiStrings.ok; clicked => { root.save-settings(); root.settings-open = false; root.self-host-settings-open = false; } }
CompactButton { x: root.compact-language ? 104px : 130px; y: 297px; width: root.compact-language ? 34px : 56px; text: UiStrings.cancel; clicked => { root.cancel-settings(); root.settings-open = false; root.self-host-settings-open = false; } }
}
}
@@ -1284,7 +1542,6 @@ export component MainWindow inherits Window {
LineEdit { x: 1px; y: 1.5px; width: parent.width - 2px; height: parent.height - 2px; text <=> root.server-host; font-size: ImGuiFontStyle.body; }
LineEditFocusUnderlineMask { x: 5px; y: parent.height - 3px; width: parent.width - 10px; }
}
Rectangle { x: 7px; y: 57px; width: parent.width - 14px; height: 1px; background: ImGuiLineStyle.separator; }
Text { x: 8px; y: 61px; width: root.compact-language ? 88px : 165px; height: 24px; text: UiStrings.server-port; font-size: ImGuiFontStyle.body; overflow: elide; vertical-alignment: center; }
Rectangle {
x: root.compact-language ? 100px : 173px;
@@ -1298,7 +1555,6 @@ export component MainWindow inherits Window {
LineEdit { x: 1px; y: 1.5px; width: parent.width - 2px; height: parent.height - 2px; text <=> root.server-port; input-type: InputType.number; font-size: ImGuiFontStyle.body; }
LineEditFocusUnderlineMask { x: 5px; y: parent.height - 3px; width: parent.width - 10px; }
}
Rectangle { x: 7px; y: 87px; width: parent.width - 14px; height: 1px; background: ImGuiLineStyle.separator; }
Text { x: 8px; y: 91px; width: root.compact-language ? 88px : 165px; height: 24px; text: UiStrings.coturn-port; font-size: ImGuiFontStyle.body; overflow: elide; vertical-alignment: center; }
Rectangle {
x: root.compact-language ? 100px : 173px;
@@ -1312,7 +1568,6 @@ export component MainWindow inherits Window {
LineEdit { x: 1px; y: 1.5px; width: parent.width - 2px; height: parent.height - 2px; text <=> root.coturn-port; input-type: InputType.number; font-size: ImGuiFontStyle.body; }
LineEditFocusUnderlineMask { x: 5px; y: parent.height - 3px; width: parent.width - 10px; }
}
Rectangle { x: 7px; y: 117px; width: parent.width - 14px; height: 1px; background: ImGuiLineStyle.separator; }
CompactButton { x: root.compact-language ? 93px : 135px; y: 130px; primary: true; text: UiStrings.ok; clicked => { root.save-self-hosted-settings(); root.self-host-settings-open = false; } }
CompactButton { x: root.compact-language ? 134px : 176px; y: 130px; width: root.compact-language ? 34px : 56px; text: UiStrings.cancel; clicked => { root.cancel-self-hosted-settings(); root.self-host-settings-open = false; } }
}
@@ -1341,19 +1596,19 @@ export component MainWindow inherits Window {
Rectangle { y: parent.height - 7px; height: 7px; background: #d3d3d3; }
Text { x: 6px; text: UiStrings.about; font-size: ImGuiFontStyle.base; font-weight: 600; color: #202124; vertical-alignment: center; }
}
Text { x: 30px; y: 29px; width: 240px; height: 20px; text: UiStrings.version + ": CrossDesk " + root.current-version; color: #202124; font-size: ImGuiFontStyle.body; vertical-alignment: center; }
if root.update-available: Text { x: 30px; y: 51px; width: 240px; height: 20px; text: UiStrings.update-available + ":"; color: #202124; font-size: ImGuiFontStyle.body; vertical-alignment: center; }
Text { x: 30px; y: 40px; width: 240px; height: 20px; text: UiStrings.version + ": CrossDesk " + root.current-version; color: #202124; font-size: ImGuiFontStyle.body; vertical-alignment: center; }
if root.update-available: Text { x: 30px; y: 60px; width: 240px; height: 20px; text: UiStrings.update-available + ":"; color: #202124; font-size: ImGuiFontStyle.body; vertical-alignment: center; }
if root.update-available: Rectangle {
x: 30px;
y: 70px;
y: 60px;
width: 240px;
height: 20px;
version-link := Text { text: root.latest-version; color: version-touch.has-hover ? #174ea6 : #334fd1; font-size: ImGuiFontStyle.body; horizontal-alignment: center; vertical-alignment: center; }
version-touch := TouchArea { mouse-cursor: pointer; clicked => { root.open-download(); } }
}
Text { x: 30px; y: 96px; width: 240px; height: 20px; text: UiStrings.copyright; color: #202124; font-size: ImGuiFontStyle.body; vertical-alignment: center; }
Text { x: 30px; y: 118px; width: 240px; height: 20px; text: UiStrings.license; color: #202124; font-size: ImGuiFontStyle.body; vertical-alignment: center; }
CompactButton { x: 133px; y: 157px; primary: true; text: UiStrings.ok; clicked => { root.about-open = false; } }
Text { x: 30px; y: 85px; width: 240px; height: 20px; text: UiStrings.copyright; color: #202124; font-size: ImGuiFontStyle.body; vertical-alignment: center; }
Text { x: 30px; y: 105px; width: 240px; height: 20px; text: UiStrings.license; color: #202124; font-size: ImGuiFontStyle.body; vertical-alignment: center; }
CompactButton { x: 133px; y: 145px; primary: true; text: UiStrings.ok; clicked => { root.about-open = false; } }
}
}
@@ -1364,24 +1619,43 @@ export component MainWindow inherits Window {
TouchArea { clicked => { focus-sink.focus(); } }
DialogSurface {
x: 120px;
y: 65px;
y: 52px;
width: 400px;
height: 320px;
height: 346px;
border-radius: 7px;
Text { x: 40px; y: 22px; width: 320px; height: 24px; text: UiStrings.update-available + ": " + root.latest-version; color: #202124; font-size: ImGuiFontStyle.prominent; vertical-alignment: center; }
Text { x: 30px; y: 22px; width: 320px; height: 24px; text: UiStrings.update-available + ": " + root.latest-version; color: #202124; font-size: ImGuiFontStyle.prominent; vertical-alignment: center; }
Rectangle {
x: 40px;
x: 30px;
y: 47px;
width: 320px;
height: 22px;
Text { text: UiStrings.access-website + "https://crossdesk.cn"; color: website-touch.has-hover ? #174ea6 : #334fd1; font-size: ImGuiFontStyle.body; vertical-alignment: center; }
Text {
x: 0px;
width: parent.width;
height: parent.height;
text: UiStrings.access-website + "https://crossdesk.cn";
color: website-touch.has-hover ? #174ea6 : #334fd1;
font-size: ImGuiFontStyle.body;
horizontal-alignment: left;
vertical-alignment: center;
}
website-touch := TouchArea { mouse-cursor: pointer; clicked => { root.open-download(); } }
}
Text {
x: 30px;
y: 75px;
width: 320px;
height: 20px;
text: UiStrings.release-notes;
color: #202124;
font-size: 12px;
vertical-alignment: center;
}
Rectangle {
x: 20px;
y: 74px;
x: 30px;
y: 105px;
width: 360px;
height: 200px;
height: 190px;
background: white;
border-width: 1px;
border-color: #b9bdc3;
@@ -1392,19 +1666,24 @@ export component MainWindow inherits Window {
height: parent.height - 2px;
VerticalLayout {
width: 340px;
padding-left: 19px;
padding-left: 8px;
padding-right: 8px;
padding-top: 8px;
padding-bottom: 8px;
spacing: 7px;
if root.release-name != "": Text { width: 312px; text: root.release-name; color: #202124; font-size: ImGuiFontStyle.body; wrap: word-wrap; }
Text { width: 312px; text: root.release-notes; color: #202124; font-size: ImGuiFontStyle.body; wrap: word-wrap; }
if root.release-date != "": Text { width: 312px; text: UiStrings.release-date-label + root.release-date; color: #4f555e; font-size: ImGuiFontStyle.small; wrap: word-wrap; }
VerticalLayout {
width: 312px;
spacing: 10px;
for block in root.release-note-blocks: VerticalLayout {
padding-top: block.section-gap ? 10px : 0px;
StyledText { width: 312px; text: block.content; default-color: #202124; default-font-size: ImGuiFontStyle.body; }
}
}
}
}
}
CompactButton { x: root.compact-language ? 163px : 130px; y: 283px; width: root.compact-language ? 34px : 70px; primary: true; text: UiStrings.download; clicked => { root.open-download(); root.update-open = false; } }
CompactButton { x: root.compact-language ? 204px : 207px; y: 283px; width: root.compact-language ? 34px : 56px; text: UiStrings.later; clicked => { root.update-open = false; } }
CompactButton { x: root.compact-language ? 163px : 130px; y: 309px; width: root.compact-language ? 34px : 70px; primary: true; text: UiStrings.download; clicked => { root.open-download(); root.update-open = false; } }
CompactButton { x: root.compact-language ? 204px : 207px; y: 309px; width: root.compact-language ? 34px : 56px; text: UiStrings.later; clicked => { root.update-open = false; } }
}
}
@@ -1478,4 +1757,19 @@ export component MainWindow inherits Window {
}
}
}
}
if root.custom-titlebar: Rectangle {
x: 0px;
y: 0px;
width: root.width;
height: root.height;
background: transparent;
border-width: 1px;
border-color: root.wayland-titlebar ? #aaa8a6 : #8b8b8b;
border-radius:
root.wayland-titlebar ? root.wayland-corner-radius : 0px;
z: 200;
}
}
}
+194 -6
View File
@@ -112,13 +112,16 @@ component ControlBarButton inherits Rectangle {
export component StreamWindow inherits Window {
title: "CrossDesk";
preferred-width: 1280px;
preferred-height: 720px;
preferred-height: 720px + (root.custom-titlebar ? 32px : 0px);
min-width: 640px;
min-height: 360px;
no-frame: false;
background: black;
min-height: 360px + (root.custom-titlebar ? 32px : 0px);
no-frame: root.custom-titlebar;
background: root.custom-titlebar ? transparent : black;
default-font-size: ImGuiFontStyle.base;
in property <bool> custom-titlebar: false;
in property <bool> window-active: true;
in property <bool> window-maximized: false;
in property <[StreamTab]> tabs;
in-out property <int> selected-tab: 0;
in property <image> frame;
@@ -153,9 +156,14 @@ export component StreamWindow inherits Window {
callback toggle-stats;
callback toggle-fullscreen;
callback disconnect;
callback stream-title-drag(int, length, length);
callback minimize-stream-window;
callback toggle-maximize-stream-window;
callback close-stream-window;
callback pointer-input(PointerEventButton, PointerEventKind, length, length);
callback scroll-input(length, length, length, length);
callback key-input(string, bool, bool, bool, bool, bool);
callback keyboard-focus-changed(bool);
private property <bool> control-expanded: true;
private property <bool> display-menu-open: false;
@@ -169,6 +177,9 @@ export component StreamWindow inherits Window {
private property <length> control-press-x: 0px;
private property <length> control-press-y: 0px;
private property <int> dragging-tab: -1;
private property <length> titlebar-height:
root.custom-titlebar && !root.fullscreen-enabled ? 32px : 0px;
private property <length> window-corner-radius: 12px;
callback begin-control-drag(length, length);
callback move-control-drag(length, length);
callback end-control-drag;
@@ -202,10 +213,167 @@ export component StreamWindow inherits Window {
}
}
Rectangle {
window-surface := Rectangle {
width: root.width;
height: root.height;
background: black;
border-radius:
root.custom-titlebar && !root.fullscreen-enabled && !root.window-maximized
? root.window-corner-radius : 0px;
clip: root.custom-titlebar && !root.fullscreen-enabled;
if root.custom-titlebar && !root.fullscreen-enabled: Rectangle {
x: 0px;
y: 0px;
width: parent.width;
height: root.titlebar-height;
background: #f6f5f4;
z: 100;
Text {
x: 120px;
width: parent.width - 240px;
height: parent.height;
text: "CrossDesk";
color: root.window-active ? #3d3d3d : #747474;
font-size: 12px;
font-weight: 600;
horizontal-alignment: center;
vertical-alignment: center;
overflow: elide;
}
stream-title-drag-area := TouchArea {
width: parent.width - 120px;
property <int> phase: self.pressed ? 1 : 0;
changed phase => {
root.stream-title-drag(phase, self.mouse-x, self.mouse-y);
}
moved => {
if (self.pressed) {
root.stream-title-drag(2, self.mouse-x, self.mouse-y);
}
}
double-clicked => {
root.toggle-maximize-stream-window();
}
}
stream-minimize-button := Rectangle {
x: parent.width - 114px;
y: 4px;
width: 24px;
height: 24px;
border-radius: 12px;
background: stream-minimize-touch.pressed ? #c0bfbc
: stream-minimize-touch.has-hover ? #deddda
: root.window-active ? #e6e6e6
: transparent;
accessible-role: button;
accessible-label: "Minimize";
accessible-action-default => { stream-minimize-touch.clicked(); }
Rectangle {
x: 8px;
y: 13px;
width: 8px;
height: 0.75px;
background: root.window-active
|| stream-minimize-touch.has-hover
|| stream-minimize-touch.pressed ? #454545 : #898989;
}
stream-minimize-touch := TouchArea {
clicked => { root.minimize-stream-window(); }
}
}
stream-maximize-button := Rectangle {
x: parent.width - 74px;
y: 4px;
width: 24px;
height: 24px;
border-radius: 12px;
background: stream-maximize-touch.pressed ? #c0bfbc
: stream-maximize-touch.has-hover ? #deddda
: root.window-active ? #e6e6e6
: transparent;
accessible-role: button;
accessible-label: root.window-maximized ? "Restore" : "Maximize";
accessible-action-default => { stream-maximize-touch.clicked(); }
if !root.window-maximized: Rectangle {
x: 8px;
y: 8px;
width: 8px;
height: 8px;
background: transparent;
border-width: 1px;
border-color: root.window-active
|| stream-maximize-touch.has-hover
|| stream-maximize-touch.pressed ? #454545 : #898989;
}
if root.window-maximized: Rectangle {
x: 9px;
y: 7px;
width: 8px;
height: 8px;
background: transparent;
border-width: 1px;
border-color: root.window-active
|| stream-maximize-touch.has-hover
|| stream-maximize-touch.pressed ? #454545 : #898989;
}
if root.window-maximized: Rectangle {
x: 7px;
y: 9px;
width: 8px;
height: 8px;
background: stream-maximize-button.background;
border-width: 1px;
border-color: root.window-active
|| stream-maximize-touch.has-hover
|| stream-maximize-touch.pressed ? #454545 : #898989;
}
stream-maximize-touch := TouchArea {
clicked => { root.toggle-maximize-stream-window(); }
}
}
stream-close-button := Rectangle {
x: parent.width - 34px;
y: 4px;
width: 24px;
height: 24px;
border-radius: 12px;
background: stream-close-touch.pressed ? #b2161d
: stream-close-touch.has-hover ? #e01b24
: root.window-active ? #e6e6e6
: transparent;
accessible-role: button;
accessible-label: "Close";
accessible-action-default => { stream-close-touch.clicked(); }
Path {
width: parent.width;
height: parent.height;
viewbox-width: 24;
viewbox-height: 24;
commands: "M 14.75 14.75 L 8.25 8.25 M 14.75 8.25 L 8.25 14.75";
stroke: stream-close-touch.has-hover
|| stream-close-touch.pressed ? white
: root.window-active ? #454545 : #898989;
stroke-width: 1px;
}
stream-close-touch := TouchArea {
clicked => { root.close-stream-window(); }
}
}
Rectangle {
y: parent.height - 1px;
height: 1px;
background: #d5d3d0;
}
}
content-layer := Rectangle {
y: root.titlebar-height;
width: parent.width;
height: parent.height - self.y;
background: black;
clip: true;
// Keep the ImGui tab strip behavior, including pointer-drag reordering.
Rectangle {
@@ -319,6 +487,8 @@ export component StreamWindow inherits Window {
input-focus := FocusScope {
focus-on-click: true;
focus-gained => { root.keyboard-focus-changed(true); }
focus-lost => { root.keyboard-focus-changed(false); }
key-pressed(event) => {
root.key-input(event.text, true, event.modifiers.control, event.modifiers.alt, event.modifiers.shift, event.modifiers.meta);
accept
@@ -333,6 +503,9 @@ export component StreamWindow inherits Window {
&& self.mouse-x <= control.x + control.width
&& self.mouse-y >= control.y
&& self.mouse-y <= control.y + control.height;
if event.kind == PointerEventKind.down && !over-control {
input-focus.focus();
}
if event.kind == PointerEventKind.down
&& event.button == PointerEventButton.left
&& over-control {
@@ -341,7 +514,10 @@ export component StreamWindow inherits Window {
&& (event.kind == PointerEventKind.up
|| event.kind == PointerEventKind.cancel) {
root.end-control-drag();
} else if !root.control-dragging {
// The control bar overlays the remote video. Events in
// its full rectangle (including gaps between buttons)
// are local UI input and must never reach the peer.
} else if !root.control-dragging && !over-control {
root.pointer-input(event.button, event.kind, self.mouse-x, self.mouse-y);
}
}
@@ -645,5 +821,17 @@ export component StreamWindow inherits Window {
}
}
}
}
if root.custom-titlebar && !root.fullscreen-enabled: Rectangle {
width: parent.width;
height: parent.height;
background: transparent;
border-width: 1px;
border-color: #aaa8a6;
border-radius:
root.window-maximized ? 0px : root.window-corner-radius;
z: 200;
}
}
}
@@ -1,4 +1,3 @@
#include <algorithm>
#include <string>
#include "application/gui_application.h"
@@ -8,51 +7,9 @@
namespace crossdesk {
std::string CleanMarkdown(const std::string &markdown) {
std::string result = markdown;
// remove # title mark
size_t pos = 0;
while (pos < result.length()) {
if (result[pos] == '\n' || pos == 0) {
size_t line_start = (result[pos] == '\n') ? pos + 1 : pos;
if (line_start < result.length() && result[line_start] == '#') {
size_t hash_end = line_start;
while (hash_end < result.length() &&
(result[hash_end] == '#' || result[hash_end] == ' ')) {
hash_end++;
}
result.erase(line_start, hash_end - line_start);
pos = line_start;
continue;
}
}
pos++;
}
// remove ** bold mark
pos = 0;
while ((pos = result.find("**", pos)) != std::string::npos) {
result.erase(pos, 2);
}
// remove all spaces
result.erase(std::remove(result.begin(), result.end(), ' '), result.end());
// replace . with 、
pos = 0;
while ((pos = result.find('.', pos)) != std::string::npos) {
result.replace(pos, 1, "");
pos += 1; // Move to next position after the replacement
}
return result;
}
int GuiApplication::UpdateNotificationWindow() {
if (show_update_notification_window_ && update_available_) {
const ImGuiViewport *viewport = ImGui::GetMainViewport();
const ImGuiViewport* viewport = ImGui::GetMainViewport();
float update_notification_window_width = title_bar_button_width_ * 10.0f;
float update_notification_window_height = title_bar_button_width_ * 8.0f;
@@ -142,8 +99,7 @@ int GuiApplication::UpdateNotificationWindow() {
// release notes
if (!release_notes_.empty()) {
ImGui::SetCursorPosX(update_notification_window_width * 0.05f);
std::string cleaned_notes = CleanMarkdown(release_notes_);
ImGui::TextWrapped("%s", cleaned_notes.c_str());
ImGui::TextWrapped("%s", release_notes_.c_str());
ImGui::Spacing();
}
@@ -204,4 +160,4 @@ int GuiApplication::UpdateNotificationWindow() {
return 0;
}
} // namespace crossdesk
} // namespace crossdesk
@@ -24,6 +24,7 @@
#include "path_manager.h"
#include "rd_log.h"
#include "session_helper_shared.h"
#include "windows_input_marker.h"
namespace {
@@ -828,6 +829,8 @@ InputInjectionResult InjectKeyboardInput(
INPUT input = {0};
input.type = INPUT_KEYBOARD;
input.ki.dwExtraInfo = static_cast<ULONG_PTR>(
crossdesk::kInjectedKeyboardInputMarker);
const bool prefer_vk = PreferSideSpecificVkInjection(key_code);
const UINT resolved_scan_code =
+6 -5
View File
@@ -57,7 +57,7 @@ void ScaleNv12ToABGR(char* src, int src_w, int src_h, int dst_w, int dst_h,
v_fit.data(), (fit_w + 1) / 2, abgr.data(), fit_w * 4,
fit_w, fit_h);
memset(dst_rgba, 0, dst_w * dst_h * 4);
std::memset(dst_rgba, 0, dst_w * dst_h * 4);
for (int i = 0; i < dst_w * dst_h; ++i) {
dst_rgba[i * 4 + 3] = static_cast<char>(0xFF);
}
@@ -65,7 +65,8 @@ void ScaleNv12ToABGR(char* src, int src_w, int src_h, int dst_w, int dst_h,
for (int row = 0; row < fit_h; ++row) {
int dst_offset =
((row + (dst_h - fit_h) / 2) * dst_w + (dst_w - fit_w) / 2) * 4;
memcpy(dst_rgba + dst_offset, abgr.data() + row * fit_w * 4, fit_w * 4);
std::memcpy(dst_rgba + dst_offset, abgr.data() + row * fit_w * 4,
fit_w * 4);
}
}
@@ -85,8 +86,8 @@ Thumbnail::Thumbnail(std::string save_path, unsigned char* aes128_key,
save_path_ = save_path;
}
memcpy(aes128_key_, aes128_key, sizeof(aes128_key_));
memcpy(aes128_iv_, aes128_iv, sizeof(aes128_iv_));
std::memcpy(aes128_key_, aes128_key, sizeof(aes128_key_));
std::memcpy(aes128_iv_, aes128_iv, sizeof(aes128_iv_));
std::filesystem::create_directories(save_path_);
}
@@ -116,7 +117,7 @@ int Thumbnail::SaveToThumbnail(const char* yuv420p, int width, int height,
thumbnail_height_, rgba_buffer_);
} else {
// If yuv420p is null, fill the buffer with black pixels
memset(rgba_buffer_, 0x00, thumbnail_width_ * thumbnail_height_ * 4);
std::memset(rgba_buffer_, 0x00, thumbnail_width_ * thumbnail_height_ * 4);
for (int i = 0; i < thumbnail_width_ * thumbnail_height_; ++i) {
// Set alpha channel to opaque
rgba_buffer_[i * 4 + 3] = static_cast<char>(0xFF);
+5 -4
View File
@@ -7,6 +7,7 @@
#ifndef _THUMBNAIL_H_
#define _THUMBNAIL_H_
#include <cstring>
#include <filesystem>
#include <map>
#include <unordered_map>
@@ -52,18 +53,18 @@ class Thumbnail {
int DeleteAllFilesInDirectory();
int GetKey(unsigned char* aes128_key) {
memcpy(aes128_key, aes128_key_, sizeof(aes128_key_));
std::memcpy(aes128_key, aes128_key_, sizeof(aes128_key_));
return sizeof(aes128_key_);
}
int GetIv(unsigned char* aes128_iv) {
memcpy(aes128_iv, aes128_iv_, sizeof(aes128_iv_));
std::memcpy(aes128_iv, aes128_iv_, sizeof(aes128_iv_));
return sizeof(aes128_iv_);
}
int GetKeyAndIv(unsigned char* aes128_key, unsigned char* aes128_iv) {
memcpy(aes128_key, aes128_key_, sizeof(aes128_key_));
memcpy(aes128_iv, aes128_iv_, sizeof(aes128_iv_));
std::memcpy(aes128_key, aes128_key_, sizeof(aes128_key_));
std::memcpy(aes128_iv, aes128_iv_, sizeof(aes128_iv_));
return 0;
}
+34 -11
View File
@@ -19,7 +19,7 @@ std::filesystem::path FindRepoRoot() {
return {};
}
std::string ReadFile(const std::filesystem::path &path) {
std::string ReadFile(const std::filesystem::path& path) {
std::ifstream file(path, std::ios::binary);
if (!file) {
return {};
@@ -30,8 +30,8 @@ std::string ReadFile(const std::filesystem::path &path) {
return stream.str();
}
bool ExpectContains(const char *name, const std::string &value,
const std::string &expected) {
bool ExpectContains(const char* name, const std::string& value,
const std::string& expected) {
if (value.find(expected) != std::string::npos) {
return true;
}
@@ -40,8 +40,8 @@ bool ExpectContains(const char *name, const std::string &value,
return false;
}
bool ExpectNotContains(const char *name, const std::string &value,
const std::string &unexpected) {
bool ExpectNotContains(const char* name, const std::string& value,
const std::string& unexpected) {
if (value.find(unexpected) == std::string::npos) {
return true;
}
@@ -73,6 +73,10 @@ int main() {
ReadFile(repo_root / "src/gui/runtime/remote_session.h");
const std::string connection_runtime_cpp =
ReadFile(repo_root / "src/gui/runtime/connection_runtime.cpp");
const std::string peer_event_handler_cpp =
ReadFile(repo_root / "src/gui/runtime/peer_event_handler.cpp");
const std::string application_state_h =
ReadFile(repo_root / "src/gui/application/application_state.h");
bool ok = true;
ok &= ExpectContains("minirtc.h", minirtc_api, "RemoteUnavailable");
@@ -81,6 +85,11 @@ int main() {
"\"Remote unavailable\"");
ok &= ExpectContains("peer_connection.cpp", peer_connection,
"ConnectionStatus::RemoteUnavailable");
ok &= ExpectContains("peer_connection.cpp", peer_connection,
"IsCurrentPeerConnection");
ok &= ExpectContains(
"peer_connection.cpp", peer_connection,
"on_connection_status_(ConnectionStatus::Closed, remote_user_id.data()");
ok &= ExpectNotContains("peer_connection.cpp", peer_connection,
"\"Device offline\"");
ok &= ExpectNotContains("peer_connection.cpp", peer_connection,
@@ -95,16 +104,30 @@ int main() {
"return localization::device_offline[language];");
ok &= ExpectContains("runtime_state.h", runtime_state_h,
"pending_presence_probe_started_at_");
ok &= ExpectContains("remote_session.h", remote_session_h,
"connection_attempt_started_at_");
ok &= ExpectContains("connection_runtime.cpp", connection_runtime_cpp,
"HandleConnectionTimeouts");
"HandlePresenceProbeTimeout");
ok &= ExpectContains("connection_runtime.cpp", connection_runtime_cpp,
"kPresenceProbeTimeout");
ok &= ExpectNotContains("remote_session.h", remote_session_h,
"connection_attempt_started_at_");
ok &= ExpectNotContains("remote_session.h", remote_session_h,
"connection_attempt_active_");
ok &= ExpectNotContains("connection_runtime.cpp", connection_runtime_cpp,
"kConnectionAttemptTimeout");
ok &= ExpectNotContains("connection_runtime.cpp", connection_runtime_cpp,
"ConnectionStatus::Failed");
ok &= ExpectContains("peer_event_handler.cpp", peer_event_handler_cpp,
"props->connection_status_.store(status);");
ok &= ExpectContains("peer_event_handler.cpp", peer_event_handler_cpp,
"case ConnectionStatus::Failed:");
ok &= ExpectContains("connection_runtime.cpp", connection_runtime_cpp,
"kConnectionAttemptTimeout");
ok &= ExpectContains("connection_runtime.cpp", connection_runtime_cpp,
"connection_attempt_started_at_");
"HandleServerControllerDisconnected");
ok &= ExpectContains("peer_event_handler.cpp", peer_event_handler_cpp,
"HandleServerControllerDisconnected(remote_id");
ok &= ExpectNotContains("connection_runtime.cpp", connection_runtime_cpp,
"ControllerHeartbeat");
ok &= ExpectContains("application_state.h", application_state_h,
"std::atomic<bool> need_to_destroy_server_window_");
return ok ? 0 : 1;
}
+35 -27
View File
@@ -18,7 +18,7 @@ std::filesystem::path FindRepoRoot() {
return {};
}
std::string ReadFile(const std::filesystem::path &path) {
std::string ReadFile(const std::filesystem::path& path) {
std::ifstream file(path, std::ios::binary);
if (!file) {
return {};
@@ -28,8 +28,8 @@ std::string ReadFile(const std::filesystem::path &path) {
return stream.str();
}
bool ExpectContains(const char *name, const std::string &value,
const std::string &expected) {
bool ExpectContains(const char* name, const std::string& value,
const std::string& expected) {
if (value.find(expected) != std::string::npos) {
return true;
}
@@ -37,8 +37,8 @@ bool ExpectContains(const char *name, const std::string &value,
return false;
}
bool ExpectNotContains(const char *name, const std::string &value,
const std::string &unexpected) {
bool ExpectNotContains(const char* name, const std::string& value,
const std::string& unexpected) {
if (value.find(unexpected) == std::string::npos) {
return true;
}
@@ -46,8 +46,8 @@ bool ExpectNotContains(const char *name, const std::string &value,
return false;
}
bool ExpectContainsAtLeast(const char *name, const std::string &value,
const std::string &expected, size_t min_count) {
bool ExpectContainsAtLeast(const char* name, const std::string& value,
const std::string& expected, size_t min_count) {
size_t count = 0;
size_t pos = 0;
while ((pos = value.find(expected, pos)) != std::string::npos) {
@@ -62,7 +62,7 @@ bool ExpectContainsAtLeast(const char *name, const std::string &value,
return false;
}
} // namespace
} // namespace
int main() {
const std::filesystem::path repo_root = FindRepoRoot();
@@ -84,14 +84,16 @@ int main() {
"root.shortcut-menu-open = false;");
ok &= ExpectContains("stream_window.slint", stream,
"if root.display-menu-open: Rectangle");
ok &= ExpectContains("stream_window.slint", stream,
"display-touch.has-hover");
ok &=
ExpectContains("stream_window.slint", stream, "display-touch.has-hover");
ok &= ExpectContains("stream_window.slint", stream,
"root.switch-display(index)");
ok &= ExpectContains("stream_window.slint", stream,
"if root.shortcut-menu-open: Rectangle");
ok &=
ExpectContains("stream_window.slint", stream, "shortcut-touch.has-hover");
ok &= ExpectContains("stream_window.slint", stream,
"shortcut-touch.has-hover");
"} else if !root.control-dragging && !over-control {");
ok &= ExpectContains("common.slint", common,
"if touch.has-hover && root.tooltip != \"\": Rectangle");
@@ -100,32 +102,38 @@ int main() {
"tooltip: StreamStrings.select-display");
ok &= ExpectContains("stream_window.slint", stream,
"tooltip: StreamStrings.send-shortcut");
ok &= ExpectContains("stream_window.slint", stream,
"root.mouse-control-enabled ? StreamStrings.release-mouse : StreamStrings.control-mouse");
ok &= ExpectContains("stream_window.slint", stream,
"root.audio-enabled ? StreamStrings.mute : StreamStrings.audio");
ok &=
ExpectContains("stream_window.slint", stream,
"root.mouse-control-enabled ? StreamStrings.release-mouse "
": StreamStrings.control-mouse");
ok &= ExpectContains(
"stream_window.slint", stream,
"root.audio-enabled ? StreamStrings.mute : StreamStrings.audio");
ok &= ExpectContains("stream_window.slint", stream,
"tooltip: StreamStrings.select-file");
ok &= ExpectContains("stream_window.slint", stream,
"root.stats-visible ? StreamStrings.hide-stats : StreamStrings.show-stats");
ok &= ExpectContains("stream_window.slint", stream,
"root.fullscreen-enabled ? StreamStrings.exit-fullscreen : StreamStrings.fullscreen");
"root.stats-visible ? StreamStrings.hide-stats : "
"StreamStrings.show-stats");
ok &=
ExpectContains("stream_window.slint", stream,
"root.fullscreen-enabled ? StreamStrings.exit-fullscreen "
": StreamStrings.fullscreen");
ok &= ExpectContains("stream_window.slint", stream,
"tooltip: StreamStrings.disconnect");
ok &= ExpectContains("stream_window.slint", stream,
"root.control-expanded ? StreamStrings.collapse-control : StreamStrings.expand-control");
"root.control-expanded ? StreamStrings.collapse-control "
": StreamStrings.expand-control");
ok &= ExpectContains("gui_application.cpp", application,
"set_select_display(");
ok &= ExpectContains("gui_application.cpp", application,
"set_show_stats(");
ok &= ExpectContains("gui_application.cpp", application,
"set_expand_control(");
ok &=
ExpectContains("gui_application.cpp", application, "set_select_display(");
ok &= ExpectContains("gui_application.cpp", application, "set_show_stats(");
ok &=
ExpectContains("gui_application.cpp", application, "set_expand_control(");
ok &= ExpectContains("gui_application.cpp", application,
"set_collapse_control(");
ok &= ExpectContains("gui_application.cpp", application,
"(*ui_->stream)->global<ui::StreamStrings>()");
ok &= ExpectNotContains("gui_application.cpp", application,
"#include <imgui");
ok &=
ExpectNotContains("gui_application.cpp", application, "#include <imgui");
return ok ? 0 : 1;
}
@@ -0,0 +1,52 @@
#include <X11/Xlib.h>
#include <chrono>
#include <cstdio>
#include <thread>
#include "keyboard_capturer.h"
int main() {
Display* receiver = XOpenDisplay(nullptr);
if (!receiver) {
std::fprintf(stderr, "open display failed\n");
return 1;
}
const int screen = DefaultScreen(receiver);
Window window = XCreateSimpleWindow(receiver, RootWindow(receiver, screen),
0, 0, 100, 100, 0, 0, 0);
XSelectInput(receiver, window, KeyPressMask | KeyReleaseMask);
XMapWindow(receiver, window);
XSync(receiver, False);
std::this_thread::sleep_for(std::chrono::milliseconds(100));
XSetInputFocus(receiver, window, RevertToParent, CurrentTime);
XSync(receiver, False);
crossdesk::KeyboardCapturer keyboard;
// Exercise the scan-code fallback: the controller supplied no usable VK,
// but did preserve the set-1 scan code for the A key.
const int down = keyboard.SendKeyboardCommand(0, true, 0x1E, false);
const int up = keyboard.SendKeyboardCommand(0, false, 0x1E, false);
bool saw_down = false;
bool saw_up = false;
const auto deadline = std::chrono::steady_clock::now() +
std::chrono::seconds(2);
while (std::chrono::steady_clock::now() < deadline &&
(!saw_down || !saw_up)) {
while (XPending(receiver) > 0) {
XEvent event{};
XNextEvent(receiver, &event);
saw_down = saw_down || event.type == KeyPress;
saw_up = saw_up || event.type == KeyRelease;
}
std::this_thread::sleep_for(std::chrono::milliseconds(10));
}
XDestroyWindow(receiver, window);
XCloseDisplay(receiver);
std::printf("down_ret=%d up_ret=%d saw_down=%d saw_up=%d\n", down, up,
saw_down, saw_up);
return down == 0 && up == 0 && saw_down && saw_up ? 0 : 2;
}
+136
View File
@@ -1,29 +1,109 @@
#include "crossdesk_ui.h"
#include "fa_solid_900.h"
#include <cassert>
#include <cstdlib>
#include <fstream>
#include <memory>
#include <string>
#include <vector>
namespace {
// Set the CROSSDESK_*_UI_SNAPSHOT variables while using Slint's software
// renderer to export deterministic active and inactive visual-check images.
bool WriteWindowSnapshot(const slint::Window &window, const char *path) {
const auto snapshot = window.take_snapshot();
if (!snapshot) {
return false;
}
std::ofstream output(path, std::ios::binary);
output << "P6\n"
<< snapshot->width() << ' ' << snapshot->height() << "\n255\n";
for (const auto &pixel : *snapshot) {
output.put(static_cast<char>(pixel.r));
output.put(static_cast<char>(pixel.g));
output.put(static_cast<char>(pixel.b));
}
return output.good();
}
bool RegisterFontAwesome(slint::Window &window) {
return !window.window_handle()
.register_font_from_data(fa_solid_900_ttf,
fa_solid_900_ttf_len)
.has_value();
}
} // namespace
int main() {
auto window = crossdesk::ui::MainWindow::create();
auto stream = crossdesk::ui::StreamWindow::create();
auto server = crossdesk::ui::ServerWindow::create();
if (!RegisterFontAwesome(window->window()) ||
!RegisterFontAwesome(stream->window()) ||
!RegisterFontAwesome(server->window())) {
return 1;
}
window->set_local_id("123 456 789");
window->set_local_password("123456");
window->set_connection_dialog_open(true);
window->set_connection_password_required(true);
window->set_custom_titlebar(true);
window->set_wayland_titlebar(true);
window->set_settings_session_active(true);
window->set_hardware_codec_available(false);
window->set_portable_service_settings_visible(true);
assert(std::string(window->get_local_id()) == "123 456 789");
assert(window->get_connection_dialog_open());
assert(window->get_connection_password_required());
assert(window->get_custom_titlebar());
assert(window->get_wayland_titlebar());
assert(window->get_settings_session_active());
assert(!window->get_hardware_codec_available());
assert(window->get_portable_service_settings_visible());
if (const char *snapshot_path =
std::getenv("CROSSDESK_MAIN_UI_SNAPSHOT")) {
window->show();
const bool snapshot_written =
WriteWindowSnapshot(window->window(), snapshot_path);
window->hide();
if (!snapshot_written) {
return 2;
}
}
if (const char *snapshot_path =
std::getenv("CROSSDESK_MAIN_INACTIVE_UI_SNAPSHOT")) {
window->set_window_active(false);
window->show();
const bool snapshot_written =
WriteWindowSnapshot(window->window(), snapshot_path);
window->hide();
window->set_window_active(true);
if (!snapshot_written) {
return 3;
}
}
window->set_settings_open(true);
window->set_about_open(true);
assert(window->get_settings_open());
assert(window->get_about_open());
if (const char *snapshot_path =
std::getenv("CROSSDESK_SETTINGS_UI_SNAPSHOT")) {
window->set_about_open(false);
window->set_connection_dialog_open(false);
window->show();
const bool snapshot_written =
WriteWindowSnapshot(window->window(), snapshot_path);
window->hide();
if (!snapshot_written) {
return 6;
}
window->set_connection_dialog_open(true);
window->set_about_open(true);
}
window->set_remote_id_input("987654321");
window->invoke_reset_remote_id();
assert(std::string(window->get_remote_id_input()).empty());
@@ -62,7 +142,37 @@ int main() {
stream->set_tabs(
std::make_shared<slint::VectorModel<crossdesk::ui::StreamTab>>(
std::vector{tab}));
stream->set_custom_titlebar(true);
stream->set_window_maximized(true);
assert(stream->get_tabs()->row_count() == 1);
assert(stream->get_custom_titlebar());
assert(stream->get_window_maximized());
if (const char *snapshot_path =
std::getenv("CROSSDESK_STREAM_UI_SNAPSHOT")) {
stream->set_window_maximized(false);
stream->show();
const bool snapshot_written =
WriteWindowSnapshot(stream->window(), snapshot_path);
stream->hide();
stream->set_window_maximized(true);
if (!snapshot_written) {
return 4;
}
}
if (const char *snapshot_path =
std::getenv("CROSSDESK_STREAM_INACTIVE_UI_SNAPSHOT")) {
stream->set_window_maximized(false);
stream->set_window_active(false);
stream->show();
const bool snapshot_written =
WriteWindowSnapshot(stream->window(), snapshot_path);
stream->hide();
stream->set_window_active(true);
stream->set_window_maximized(true);
if (!snapshot_written) {
return 5;
}
}
bool tab_reordered = false;
stream->on_reorder_tab([&](int index, float x, float width) {
@@ -71,6 +181,32 @@ int main() {
stream->invoke_reorder_tab(0, 120.0f, 110.0f);
assert(tab_reordered);
bool maximize_requested = false;
stream->on_toggle_maximize_stream_window(
[&] { maximize_requested = true; });
stream->invoke_toggle_maximize_stream_window();
assert(maximize_requested);
bool keyboard_focus_changed = false;
bool keyboard_input_received = false;
stream->on_keyboard_focus_changed(
[&](bool focused) { keyboard_focus_changed = focused; });
stream->on_key_input(
[&](slint::SharedString text, bool pressed, bool, bool, bool, bool) {
keyboard_input_received = std::string(text) == "a" && pressed;
});
stream->window().set_size(
slint::LogicalSize(slint::Size<float>{1280.0f, 720.0f}));
stream->window().dispatch_pointer_press_event(
slint::LogicalPosition(slint::Point<float>{640.0f, 360.0f}),
slint::PointerEventButton::Left);
stream->window().dispatch_pointer_release_event(
slint::LogicalPosition(slint::Point<float>{640.0f, 360.0f}),
slint::PointerEventButton::Left);
stream->window().dispatch_key_press_event("a");
assert(keyboard_focus_changed);
assert(keyboard_input_received);
crossdesk::ui::FileTransferEntry transfer;
transfer.name = "archive.zip";
transfer.status = "Sending";
+120
View File
@@ -0,0 +1,120 @@
#include <filesystem>
#include <fstream>
#include <iostream>
#include <sstream>
#include <string>
namespace {
std::filesystem::path FindRepoRoot() {
std::filesystem::path current = std::filesystem::current_path();
while (!current.empty()) {
if (std::filesystem::exists(current / "xmake.lua") &&
std::filesystem::exists(
current / "submodules/minirtc/src/transport/ice_transport_controller.cpp")) {
return current;
}
current = current.parent_path();
}
return {};
}
std::string ReadFile(const std::filesystem::path& path) {
std::ifstream file(path, std::ios::binary);
if (!file) {
return {};
}
std::ostringstream stream;
stream << file.rdbuf();
return stream.str();
}
std::string ExtractSection(const std::string& source, const std::string& begin,
const std::string& end) {
const size_t begin_pos = source.find(begin);
if (begin_pos == std::string::npos) {
return {};
}
const size_t end_pos = source.find(end, begin_pos + begin.size());
if (end_pos == std::string::npos) {
return {};
}
return source.substr(begin_pos, end_pos - begin_pos);
}
bool ExpectContains(const char* name, const std::string& value,
const std::string& expected) {
if (value.find(expected) != std::string::npos) {
return true;
}
std::cerr << name << " missing expected text: " << expected << "\n";
return false;
}
bool ExpectNotContains(const char* name, const std::string& value,
const std::string& unexpected) {
if (value.find(unexpected) == std::string::npos) {
return true;
}
std::cerr << name << " contains unsafe text: " << unexpected << "\n";
return false;
}
} // namespace
int main() {
const std::filesystem::path repo_root = FindRepoRoot();
if (repo_root.empty()) {
std::cerr << "failed to locate repository root\n";
return 1;
}
const std::string controller = ReadFile(
repo_root /
"submodules/minirtc/src/transport/ice_transport_controller.cpp");
const std::string destroy = ExtractSection(
controller, "void IceTransportController::Destroy()",
"uint32_t IceTransportController::AddVideoSendChannel");
const std::string send_video = ExtractSection(
controller, "int IceTransportController::SendVideo(",
"void IceTransportController::MaybeDegradeResolutionOnEncodeTime");
const std::string on_video_encoded = ExtractSection(
controller, "int IceTransportController::OnVideoEncoded(",
"int IceTransportController::SendAudio(");
bool ok = true;
ok &= ExpectContains("Destroy", destroy,
"std::unique_lock lock(stream_senders_mutex_);");
ok &= ExpectContains("Destroy", destroy,
"senders.swap(stream_senders_);");
ok &= ExpectContains("Destroy", destroy,
"codecs.push_back(std::move(context->codec));");
ok &= ExpectContains("Destroy", destroy, "senders.clear();");
ok &= ExpectContains("Destroy", destroy, "codecs.clear();");
ok &= ExpectNotContains("Destroy", destroy,
"std::shared_lock lock(stream_senders_mutex_);");
ok &= ExpectContains("SendVideo", send_video,
"std::weak_ptr<IceTransportController> weak_self");
ok &= ExpectContains("SendVideo", send_video,
"std::weak_ptr<StreamContext> weak_context");
ok &= ExpectContains(
"SendVideo", send_video,
"[weak_self, weak_context, channel_name, queue_delay_ms");
ok &= ExpectContains("SendVideo", send_video,
"return self->OnVideoEncoded(");
ok &= ExpectNotContains("SendVideo", send_video,
"[this, channel_name, context");
ok &= ExpectContains("OnVideoEncoded", on_video_encoded,
"if (!is_running_.load())");
ok &= ExpectContains("OnVideoEncoded", on_video_encoded,
"std::shared_lock lock(stream_senders_mutex_);");
ok &= ExpectContains("OnVideoEncoded", on_video_encoded,
"it->second != context");
return ok ? 0 : 1;
}
+35
View File
@@ -0,0 +1,35 @@
package("rust")
set_kind("toolchain")
set_homepage("https://rust-lang.org")
set_description("Rust is a general-purpose programming language emphasizing performance, type safety, and concurrency.")
add_versions("1.92.0", "")
add_deps("ca-certificates", {host = true, private = true})
add_deps("rustup", {host = true, private = true, system = false})
on_install(function(package)
import("core.tools.rustc.target_triple")
local host_target = assert(target_triple(package:plat(), package:arch()),
"failed to determine the Rust host target triple")
local version = package:version():shortstr()
local toolchain_name = version .. "-" .. host_target
local rustup_home = assert(os.getenv("RUSTUP_HOME"), "cannot find rustup home")
os.vrunv("rustup", {"install", "--no-self-update", toolchain_name})
os.vmv(path.join(rustup_home, "toolchains", toolchain_name, "*"), package:installdir())
-- Keep the Xmake-managed toolchain isolated from rustup after installation.
os.vrm(path.join(rustup_home, "toolchains", toolchain_name))
os.vrm(path.join(rustup_home, "update-hashes", toolchain_name))
package:addenv("RC", "bin/rustc" .. (is_host("windows") and ".exe" or ""))
package:mark_as_pathenv("RC")
end)
on_test(function(package)
os.vrun("cargo --version")
os.vrun("rustc --version")
end)
package_end()
+18 -1
View File
@@ -9,9 +9,13 @@ package("slint")
add_configs("shared", {description = "Build the Slint runtime as a shared library", default = true, type = "boolean", readonly = true})
add_deps("cmake")
add_deps("cmake~slint >=3.21 <4.0", {host = true, private = true, system = false})
add_deps("rust 1.92.0", {host = true, private = true, system = false})
on_load(function(package)
if package:is_plat("linux") then
package:add("deps", "fontconfig", {private = true, system = false})
end
package:add("includedirs", "include/slint")
if package:is_plat("windows") then
package:add("links", "slint_cpp.dll")
@@ -21,6 +25,13 @@ package("slint")
end)
on_install("windows", "linux", "macosx", function(package)
for _, dep in ipairs(package:orderdeps()) do
if dep:name() == "cmake" and dep:label() == "slint" then
os.addenv("PATH", dep:installdir("bin"))
import("core.cache.detectcache"):set2("find_program", "cmake", nil)
break
end
end
local configs = {
"-DSLINT_BUILD_TESTING=OFF",
"-DSLINT_BUILD_EXAMPLES=OFF",
@@ -38,6 +49,12 @@ package("slint")
"-DSLINT_STYLE=fluent",
"-DBUILD_SHARED_LIBS=ON"
}
if package:is_plat("windows") then
local rc = assert(package:build_getenv("mrc"),
"failed to find the Windows resource compiler")
rc = rc:gsub("\\", "/")
table.insert(configs, "-DCMAKE_RC_COMPILER=" .. rc)
end
import("package.tools.cmake").install(package, configs)
end)
+1 -1
View File
@@ -1 +1 @@
includes("cpp-httplib", "slint")
includes("cpp-httplib", "rust", "slint")
+1 -1
View File
@@ -51,7 +51,7 @@ function setup_options_and_dependencies()
add_defines("CROSSDESK_DEBUG")
end
add_requireconfs("*.python", {version = "3.12", override = true, configs = {pgo = false}})
add_requireconfs("**.python", {version = "3.12", override = true, configs = {pgo = false}})
add_requires("spdlog 1.14.1", {system = false})
add_requires("slint 1.17.1", {configs = {shared = true}})
add_requires("libsdl3 3.2.26", {configs = {shared = false}})
+3 -1
View File
@@ -64,10 +64,12 @@ function setup_platform_settings()
"/usr/local/include/pipewire-0.3",
"/usr/include/pipewire",
"/usr/local/include/pipewire",
"/opt/crossdesk-pipewire-sdk/include/pipewire-0.3",
"/usr/include/spa-0.2",
"/usr/local/include/spa-0.2",
"/usr/include/spa",
"/usr/local/include/spa"
"/usr/local/include/spa",
"/opt/crossdesk-pipewire-sdk/include/spa-0.2"
}, {system = true})
for _, include_dir in ipairs(collect_dbus_arch_include_dirs()) do
add_includedirs(include_dir, {system = true})
+37 -1
View File
@@ -3,6 +3,22 @@ function setup_targets()
includes("submodules", "thirdparty")
local function copy_slint_runtime(target)
if not target:is_plat("windows") then
return
end
local slint = target:pkg("slint")
if not slint and target:dep("gui") then
slint = target:dep("gui"):pkg("slint")
end
assert(slint, "the Slint package is required to copy its Windows runtime")
local runtime_dir = path.join(slint:installdir(), "lib")
local runtime_dll = path.join(runtime_dir, "slint_cpp.dll")
assert(os.isfile(runtime_dll), "Slint runtime not found: " .. runtime_dll)
os.cp(runtime_dll, target:targetdir())
end
local crossdesk_windows_resource = "scripts/windows/crossdesk.rc"
if is_config("CROSSDESK_PORTABLE", true) then
crossdesk_windows_resource = "scripts/windows/crossdesk_portable.rc"
@@ -57,6 +73,11 @@ function setup_targets()
set_default(false)
add_files("tests/connection_status_protocol_test.cpp")
target("video_callback_lifetime_test")
set_kind("binary")
set_default(false)
add_files("tests/video_callback_lifetime_test.cpp")
target("windows_manifest_resource_test")
set_kind("binary")
set_default(false)
@@ -87,10 +108,15 @@ function setup_targets()
set_kind("binary")
set_languages("c++20")
set_default(false)
if is_os("windows") then
add_cxxflags("/bigobj")
end
add_packages("slint")
add_includedirs("src/gui/assets/fonts")
add_rules("slint")
add_files("src/gui/ui/crossdesk_ui.slint")
add_files("tests/slint_ui_smoke_test.cpp")
after_build(copy_slint_runtime)
target("version_checker_test")
set_kind("binary")
@@ -171,6 +197,14 @@ function setup_targets()
"src/device_controller/keyboard/linux", {public = true})
end
if is_os("linux") then
target("linux_keyboard_x11_integration_test")
set_kind("binary")
set_default(false)
add_deps("device_controller")
add_files("tests/linux_keyboard_x11_integration_test.cpp")
end
target("thumbnail")
set_kind("object")
add_packages("libyuv", "openssl3")
@@ -240,6 +274,7 @@ function setup_targets()
"src/gui/features/settings/*.cpp", "src/gui/ui/crossdesk_ui.slint")
add_includedirs("src/gui", {public = true})
if is_os("windows") then
add_cxxflags("/bigobj")
add_files("src/gui/platform/tray/win_tray.cpp")
add_includedirs("src/service/windows", {public = true})
elseif is_os("macosx") then
@@ -280,7 +315,7 @@ function setup_targets()
add_links("Advapi32", "User32", "Wtsapi32", "Gdi32")
add_files("src/service/windows/session_helper_main.cpp")
add_files(crossdesk_windows_resource)
add_includedirs("src/service/windows", {public = true})
add_includedirs("src/common", "src/service/windows", {public = true})
end
target("crossdesk")
@@ -295,4 +330,5 @@ function setup_targets()
add_deps("wgc_plugin", "crossdesk_service", "crossdesk_session_helper")
add_files(crossdesk_windows_resource)
end
after_build(copy_slint_runtime)
end