Compare commits

..

2 Commits

Author SHA1 Message Date
Ferdinand Schober
42521a1997 only alert on actual error 2025-03-22 22:12:17 +01:00
Ferdinand Schober
6e66f47887 ask the user to accept unauthorized devices 2025-03-22 21:56:13 +01:00
58 changed files with 1649 additions and 2278 deletions

View File

@@ -1,46 +1,40 @@
name: Nix Binary Cache name: Binary Cache
on:
push:
branches: ["main"]
pull_request:
branches: ["main"]
workflow_dispatch:
on: [push, pull_request, workflow_dispatch]
jobs: jobs:
nix: nix:
strategy: strategy:
matrix: matrix:
os: os:
- ubuntu-latest - ubuntu-latest
- macos-15-intel - macos-13
- macos-latest - macos-14
name: "Build" name: "Build"
runs-on: ${{ matrix.os }} runs-on: ${{ matrix.os }}
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v4
with: with:
submodules: recursive submodules: recursive
# - uses: DeterminateSystems/nix-installer-action@main - uses: DeterminateSystems/nix-installer-action@main
# with: with:
# logger: pretty logger: pretty
# - uses: DeterminateSystems/magic-nix-cache-action@main - uses: DeterminateSystems/magic-nix-cache-action@main
- uses: cachix/install-nix-action@v31 - uses: cachix/cachix-action@v14
- uses: cachix/cachix-action@v16 with:
with: name: lan-mouse
name: lan-mouse authToken: '${{ secrets.CACHIX_AUTH_TOKEN }}'
authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}"
- name: Build lan-mouse (x86_64-linux) - name: Build lan-mouse (x86_64-linux)
if: matrix.os == 'ubuntu-latest' if: matrix.os == 'ubuntu-latest'
run: nix build --print-build-logs --show-trace .#packages.x86_64-linux.lan-mouse run: nix build --print-build-logs --show-trace .#packages.x86_64-linux.lan-mouse
- name: Build lan-mouse (x86_64-darwin) - name: Build lan-mouse (x86_64-darwin)
if: matrix.os == 'macos-15-intel' if: matrix.os == 'macos-13'
run: nix build --print-build-logs --show-trace .#packages.x86_64-darwin.lan-mouse run: nix build --print-build-logs --show-trace .#packages.x86_64-darwin.lan-mouse
- name: Build lan-mouse (aarch64-darwin)
if: matrix.os == 'macos-14'
run: nix build --print-build-logs --show-trace .#packages.aarch64-darwin.lan-mouse
- name: Build lan-mouse (aarch64-darwin)
if: matrix.os == 'macos-latest'
run: nix build --print-build-logs --show-trace .#packages.aarch64-darwin.lan-mouse

View File

@@ -11,7 +11,7 @@ env:
jobs: jobs:
linux-release-build: linux-release-build:
runs-on: ubuntu-22.04 runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- name: install dependencies - name: install dependencies
@@ -80,7 +80,7 @@ jobs:
path: lan-mouse-windows.zip path: lan-mouse-windows.zip
macos-release-build: macos-release-build:
runs-on: macos-15-intel runs-on: macos-13
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- name: install dependencies - name: install dependencies
@@ -94,9 +94,7 @@ jobs:
- name: Install cargo bundle - name: Install cargo bundle
run: cargo install cargo-bundle run: cargo install cargo-bundle
- name: Bundle - name: Bundle
run: | run: cargo bundle --release
cargo bundle --release
scripts/copy-macos-dylib.sh "target/release/bundle/osx/Lan Mouse.app/Contents/MacOS/lan-mouse"
- name: Zip bundle - name: Zip bundle
run: | run: |
cd target/release/bundle/osx cd target/release/bundle/osx
@@ -122,9 +120,7 @@ jobs:
- name: Install cargo bundle - name: Install cargo bundle
run: cargo install cargo-bundle run: cargo install cargo-bundle
- name: Bundle - name: Bundle
run: | run: cargo bundle --release
cargo bundle --release
scripts/copy-macos-dylib.sh "target/release/bundle/osx/Lan Mouse.app/Contents/MacOS/lan-mouse"
- name: Zip bundle - name: Zip bundle
run: | run: |
cd target/release/bundle/osx cd target/release/bundle/osx

View File

@@ -10,87 +10,145 @@ env:
CARGO_TERM_COLOR: always CARGO_TERM_COLOR: always
jobs: jobs:
fmt: build-linux:
name: Formatting
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- name: cargo fmt - name: install dependencies
run: cargo fmt --check run: |
sudo apt-get update
sudo apt-get install libx11-dev libxtst-dev
sudo apt-get install libadwaita-1-dev libgtk-4-dev
- name: Build
run: cargo build --verbose
- name: Run tests
run: cargo test --verbose
- name: Check Formatting
run: cargo fmt --check
- name: Clippy
run: cargo clippy --all-features --all-targets -- --deny warnings
- name: Upload build artifact
uses: actions/upload-artifact@v4
with:
name: lan-mouse
path: target/debug/lan-mouse
build-windows:
runs-on: windows-latest
ci:
name: ${{ matrix.job }} ${{ matrix.os }}
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os:
- ubuntu-latest
- windows-latest
- macos-latest
- macos-15-intel
job:
- build
- check
- clippy
- test
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- uses: Swatinem/rust-cache@v2 - uses: actions/setup-python@v5
- name: Install Linux deps with:
if: runner.os == 'Linux' python-version: '3.11'
run: | # needed for cache restore
sudo apt-get update - name: create gtk dir
sudo apt-get install libx11-dev libxtst-dev libadwaita-1-dev libgtk-4-dev run: mkdir C:\gtk-build\gtk\x64\release
- name: Install macOS dependencies - uses: actions/cache@v3
if: runner.os == 'macOS' id: cache
run: brew install gtk4 libadwaita imagemagick with:
- name: Install Windows Dependencies - create gtk dir path: c:/gtk-build/gtk/x64/release/**
if: runner.os == 'Windows' key: gtk-windows-build
run: mkdir C:\gtk-build\gtk\x64\release restore-keys: gtk-windows-build
- name: Install Windows Dependencies - install gtk from cache - name: Update path
uses: actions/cache@v3 run: |
if: runner.os == 'Windows' echo "PKG_CONFIG=C:\gtk-build\gtk\x64\release\bin\pkgconf.exe" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append
id: cache echo "C:\pkg-config-lite-0.28-1\bin" | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append
with: echo "C:\gtk-build\gtk\x64\release\bin" | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append
path: c:/gtk-build/gtk/x64/release/** echo $env:GITHUB_PATH
key: gtk-windows-build echo $env:PATH
restore-keys: gtk-windows-build - name: Install dependencies
- name: Install Windows Dependencies - update PATH if: steps.cache.outputs.cache-hit != 'true'
if: runner.os == 'Windows' run: |
run: | # choco install msys2
echo "PKG_CONFIG=C:\gtk-build\gtk\x64\release\bin\pkgconf.exe" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append # choco install visualstudio2022-workload-vctools
echo "C:\pkg-config-lite-0.28-1\bin" | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append # choco install pkgconfiglite
echo "C:\gtk-build\gtk\x64\release\bin" | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append py -m venv .venv
echo $env:GITHUB_PATH .venv\Scripts\activate.ps1
echo $env:PATH py -m pip install gvsbuild
- name: Install Windows dependencies - build gtk # see https://github.com/wingtk/gvsbuild/pull/1004
if: runner.os == 'Windows' && steps.cache.outputs.cache-hit != 'true' Move-Item "C:\Program Files\Git\usr\bin" "C:\Program Files\Git\usr\notbin"
run: | Move-Item "C:\Program Files\Git\bin" "C:\Program Files\Git\notbin"
# choco install msys2 gvsbuild build --msys-dir=C:\msys64 gtk4 libadwaita librsvg
# choco install visualstudio2022-workload-vctools Move-Item "C:\Program Files\Git\usr\notbin" "C:\Program Files\Git\usr\bin"
# choco install pkgconfiglite Move-Item "C:\Program Files\Git\notbin" "C:\Program Files\Git\bin"
py -m venv .venv - name: Build
.venv\Scripts\activate.ps1 run: cargo build --verbose
py -m pip install gvsbuild - name: Run tests
gvsbuild build --msys-dir=C:\msys64 gtk4 libadwaita librsvg run: cargo test --verbose
- name: cargo build - name: Check Formatting
if: matrix.job == 'build' run: cargo fmt --check
run: cargo build - name: Clippy
run: cargo clippy --all-features --all-targets -- --deny warnings
- name: Copy Gtk Dlls
run: Get-Childitem -Path "C:\\gtk-build\\gtk\\x64\\release\\bin\\*.dll" -File -Recurse | Copy-Item -Destination "target\\debug"
- name: Upload build artifact
uses: actions/upload-artifact@v4
with:
name: lan-mouse-windows
path: |
target/debug/lan-mouse.exe
target/debug/*.dll
- name: cargo check build-macos:
if: matrix.job == 'check' runs-on: macos-13
run: cargo check --workspace --all-targets --all-features steps:
- uses: actions/checkout@v4
- name: install dependencies
run: brew install gtk4 libadwaita imagemagick
- name: Build
run: cargo build --verbose
- name: Run tests
run: cargo test --verbose
- name: Check Formatting
run: cargo fmt --check
- name: Clippy
run: cargo clippy --all-features --all-targets -- --deny warnings
- name: Make icns
run: scripts/makeicns.sh
- name: Install cargo bundle
run: cargo install cargo-bundle
- name: Bundle
run: cargo bundle
- name: Zip bundle
run: |
cd target/debug/bundle/osx
zip -r "Lan Mouse macOS (Intel).zip" "Lan Mouse.app"
- name: Upload build artifact
uses: actions/upload-artifact@v4
with:
name: Lan Mouse macOS (Intel)
path: target/debug/bundle/osx/Lan Mouse macOS (Intel).zip
- name: cargo test build-macos-aarch64:
if: matrix.job == 'test' runs-on: macos-14
run: cargo test --workspace --all-features steps:
- uses: actions/checkout@v4
- name: cargo clippy - name: install dependencies
if: matrix.job == 'clippy' run: brew install gtk4 libadwaita imagemagick
run: cargo clippy --workspace --all-targets --all-features -- -D warnings - name: Build
run: cargo build --verbose
- uses: clechasseur/rs-clippy-check@v4 - name: Run tests
if: matrix.job == 'clippy' run: cargo test --verbose
with: - name: Check Formatting
args: --workspace --all-targets --all-features run: cargo fmt --check
- name: Clippy
run: cargo clippy --all-features --all-targets -- --deny warnings
- name: Make icns
run: scripts/makeicns.sh
- name: Install cargo bundle
run: cargo install cargo-bundle
- name: Bundle
run: cargo bundle
- name: Zip bundle
run: |
cd target/debug/bundle/osx
zip -r "Lan Mouse macOS (ARM).zip" "Lan Mouse.app"
- name: Upload build artifact
uses: actions/upload-artifact@v4
with:
name: Lan Mouse macOS (ARM)
path: target/debug/bundle/osx/Lan Mouse macOS (ARM).zip

View File

@@ -7,7 +7,7 @@ on:
jobs: jobs:
linux-release-build: linux-release-build:
runs-on: ubuntu-22.04 runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- name: install dependencies - name: install dependencies
@@ -76,7 +76,7 @@ jobs:
path: lan-mouse-windows.zip path: lan-mouse-windows.zip
macos-release-build: macos-release-build:
runs-on: macos-15-intel runs-on: macos-13
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- name: install dependencies - name: install dependencies
@@ -90,9 +90,7 @@ jobs:
- name: Install cargo bundle - name: Install cargo bundle
run: cargo install cargo-bundle run: cargo install cargo-bundle
- name: Bundle - name: Bundle
run: | run: cargo bundle --release
cargo bundle --release
scripts/copy-macos-dylib.sh "target/release/bundle/osx/Lan Mouse.app/Contents/MacOS/lan-mouse"
- name: Zip bundle - name: Zip bundle
run: | run: |
cd target/release/bundle/osx cd target/release/bundle/osx
@@ -118,9 +116,7 @@ jobs:
- name: Install cargo bundle - name: Install cargo bundle
run: cargo install cargo-bundle run: cargo install cargo-bundle
- name: Bundle - name: Bundle
run: | run: cargo bundle --release
cargo bundle --release
scripts/copy-macos-dylib.sh "target/release/bundle/osx/Lan Mouse.app/Contents/MacOS/lan-mouse"
- name: Zip bundle - name: Zip bundle
run: | run: |
cd target/release/bundle/osx cd target/release/bundle/osx

4
.gitignore vendored
View File

@@ -8,7 +8,3 @@ result
*.pem *.pem
*.csr *.csr
extfile.conf extfile.conf
# flatpak files
.flatpak-builder
repo

View File

@@ -1,4 +0,0 @@
style_edition = "2024"
max_width = 100
tab_spaces = 4

2062
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -24,7 +24,7 @@ strip = true
panic = "abort" panic = "abort"
[build-dependencies] [build-dependencies]
shadow-rs = "1.2.0" shadow-rs = "0.38.0"
[dependencies] [dependencies]
input-event = { path = "input-event", version = "0.3.0" } input-event = { path = "input-event", version = "0.3.0" }
@@ -34,11 +34,10 @@ lan-mouse-cli = { path = "lan-mouse-cli", version = "0.2.0" }
lan-mouse-gtk = { path = "lan-mouse-gtk", version = "0.2.0", optional = true } lan-mouse-gtk = { path = "lan-mouse-gtk", version = "0.2.0", optional = true }
lan-mouse-ipc = { path = "lan-mouse-ipc", version = "0.2.0" } lan-mouse-ipc = { path = "lan-mouse-ipc", version = "0.2.0" }
lan-mouse-proto = { path = "lan-mouse-proto", version = "0.2.0" } lan-mouse-proto = { path = "lan-mouse-proto", version = "0.2.0" }
shadow-rs = { version = "1.2.0", features = ["metadata"] } shadow-rs = { version = "0.38.0", features = ["metadata"] }
hickory-resolver = "0.25.2" hickory-resolver = "0.24.1"
toml = "0.8" toml = "0.8"
toml_edit = { version = "0.22", features = ["serde"] }
serde = { version = "1.0", features = ["derive"] } serde = { version = "1.0", features = ["derive"] }
log = "0.4.20" log = "0.4.20"
env_logger = "0.11.3" env_logger = "0.11.3"
@@ -59,8 +58,8 @@ slab = "0.4.9"
thiserror = "2.0.0" thiserror = "2.0.0"
tokio-util = "0.7.11" tokio-util = "0.7.11"
local-channel = "0.1.5" local-channel = "0.1.5"
webrtc-dtls = { version = "0.12.0", features = ["pem"] } webrtc-dtls = { version = "0.10.0", features = ["pem"] }
webrtc-util = "0.11.0" webrtc-util = "0.9.0"
rustls = { version = "0.23.12", default-features = false, features = [ rustls = { version = "0.23.12", default-features = false, features = [
"std", "std",
"ring", "ring",

View File

@@ -81,37 +81,15 @@ paru -S lan-mouse-git
- flake: [README.md](./nix/README.md) - flake: [README.md](./nix/README.md)
</details> </details>
<details>
<summary>Fedora</summary>
You can install Lan Mouse from the [Terra Repository](https://terra.fyralabs.com).
After enabling Terra:
```sh
dnf install lan-mouse
```
</details>
<details>
<summary>MacOS</summary>
- Download the package for your Mac (Intel or ARM) from the releases page
- Unzip it
- Remove the quarantine with `xattr -rd com.apple.quarantine "Lan Mouse.app"`
- Launch the app
- Grant accessibility permissions in System Preferences
</details>
<details> <details>
<summary>Manual Installation</summary> <summary>Manual Installation</summary>
First make sure to [install the necessary dependencies](#installing-dependencies-for-development--compiling-from-source). First make sure to [install the necessary dependencies](#installing-dependencies).
Precompiled release binaries for Windows, MacOS and Linux are available in the [releases section](https://github.com/feschber/lan-mouse/releases). Precompiled release binaries for Windows, MacOS and Linux are available in the [releases section](https://github.com/feschber/lan-mouse/releases).
For Windows, the depenedencies are included in the .zip file, for other operating systems see [Installing Dependencies](#installing-dependencies-for-development--compiling-from-source). For Windows, the depenedencies are included in the .zip file, for other operating systems see [Installing Dependencies](#installing-dependencies).
Alternatively, the `lan-mouse` binary can be compiled from source (see below). Alternatively, the `lan-mouse` binary can be compiled from source (see below).
@@ -183,15 +161,7 @@ For a detailed list of available features, checkout the [Cargo.toml](./Cargo.tom
<summary>MacOS</summary> <summary>MacOS</summary>
```sh ```sh
# Install dependencies brew install libadwaita pkg-config
brew install libadwaita pkg-config imagemagick
cargo install cargo-bundle
# Create the macOS icon file
scripts/makeicns.sh
# Create the .app bundle
cargo bundle
# Copy all dynamic libraries into the bundle, and update the bundle to find them there
scripts/copy-macos-dylib.sh
``` ```
</details> </details>
@@ -321,9 +291,6 @@ To do so, use the `daemon` subcommand:
```sh ```sh
lan-mouse daemon lan-mouse daemon
``` ```
</details>
## Systemd Service
In order to start lan-mouse with a graphical session automatically, In order to start lan-mouse with a graphical session automatically,
the [systemd-service](service/lan-mouse.service) can be used: the [systemd-service](service/lan-mouse.service) can be used:
@@ -335,9 +302,7 @@ cp service/lan-mouse.service ~/.config/systemd/user
systemctl --user daemon-reload systemctl --user daemon-reload
systemctl --user enable --now lan-mouse.service systemctl --user enable --now lan-mouse.service
``` ```
> [!Important] </details>
> Make sure to point `ExecStart=/usr/bin/lan-mouse daemon` to the actual `lan-mouse` binary (in case it is not under `/usr/bin`, e.g. when installed manually.
## Configuration ## Configuration
To automatically load clients on startup, the file `$XDG_CONFIG_HOME/lan-mouse/config.toml` is parsed. To automatically load clients on startup, the file `$XDG_CONFIG_HOME/lan-mouse/config.toml` is parsed.

View File

@@ -1,50 +0,0 @@
# yaml-language-server: $schema=https://raw.githubusercontent.com/flatpak/flatpak-builder/refs/heads/main/data/flatpak-manifest.schema.json
app-id: de.feschber.LanMouse
runtime: org.gnome.Platform
runtime-version: "48"
sdk: org.gnome.Sdk
sdk-extensions:
- org.freedesktop.Sdk.Extension.rust-stable
- org.freedesktop.Sdk.Extension.llvm20
command: /app/bin/lan-mouse
build-options:
append-path: "/usr/lib/sdk/rust-stable/bin:/usr/lib/sdk/llvm20/bin"
env:
"CARGO_TARGET_X86_64_UNKNOWN_LINUX_GNU_LINKER": "clang"
"CARGO_TARGET_X86_64_UNKNOWN_LINUX_GNU_RUSTFLAGS": "-C link-arg=-fuse-ld=/usr/lib/sdk/rust-stable/bin/mold"
"CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_LINKER": "clang"
"CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_RUSTFLAGS": "-C link-arg=-fuse-ld=/usr/lib/sdk/rust-stable/bin/mold"
build-args:
"--share=network"
prepend-ld-library-path:
"/usr/lib/sdk/llvm19/lib"
finish-args:
- "--socket=wayland"
- "--socket=fallback-x11"
- "--device=dri"
- "--socket=session-bus"
- "--share=network"
- "--filesystem=xdg-config"
- "--env=RUST_BACKTRACE=1"
- "--env=RUST_LOG=lan-mouse=debug"
- "--env=GTK_PATH=/app/lib/gtk-4.0"
modules:
- name: lan-mouse
buildsystem: simple
build-options:
build-args:
- "--share=network"
append-path: /usr/lib/sdk/rust-stable/bin
env:
CARGO_HOME: /run/build/lan-mouse/cargo
build-commands:
- cargo fetch --manifest-path Cargo.toml --verbose
- cargo build
- install -Dm0755 target/debug/lan-mouse /app/bin/lan-mouse
- install -Dm0644 lan-mouse-gtk/resources/de.feschber.LanMouse.svg ${FLATPAK_DEST}/share/icons/hicolor/scalable/apps/${FLATPAK_ID}.svg
- install -Dm0644 de.feschber.LanMouse.desktop ${FLATPAK_DEST}/share/applications/${FLATPAK_ID}.desktop
sources:
- type: dir
path: ..

14
de.feschber.LanMouse.yml Normal file
View File

@@ -0,0 +1,14 @@
app-id: de.feschber.LanMouse
runtime: org.freedesktop.Platform
runtime-version: '22.08'
sdk: org.freedesktop.Sdk
command: target/release/lan-mouse
modules:
- name: hello
buildsystem: simple
build-commands:
- cargo build --release
- install -D lan-mouse /app/bin/lan-mouse
sources:
- type: file
path: target/release/lan-mouse

1
dylibs/.gitignore vendored
View File

@@ -1 +0,0 @@
*

12
flake.lock generated
View File

@@ -2,11 +2,11 @@
"nodes": { "nodes": {
"nixpkgs": { "nixpkgs": {
"locked": { "locked": {
"lastModified": 1752687322, "lastModified": 1740560979,
"narHash": "sha256-RKwfXA4OZROjBTQAl9WOZQFm7L8Bo93FQwSJpAiSRvo=", "narHash": "sha256-Vr3Qi346M+8CjedtbyUevIGDZW8LcA1fTG0ugPY/Hic=",
"owner": "nixos", "owner": "nixos",
"repo": "nixpkgs", "repo": "nixpkgs",
"rev": "6e987485eb2c77e5dcc5af4e3c70843711ef9251", "rev": "5135c59491985879812717f4c9fea69604e7f26f",
"type": "github" "type": "github"
}, },
"original": { "original": {
@@ -29,11 +29,11 @@
] ]
}, },
"locked": { "locked": {
"lastModified": 1752806774, "lastModified": 1740623427,
"narHash": "sha256-4cHeoR2roN7d/3J6gT+l6o7J2hTrBIUiCwVdDNMeXzE=", "narHash": "sha256-3SdPQrZoa4odlScFDUHd4CUPQ/R1gtH4Mq9u8CBiK8M=",
"owner": "oxalica", "owner": "oxalica",
"repo": "rust-overlay", "repo": "rust-overlay",
"rev": "3c90219b3ba1c9790c45a078eae121de48a39c55", "rev": "d342e8b5fd88421ff982f383c853f0fc78a847ab",
"type": "github" "type": "github"
}, },
"original": { "original": {

View File

@@ -23,7 +23,6 @@ tokio = { version = "1.32.0", features = [
"rt", "rt",
"sync", "sync",
"signal", "signal",
"time",
] } ] }
once_cell = "1.19.0" once_cell = "1.19.0"
async-trait = "0.1.81" async-trait = "0.1.81"
@@ -41,21 +40,21 @@ wayland-protocols-wlr = { version = "0.3.1", features = [
"client", "client",
], optional = true } ], optional = true }
x11 = { version = "2.21.0", features = ["xlib", "xtest"], optional = true } x11 = { version = "2.21.0", features = ["xlib", "xtest"], optional = true }
ashpd = { version = "0.11.0", default-features = false, features = [ ashpd = { version = "0.10", default-features = false, features = [
"tokio", "tokio",
], optional = true } ], optional = true }
reis = { version = "0.5.0", features = ["tokio"], optional = true } reis = { version = "0.4", features = ["tokio"], optional = true }
[target.'cfg(target_os="macos")'.dependencies] [target.'cfg(target_os="macos")'.dependencies]
core-graphics = { version = "0.25.0", features = ["highsierra"] } core-graphics = { version = "0.24.0", features = ["highsierra"] }
core-foundation = "0.10.0" core-foundation = "0.10.0"
core-foundation-sys = "0.8.6" core-foundation-sys = "0.8.6"
libc = "0.2.155" libc = "0.2.155"
keycode = "1.0.0" keycode = "0.4.0"
bitflags = "2.6.0" bitflags = "2.6.0"
[target.'cfg(windows)'.dependencies] [target.'cfg(windows)'.dependencies]
windows = { version = "0.61.2", features = [ windows = { version = "0.58.0", features = [
"Win32_System_LibraryLoader", "Win32_System_LibraryLoader",
"Win32_System_Threading", "Win32_System_Threading",
"Win32_Foundation", "Win32_Foundation",

View File

@@ -1,6 +1,6 @@
use std::f64::consts::PI; use std::f64::consts::PI;
use std::pin::Pin; use std::pin::Pin;
use std::task::{Context, Poll, ready}; use std::task::{ready, Context, Poll};
use std::time::Duration; use std::time::Duration;
use async_trait::async_trait; use async_trait::async_trait;

View File

@@ -12,9 +12,9 @@ pub enum InputCaptureError {
use std::io; use std::io;
#[cfg(all(unix, feature = "layer_shell", not(target_os = "macos")))] #[cfg(all(unix, feature = "layer_shell", not(target_os = "macos")))]
use wayland_client::{ use wayland_client::{
ConnectError, DispatchError,
backend::WaylandError, backend::WaylandError,
globals::{BindError, GlobalError}, globals::{BindError, GlobalError},
ConnectError, DispatchError,
}; };
#[cfg(all(unix, feature = "libei", not(target_os = "macos")))] #[cfg(all(unix, feature = "libei", not(target_os = "macos")))]

View File

@@ -7,7 +7,7 @@ use std::{
io::{self, ErrorKind}, io::{self, ErrorKind},
os::fd::{AsFd, RawFd}, os::fd::{AsFd, RawFd},
pin::Pin, pin::Pin,
task::{Context, Poll, ready}, task::{ready, Context, Poll},
}; };
use tokio::io::unix::AsyncFd; use tokio::io::unix::AsyncFd;
@@ -45,10 +45,9 @@ use wayland_protocols_wlr::layer_shell::v1::client::{
}; };
use wayland_client::{ use wayland_client::{
Connection, Dispatch, DispatchError, EventQueue, QueueHandle, WEnum,
backend::{ReadEventsGuard, WaylandError}, backend::{ReadEventsGuard, WaylandError},
delegate_noop, delegate_noop,
globals::{Global, GlobalList, GlobalListContents, registry_queue_init}, globals::{registry_queue_init, Global, GlobalList, GlobalListContents},
protocol::{ protocol::{
wl_buffer, wl_compositor, wl_buffer, wl_compositor,
wl_keyboard::{self, WlKeyboard}, wl_keyboard::{self, WlKeyboard},
@@ -59,6 +58,7 @@ use wayland_client::{
wl_seat, wl_shm, wl_shm_pool, wl_seat, wl_shm, wl_shm_pool,
wl_surface::WlSurface, wl_surface::WlSurface,
}, },
Connection, Dispatch, DispatchError, EventQueue, QueueHandle, WEnum,
}; };
use input_event::{Event, KeyboardEvent, PointerEvent}; use input_event::{Event, KeyboardEvent, PointerEvent};
@@ -66,8 +66,8 @@ use input_event::{Event, KeyboardEvent, PointerEvent};
use crate::{CaptureError, CaptureEvent}; use crate::{CaptureError, CaptureEvent};
use super::{ use super::{
Capture, Position,
error::{LayerShellCaptureCreationError, WaylandBindError}, error::{LayerShellCaptureCreationError, WaylandBindError},
Capture, Position,
}; };
struct Globals { struct Globals {
@@ -535,7 +535,7 @@ impl State {
fn update_windows(&mut self) { fn update_windows(&mut self) {
log::info!("active outputs: "); log::info!("active outputs: ");
for output in self.outputs.iter().filter(|o| o.info.is_some()) { for output in self.outputs.iter().filter(|o| o.info.is_some()) {
log::info!(" * {output}"); log::info!(" * {}", output);
} }
self.active_windows.clear(); self.active_windows.clear();
@@ -582,17 +582,17 @@ impl Inner {
match self.queue.dispatch_pending(&mut self.state) { match self.queue.dispatch_pending(&mut self.state) {
Ok(_) => {} Ok(_) => {}
Err(DispatchError::Backend(WaylandError::Io(e))) => { Err(DispatchError::Backend(WaylandError::Io(e))) => {
log::error!("Wayland Error: {e}"); log::error!("Wayland Error: {}", e);
} }
Err(DispatchError::Backend(e)) => { Err(DispatchError::Backend(e)) => {
panic!("backend error: {e}"); panic!("backend error: {}", e);
} }
Err(DispatchError::BadMessage { Err(DispatchError::BadMessage {
sender_id, sender_id,
interface, interface,
opcode, opcode,
}) => { }) => {
panic!("bad message {sender_id}, {interface} , {opcode}"); panic!("bad message {}, {} , {}", sender_id, interface, opcode);
} }
} }
} }
@@ -813,7 +813,7 @@ impl Dispatch<WlPointer, ()> for State {
})), })),
)); ));
} }
wl_pointer::Event::Frame => { wl_pointer::Event::Frame {} => {
// TODO properly handle frame events // TODO properly handle frame events
// we simply insert a frame event on the client side // we simply insert a frame event on the client side
// after each event for now // after each event for now
@@ -974,7 +974,7 @@ impl Dispatch<ZxdgOutputV1, u32> for State {
.find(|o| o.global.name == *name) .find(|o| o.global.name == *name)
.expect("output"); .expect("output");
log::debug!("xdg_output {name} - {event:?}"); log::debug!("xdg_output {name} - {:?}", event);
match event { match event {
zxdg_output_v1::Event::LogicalPosition { x, y } => { zxdg_output_v1::Event::LogicalPosition { x, y } => {
output.pending_info.position = (x, y); output.pending_info.position = (x, y);
@@ -1010,7 +1010,7 @@ impl Dispatch<WlOutput, u32> for State {
_conn: &Connection, _conn: &Connection,
_qhandle: &QueueHandle<Self>, _qhandle: &QueueHandle<Self>,
) { ) {
log::debug!("wl_output {name} - {event:?}"); log::debug!("wl_output {name} - {:?}", event);
if let wl_output::Event::Done = event { if let wl_output::Event::Done = event {
state.update_output_info(*name); state.update_output_info(*name);
} }

View File

@@ -2,14 +2,14 @@ use std::{
collections::{HashMap, HashSet, VecDeque}, collections::{HashMap, HashSet, VecDeque},
fmt::Display, fmt::Display,
mem::swap, mem::swap,
task::{Poll, ready}, task::{ready, Poll},
}; };
use async_trait::async_trait; use async_trait::async_trait;
use futures::StreamExt; use futures::StreamExt;
use futures_core::Stream; use futures_core::Stream;
use input_event::{Event, KeyboardEvent, scancode}; use input_event::{scancode, Event, KeyboardEvent};
pub use error::{CaptureCreationError, CaptureError, InputCaptureError}; pub use error::{CaptureCreationError, CaptureError, InputCaptureError};
@@ -79,7 +79,7 @@ impl Display for Position {
Position::Top => "top", Position::Top => "top",
Position::Bottom => "bottom", Position::Bottom => "bottom",
}; };
write!(f, "{pos}") write!(f, "{}", pos)
} }
} }

View File

@@ -1,10 +1,10 @@
use ashpd::{ use ashpd::{
desktop::{ desktop::{
Session,
input_capture::{ input_capture::{
Activated, ActivatedBarrier, Barrier, BarrierID, Capabilities, InputCapture, Region, Activated, ActivatedBarrier, Barrier, BarrierID, Capabilities, InputCapture, Region,
Zones, Zones,
}, },
Session,
}, },
enumflags2::BitFlags, enumflags2::BitFlags,
}; };
@@ -28,8 +28,8 @@ use std::{
}; };
use tokio::{ use tokio::{
sync::{ sync::{
Notify,
mpsc::{self, Receiver, Sender}, mpsc::{self, Receiver, Sender},
Notify,
}, },
task::JoinHandle, task::JoinHandle,
}; };
@@ -42,8 +42,8 @@ use input_event::Event;
use crate::CaptureEvent; use crate::CaptureEvent;
use super::{ use super::{
Capture as LanMouseInputCapture, Position,
error::{CaptureError, LibeiCaptureCreationError}, error::{CaptureError, LibeiCaptureCreationError},
Capture as LanMouseInputCapture, Position,
}; };
/* there is a bug in xdg-remote-desktop-portal-gnome / mutter that /* there is a bug in xdg-remote-desktop-portal-gnome / mutter that
@@ -587,13 +587,9 @@ impl LanMouseInputCapture for LibeiInputCapture<'_> {
self.cancellation_token.cancel(); self.cancellation_token.cancel();
let task = &mut self.capture_task; let task = &mut self.capture_task;
log::debug!("waiting for capture to terminate..."); log::debug!("waiting for capture to terminate...");
let res = if !task.is_finished() { let res = task.await.expect("libei task panic");
task.await.expect("libei task panic")
} else {
Ok(())
};
self.terminated = true;
log::debug!("done!"); log::debug!("done!");
self.terminated = true;
res res
} }
} }

View File

@@ -1,42 +1,31 @@
use super::{Capture, CaptureError, CaptureEvent, Position, error::MacosCaptureCreationError}; use super::{error::MacosCaptureCreationError, Capture, CaptureError, CaptureEvent, Position};
use async_trait::async_trait; use async_trait::async_trait;
use bitflags::bitflags; use bitflags::bitflags;
use core_foundation::{ use core_foundation::base::{kCFAllocatorDefault, CFRelease};
base::{CFRelease, kCFAllocatorDefault}, use core_foundation::date::CFTimeInterval;
date::CFTimeInterval, use core_foundation::number::{kCFBooleanTrue, CFBooleanRef};
number::{CFBooleanRef, kCFBooleanTrue}, use core_foundation::runloop::{kCFRunLoopCommonModes, CFRunLoop, CFRunLoopSource};
runloop::{CFRunLoop, CFRunLoopSource, kCFRunLoopCommonModes}, use core_foundation::string::{kCFStringEncodingUTF8, CFStringCreateWithCString, CFStringRef};
string::{CFStringCreateWithCString, CFStringRef, kCFStringEncodingUTF8}, use core_graphics::base::{kCGErrorSuccess, CGError};
}; use core_graphics::display::{CGDisplay, CGPoint};
use core_graphics::{ use core_graphics::event::{
base::{CGError, kCGErrorSuccess}, CGEvent, CGEventFlags, CGEventTap, CGEventTapLocation, CGEventTapOptions, CGEventTapPlacement,
display::{CGDisplay, CGPoint}, CGEventTapProxy, CGEventType, EventField,
event::{
CGEvent, CGEventFlags, CGEventTap, CGEventTapLocation, CGEventTapOptions,
CGEventTapPlacement, CGEventTapProxy, CGEventType, CallbackResult, EventField,
},
event_source::{CGEventSource, CGEventSourceStateID},
}; };
use core_graphics::event_source::{CGEventSource, CGEventSourceStateID};
use futures_core::Stream; use futures_core::Stream;
use input_event::{ use input_event::{Event, KeyboardEvent, PointerEvent, BTN_LEFT, BTN_MIDDLE, BTN_RIGHT};
BTN_BACK, BTN_FORWARD, BTN_LEFT, BTN_MIDDLE, BTN_RIGHT, Event, KeyboardEvent, PointerEvent,
};
use keycode::{KeyMap, KeyMapping}; use keycode::{KeyMap, KeyMapping};
use libc::c_void; use libc::c_void;
use once_cell::unsync::Lazy; use once_cell::unsync::Lazy;
use std::{ use std::collections::HashSet;
collections::HashSet, use std::ffi::{c_char, CString};
ffi::{CString, c_char}, use std::pin::Pin;
pin::Pin, use std::sync::Arc;
sync::Arc, use std::task::{ready, Context, Poll};
task::{Context, Poll, ready}, use std::thread::{self};
thread::{self}, use tokio::sync::mpsc::{self, Receiver, Sender};
}; use tokio::sync::{oneshot, Mutex};
use tokio::sync::{
Mutex,
mpsc::{self, Receiver, Sender},
oneshot,
};
#[derive(Debug, Default)] #[derive(Debug, Default)]
struct Bounds { struct Bounds {
@@ -48,16 +37,9 @@ struct Bounds {
#[derive(Debug)] #[derive(Debug)]
struct InputCaptureState { struct InputCaptureState {
/// active capture positions
active_clients: Lazy<HashSet<Position>>, active_clients: Lazy<HashSet<Position>>,
/// the currently entered capture position, if any
current_pos: Option<Position>, current_pos: Option<Position>,
/// position where the cursor was captured
enter_position: Option<CGPoint>,
/// bounds of the input capture area
bounds: Bounds, bounds: Bounds,
/// current state of modifier keys
modifier_state: XMods,
} }
#[derive(Debug)] #[derive(Debug)]
@@ -74,9 +56,7 @@ impl InputCaptureState {
let mut res = Self { let mut res = Self {
active_clients: Lazy::new(HashSet::new), active_clients: Lazy::new(HashSet::new),
current_pos: None, current_pos: None,
enter_position: None,
bounds: Bounds::default(), bounds: Bounds::default(),
modifier_state: Default::default(),
}; };
res.update_bounds()?; res.update_bounds()?;
Ok(res) Ok(res)
@@ -116,34 +96,45 @@ impl InputCaptureState {
Ok(()) Ok(())
} }
/// start the input capture by // We can't disable mouse movement when in a client so we need to reset the cursor position
fn start_capture(&mut self, event: &CGEvent, position: Position) -> Result<(), CaptureError> { // to the edge of the screen, the cursor will be hidden but we dont want it to appear in a
let mut location = event.location(); // random location when we exit the client
let edge_offset = 1.0; fn reset_mouse_position(&self, event: &CGEvent) -> Result<(), CaptureError> {
// move cursor location to display bounds if let Some(pos) = self.current_pos {
match position { let location = event.location();
Position::Left => location.x = self.bounds.xmin + edge_offset, let edge_offset = 1.0;
Position::Right => location.x = self.bounds.xmax - edge_offset,
Position::Top => location.y = self.bounds.ymin + edge_offset,
Position::Bottom => location.y = self.bounds.ymax - edge_offset,
};
self.enter_position = Some(location);
self.reset_cursor()
}
/// resets the cursor to the position, where the capture started // After the cursor is warped no event is produced but the next event
fn reset_cursor(&mut self) -> Result<(), CaptureError> { // will carry the delta from the warp so only half the delta is needed to move the cursor
let pos = self.enter_position.expect("capture active"); let delta_y = event.get_double_value_field(EventField::MOUSE_EVENT_DELTA_Y) / 2.0;
log::trace!("Resetting cursor position to: {}, {}", pos.x, pos.y); let delta_x = event.get_double_value_field(EventField::MOUSE_EVENT_DELTA_X) / 2.0;
CGDisplay::warp_mouse_cursor_position(pos).map_err(CaptureError::WarpCursor)
}
fn hide_cursor(&self) -> Result<(), CaptureError> { let mut new_x = location.x + delta_x;
CGDisplay::hide_cursor(&CGDisplay::main()).map_err(CaptureError::CoreGraphics) let mut new_y = location.y + delta_y;
}
fn show_cursor(&self) -> Result<(), CaptureError> { match pos {
CGDisplay::show_cursor(&CGDisplay::main()).map_err(CaptureError::CoreGraphics) Position::Left => {
new_x = self.bounds.xmin + edge_offset;
}
Position::Right => {
new_x = self.bounds.xmax - edge_offset;
}
Position::Top => {
new_y = self.bounds.ymin + edge_offset;
}
Position::Bottom => {
new_y = self.bounds.ymax - edge_offset;
}
}
let new_pos = CGPoint::new(new_x, new_y);
log::trace!("Resetting cursor position to: {new_x}, {new_y}");
return CGDisplay::warp_mouse_cursor_position(new_pos)
.map_err(CaptureError::WarpCursor);
}
Err(CaptureError::ResetMouseWithoutClient)
} }
async fn handle_producer_event( async fn handle_producer_event(
@@ -154,13 +145,15 @@ impl InputCaptureState {
match producer_event { match producer_event {
ProducerEvent::Release => { ProducerEvent::Release => {
if self.current_pos.is_some() { if self.current_pos.is_some() {
self.show_cursor()?; CGDisplay::show_cursor(&CGDisplay::main())
.map_err(CaptureError::CoreGraphics)?;
self.current_pos = None; self.current_pos = None;
} }
} }
ProducerEvent::Grab(pos) => { ProducerEvent::Grab(pos) => {
if self.current_pos.is_none() { if self.current_pos.is_none() {
self.hide_cursor()?; CGDisplay::hide_cursor(&CGDisplay::main())
.map_err(CaptureError::CoreGraphics)?;
self.current_pos = Some(pos); self.current_pos = Some(pos);
} }
} }
@@ -170,7 +163,8 @@ impl InputCaptureState {
ProducerEvent::Destroy(p) => { ProducerEvent::Destroy(p) => {
if let Some(current) = self.current_pos { if let Some(current) = self.current_pos {
if current == p { if current == p {
self.show_cursor()?; CGDisplay::show_cursor(&CGDisplay::main())
.map_err(CaptureError::CoreGraphics)?;
self.current_pos = None; self.current_pos = None;
}; };
} }
@@ -186,7 +180,6 @@ fn get_events(
ev_type: &CGEventType, ev_type: &CGEventType,
ev: &CGEvent, ev: &CGEvent,
result: &mut Vec<CaptureEvent>, result: &mut Vec<CaptureEvent>,
modifier_state: &mut XMods,
) -> Result<(), CaptureError> { ) -> Result<(), CaptureError> {
fn map_pointer_event(ev: &CGEvent) -> PointerEvent { fn map_pointer_event(ev: &CGEvent) -> PointerEvent {
PointerEvent::Motion { PointerEvent::Motion {
@@ -222,42 +215,29 @@ fn get_events(
}))); })));
} }
CGEventType::FlagsChanged => { CGEventType::FlagsChanged => {
let mut depressed = XMods::empty(); let mut mods = XMods::empty();
let mut mods_locked = XMods::empty(); let mut mods_locked = XMods::empty();
let cg_flags = ev.get_flags(); let cg_flags = ev.get_flags();
if cg_flags.contains(CGEventFlags::CGEventFlagShift) { if cg_flags.contains(CGEventFlags::CGEventFlagShift) {
depressed |= XMods::ShiftMask; mods |= XMods::ShiftMask;
} }
if cg_flags.contains(CGEventFlags::CGEventFlagControl) { if cg_flags.contains(CGEventFlags::CGEventFlagControl) {
depressed |= XMods::ControlMask; mods |= XMods::ControlMask;
} }
if cg_flags.contains(CGEventFlags::CGEventFlagAlternate) { if cg_flags.contains(CGEventFlags::CGEventFlagAlternate) {
depressed |= XMods::Mod1Mask; mods |= XMods::Mod1Mask;
} }
if cg_flags.contains(CGEventFlags::CGEventFlagCommand) { if cg_flags.contains(CGEventFlags::CGEventFlagCommand) {
depressed |= XMods::Mod4Mask; mods |= XMods::Mod4Mask;
} }
if cg_flags.contains(CGEventFlags::CGEventFlagAlphaShift) { if cg_flags.contains(CGEventFlags::CGEventFlagAlphaShift) {
depressed |= XMods::LockMask; mods |= XMods::LockMask;
mods_locked |= XMods::LockMask; mods_locked |= XMods::LockMask;
} }
// check if pressed or released
let state = if depressed > *modifier_state { 1 } else { 0 };
*modifier_state = depressed;
if let Ok(key) = map_key(ev) {
let key_event = CaptureEvent::Input(Event::Keyboard(KeyboardEvent::Key {
time: 0,
key,
state,
}));
result.push(key_event);
}
let modifier_event = KeyboardEvent::Modifiers { let modifier_event = KeyboardEvent::Modifiers {
depressed: depressed.bits(), depressed: mods.bits(),
latched: 0, latched: 0,
locked: mods_locked.bits(), locked: mods_locked.bits(),
group: 0, group: 0,
@@ -306,73 +286,35 @@ fn get_events(
}))) })))
} }
CGEventType::OtherMouseDown => { CGEventType::OtherMouseDown => {
let btn_num = ev.get_integer_value_field(EventField::MOUSE_EVENT_BUTTON_NUMBER);
let button = match btn_num {
3 => BTN_BACK,
4 => BTN_FORWARD,
_ => BTN_MIDDLE,
};
result.push(CaptureEvent::Input(Event::Pointer(PointerEvent::Button { result.push(CaptureEvent::Input(Event::Pointer(PointerEvent::Button {
time: 0, time: 0,
button, button: BTN_MIDDLE,
state: 1, state: 1,
}))) })))
} }
CGEventType::OtherMouseUp => { CGEventType::OtherMouseUp => {
let btn_num = ev.get_integer_value_field(EventField::MOUSE_EVENT_BUTTON_NUMBER);
let button = match btn_num {
3 => BTN_BACK,
4 => BTN_FORWARD,
_ => BTN_MIDDLE,
};
result.push(CaptureEvent::Input(Event::Pointer(PointerEvent::Button { result.push(CaptureEvent::Input(Event::Pointer(PointerEvent::Button {
time: 0, time: 0,
button, button: BTN_MIDDLE,
state: 0, state: 0,
}))) })))
} }
CGEventType::ScrollWheel => { CGEventType::ScrollWheel => {
if ev.get_integer_value_field(EventField::SCROLL_WHEEL_EVENT_IS_CONTINUOUS) != 0 { let v = ev.get_integer_value_field(EventField::SCROLL_WHEEL_EVENT_POINT_DELTA_AXIS_1);
let v = let h = ev.get_integer_value_field(EventField::SCROLL_WHEEL_EVENT_POINT_DELTA_AXIS_2);
ev.get_integer_value_field(EventField::SCROLL_WHEEL_EVENT_POINT_DELTA_AXIS_1); if v != 0 {
let h = result.push(CaptureEvent::Input(Event::Pointer(PointerEvent::Axis {
ev.get_integer_value_field(EventField::SCROLL_WHEEL_EVENT_POINT_DELTA_AXIS_2); time: 0,
if v != 0 { axis: 0, // Vertical
result.push(CaptureEvent::Input(Event::Pointer(PointerEvent::Axis { value: v as f64,
time: 0, })));
axis: 0, // Vertical }
value: v as f64, if h != 0 {
}))); result.push(CaptureEvent::Input(Event::Pointer(PointerEvent::Axis {
} time: 0,
if h != 0 { axis: 1, // Horizontal
result.push(CaptureEvent::Input(Event::Pointer(PointerEvent::Axis { value: h as f64,
time: 0, })));
axis: 1, // Horizontal
value: h as f64,
})));
}
} else {
// line based scrolling
const LINES_PER_STEP: i32 = 3;
const V120_STEPS_PER_LINE: i32 = 120 / LINES_PER_STEP;
let v = ev.get_integer_value_field(EventField::SCROLL_WHEEL_EVENT_DELTA_AXIS_1);
let h = ev.get_integer_value_field(EventField::SCROLL_WHEEL_EVENT_DELTA_AXIS_2);
if v != 0 {
result.push(CaptureEvent::Input(Event::Pointer(
PointerEvent::AxisDiscrete120 {
axis: 0, // Vertical
value: V120_STEPS_PER_LINE * v as i32,
},
)));
}
if h != 0 {
result.push(CaptureEvent::Input(Event::Pointer(
PointerEvent::AxisDiscrete120 {
axis: 1, // Horizontal
value: V120_STEPS_PER_LINE * h as i32,
},
)));
}
} }
} }
_ => (), _ => (),
@@ -406,7 +348,7 @@ fn create_event_tap<'a>(
move |_proxy: CGEventTapProxy, event_type: CGEventType, cg_ev: &CGEvent| { move |_proxy: CGEventTapProxy, event_type: CGEventType, cg_ev: &CGEvent| {
log::trace!("Got event from tap: {event_type:?}"); log::trace!("Got event from tap: {event_type:?}");
let mut state = client_state.blocking_lock(); let mut state = client_state.blocking_lock();
let mut capture_position = None; let mut pos = None;
let mut res_events = vec![]; let mut res_events = vec![];
if matches!( if matches!(
@@ -423,34 +365,22 @@ fn create_event_tap<'a>(
// Are we in a client? // Are we in a client?
if let Some(current_pos) = state.current_pos { if let Some(current_pos) = state.current_pos {
capture_position = Some(current_pos); pos = Some(current_pos);
get_events( get_events(&event_type, cg_ev, &mut res_events).unwrap_or_else(|e| {
&event_type,
cg_ev,
&mut res_events,
&mut state.modifier_state,
)
.unwrap_or_else(|e| {
log::error!("Failed to get events: {e}"); log::error!("Failed to get events: {e}");
}); });
// Keep (hidden) cursor at the edge of the screen // Keep (hidden) cursor at the edge of the screen
if matches!( if matches!(event_type, CGEventType::MouseMoved) {
event_type, state.reset_mouse_position(cg_ev).unwrap_or_else(|e| {
CGEventType::MouseMoved log::error!("Failed to reset mouse position: {e}");
| CGEventType::LeftMouseDragged })
| CGEventType::RightMouseDragged
| CGEventType::OtherMouseDragged
) {
state.reset_cursor().unwrap_or_else(|e| log::warn!("{e}"));
} }
} else if matches!(event_type, CGEventType::MouseMoved) { }
// Did we cross a barrier? // Did we cross a barrier?
else if matches!(event_type, CGEventType::MouseMoved) {
if let Some(new_pos) = state.crossed(cg_ev) { if let Some(new_pos) = state.crossed(cg_ev) {
capture_position = Some(new_pos); pos = Some(new_pos);
state
.start_capture(cg_ev, new_pos)
.unwrap_or_else(|e| log::warn!("{e}"));
res_events.push(CaptureEvent::Begin); res_events.push(CaptureEvent::Begin);
notify_tx notify_tx
.blocking_send(ProducerEvent::Grab(new_pos)) .blocking_send(ProducerEvent::Grab(new_pos))
@@ -458,19 +388,17 @@ fn create_event_tap<'a>(
} }
} }
if let Some(pos) = capture_position { if let Some(pos) = pos {
res_events.iter().for_each(|e| { res_events.iter().for_each(|e| {
// error must be ignored, since the event channel event_tx
// may already be closed when the InputCapture instance is dropped. .blocking_send((pos, *e))
let _ = event_tx.blocking_send((pos, *e)); .expect("Failed to send event");
}); });
// Returning Drop should stop the event from being processed // Returning None should stop the event from being processed
// but core fundation still returns the event // but core fundation still returns the event
cg_ev.set_type(CGEventType::Null); cg_ev.set_type(CGEventType::Null);
CallbackResult::Drop
} else {
CallbackResult::Keep
} }
Some(cg_ev.to_owned())
}; };
let tap = CGEventTap::new( let tap = CGEventTap::new(
@@ -483,7 +411,7 @@ fn create_event_tap<'a>(
.map_err(|_| MacosCaptureCreationError::EventTapCreation)?; .map_err(|_| MacosCaptureCreationError::EventTapCreation)?;
let tap_source: CFRunLoopSource = tap let tap_source: CFRunLoopSource = tap
.mach_port() .mach_port
.create_runloop_source(0) .create_runloop_source(0)
.expect("Failed creating loop source"); .expect("Failed creating loop source");
@@ -498,8 +426,8 @@ fn event_tap_thread(
client_state: Arc<Mutex<InputCaptureState>>, client_state: Arc<Mutex<InputCaptureState>>,
event_tx: Sender<(Position, CaptureEvent)>, event_tx: Sender<(Position, CaptureEvent)>,
notify_tx: Sender<ProducerEvent>, notify_tx: Sender<ProducerEvent>,
ready: std::sync::mpsc::Sender<Result<CFRunLoop, MacosCaptureCreationError>>, ready: std::sync::mpsc::Sender<Result<(), MacosCaptureCreationError>>,
exit: oneshot::Sender<()>, exit: oneshot::Sender<Result<(), &'static str>>,
) { ) {
let _tap = match create_event_tap(client_state, notify_tx, event_tx) { let _tap = match create_event_tap(client_state, notify_tx, event_tx) {
Err(e) => { Err(e) => {
@@ -507,22 +435,18 @@ fn event_tap_thread(
return; return;
} }
Ok(tap) => { Ok(tap) => {
let run_loop = CFRunLoop::get_current(); ready.send(Ok(())).expect("channel closed");
ready.send(Ok(run_loop)).expect("channel closed");
tap tap
} }
}; };
log::debug!("running CFRunLoop...");
CFRunLoop::run_current(); CFRunLoop::run_current();
log::debug!("event tap thread exiting!...");
let _ = exit.send(()); let _ = exit.send(Err("tap thread exited"));
} }
pub struct MacOSInputCapture { pub struct MacOSInputCapture {
event_rx: Receiver<(Position, CaptureEvent)>, event_rx: Receiver<(Position, CaptureEvent)>,
notify_tx: Sender<ProducerEvent>, notify_tx: Sender<ProducerEvent>,
run_loop: CFRunLoop,
} }
impl MacOSInputCapture { impl MacOSInputCapture {
@@ -551,41 +475,36 @@ impl MacOSInputCapture {
}); });
// wait for event tap creation result // wait for event tap creation result
let run_loop = ready_rx.recv().expect("channel closed")?; ready_rx.recv().expect("channel closed")?;
let _tap_task: tokio::task::JoinHandle<()> = tokio::task::spawn_local(async move { let _tap_task: tokio::task::JoinHandle<()> = tokio::task::spawn_local(async move {
loop { loop {
tokio::select! { tokio::select! {
producer_event = notify_rx.recv() => { producer_event = notify_rx.recv() => {
let Some(producer_event) = producer_event else { let producer_event = producer_event.expect("channel closed");
break;
};
let mut state = state.lock().await; let mut state = state.lock().await;
state.handle_producer_event(producer_event).await.unwrap_or_else(|e| { state.handle_producer_event(producer_event).await.unwrap_or_else(|e| {
log::error!("Failed to handle producer event: {e}"); log::error!("Failed to handle producer event: {e}");
}) })
} }
_ = &mut tap_exit_rx => break,
res = &mut tap_exit_rx => {
if let Err(e) = res.expect("channel closed") {
log::error!("Tap thread failed: {:?}", e);
break;
}
}
} }
} }
// show cursor
let _ = CGDisplay::show_cursor(&CGDisplay::main());
}); });
Ok(Self { Ok(Self {
event_rx, event_rx,
notify_tx, notify_tx,
run_loop,
}) })
} }
} }
impl Drop for MacOSInputCapture {
fn drop(&mut self) {
self.run_loop.stop();
}
}
#[async_trait] #[async_trait]
impl Capture for MacOSInputCapture { impl Capture for MacOSInputCapture {
async fn create(&mut self, pos: Position) -> Result<(), CaptureError> { async fn create(&mut self, pos: Position) -> Result<(), CaptureError> {
@@ -660,7 +579,6 @@ unsafe fn configure_cf_settings() -> Result<(), MacosCaptureCreationError> {
let event_source = CGEventSource::new(CGEventSourceStateID::CombinedSessionState) let event_source = CGEventSource::new(CGEventSourceStateID::CombinedSessionState)
.map_err(|_| MacosCaptureCreationError::EventSourceCreation)?; .map_err(|_| MacosCaptureCreationError::EventSourceCreation)?;
CGEventSourceSetLocalEventsSuppressionInterval(event_source, 0.05); CGEventSourceSetLocalEventsSuppressionInterval(event_source, 0.05);
// FIXME Memory Leak
// This is a private settings that allows the cursor to be hidden while in the background. // This is a private settings that allows the cursor to be hidden while in the background.
// It is used by Barrier and other apps. // It is used by Barrier and other apps.

View File

@@ -5,7 +5,7 @@ use futures::Stream;
use std::pin::Pin; use std::pin::Pin;
use std::task::ready; use std::task::ready;
use tokio::sync::mpsc::{Receiver, channel}; use tokio::sync::mpsc::{channel, Receiver};
use super::{Capture, CaptureError, CaptureEvent, Position}; use super::{Capture, CaptureError, CaptureEvent, Position};

View File

@@ -6,32 +6,33 @@ use std::default::Default;
use std::sync::atomic::{AtomicBool, AtomicI32, Ordering}; use std::sync::atomic::{AtomicBool, AtomicI32, Ordering};
use std::sync::{Arc, Condvar, Mutex}; use std::sync::{Arc, Condvar, Mutex};
use std::thread; use std::thread;
use tokio::sync::mpsc::Sender;
use tokio::sync::mpsc::error::TrySendError; use tokio::sync::mpsc::error::TrySendError;
use windows::Win32::Foundation::{FALSE, HWND, LPARAM, LRESULT, RECT, WPARAM}; use tokio::sync::mpsc::Sender;
use windows::core::{w, PCWSTR};
use windows::Win32::Foundation::{FALSE, HINSTANCE, HWND, LPARAM, LRESULT, RECT, WPARAM};
use windows::Win32::Graphics::Gdi::{ use windows::Win32::Graphics::Gdi::{
DEVMODEW, DISPLAY_DEVICE_ATTACHED_TO_DESKTOP, DISPLAY_DEVICEW, ENUM_CURRENT_SETTINGS, EnumDisplayDevicesW, EnumDisplaySettingsW, DEVMODEW, DISPLAY_DEVICEW,
EnumDisplayDevicesW, EnumDisplaySettingsW, DISPLAY_DEVICE_ATTACHED_TO_DESKTOP, ENUM_CURRENT_SETTINGS,
}; };
use windows::Win32::System::LibraryLoader::GetModuleHandleW; use windows::Win32::System::LibraryLoader::GetModuleHandleW;
use windows::Win32::System::Threading::GetCurrentThreadId; use windows::Win32::System::Threading::GetCurrentThreadId;
use windows::core::{PCWSTR, w};
use windows::Win32::UI::WindowsAndMessaging::{ use windows::Win32::UI::WindowsAndMessaging::{
CallNextHookEx, CreateWindowExW, DispatchMessageW, EDD_GET_DEVICE_INTERFACE_NAME, GetMessageW, CallNextHookEx, CreateWindowExW, DispatchMessageW, GetMessageW, PostThreadMessageW,
HOOKPROC, KBDLLHOOKSTRUCT, LLKHF_EXTENDED, MSG, MSLLHOOKSTRUCT, PostThreadMessageW, RegisterClassW, SetWindowsHookExW, TranslateMessage, EDD_GET_DEVICE_INTERFACE_NAME, HHOOK,
RegisterClassW, SetWindowsHookExW, TranslateMessage, WH_KEYBOARD_LL, WH_MOUSE_LL, WINDOW_STYLE, HMENU, HOOKPROC, KBDLLHOOKSTRUCT, LLKHF_EXTENDED, MSG, MSLLHOOKSTRUCT, WH_KEYBOARD_LL,
WM_DISPLAYCHANGE, WM_KEYDOWN, WM_KEYUP, WM_LBUTTONDOWN, WM_LBUTTONUP, WM_MBUTTONDOWN, WH_MOUSE_LL, WINDOW_STYLE, WM_DISPLAYCHANGE, WM_KEYDOWN, WM_KEYUP, WM_LBUTTONDOWN,
WM_MBUTTONUP, WM_MOUSEHWHEEL, WM_MOUSEMOVE, WM_MOUSEWHEEL, WM_RBUTTONDOWN, WM_RBUTTONUP, WM_LBUTTONUP, WM_MBUTTONDOWN, WM_MBUTTONUP, WM_MOUSEMOVE, WM_MOUSEWHEEL, WM_RBUTTONDOWN,
WM_SYSKEYDOWN, WM_SYSKEYUP, WM_USER, WM_XBUTTONDOWN, WM_XBUTTONUP, WNDCLASSW, WNDPROC, WM_RBUTTONUP, WM_SYSKEYDOWN, WM_SYSKEYUP, WM_USER, WM_XBUTTONDOWN, WM_XBUTTONUP, WNDCLASSW,
WNDPROC,
}; };
use input_event::{ use input_event::{
BTN_BACK, BTN_FORWARD, BTN_LEFT, BTN_MIDDLE, BTN_RIGHT, Event, KeyboardEvent, PointerEvent,
scancode::{self, Linux}, scancode::{self, Linux},
Event, KeyboardEvent, PointerEvent, BTN_BACK, BTN_FORWARD, BTN_LEFT, BTN_MIDDLE, BTN_RIGHT,
}; };
use super::{CaptureEvent, Position, display_util}; use super::{display_util, CaptureEvent, Position};
pub(crate) struct EventThread { pub(crate) struct EventThread {
request_buffer: Arc<Mutex<Vec<ClientUpdate>>>, request_buffer: Arc<Mutex<Vec<ClientUpdate>>>,
@@ -127,7 +128,7 @@ thread_local! {
fn get_msg() -> Option<MSG> { fn get_msg() -> Option<MSG> {
unsafe { unsafe {
let mut msg = std::mem::zeroed(); let mut msg = std::mem::zeroed();
let ret = GetMessageW(addr_of_mut!(msg), None, 0, 0); let ret = GetMessageW(addr_of_mut!(msg), HWND::default(), 0, 0);
match ret.0 { match ret.0 {
0 => None, 0 => None,
x if x > 0 => Some(msg), x if x > 0 => Some(msg),
@@ -175,15 +176,14 @@ fn start_routine(
/* register hooks */ /* register hooks */
unsafe { unsafe {
let _ = SetWindowsHookExW(WH_MOUSE_LL, mouse_proc, None, 0).unwrap(); let _ = SetWindowsHookExW(WH_MOUSE_LL, mouse_proc, HINSTANCE::default(), 0).unwrap();
let _ = SetWindowsHookExW(WH_KEYBOARD_LL, kybrd_proc, None, 0).unwrap(); let _ = SetWindowsHookExW(WH_KEYBOARD_LL, kybrd_proc, HINSTANCE::default(), 0).unwrap();
} }
let instance = unsafe { GetModuleHandleW(None).unwrap() }; let instance = unsafe { GetModuleHandleW(None).unwrap() };
let instance = instance.into();
let window_class: WNDCLASSW = WNDCLASSW { let window_class: WNDCLASSW = WNDCLASSW {
lpfnWndProc: window_proc, lpfnWndProc: window_proc,
hInstance: instance, hInstance: instance.into(),
lpszClassName: w!("lan-mouse-message-window-class"), lpszClassName: w!("lan-mouse-message-window-class"),
..Default::default() ..Default::default()
}; };
@@ -213,9 +213,9 @@ fn start_routine(
0, 0,
0, 0,
0, 0,
None, HWND::default(),
None, HMENU::default(),
Some(instance), instance,
None, None,
) )
.expect("CreateWindowExW"); .expect("CreateWindowExW");
@@ -312,7 +312,7 @@ unsafe extern "system" fn mouse_proc(ncode: i32, wparam: WPARAM, lparam: LPARAM)
/* no client was active */ /* no client was active */
if !active { if !active {
return CallNextHookEx(None, ncode, wparam, lparam); return CallNextHookEx(HHOOK::default(), ncode, wparam, lparam);
} }
/* get active client if any */ /* get active client if any */
@@ -337,7 +337,7 @@ unsafe extern "system" fn mouse_proc(ncode: i32, wparam: WPARAM, lparam: LPARAM)
unsafe extern "system" fn kybrd_proc(ncode: i32, wparam: WPARAM, lparam: LPARAM) -> LRESULT { unsafe extern "system" fn kybrd_proc(ncode: i32, wparam: WPARAM, lparam: LPARAM) -> LRESULT {
/* get active client if any */ /* get active client if any */
let Some(client) = ACTIVE_CLIENT.get() else { let Some(client) = ACTIVE_CLIENT.get() else {
return CallNextHookEx(None, ncode, wparam, lparam); return CallNextHookEx(HHOOK::default(), ncode, wparam, lparam);
}; };
/* convert to key event */ /* convert to key event */
@@ -388,10 +388,7 @@ fn enumerate_displays(display_rects: &mut Vec<RECT>) {
if ret == FALSE { if ret == FALSE {
break; break;
} }
if device if device.StateFlags & DISPLAY_DEVICE_ATTACHED_TO_DESKTOP != 0 {
.StateFlags
.contains(DISPLAY_DEVICE_ATTACHED_TO_DESKTOP)
{
devices.push(device.DeviceName); devices.push(device.DeviceName);
} }
} }
@@ -540,10 +537,6 @@ fn to_mouse_event(wparam: WPARAM, lparam: LPARAM) -> Option<PointerEvent> {
state: if p == WM_XBUTTONDOWN as usize { 1 } else { 0 }, state: if p == WM_XBUTTONDOWN as usize { 1 } else { 0 },
}) })
} }
WPARAM(p) if p == WM_MOUSEHWHEEL as usize => Some(PointerEvent::AxisDiscrete120 {
axis: 1, // Horizontal
value: mouse_low_level.mouseData as i32 >> 16,
}),
w => { w => {
log::warn!("unknown mouse event: {w:?}"); log::warn!("unknown mouse event: {w:?}");
None None

View File

@@ -3,7 +3,7 @@ use std::task::Poll;
use async_trait::async_trait; use async_trait::async_trait;
use futures_core::Stream; use futures_core::Stream;
use super::{Capture, CaptureError, CaptureEvent, Position, error::X11InputCaptureCreationError}; use super::{error::X11InputCaptureCreationError, Capture, CaptureError, CaptureEvent, Position};
pub struct X11InputCapture {} pub struct X11InputCapture {}

View File

@@ -21,7 +21,6 @@ tokio = { version = "1.32.0", features = [
"rt", "rt",
"sync", "sync",
"signal", "signal",
"time"
] } ] }
once_cell = "1.19.0" once_cell = "1.19.0"
@@ -40,18 +39,18 @@ wayland-protocols-misc = { version = "0.3.1", features = [
"client", "client",
], optional = true } ], optional = true }
x11 = { version = "2.21.0", features = ["xlib", "xtest"], optional = true } x11 = { version = "2.21.0", features = ["xlib", "xtest"], optional = true }
ashpd = { version = "0.11.0", default-features = false, features = [ ashpd = { version = "0.10", default-features = false, features = [
"tokio", "tokio",
], optional = true } ], optional = true }
reis = { version = "0.5.0", features = ["tokio"], optional = true } reis = { version = "0.4", features = ["tokio"], optional = true }
[target.'cfg(target_os="macos")'.dependencies] [target.'cfg(target_os="macos")'.dependencies]
bitflags = "2.6.0" bitflags = "2.6.0"
core-graphics = { version = "0.25.0", features = ["highsierra"] } core-graphics = { version = "0.24.0", features = ["highsierra"] }
keycode = "1.0.0" keycode = "0.4.0"
[target.'cfg(windows)'.dependencies] [target.'cfg(windows)'.dependencies]
windows = { version = "0.61.2", features = [ windows = { version = "0.58.0", features = [
"Win32_System_LibraryLoader", "Win32_System_LibraryLoader",
"Win32_System_Threading", "Win32_System_Threading",
"Win32_Foundation", "Win32_Foundation",

View File

@@ -11,15 +11,15 @@ pub enum InputEmulationError {
any(feature = "remote_desktop_portal", feature = "libei"), any(feature = "remote_desktop_portal", feature = "libei"),
not(target_os = "macos") not(target_os = "macos")
))] ))]
use ashpd::{Error::Response, desktop::ResponseError}; use ashpd::{desktop::ResponseError, Error::Response};
use std::io; use std::io;
use thiserror::Error; use thiserror::Error;
#[cfg(all(unix, feature = "wlroots", not(target_os = "macos")))] #[cfg(all(unix, feature = "wlroots", not(target_os = "macos")))]
use wayland_client::{ use wayland_client::{
ConnectError, DispatchError,
backend::WaylandError, backend::WaylandError,
globals::{BindError, GlobalError}, globals::{BindError, GlobalError},
ConnectError, DispatchError,
}; };
#[derive(Debug, Error)] #[derive(Debug, Error)]

View File

@@ -1,26 +1,25 @@
use futures::{StreamExt, future}; use futures::{future, StreamExt};
use std::{ use std::{
env, fs, io, io,
os::{fd::OwnedFd, unix::net::UnixStream}, os::{fd::OwnedFd, unix::net::UnixStream},
path::PathBuf,
sync::{ sync::{
Arc, Mutex, RwLock,
atomic::{AtomicBool, Ordering}, atomic::{AtomicBool, Ordering},
Arc, Mutex, RwLock,
}, },
time::{SystemTime, UNIX_EPOCH}, time::{SystemTime, UNIX_EPOCH},
}; };
use tokio::task::JoinHandle; use tokio::task::JoinHandle;
use ashpd::desktop::{ use ashpd::desktop::{
PersistMode, Session,
remote_desktop::{DeviceType, RemoteDesktop}, remote_desktop::{DeviceType, RemoteDesktop},
PersistMode, Session,
}; };
use async_trait::async_trait; use async_trait::async_trait;
use reis::{ use reis::{
ei::{ ei::{
self, Button, Keyboard, Pointer, Scroll, button::ButtonState, handshake::ContextType, self, button::ButtonState, handshake::ContextType, keyboard::KeyState, Button, Keyboard,
keyboard::KeyState, Pointer, Scroll,
}, },
event::{self, Connection, DeviceCapability, DeviceEvent, EiEvent, SeatEvent}, event::{self, Connection, DeviceCapability, DeviceEvent, EiEvent, SeatEvent},
tokio::EiConvertEventStream, tokio::EiConvertEventStream,
@@ -30,7 +29,7 @@ use input_event::{Event, KeyboardEvent, PointerEvent};
use crate::error::EmulationError; use crate::error::EmulationError;
use super::{Emulation, EmulationHandle, error::LibeiEmulationCreationError}; use super::{error::LibeiEmulationCreationError, Emulation, EmulationHandle};
#[derive(Clone, Default)] #[derive(Clone, Default)]
struct Devices { struct Devices {
@@ -51,45 +50,10 @@ pub(crate) struct LibeiEmulation<'a> {
session: Session<'a, RemoteDesktop<'a>>, session: Session<'a, RemoteDesktop<'a>>,
} }
/// Get the path to the RemoteDesktop token file async fn get_ei_fd<'a>(
fn get_token_file_path() -> PathBuf { ) -> Result<(RemoteDesktop<'a>, Session<'a, RemoteDesktop<'a>>, OwnedFd), ashpd::Error> {
let cache_dir = env::var("XDG_CACHE_HOME")
.ok()
.map(PathBuf::from)
.unwrap_or_else(|| {
let home = env::var("HOME").expect("HOME not set");
PathBuf::from(home).join(".cache")
});
cache_dir.join("lan-mouse").join("remote-desktop.token")
}
/// Read the RemoteDesktop token from file
fn read_token() -> Option<String> {
let token_path = get_token_file_path();
match fs::read_to_string(&token_path) {
Ok(token) => Some(token.trim().to_string()),
Err(_) => None,
}
}
/// Write the RemoteDesktop token to file
fn write_token(token: &str) -> io::Result<()> {
let token_path = get_token_file_path();
if let Some(parent) = token_path.parent() {
fs::create_dir_all(parent)?;
}
fs::write(&token_path, token)?;
Ok(())
}
async fn get_ei_fd<'a>()
-> Result<(RemoteDesktop<'a>, Session<'a, RemoteDesktop<'a>>, OwnedFd), ashpd::Error> {
let remote_desktop = RemoteDesktop::new().await?; let remote_desktop = RemoteDesktop::new().await?;
let restore_token = read_token();
log::debug!("creating session ..."); log::debug!("creating session ...");
let session = remote_desktop.create_session().await?; let session = remote_desktop.create_session().await?;
@@ -98,20 +62,13 @@ async fn get_ei_fd<'a>()
.select_devices( .select_devices(
&session, &session,
DeviceType::Keyboard | DeviceType::Pointer, DeviceType::Keyboard | DeviceType::Pointer,
restore_token.as_deref(), None,
PersistMode::ExplicitlyRevoked, PersistMode::ExplicitlyRevoked,
) )
.await?; .await?;
log::info!("requesting permission for input emulation"); log::info!("requesting permission for input emulation");
let start_response = remote_desktop.start(&session, None).await?.response()?; let _devices = remote_desktop.start(&session, None).await?.response()?;
// The restore token is only valid once, we need to re-save it each time
if let Some(token_str) = start_response.restore_token() {
if let Err(e) = write_token(token_str) {
log::warn!("failed to save RemoteDesktop token: {}", e);
}
}
let fd = remote_desktop.connect_to_eis(&session).await?; let fd = remote_desktop.connect_to_eis(&session).await?;
Ok((remote_desktop, session, fd)) Ok((remote_desktop, session, fd))

View File

@@ -1,4 +1,4 @@
use super::{Emulation, EmulationHandle, error::EmulationError}; use super::{error::EmulationError, Emulation, EmulationHandle};
use async_trait::async_trait; use async_trait::async_trait;
use bitflags::bitflags; use bitflags::bitflags;
use core_graphics::base::CGFloat; use core_graphics::base::CGFloat;
@@ -10,50 +10,53 @@ use core_graphics::event::{
ScrollEventUnit, ScrollEventUnit,
}; };
use core_graphics::event_source::{CGEventSource, CGEventSourceStateID}; use core_graphics::event_source::{CGEventSource, CGEventSourceStateID};
use input_event::{ use input_event::{scancode, Event, KeyboardEvent, PointerEvent};
BTN_BACK, BTN_FORWARD, BTN_LEFT, BTN_MIDDLE, BTN_RIGHT, Event, KeyboardEvent, PointerEvent,
scancode,
};
use keycode::{KeyMap, KeyMapping}; use keycode::{KeyMap, KeyMapping};
use std::cell::Cell; use std::cell::Cell;
use std::collections::HashSet; use std::ops::{Index, IndexMut};
use std::rc::Rc; use std::rc::Rc;
use std::sync::Arc; use std::sync::Arc;
use std::time::{Duration, Instant}; use std::time::Duration;
use tokio::{sync::Notify, task::JoinHandle}; use tokio::{sync::Notify, task::JoinHandle};
use super::error::MacOSEmulationCreationError; use super::error::MacOSEmulationCreationError;
const DEFAULT_REPEAT_DELAY: Duration = Duration::from_millis(500); const DEFAULT_REPEAT_DELAY: Duration = Duration::from_millis(500);
const DEFAULT_REPEAT_INTERVAL: Duration = Duration::from_millis(32); const DEFAULT_REPEAT_INTERVAL: Duration = Duration::from_millis(32);
const DOUBLE_CLICK_INTERVAL: Duration = Duration::from_millis(500);
pub(crate) struct MacOSEmulation { pub(crate) struct MacOSEmulation {
/// global event source for all events
event_source: CGEventSource, event_source: CGEventSource,
/// task handle for key repeats
repeat_task: Option<JoinHandle<()>>, repeat_task: Option<JoinHandle<()>>,
/// current state of the mouse buttons (tracked by evdev button code) button_state: ButtonState,
pressed_buttons: HashSet<u32>,
/// button previously pressed (evdev button code)
previous_button: Option<u32>,
/// timestamp of previous click (button down)
previous_button_click: Option<Instant>,
/// click state, i.e. number of clicks in quick succession
button_click_state: i64,
/// current modifier state
modifier_state: Rc<Cell<XMods>>, modifier_state: Rc<Cell<XMods>>,
/// notify to cancel key repeats
notify_repeat_task: Arc<Notify>, notify_repeat_task: Arc<Notify>,
} }
/// Maps an evdev button code to the CGEventType used for drag events. struct ButtonState {
fn drag_event_type(button: u32) -> CGEventType { left: bool,
match button { right: bool,
BTN_LEFT => CGEventType::LeftMouseDragged, center: bool,
BTN_RIGHT => CGEventType::RightMouseDragged, }
// middle, back, forward, and any other button all use OtherMouseDragged
_ => CGEventType::OtherMouseDragged, impl Index<CGMouseButton> for ButtonState {
type Output = bool;
fn index(&self, index: CGMouseButton) -> &Self::Output {
match index {
CGMouseButton::Left => &self.left,
CGMouseButton::Right => &self.right,
CGMouseButton::Center => &self.center,
}
}
}
impl IndexMut<CGMouseButton> for ButtonState {
fn index_mut(&mut self, index: CGMouseButton) -> &mut Self::Output {
match index {
CGMouseButton::Left => &mut self.left,
CGMouseButton::Right => &mut self.right,
CGMouseButton::Center => &mut self.center,
}
} }
} }
@@ -63,12 +66,14 @@ impl MacOSEmulation {
pub(crate) fn new() -> Result<Self, MacOSEmulationCreationError> { pub(crate) fn new() -> Result<Self, MacOSEmulationCreationError> {
let event_source = CGEventSource::new(CGEventSourceStateID::CombinedSessionState) let event_source = CGEventSource::new(CGEventSourceStateID::CombinedSessionState)
.map_err(|_| MacOSEmulationCreationError::EventSourceCreation)?; .map_err(|_| MacOSEmulationCreationError::EventSourceCreation)?;
let button_state = ButtonState {
left: false,
right: false,
center: false,
};
Ok(Self { Ok(Self {
event_source, event_source,
pressed_buttons: HashSet::new(), button_state,
previous_button: None,
previous_button_click: None,
button_click_state: 0,
repeat_task: None, repeat_task: None,
notify_repeat_task: Arc::new(Notify::new()), notify_repeat_task: Arc::new(Notify::new()),
modifier_state: Rc::new(Cell::new(XMods::empty())), modifier_state: Rc::new(Cell::new(XMods::empty())),
@@ -84,9 +89,6 @@ impl MacOSEmulation {
// there can only be one repeating key and it's // there can only be one repeating key and it's
// always the last to be pressed // always the last to be pressed
self.cancel_repeat_task().await; self.cancel_repeat_task().await;
// initial key event
key_event(self.event_source.clone(), key, 1, self.modifier_state.get());
// repeat task
let event_source = self.event_source.clone(); let event_source = self.event_source.clone();
let notify = self.notify_repeat_task.clone(); let notify = self.notify_repeat_task.clone();
let modifiers = self.modifier_state.clone(); let modifiers = self.modifier_state.clone();
@@ -159,12 +161,12 @@ fn get_display_at_point(x: CGFloat, y: CGFloat) -> Option<CGDirectDisplayID> {
}; };
if error != 0 { if error != 0 {
log::warn!("error getting displays at point ({x}, {y}): {error}"); log::warn!("error getting displays at point ({}, {}): {}", x, y, error);
return Option::None; return Option::None;
} }
if display_count == 0 { if display_count == 0 {
log::debug!("no displays found at point ({x}, {y})"); log::debug!("no displays found at point ({}, {})", x, y);
return Option::None; return Option::None;
} }
@@ -222,192 +224,150 @@ impl Emulation for MacOSEmulation {
event: Event, event: Event,
_handle: EmulationHandle, _handle: EmulationHandle,
) -> Result<(), EmulationError> { ) -> Result<(), EmulationError> {
log::trace!("{event:?}");
match event { match event {
Event::Pointer(pointer_event) => { Event::Pointer(pointer_event) => match pointer_event {
match pointer_event { PointerEvent::Motion { time: _, dx, dy } => {
PointerEvent::Motion { time: _, dx, dy } => { let mut mouse_location = match self.get_mouse_location() {
let mut mouse_location = match self.get_mouse_location() { Some(l) => l,
Some(l) => l, None => {
None => { log::warn!("could not get mouse location!");
log::warn!("could not get mouse location!"); return Ok(());
return Ok(());
}
};
let (new_mouse_x, new_mouse_y) =
clamp_to_screen_space(mouse_location.x, mouse_location.y, dx, dy);
mouse_location.x = new_mouse_x;
mouse_location.y = new_mouse_y;
// If any button is held, emit a drag event for it;
// otherwise emit a normal mouse-moved event.
let event_type = self
.pressed_buttons
.iter()
.next()
.map(|&btn| drag_event_type(btn))
.unwrap_or(CGEventType::MouseMoved);
let event = match CGEvent::new_mouse_event(
self.event_source.clone(),
event_type,
mouse_location,
CGMouseButton::Left,
) {
Ok(e) => e,
Err(_) => {
log::warn!("mouse event creation failed!");
return Ok(());
}
};
event.set_integer_value_field(EventField::MOUSE_EVENT_DELTA_X, dx as i64);
event.set_integer_value_field(EventField::MOUSE_EVENT_DELTA_Y, dy as i64);
event.post(CGEventTapLocation::HID);
}
PointerEvent::Button {
time: _,
button,
state,
} => {
// button number for OtherMouse events (3 = back, 4 = forward, etc.)
let cg_button_number: Option<i64> = match button {
BTN_BACK => Some(3),
BTN_FORWARD => Some(4),
_ => None,
};
let (event_type, mouse_button) = match (button, state) {
(BTN_LEFT, 1) => (CGEventType::LeftMouseDown, CGMouseButton::Left),
(BTN_LEFT, 0) => (CGEventType::LeftMouseUp, CGMouseButton::Left),
(BTN_RIGHT, 1) => (CGEventType::RightMouseDown, CGMouseButton::Right),
(BTN_RIGHT, 0) => (CGEventType::RightMouseUp, CGMouseButton::Right),
(BTN_MIDDLE, 1) => (CGEventType::OtherMouseDown, CGMouseButton::Center),
(BTN_MIDDLE, 0) => (CGEventType::OtherMouseUp, CGMouseButton::Center),
(BTN_BACK, 1) | (BTN_FORWARD, 1) => {
(CGEventType::OtherMouseDown, CGMouseButton::Center)
}
(BTN_BACK, 0) | (BTN_FORWARD, 0) => {
(CGEventType::OtherMouseUp, CGMouseButton::Center)
}
_ => {
log::warn!("invalid button event: {button},{state}");
return Ok(());
}
};
// store button state using the evdev button code so
// back, forward, and middle are tracked independently
if state == 1 {
self.pressed_buttons.insert(button);
} else {
self.pressed_buttons.remove(&button);
} }
};
// update double-click tracking using the evdev button let (new_mouse_x, new_mouse_y) =
// code so that back/forward don't alias with middle clamp_to_screen_space(mouse_location.x, mouse_location.y, dx, dy);
if state == 1 {
if self.previous_button == Some(button)
&& self
.previous_button_click
.is_some_and(|i| i.elapsed() < DOUBLE_CLICK_INTERVAL)
{
self.button_click_state += 1;
} else {
self.button_click_state = 1;
}
self.previous_button = Some(button);
self.previous_button_click = Some(Instant::now());
}
log::debug!("click_state: {}", self.button_click_state); mouse_location.x = new_mouse_x;
let location = self.get_mouse_location().unwrap(); mouse_location.y = new_mouse_y;
let event = match CGEvent::new_mouse_event(
self.event_source.clone(), let mut event_type = CGEventType::MouseMoved;
event_type, if self.button_state.left {
location, event_type = CGEventType::LeftMouseDragged
mouse_button, } else if self.button_state.right {
) { event_type = CGEventType::RightMouseDragged
Ok(e) => e, } else if self.button_state.center {
Err(()) => { event_type = CGEventType::OtherMouseDragged
log::warn!("mouse event creation failed!"); };
return Ok(()); let event = match CGEvent::new_mouse_event(
} self.event_source.clone(),
}; event_type,
event.set_integer_value_field( mouse_location,
EventField::MOUSE_EVENT_CLICK_STATE, CGMouseButton::Left,
self.button_click_state, ) {
); Ok(e) => e,
// Set the button number for extra buttons (back=3, forward=4) Err(_) => {
if let Some(btn_num) = cg_button_number { log::warn!("mouse event creation failed!");
event.set_integer_value_field( return Ok(());
EventField::MOUSE_EVENT_BUTTON_NUMBER,
btn_num,
);
} }
event.post(CGEventTapLocation::HID); };
} event.set_integer_value_field(EventField::MOUSE_EVENT_DELTA_X, dx as i64);
PointerEvent::Axis { event.set_integer_value_field(EventField::MOUSE_EVENT_DELTA_Y, dy as i64);
time: _, event.post(CGEventTapLocation::HID);
axis,
value,
} => {
let value = value as i32;
let (count, wheel1, wheel2, wheel3) = match axis {
0 => (1, value, 0, 0), // 0 = vertical => 1 scroll wheel device (y axis)
1 => (2, 0, value, 0), // 1 = horizontal => 2 scroll wheel devices (y, x) -> (0, x)
_ => {
log::warn!("invalid scroll event: {axis}, {value}");
return Ok(());
}
};
let event = match CGEvent::new_scroll_event(
self.event_source.clone(),
ScrollEventUnit::PIXEL,
count,
wheel1,
wheel2,
wheel3,
) {
Ok(e) => e,
Err(()) => {
log::warn!("scroll event creation failed!");
return Ok(());
}
};
event.post(CGEventTapLocation::HID);
}
PointerEvent::AxisDiscrete120 { axis, value } => {
const LINES_PER_STEP: i32 = 3;
let (count, wheel1, wheel2, wheel3) = match axis {
0 => (1, value / (120 / LINES_PER_STEP), 0, 0), // 0 = vertical => 1 scroll wheel device (y axis)
1 => (2, 0, value / (120 / LINES_PER_STEP), 0), // 1 = horizontal => 2 scroll wheel devices (y, x) -> (0, x)
_ => {
log::warn!("invalid scroll event: {axis}, {value}");
return Ok(());
}
};
let event = match CGEvent::new_scroll_event(
self.event_source.clone(),
ScrollEventUnit::LINE,
count,
wheel1,
wheel2,
wheel3,
) {
Ok(e) => e,
Err(()) => {
log::warn!("scroll event creation failed!");
return Ok(());
}
};
event.post(CGEventTapLocation::HID);
}
} }
PointerEvent::Button {
time: _,
button,
state,
} => {
let (event_type, mouse_button) = match (button, state) {
(b, 1) if b == input_event::BTN_LEFT => {
(CGEventType::LeftMouseDown, CGMouseButton::Left)
}
(b, 0) if b == input_event::BTN_LEFT => {
(CGEventType::LeftMouseUp, CGMouseButton::Left)
}
(b, 1) if b == input_event::BTN_RIGHT => {
(CGEventType::RightMouseDown, CGMouseButton::Right)
}
(b, 0) if b == input_event::BTN_RIGHT => {
(CGEventType::RightMouseUp, CGMouseButton::Right)
}
(b, 1) if b == input_event::BTN_MIDDLE => {
(CGEventType::OtherMouseDown, CGMouseButton::Center)
}
(b, 0) if b == input_event::BTN_MIDDLE => {
(CGEventType::OtherMouseUp, CGMouseButton::Center)
}
_ => {
log::warn!("invalid button event: {button},{state}");
return Ok(());
}
};
// store button state
self.button_state[mouse_button] = state == 1;
// reset button click state in case it's not a button event let location = self.get_mouse_location().unwrap();
if !matches!(pointer_event, PointerEvent::Button { .. }) { let event = match CGEvent::new_mouse_event(
self.button_click_state = 0; self.event_source.clone(),
event_type,
location,
mouse_button,
) {
Ok(e) => e,
Err(()) => {
log::warn!("mouse event creation failed!");
return Ok(());
}
};
event.post(CGEventTapLocation::HID);
} }
} PointerEvent::Axis {
time: _,
axis,
value,
} => {
let value = value as i32;
let (count, wheel1, wheel2, wheel3) = match axis {
0 => (1, value, 0, 0), // 0 = vertical => 1 scroll wheel device (y axis)
1 => (2, 0, value, 0), // 1 = horizontal => 2 scroll wheel devices (y, x) -> (0, x)
_ => {
log::warn!("invalid scroll event: {axis}, {value}");
return Ok(());
}
};
let event = match CGEvent::new_scroll_event(
self.event_source.clone(),
ScrollEventUnit::PIXEL,
count,
wheel1,
wheel2,
wheel3,
) {
Ok(e) => e,
Err(()) => {
log::warn!("scroll event creation failed!");
return Ok(());
}
};
event.post(CGEventTapLocation::HID);
}
PointerEvent::AxisDiscrete120 { axis, value } => {
let (count, wheel1, wheel2, wheel3) = match axis {
0 => (1, value, 0, 0), // 0 = vertical => 1 scroll wheel device (y axis)
1 => (2, 0, value, 0), // 1 = horizontal => 2 scroll wheel devices (y, x) -> (0, x)
_ => {
log::warn!("invalid scroll event: {axis}, {value}");
return Ok(());
}
};
let event = match CGEvent::new_scroll_event(
self.event_source.clone(),
ScrollEventUnit::PIXEL,
count,
wheel1,
wheel2,
wheel3,
) {
Ok(e) => e,
Err(()) => {
log::warn!("scroll event creation failed!");
return Ok(());
}
};
event.post(CGEventTapLocation::HID);
}
},
Event::Keyboard(keyboard_event) => match keyboard_event { Event::Keyboard(keyboard_event) => match keyboard_event {
KeyboardEvent::Key { KeyboardEvent::Key {
time: _, time: _,
@@ -421,15 +381,18 @@ impl Emulation for MacOSEmulation {
return Ok(()); return Ok(());
} }
}; };
let is_modifier = update_modifiers(&self.modifier_state, key, state);
if is_modifier {
modifier_event(self.event_source.clone(), self.modifier_state.get());
}
match state { match state {
// pressed // pressed
1 => self.spawn_repeat_task(code).await, 1 => self.spawn_repeat_task(code).await,
_ => self.cancel_repeat_task().await, _ => self.cancel_repeat_task().await,
} }
update_modifiers(&self.modifier_state, key, state);
key_event(
self.event_source.clone(),
code,
state,
self.modifier_state.get(),
);
} }
KeyboardEvent::Modifiers { KeyboardEvent::Modifiers {
depressed, depressed,

View File

@@ -1,22 +1,22 @@
use super::error::{EmulationError, WindowsEmulationCreationError}; use super::error::{EmulationError, WindowsEmulationCreationError};
use input_event::{ use input_event::{
BTN_BACK, BTN_FORWARD, BTN_LEFT, BTN_MIDDLE, BTN_RIGHT, Event, KeyboardEvent, PointerEvent, scancode, Event, KeyboardEvent, PointerEvent, BTN_BACK, BTN_FORWARD, BTN_LEFT, BTN_MIDDLE,
scancode, BTN_RIGHT,
}; };
use async_trait::async_trait; use async_trait::async_trait;
use std::ops::BitOrAssign; use std::ops::BitOrAssign;
use std::time::Duration; use std::time::Duration;
use tokio::task::AbortHandle; use tokio::task::AbortHandle;
use windows::Win32::UI::Input::KeyboardAndMouse::{
SendInput, INPUT_0, KEYEVENTF_EXTENDEDKEY, MOUSEEVENTF_XDOWN, MOUSEEVENTF_XUP,
};
use windows::Win32::UI::Input::KeyboardAndMouse::{ use windows::Win32::UI::Input::KeyboardAndMouse::{
INPUT, INPUT_KEYBOARD, INPUT_MOUSE, KEYBDINPUT, KEYEVENTF_KEYUP, KEYEVENTF_SCANCODE, INPUT, INPUT_KEYBOARD, INPUT_MOUSE, KEYBDINPUT, KEYEVENTF_KEYUP, KEYEVENTF_SCANCODE,
MOUSEEVENTF_HWHEEL, MOUSEEVENTF_LEFTDOWN, MOUSEEVENTF_LEFTUP, MOUSEEVENTF_MIDDLEDOWN, MOUSEEVENTF_HWHEEL, MOUSEEVENTF_LEFTDOWN, MOUSEEVENTF_LEFTUP, MOUSEEVENTF_MIDDLEDOWN,
MOUSEEVENTF_MIDDLEUP, MOUSEEVENTF_MOVE, MOUSEEVENTF_RIGHTDOWN, MOUSEEVENTF_RIGHTUP, MOUSEEVENTF_MIDDLEUP, MOUSEEVENTF_MOVE, MOUSEEVENTF_RIGHTDOWN, MOUSEEVENTF_RIGHTUP,
MOUSEEVENTF_WHEEL, MOUSEINPUT, MOUSEEVENTF_WHEEL, MOUSEINPUT,
}; };
use windows::Win32::UI::Input::KeyboardAndMouse::{
INPUT_0, KEYEVENTF_EXTENDEDKEY, MOUSEEVENTF_XDOWN, MOUSEEVENTF_XUP, SendInput,
};
use windows::Win32::UI::WindowsAndMessaging::{XBUTTON1, XBUTTON2}; use windows::Win32::UI::WindowsAndMessaging::{XBUTTON1, XBUTTON2};
use super::{Emulation, EmulationHandle}; use super::{Emulation, EmulationHandle};

View File

@@ -1,6 +1,6 @@
use crate::error::EmulationError; use crate::error::EmulationError;
use super::{Emulation, error::WlrootsEmulationCreationError}; use super::{error::WlrootsEmulationCreationError, Emulation};
use async_trait::async_trait; use async_trait::async_trait;
use bitflags::bitflags; use bitflags::bitflags;
use std::collections::HashMap; use std::collections::HashMap;
@@ -8,11 +8,11 @@ use std::io;
use std::os::fd::{AsFd, OwnedFd}; use std::os::fd::{AsFd, OwnedFd};
use std::sync::{Arc, Mutex}; use std::sync::{Arc, Mutex};
use std::time::{SystemTime, UNIX_EPOCH}; use std::time::{SystemTime, UNIX_EPOCH};
use wayland_client::WEnum;
use wayland_client::backend::WaylandError; use wayland_client::backend::WaylandError;
use wayland_client::WEnum;
use wayland_client::protocol::wl_keyboard::{self, WlKeyboard}; use wayland_client::protocol::wl_keyboard::{self, WlKeyboard};
use wayland_client::protocol::wl_pointer::{Axis, AxisSource, ButtonState}; use wayland_client::protocol::wl_pointer::{Axis, ButtonState};
use wayland_client::protocol::wl_seat::WlSeat; use wayland_client::protocol::wl_seat::WlSeat;
use wayland_protocols_wlr::virtual_pointer::v1::client::{ use wayland_protocols_wlr::virtual_pointer::v1::client::{
zwlr_virtual_pointer_manager_v1::ZwlrVirtualPointerManagerV1 as VpManager, zwlr_virtual_pointer_manager_v1::ZwlrVirtualPointerManagerV1 as VpManager,
@@ -25,15 +25,16 @@ use wayland_protocols_misc::zwp_virtual_keyboard_v1::client::{
}; };
use wayland_client::{ use wayland_client::{
Connection, Dispatch, EventQueue, QueueHandle, delegate_noop, delegate_noop,
globals::{GlobalListContents, registry_queue_init}, globals::{registry_queue_init, GlobalListContents},
protocol::{wl_registry, wl_seat}, protocol::{wl_registry, wl_seat},
Connection, Dispatch, EventQueue, QueueHandle,
}; };
use input_event::{Event, KeyboardEvent, PointerEvent, scancode}; use input_event::{scancode, Event, KeyboardEvent, PointerEvent};
use super::EmulationHandle;
use super::error::WaylandBindError; use super::error::WaylandBindError;
use super::EmulationHandle;
struct State { struct State {
keymap: Option<(u32, OwnedFd, u32)>, keymap: Option<(u32, OwnedFd, u32)>,
@@ -162,13 +163,13 @@ impl Emulation for WlrootsEmulation {
async fn create(&mut self, handle: EmulationHandle) { async fn create(&mut self, handle: EmulationHandle) {
self.state.add_client(handle); self.state.add_client(handle);
if let Err(e) = self.queue.flush() { if let Err(e) = self.queue.flush() {
log::error!("{e}"); log::error!("{}", e);
} }
} }
async fn destroy(&mut self, handle: EmulationHandle) { async fn destroy(&mut self, handle: EmulationHandle) {
self.state.destroy_client(handle); self.state.destroy_client(handle);
if let Err(e) = self.queue.flush() { if let Err(e) = self.queue.flush() {
log::error!("{e}"); log::error!("{}", e);
} }
} }
async fn terminate(&mut self) { async fn terminate(&mut self) {
@@ -209,8 +210,7 @@ impl VirtualInput {
PointerEvent::AxisDiscrete120 { axis, value } => { PointerEvent::AxisDiscrete120 { axis, value } => {
let axis: Axis = (axis as u32).try_into()?; let axis: Axis = (axis as u32).try_into()?;
self.pointer self.pointer
.axis_discrete(now, axis, value as f64 / 8., value / 120); .axis_discrete(now, axis, value as f64 / 6., value / 120);
self.pointer.axis_source(AxisSource::Wheel);
self.pointer.frame(); self.pointer.frame();
} }
} }
@@ -221,7 +221,7 @@ impl VirtualInput {
self.keyboard.key(time, key, state as u32); self.keyboard.key(time, key, state as u32);
if let Ok(mut mods) = self.modifiers.lock() { if let Ok(mut mods) = self.modifiers.lock() {
if mods.update_by_key_event(key, state) { if mods.update_by_key_event(key, state) {
log::trace!("Key triggers modifier change: {mods:?}"); log::trace!("Key triggers modifier change: {:?}", mods);
self.keyboard.modifiers( self.keyboard.modifiers(
mods.mask_pressed().bits(), mods.mask_pressed().bits(),
0, 0,
@@ -330,7 +330,7 @@ impl XMods {
fn update_by_key_event(&mut self, key: u32, state: u8) -> bool { fn update_by_key_event(&mut self, key: u32, state: u8) -> bool {
if let Ok(key) = scancode::Linux::try_from(key) { if let Ok(key) = scancode::Linux::try_from(key) {
log::trace!("Attempting to process modifier from: {key:#?}"); log::trace!("Attempting to process modifier from: {:#?}", key);
let pressed_mask = match key { let pressed_mask = match key {
scancode::Linux::KeyLeftShift | scancode::Linux::KeyRightShift => XMods::ShiftMask, scancode::Linux::KeyLeftShift | scancode::Linux::KeyRightShift => XMods::ShiftMask,
scancode::Linux::KeyLeftCtrl | scancode::Linux::KeyRightCtrl => XMods::ControlMask, scancode::Linux::KeyLeftCtrl | scancode::Linux::KeyRightCtrl => XMods::ControlMask,
@@ -348,7 +348,7 @@ impl XMods {
// unchanged // unchanged
if pressed_mask.is_empty() && locked_mask.is_empty() { if pressed_mask.is_empty() && locked_mask.is_empty() {
log::trace!("{key:#?} is not a modifier key"); log::trace!("{:#?} is not a modifier key", key);
return false; return false;
} }
match state { match state {

View File

@@ -6,12 +6,12 @@ use x11::{
}; };
use input_event::{ use input_event::{
BTN_BACK, BTN_FORWARD, BTN_LEFT, BTN_MIDDLE, BTN_RIGHT, Event, KeyboardEvent, PointerEvent, Event, KeyboardEvent, PointerEvent, BTN_BACK, BTN_FORWARD, BTN_LEFT, BTN_MIDDLE, BTN_RIGHT,
}; };
use crate::error::EmulationError; use crate::error::EmulationError;
use super::{Emulation, EmulationHandle, error::X11EmulationCreationError}; use super::{error::X11EmulationCreationError, Emulation, EmulationHandle};
pub(crate) struct X11Emulation { pub(crate) struct X11Emulation {
display: *mut xlib::Display, display: *mut xlib::Display,
@@ -23,7 +23,7 @@ impl X11Emulation {
pub(crate) fn new() -> Result<Self, X11EmulationCreationError> { pub(crate) fn new() -> Result<Self, X11EmulationCreationError> {
let display = unsafe { let display = unsafe {
match xlib::XOpenDisplay(ptr::null()) { match xlib::XOpenDisplay(ptr::null()) {
d if std::ptr::eq(d, ptr::null_mut::<xlib::Display>()) => { d if d == ptr::null::<xlib::Display>() as *mut xlib::Display => {
Err(X11EmulationCreationError::OpenDisplay) Err(X11EmulationCreationError::OpenDisplay)
} }
display => Ok(display), display => Ok(display),

View File

@@ -1,7 +1,7 @@
use ashpd::{ use ashpd::{
desktop::{ desktop::{
PersistMode, Session,
remote_desktop::{Axis, DeviceType, KeyState, RemoteDesktop}, remote_desktop::{Axis, DeviceType, KeyState, RemoteDesktop},
PersistMode, Session,
}, },
zbus::AsyncDrop, zbus::AsyncDrop,
}; };
@@ -15,7 +15,7 @@ use input_event::{
use crate::error::EmulationError; use crate::error::EmulationError;
use super::{Emulation, EmulationHandle, error::XdpEmulationCreationError}; use super::{error::XdpEmulationCreationError, Emulation, EmulationHandle};
pub(crate) struct DesktopPortalEmulation<'a> { pub(crate) struct DesktopPortalEmulation<'a> {
proxy: RemoteDesktop<'a>, proxy: RemoteDesktop<'a>,
@@ -143,6 +143,7 @@ impl Emulation for DesktopPortalEmulation<'_> {
impl AsyncDrop for DesktopPortalEmulation<'_> { impl AsyncDrop for DesktopPortalEmulation<'_> {
#[doc = r" Perform the async cleanup."] #[doc = r" Perform the async cleanup."]
#[must_use]
#[allow(clippy::type_complexity, clippy::type_repetition_in_bounds)] #[allow(clippy::type_complexity, clippy::type_repetition_in_bounds)]
fn async_drop<'async_trait>( fn async_drop<'async_trait>(
self, self,

View File

@@ -14,7 +14,7 @@ serde = { version = "1.0", features = ["derive"] }
thiserror = "2.0.0" thiserror = "2.0.0"
[target.'cfg(all(unix, not(target_os="macos")))'.dependencies] [target.'cfg(all(unix, not(target_os="macos")))'.dependencies]
reis = { version = "0.5.0", optional = true } reis = { version = "0.4", optional = true }
[features] [features]
default = ["libei"] default = ["libei"]

View File

@@ -112,8 +112,8 @@ impl Display for KeyboardEvent {
impl Display for Event { impl Display for Event {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self { match self {
Event::Pointer(p) => write!(f, "{p}"), Event::Pointer(p) => write!(f, "{}", p),
Event::Keyboard(k) => write!(f, "{k}"), Event::Keyboard(k) => write!(f, "{}", k),
} }
} }
} }

View File

@@ -5,8 +5,8 @@ use std::{net::IpAddr, time::Duration};
use thiserror::Error; use thiserror::Error;
use lan_mouse_ipc::{ use lan_mouse_ipc::{
ClientHandle, ConnectionError, FrontendEvent, FrontendRequest, IpcError, Position, connect_async, ClientHandle, ConnectionError, FrontendEvent, FrontendRequest, IpcError,
connect_async, Position,
}; };
#[derive(Debug, Error)] #[derive(Debug, Error)]
@@ -71,8 +71,6 @@ enum CliSubcommand {
}, },
/// deauthorize a public key /// deauthorize a public key
RemoveAuthorizedKey { sha256_fingerprint: String }, RemoveAuthorizedKey { sha256_fingerprint: String },
/// save configuration to file
SaveConfig,
} }
pub async fn run(args: CliArgs) -> Result<(), CliError> { pub async fn run(args: CliArgs) -> Result<(), CliError> {
@@ -164,7 +162,6 @@ async fn execute(cmd: CliSubcommand) -> Result<(), CliError> {
tx.request(FrontendRequest::RemoveAuthorizedKey(sha256_fingerprint)) tx.request(FrontendRequest::RemoveAuthorizedKey(sha256_fingerprint))
.await? .await?
} }
CliSubcommand::SaveConfig => tx.request(FrontendRequest::SaveConfiguration).await?,
} }
Ok(()) Ok(())
} }

View File

@@ -4,9 +4,8 @@ use adw::prelude::*;
use adw::subclass::prelude::*; use adw::subclass::prelude::*;
use glib::subclass::InitializingObject; use glib::subclass::InitializingObject;
use gtk::{ use gtk::{
Button, CompositeTemplate, Label,
glib::{self, subclass::Signal}, glib::{self, subclass::Signal},
template_callbacks, template_callbacks, Button, CompositeTemplate, Label,
}; };
#[derive(CompositeTemplate, Default)] #[derive(CompositeTemplate, Default)]

View File

@@ -4,7 +4,7 @@ use adw::prelude::*;
use adw::subclass::prelude::*; use adw::subclass::prelude::*;
use gtk::glib::{self, Object}; use gtk::glib::{self, Object};
use lan_mouse_ipc::{DEFAULT_PORT, Position}; use lan_mouse_ipc::{Position, DEFAULT_PORT};
use super::ClientObject; use super::ClientObject;

View File

@@ -1,11 +1,11 @@
use std::cell::RefCell; use std::cell::RefCell;
use adw::subclass::prelude::*; use adw::subclass::prelude::*;
use adw::{ActionRow, ComboRow, prelude::*}; use adw::{prelude::*, ActionRow, ComboRow};
use glib::{Binding, subclass::InitializingObject}; use glib::{subclass::InitializingObject, Binding};
use gtk::glib::subclass::Signal; use gtk::glib::subclass::Signal;
use gtk::glib::{SignalHandlerId, clone}; use gtk::glib::{clone, SignalHandlerId};
use gtk::{Button, CompositeTemplate, Entry, Switch, glib}; use gtk::{glib, Button, CompositeTemplate, Entry, Switch};
use lan_mouse_ipc::Position; use lan_mouse_ipc::Position;
use std::sync::OnceLock; use std::sync::OnceLock;

View File

@@ -4,9 +4,8 @@ use adw::prelude::*;
use adw::subclass::prelude::*; use adw::subclass::prelude::*;
use glib::subclass::InitializingObject; use glib::subclass::InitializingObject;
use gtk::{ use gtk::{
Button, CompositeTemplate, Text,
glib::{self, subclass::Signal}, glib::{self, subclass::Signal},
template_callbacks, template_callbacks, Button, CompositeTemplate, Text,
}; };
#[derive(CompositeTemplate, Default)] #[derive(CompositeTemplate, Default)]
@@ -52,11 +51,9 @@ impl ObjectImpl for FingerprintWindow {
fn signals() -> &'static [Signal] { fn signals() -> &'static [Signal] {
static SIGNALS: OnceLock<Vec<Signal>> = OnceLock::new(); static SIGNALS: OnceLock<Vec<Signal>> = OnceLock::new();
SIGNALS.get_or_init(|| { SIGNALS.get_or_init(|| {
vec![ vec![Signal::builder("confirm-clicked")
Signal::builder("confirm-clicked") .param_types([String::static_type(), String::static_type()])
.param_types([String::static_type(), String::static_type()]) .build()]
.build(),
]
}) })
} }
} }

View File

@@ -8,7 +8,7 @@ use super::KeyObject;
glib::wrapper! { glib::wrapper! {
pub struct KeyRow(ObjectSubclass<imp::KeyRow>) pub struct KeyRow(ObjectSubclass<imp::KeyRow>)
@extends gtk::ListBoxRow, gtk::Widget, adw::PreferencesRow, adw::ActionRow, @extends gtk::ListBoxRow, gtk::Widget, adw::PreferencesRow, adw::ExpanderRow,
@implements gtk::Accessible, gtk::Actionable, gtk::Buildable, gtk::ConstraintTarget; @implements gtk::Accessible, gtk::Actionable, gtk::Buildable, gtk::ConstraintTarget;
} }

View File

@@ -1,11 +1,11 @@
use std::cell::RefCell; use std::cell::RefCell;
use adw::subclass::prelude::*; use adw::subclass::prelude::*;
use adw::{ActionRow, prelude::*}; use adw::{prelude::*, ActionRow};
use glib::{Binding, subclass::InitializingObject}; use glib::{subclass::InitializingObject, Binding};
use gtk::glib::clone; use gtk::glib::clone;
use gtk::glib::subclass::Signal; use gtk::glib::subclass::Signal;
use gtk::{Button, CompositeTemplate, glib}; use gtk::{glib, Button, CompositeTemplate};
use std::sync::OnceLock; use std::sync::OnceLock;
#[derive(CompositeTemplate, Default)] #[derive(CompositeTemplate, Default)]

View File

@@ -13,7 +13,7 @@ use window::Window;
use lan_mouse_ipc::FrontendEvent; use lan_mouse_ipc::FrontendEvent;
use adw::Application; use adw::Application;
use gtk::{IconTheme, gdk::Display, glib::clone, prelude::*}; use gtk::{gdk::Display, glib::clone, prelude::*, IconTheme};
use gtk::{gio, glib, prelude::ApplicationExt}; use gtk::{gio, glib, prelude::ApplicationExt};
use self::client_object::ClientObject; use self::client_object::ClientObject;

View File

@@ -4,15 +4,16 @@ use std::collections::HashMap;
use adw::prelude::*; use adw::prelude::*;
use adw::subclass::prelude::*; use adw::subclass::prelude::*;
use glib::{Object, clone}; use glib::{clone, Object};
use gtk::{ use gtk::{
NoSelection, gio, gio,
glib::{self, closure_local}, glib::{self, closure_local},
NoSelection,
}; };
use lan_mouse_ipc::{ use lan_mouse_ipc::{
ClientConfig, ClientHandle, ClientState, DEFAULT_PORT, FrontendRequest, FrontendRequestWriter, ClientConfig, ClientHandle, ClientState, FrontendRequest, FrontendRequestWriter, Position,
Position, DEFAULT_PORT,
}; };
use crate::{ use crate::{
@@ -323,7 +324,7 @@ impl Window {
pub(super) fn update_client_config(&self, handle: ClientHandle, client: ClientConfig) { pub(super) fn update_client_config(&self, handle: ClientHandle, client: ClientConfig) {
let Some(row) = self.row_for_handle(handle) else { let Some(row) = self.row_for_handle(handle) else {
log::warn!("could not find row for handle {handle}"); log::warn!("could not find row for handle {}", handle);
return; return;
}; };
row.set_hostname(client.hostname); row.set_hostname(client.hostname);
@@ -333,11 +334,11 @@ impl Window {
pub(super) fn update_client_state(&self, handle: ClientHandle, state: ClientState) { pub(super) fn update_client_state(&self, handle: ClientHandle, state: ClientState) {
let Some(row) = self.row_for_handle(handle) else { let Some(row) = self.row_for_handle(handle) else {
log::warn!("could not find row for handle {handle}"); log::warn!("could not find row for handle {}", handle);
return; return;
}; };
let Some(client_object) = self.client_object_for_handle(handle) else { let Some(client_object) = self.client_object_for_handle(handle) else {
log::warn!("could not find row for handle {handle}"); log::warn!("could not find row for handle {}", handle);
return; return;
}; };
@@ -473,9 +474,6 @@ impl Window {
} }
pub(super) fn request_authorization(&self, fingerprint: &str) { pub(super) fn request_authorization(&self, fingerprint: &str) {
if let Some(w) = self.imp().authorization_window.borrow_mut().take() {
w.close();
}
let window = AuthorizationWindow::new(fingerprint); let window = AuthorizationWindow::new(fingerprint);
window.set_transient_for(Some(self)); window.set_transient_for(Some(self));
window.connect_closure( window.connect_closure(
@@ -498,6 +496,5 @@ impl Window {
}), }),
); );
window.present(); window.present();
self.imp().authorization_window.replace(Some(window));
} }
} }

View File

@@ -1,14 +1,12 @@
use std::cell::{Cell, RefCell}; use std::cell::{Cell, RefCell};
use adw::subclass::prelude::*; use adw::subclass::prelude::*;
use adw::{ActionRow, PreferencesGroup, ToastOverlay, prelude::*}; use adw::{prelude::*, ActionRow, PreferencesGroup, ToastOverlay};
use glib::subclass::InitializingObject; use glib::subclass::InitializingObject;
use gtk::glib::clone; use gtk::glib::clone;
use gtk::{Button, CompositeTemplate, Entry, Image, Label, ListBox, gdk, gio, glib}; use gtk::{gdk, gio, glib, Button, CompositeTemplate, Entry, Image, Label, ListBox};
use lan_mouse_ipc::{DEFAULT_PORT, FrontendRequestWriter}; use lan_mouse_ipc::{FrontendRequestWriter, DEFAULT_PORT};
use crate::authorization_window::AuthorizationWindow;
#[derive(CompositeTemplate, Default)] #[derive(CompositeTemplate, Default)]
#[template(resource = "/de/feschber/LanMouse/window.ui")] #[template(resource = "/de/feschber/LanMouse/window.ui")]
@@ -51,7 +49,6 @@ pub struct Window {
pub port: Cell<u16>, pub port: Cell<u16>,
pub capture_active: Cell<bool>, pub capture_active: Cell<bool>,
pub emulation_active: Cell<bool>, pub emulation_active: Cell<bool>,
pub authorization_window: RefCell<Option<AuthorizationWindow>>,
} }
#[glib::object_subclass] #[glib::object_subclass]

View File

@@ -12,5 +12,5 @@ log = "0.4.22"
serde = { version = "1.0", features = ["derive"] } serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0.107" serde_json = "1.0.107"
thiserror = "2.0.0" thiserror = "2.0.0"
tokio = { version = "1.32.0", features = ["macros", "net", "io-util", "time"] } tokio = { version = "1.32.0", features = ["net", "io-util", "time"] }
tokio-stream = { version = "0.1.15", features = ["io-util"] } tokio-stream = { version = "0.1.15", features = ["io-util"] }

View File

@@ -1,7 +1,7 @@
use crate::{ConnectionError, FrontendEvent, FrontendRequest, IpcError}; use crate::{ConnectionError, FrontendEvent, FrontendRequest, IpcError};
use std::{ use std::{
cmp::min, cmp::min,
io::{self, BufReader, LineWriter, Lines, prelude::*}, io::{self, prelude::*, BufReader, LineWriter, Lines},
thread, thread,
time::Duration, time::Duration,
}; };

View File

@@ -1,7 +1,7 @@
use crate::{ConnectionError, FrontendEvent, FrontendRequest, IpcError}; use crate::{ConnectionError, FrontendEvent, FrontendRequest, IpcError};
use std::{ use std::{
cmp::min, cmp::min,
task::{Poll, ready}, task::{ready, Poll},
time::Duration, time::Duration,
}; };

View File

@@ -20,8 +20,8 @@ mod connect;
mod connect_async; mod connect_async;
mod listen; mod listen;
pub use connect::{FrontendEventReader, FrontendRequestWriter, connect}; pub use connect::{connect, FrontendEventReader, FrontendRequestWriter};
pub use connect_async::{AsyncFrontendEventReader, AsyncFrontendRequestWriter, connect_async}; pub use connect_async::{connect_async, AsyncFrontendEventReader, AsyncFrontendRequestWriter};
pub use listen::AsyncFrontendListener; pub use listen::AsyncFrontendListener;
#[derive(Debug, Error)] #[derive(Debug, Error)]
@@ -253,8 +253,6 @@ pub enum FrontendRequest {
RemoveAuthorizedKey(String), RemoveAuthorizedKey(String),
/// change the hook command /// change the hook command
UpdateEnterHook(u64, Option<String>), UpdateEnterHook(u64, Option<String>),
/// save config file
SaveConfiguration,
} }
#[derive(Clone, Copy, PartialEq, Eq, Debug, Default, Serialize, Deserialize)] #[derive(Clone, Copy, PartialEq, Eq, Debug, Default, Serialize, Deserialize)]

View File

@@ -1,4 +1,4 @@
use futures::{Stream, StreamExt, stream::SelectAll}; use futures::{stream::SelectAll, Stream, StreamExt};
#[cfg(unix)] #[cfg(unix)]
use std::path::PathBuf; use std::path::PathBuf;
use std::{ use std::{
@@ -45,7 +45,7 @@ impl AsyncFrontendListener {
let (socket_path, listener) = { let (socket_path, listener) = {
let socket_path = crate::default_socket_path()?; let socket_path = crate::default_socket_path()?;
log::debug!("remove socket: {socket_path:?}"); log::debug!("remove socket: {:?}", socket_path);
if socket_path.exists() { if socket_path.exists() {
// try to connect to see if some other instance // try to connect to see if some other instance
// of lan-mouse is already running // of lan-mouse is already running
@@ -63,7 +63,7 @@ impl AsyncFrontendListener {
Ok(ls) => ls, Ok(ls) => ls,
// some other lan-mouse instance has bound the socket in the meantime // some other lan-mouse instance has bound the socket in the meantime
Err(e) if e.kind() == ErrorKind::AddrInUse => { Err(e) if e.kind() == ErrorKind::AddrInUse => {
return Err(IpcListenerCreationError::AlreadyRunning); return Err(IpcListenerCreationError::AlreadyRunning)
} }
Err(e) => return Err(IpcListenerCreationError::Bind(e)), Err(e) => return Err(IpcListenerCreationError::Bind(e)),
}; };
@@ -75,7 +75,7 @@ impl AsyncFrontendListener {
Ok(ls) => ls, Ok(ls) => ls,
// some other lan-mouse instance has bound the socket in the meantime // some other lan-mouse instance has bound the socket in the meantime
Err(e) if e.kind() == ErrorKind::AddrInUse => { Err(e) if e.kind() == ErrorKind::AddrInUse => {
return Err(IpcListenerCreationError::AlreadyRunning); return Err(IpcListenerCreationError::AlreadyRunning)
} }
Err(e) => return Err(IpcListenerCreationError::Bind(e)), Err(e) => return Err(IpcListenerCreationError::Bind(e)),
}; };

View File

@@ -1,18 +1,18 @@
# Nix Flake Usage # Nix Flake Usage
## Run ## run
```bash ```bash
nix run github:feschber/lan-mouse nix run github:feschber/lan-mouse
# With params # with params
nix run github:feschber/lan-mouse -- --help nix run github:feschber/lan-mouse -- --help
``` ```
## Home-manager module ## home-manager module
Add input: add input
```nix ```nix
inputs = { inputs = {
@@ -20,27 +20,14 @@ inputs = {
} }
``` ```
Optional: add [our binary cache](https://app.cachix.org/cache/lan-mouse) to allow a faster package install. enable lan-mouse
```nix
nixConfig = {
extra-substituters = [
"https://lan-mouse.cachix.org/"
];
extra-trusted-public-keys = [
"lan-mouse.cachix.org-1:KlE2AEZUgkzNKM7BIzMQo8w9yJYqUpor1CAUNRY6OyM="
];
};
```
Enable lan-mouse:
``` nix ``` nix
{ {
inputs, inputs,
... ...
}: { }: {
# Add the Home Manager module # add the home manager module
imports = [inputs.lan-mouse.homeManagerModules.default]; imports = [inputs.lan-mouse.homeManagerModules.default];
programs.lan-mouse = { programs.lan-mouse = {

View File

@@ -1,93 +0,0 @@
#!/bin/sh
set -eu
homebrew_path=""
exec_path="target/debug/bundle/osx/Lan Mouse.app/Contents/MacOS/lan-mouse"
usage() {
cat <<EOF
$0: Copy all Homebrew libraries into the macOS app bundle.
USAGE: $0 [-h] [-b homebrew_path] [exec_path]
OPTIONS:
-h, --help Show this help message and exit
-b Path to Homebrew installation (default: $homebrew_path)
exec_path Path to the main executable in the app bundle
(default: get from `brew --prefix`)
When macOS apps are linked to dynamic libraries (.dylib files),
the fully qualified path to the library is embedded in the binary.
If the libraries come from Homebrew, that means that Homebrew must be present
and the libraries must be installed in the same location on the user's machine.
This script copies all of the Homebrew libraries that an executable links to into the app bundle
and tells all the binaries in the bundle to look for them there.
EOF
}
# Gather command-line arguments
while test $# -gt 0; do
case "$1" in
-h | --help ) usage; exit 0;;
-b | --homebrew ) homebrew_path="$1"; shift 2;;
* ) exec_path="$1"; shift;;
esac
done
if [ -z "$homebrew_path" ]; then
homebrew_path="$(brew --prefix)"
fi
# Path to the .app bundle
bundle_path=$(dirname "$(dirname "$(dirname "$exec_path")")")
# Path to the Frameworks directory
fwks_path="$bundle_path/Contents/Frameworks"
mkdir -p "$fwks_path"
# Copy and fix references for a binary (executable or dylib)
#
# This function will:
# - Copy any referenced dylibs from /opt/homebrew to the Frameworks directory
# - Update the binary to reference the local copy instead
# - Add the Frameworks directory to the binary's RPATH
# - Recursively process the copied dylibs
fix_references() {
local bin="$1"
# Get all Homebrew libraries referenced by the binary
libs=$(otool -L "$bin" | awk -v homebrew="$homebrew_path" '$0 ~ homebrew {print $1}')
echo "$libs" | while IFS= read -r old_path; do
local base_name="$(basename "$old_path")"
local dest="$fwks_path/$base_name"
if [ ! -e "$dest" ]; then
echo "Copying $old_path -> $dest"
cp -f "$old_path" "$dest"
# Ensure the copied dylib is writable so that xattr -rd /path/to/Lan\ Mouse.app works.
chmod 644 "$dest"
echo "Updating $dest to have install_name of @rpath/$base_name..."
install_name_tool -id "@rpath/$base_name" "$dest"
# Recursively process this dylib
fix_references "$dest"
fi
echo "Updating $bin to reference @rpath/$base_name..."
install_name_tool -change "$old_path" "@rpath/$base_name" "$bin"
done
}
fix_references "$exec_path"
# Ensure the main executable has our Frameworks path in its RPATH
if ! otool -l "$exec_path" | grep -q "@executable_path/../Frameworks"; then
echo "Adding RPATH to $exec_path"
install_name_tool -add_rpath "@executable_path/../Frameworks" "$exec_path"
fi
# Se-sign the .app
codesign --force --deep --sign - "$bundle_path"
echo "Done!"

View File

@@ -10,8 +10,8 @@ use input_capture::{
}; };
use input_event::scancode; use input_event::scancode;
use lan_mouse_proto::ProtoEvent; use lan_mouse_proto::ProtoEvent;
use local_channel::mpsc::{Receiver, Sender, channel}; use local_channel::mpsc::{channel, Receiver, Sender};
use tokio::task::{JoinHandle, spawn_local}; use tokio::task::{spawn_local, JoinHandle};
use tokio_util::sync::CancellationToken; use tokio_util::sync::CancellationToken;
use crate::connect::LanMouseConnection; use crate::connect::LanMouseConnection;
@@ -362,13 +362,7 @@ impl CaptureTask {
} }
async fn release_capture(&mut self, capture: &mut InputCapture) -> Result<(), CaptureError> { async fn release_capture(&mut self, capture: &mut InputCapture) -> Result<(), CaptureError> {
// If we have an active client, notify them we're leaving self.active_client.take();
if let Some(handle) = self.active_client.take() {
log::info!("sending Leave event to client {handle}");
if let Err(e) = self.conn.send(ProtoEvent::Leave(0), handle).await {
log::warn!("failed to send Leave to client {handle}: {e}");
}
}
capture.release().await capture.release().await
} }
} }

View File

@@ -15,15 +15,6 @@ pub struct ClientManager {
} }
impl ClientManager { impl ClientManager {
/// get all clients
pub fn clients(&self) -> Vec<(ClientConfig, ClientState)> {
self.clients
.borrow()
.iter()
.map(|(_, c)| c.clone())
.collect::<Vec<_>>()
}
/// add a new client to this manager /// add a new client to this manager
pub fn add_client(&self) -> ClientHandle { pub fn add_client(&self) -> ClientHandle {
self.clients.borrow_mut().insert(Default::default()) as ClientHandle self.clients.borrow_mut().insert(Default::default()) as ClientHandle

View File

@@ -5,17 +5,15 @@ use serde::{Deserialize, Serialize};
use std::collections::HashMap; use std::collections::HashMap;
use std::env::{self, VarError}; use std::env::{self, VarError};
use std::fmt::Display; use std::fmt::Display;
use std::fs::{self, File}; use std::fs;
use std::io::Write;
use std::net::IpAddr; use std::net::IpAddr;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use std::{collections::HashSet, io}; use std::{collections::HashSet, io};
use thiserror::Error; use thiserror::Error;
use toml; use toml;
use toml_edit::{self, DocumentMut};
use lan_mouse_cli::CliArgs; use lan_mouse_cli::CliArgs;
use lan_mouse_ipc::{DEFAULT_PORT, Position}; use lan_mouse_ipc::{Position, DEFAULT_PORT};
use input_event::scancode::{ use input_event::scancode::{
self, self,
@@ -46,14 +44,14 @@ fn default_path() -> Result<PathBuf, VarError> {
Ok(PathBuf::from(default_path)) Ok(PathBuf::from(default_path))
} }
#[derive(Serialize, Deserialize, Clone, Debug, Default)] #[derive(Serialize, Deserialize, Debug)]
struct ConfigToml { struct ConfigToml {
capture_backend: Option<CaptureBackend>, capture_backend: Option<CaptureBackend>,
emulation_backend: Option<EmulationBackend>, emulation_backend: Option<EmulationBackend>,
port: Option<u16>, port: Option<u16>,
release_bind: Option<Vec<scancode::Linux>>, release_bind: Option<Vec<scancode::Linux>>,
cert_path: Option<PathBuf>, cert_path: Option<PathBuf>,
clients: Option<Vec<TomlClient>>, clients: Vec<TomlClient>,
authorized_fingerprints: Option<HashMap<String, String>>, authorized_fingerprints: Option<HashMap<String, String>>,
} }
@@ -276,33 +274,6 @@ impl From<TomlClient> for ConfigClient {
} }
} }
impl From<ConfigClient> for TomlClient {
fn from(client: ConfigClient) -> Self {
let hostname = client.hostname;
let host_name = None;
let mut ips = client.ips.into_iter().collect::<Vec<_>>();
ips.sort();
let ips = Some(ips);
let port = if client.port == DEFAULT_PORT {
None
} else {
Some(client.port)
};
let position = Some(client.pos);
let activate_on_startup = if client.active { Some(true) } else { None };
let enter_hook = client.enter_hook;
Self {
hostname,
host_name,
ips,
port,
position,
activate_on_startup,
enter_hook,
}
}
}
#[derive(Debug, Error)] #[derive(Debug, Error)]
pub enum ConfigError { pub enum ConfigError {
#[error(transparent)] #[error(transparent)]
@@ -399,7 +370,6 @@ impl Config {
self.config_toml self.config_toml
.as_ref() .as_ref()
.map(|c| c.clients.clone()) .map(|c| c.clients.clone())
.unwrap_or_default()
.into_iter() .into_iter()
.flatten() .flatten()
.map(From::<TomlClient>::from) .map(From::<TomlClient>::from)
@@ -413,66 +383,4 @@ impl Config {
.and_then(|c| c.release_bind.clone()) .and_then(|c| c.release_bind.clone())
.unwrap_or(Vec::from_iter(DEFAULT_RELEASE_KEYS.iter().cloned())) .unwrap_or(Vec::from_iter(DEFAULT_RELEASE_KEYS.iter().cloned()))
} }
/// set configured clients
pub fn set_clients(&mut self, clients: Vec<ConfigClient>) {
if clients.is_empty() {
return;
}
if self.config_toml.is_none() {
self.config_toml = Some(Default::default());
}
self.config_toml.as_mut().expect("config").clients =
Some(clients.into_iter().map(|c| c.into()).collect::<Vec<_>>());
}
/// set authorized keys
pub fn set_authorized_keys(&mut self, fingerprints: HashMap<String, String>) {
if fingerprints.is_empty() {
return;
}
if self.config_toml.is_none() {
self.config_toml = Default::default();
}
self.config_toml
.as_mut()
.expect("config")
.authorized_fingerprints = Some(fingerprints);
}
pub fn write_back(&self) -> Result<(), io::Error> {
log::info!("writing config to {:?}", &self.config_path);
/* load the current configuration file */
let current_config = match fs::read_to_string(&self.config_path) {
Ok(c) => c.parse::<DocumentMut>().unwrap_or_default(),
Err(e) => {
log::info!("{:?} {e} => creating new config", self.config_path());
Default::default()
}
};
let _current_config =
toml_edit::de::from_document::<ConfigToml>(current_config).unwrap_or_default();
/* the new config */
let new_config = self.config_toml.clone().unwrap_or_default();
// let new_config = toml_edit::ser::to_document::<ConfigToml>(&new_config).expect("fixme");
let new_config = toml_edit::ser::to_string_pretty(&new_config).expect("config");
/*
* TODO merge documents => eventually we might want to split this up into clients configured
* via the config file and clients managed through the GUI / frontend.
* The latter should be saved to $XDG_DATA_HOME instead of $XDG_CONFIG_HOME,
* and clients configured through .config could be made permanent.
* For now we just override the config file.
*/
/* write new config to file */
if let Some(p) = self.config_path().parent() {
fs::create_dir_all(p)?;
}
let mut f = File::create(self.config_path())?;
f.write_all(new_config.as_bytes())?;
Ok(())
}
} }

View File

@@ -1,7 +1,7 @@
use crate::client::ClientManager; use crate::client::ClientManager;
use lan_mouse_ipc::{ClientHandle, DEFAULT_PORT}; use lan_mouse_ipc::{ClientHandle, DEFAULT_PORT};
use lan_mouse_proto::{MAX_EVENT_SIZE, ProtoEvent}; use lan_mouse_proto::{ProtoEvent, MAX_EVENT_SIZE};
use local_channel::mpsc::{Receiver, Sender, channel}; use local_channel::mpsc::{channel, Receiver, Sender};
use std::{ use std::{
cell::RefCell, cell::RefCell,
collections::{HashMap, HashSet}, collections::{HashMap, HashSet},
@@ -15,7 +15,7 @@ use thiserror::Error;
use tokio::{ use tokio::{
net::UdpSocket, net::UdpSocket,
sync::Mutex, sync::Mutex,
task::{JoinSet, spawn_local}, task::{spawn_local, JoinSet},
}; };
use webrtc_dtls::{ use webrtc_dtls::{
config::{Config, ExtendedMasterSecretType}, config::{Config, ExtendedMasterSecretType},
@@ -223,18 +223,14 @@ async fn ping_pong(
) { ) {
loop { loop {
let (buf, len) = ProtoEvent::Ping.into(); let (buf, len) = ProtoEvent::Ping.into();
if let Err(e) = conn.send(&buf[..len]).await {
// send 4 pings, at least one must be answered log::warn!("{addr}: send error `{e}`, closing connection");
for _ in 0..4 { let _ = conn.close().await;
if let Err(e) = conn.send(&buf[..len]).await { break;
log::warn!("{addr}: send error `{e}`, closing connection");
let _ = conn.close().await;
break;
}
log::trace!("PING >->->->->- {addr}");
tokio::time::sleep(Duration::from_millis(500)).await;
} }
log::trace!("PING >->->->->- {addr}");
tokio::time::sleep(Duration::from_millis(500)).await;
if !ping_response.borrow_mut().remove(&addr) { if !ping_response.borrow_mut().remove(&addr) {
log::warn!("{addr} did not respond, closing connection"); log::warn!("{addr} did not respond, closing connection");

View File

@@ -1,9 +1,9 @@
use std::{collections::HashMap, net::IpAddr}; use std::{collections::HashMap, net::IpAddr};
use local_channel::mpsc::{Receiver, Sender, channel}; use local_channel::mpsc::{channel, Receiver, Sender};
use tokio::task::{JoinHandle, spawn_local}; use tokio::task::{spawn_local, JoinHandle};
use hickory_resolver::{ResolveError, TokioResolver}; use hickory_resolver::{error::ResolveError, TokioAsyncResolver};
use tokio_util::sync::CancellationToken; use tokio_util::sync::CancellationToken;
use lan_mouse_ipc::ClientHandle; use lan_mouse_ipc::ClientHandle;
@@ -26,7 +26,7 @@ pub(crate) enum DnsEvent {
} }
struct DnsTask { struct DnsTask {
resolver: TokioResolver, resolver: TokioAsyncResolver,
request_rx: Receiver<DnsRequest>, request_rx: Receiver<DnsRequest>,
event_tx: Sender<DnsEvent>, event_tx: Sender<DnsEvent>,
cancellation_token: CancellationToken, cancellation_token: CancellationToken,
@@ -35,7 +35,7 @@ struct DnsTask {
impl DnsResolver { impl DnsResolver {
pub(crate) fn new() -> Result<Self, ResolveError> { pub(crate) fn new() -> Result<Self, ResolveError> {
let resolver = TokioResolver::builder_tokio()?.build(); let resolver = TokioAsyncResolver::tokio_from_system_conf()?;
let (request_tx, request_rx) = channel(); let (request_tx, request_rx) = channel();
let (event_tx, event_rx) = channel(); let (event_tx, event_rx) = channel();
let cancellation_token = CancellationToken::new(); let cancellation_token = CancellationToken::new();

View File

@@ -3,7 +3,7 @@ use futures::StreamExt;
use input_emulation::{EmulationHandle, InputEmulation, InputEmulationError}; use input_emulation::{EmulationHandle, InputEmulation, InputEmulationError};
use input_event::Event; use input_event::Event;
use lan_mouse_proto::{Position, ProtoEvent}; use lan_mouse_proto::{Position, ProtoEvent};
use local_channel::mpsc::{Receiver, Sender, channel}; use local_channel::mpsc::{channel, Receiver, Sender};
use std::{ use std::{
cell::Cell, cell::Cell,
collections::HashMap, collections::HashMap,
@@ -13,7 +13,7 @@ use std::{
}; };
use tokio::{ use tokio::{
select, select,
task::{JoinHandle, spawn_local}, task::{spawn_local, JoinHandle},
}; };
/// emulation handling events received from a listener /// emulation handling events received from a listener
@@ -128,7 +128,6 @@ impl ListenTask {
async fn run(mut self) { async fn run(mut self) {
let mut interval = tokio::time::interval(Duration::from_secs(5)); let mut interval = tokio::time::interval(Duration::from_secs(5));
let mut last_response = HashMap::new(); let mut last_response = HashMap::new();
let mut rejected_connections = HashMap::new();
loop { loop {
select! { select! {
e = self.listener.next() => {match e { e = self.listener.next() => {match e {
@@ -157,10 +156,7 @@ impl ListenTask {
self.event_tx.send(EmulationEvent::Connected { addr, fingerprint }).expect("channel closed"); self.event_tx.send(EmulationEvent::Connected { addr, fingerprint }).expect("channel closed");
} }
Some(ListenEvent::Rejected { fingerprint }) => { Some(ListenEvent::Rejected { fingerprint }) => {
if rejected_connections.insert(fingerprint.clone(), Instant::now()) self.event_tx.send(EmulationEvent::ConnectionAttempt { fingerprint }).expect("channel closed");
.is_none_or(|i| i.elapsed() >= Duration::from_secs(2)) {
self.event_tx.send(EmulationEvent::ConnectionAttempt { fingerprint }).expect("channel closed");
}
} }
None => break None => break
}} }}

View File

@@ -1,6 +1,6 @@
use futures::{Stream, StreamExt}; use futures::{Stream, StreamExt};
use lan_mouse_proto::{MAX_EVENT_SIZE, ProtoEvent}; use lan_mouse_proto::{ProtoEvent, MAX_EVENT_SIZE};
use local_channel::mpsc::{Receiver, Sender, channel}; use local_channel::mpsc::{channel, Receiver, Sender};
use rustls::pki_types::CertificateDer; use rustls::pki_types::CertificateDer;
use std::{ use std::{
collections::{HashMap, VecDeque}, collections::{HashMap, VecDeque},
@@ -12,7 +12,7 @@ use std::{
use thiserror::Error; use thiserror::Error;
use tokio::{ use tokio::{
sync::Mutex as AsyncMutex, sync::Mutex as AsyncMutex,
task::{JoinHandle, spawn_local}, task::{spawn_local, JoinHandle},
}; };
use webrtc_dtls::{ use webrtc_dtls::{
config::{ClientAuthType::RequireAnyClientCert, Config, ExtendedMasterSecretType}, config::{ClientAuthType::RequireAnyClientCert, Config, ExtendedMasterSecretType},
@@ -20,7 +20,7 @@ use webrtc_dtls::{
crypto::Certificate, crypto::Certificate,
listener::listen, listener::listen,
}; };
use webrtc_util::{Conn, Error, conn::Listener}; use webrtc_util::{conn::Listener, Conn, Error};
use crate::crypto; use crate::crypto;
@@ -264,7 +264,7 @@ async fn read_loop(
} }
} }
} }
log::info!("dtls client disconnected {addr:?}"); log::info!("dtls client disconnected {:?}", addr);
let mut conns = conns.lock().await; let mut conns = conns.lock().await;
let index = conns let index = conns
.iter() .iter()

View File

@@ -1,7 +1,7 @@
use crate::{ use crate::{
capture::{Capture, CaptureType, ICaptureEvent}, capture::{Capture, CaptureType, ICaptureEvent},
client::ClientManager, client::ClientManager,
config::{Config, ConfigClient}, config::Config,
connect::LanMouseConnection, connect::LanMouseConnection,
crypto, crypto,
dns::{DnsEvent, DnsResolver}, dns::{DnsEvent, DnsResolver},
@@ -9,7 +9,7 @@ use crate::{
listen::{LanMouseListener, ListenerCreationError}, listen::{LanMouseListener, ListenerCreationError},
}; };
use futures::StreamExt; use futures::StreamExt;
use hickory_resolver::ResolveError; use hickory_resolver::error::ResolveError;
use lan_mouse_ipc::{ use lan_mouse_ipc::{
AsyncFrontendListener, ClientConfig, ClientHandle, ClientState, FrontendEvent, FrontendRequest, AsyncFrontendListener, ClientConfig, ClientHandle, ClientState, FrontendEvent, FrontendRequest,
IpcError, IpcListenerCreationError, Position, Status, IpcError, IpcListenerCreationError, Position, Status,
@@ -39,8 +39,6 @@ pub enum ServiceError {
} }
pub struct Service { pub struct Service {
/// configuration
config: Config,
/// input capture /// input capture
capture: Capture, capture: Capture,
/// input emulation /// input emulation
@@ -124,7 +122,6 @@ impl Service {
let port = config.port(); let port = config.port();
let service = Self { let service = Self {
config,
capture, capture,
emulation, emulation,
frontend_listener, frontend_listener,
@@ -185,73 +182,24 @@ impl Service {
Err(e) => return log::error!("error receiving request: {e}"), Err(e) => return log::error!("error receiving request: {e}"),
}; };
match request { match request {
FrontendRequest::Activate(handle, active) => { FrontendRequest::Activate(handle, active) => self.set_client_active(handle, active),
self.set_client_active(handle, active); FrontendRequest::AuthorizeKey(desc, fp) => self.add_authorized_key(desc, fp),
self.save_config();
}
FrontendRequest::AuthorizeKey(desc, fp) => {
self.add_authorized_key(desc, fp);
self.save_config();
}
FrontendRequest::ChangePort(port) => self.change_port(port), FrontendRequest::ChangePort(port) => self.change_port(port),
FrontendRequest::Create => { FrontendRequest::Create => self.add_client(),
self.add_client(); FrontendRequest::Delete(handle) => self.remove_client(handle),
self.save_config();
}
FrontendRequest::Delete(handle) => {
self.remove_client(handle);
self.save_config();
}
FrontendRequest::EnableCapture => self.capture.reenable(), FrontendRequest::EnableCapture => self.capture.reenable(),
FrontendRequest::EnableEmulation => self.emulation.reenable(), FrontendRequest::EnableEmulation => self.emulation.reenable(),
FrontendRequest::Enumerate() => self.enumerate(), FrontendRequest::Enumerate() => self.enumerate(),
FrontendRequest::UpdateFixIps(handle, fix_ips) => { FrontendRequest::UpdateFixIps(handle, fix_ips) => self.update_fix_ips(handle, fix_ips),
self.update_fix_ips(handle, fix_ips); FrontendRequest::UpdateHostname(handle, host) => self.update_hostname(handle, host),
self.save_config(); FrontendRequest::UpdatePort(handle, port) => self.update_port(handle, port),
} FrontendRequest::UpdatePosition(handle, pos) => self.update_pos(handle, pos),
FrontendRequest::UpdateHostname(handle, host) => {
self.update_hostname(handle, host);
self.save_config();
}
FrontendRequest::UpdatePort(handle, port) => {
self.update_port(handle, port);
self.save_config();
}
FrontendRequest::UpdatePosition(handle, pos) => {
self.update_pos(handle, pos);
self.save_config();
}
FrontendRequest::ResolveDns(handle) => self.resolve(handle), FrontendRequest::ResolveDns(handle) => self.resolve(handle),
FrontendRequest::Sync => self.sync_frontend(), FrontendRequest::Sync => self.sync_frontend(),
FrontendRequest::RemoveAuthorizedKey(key) => { FrontendRequest::RemoveAuthorizedKey(key) => self.remove_authorized_key(key),
self.remove_authorized_key(key);
self.save_config();
}
FrontendRequest::UpdateEnterHook(handle, enter_hook) => { FrontendRequest::UpdateEnterHook(handle, enter_hook) => {
self.update_enter_hook(handle, enter_hook) self.update_enter_hook(handle, enter_hook)
} }
FrontendRequest::SaveConfiguration => self.save_config(),
}
}
fn save_config(&mut self) {
let clients = self.client_manager.clients();
let clients = clients
.into_iter()
.map(|(c, s)| ConfigClient {
ips: HashSet::from_iter(c.fix_ips),
hostname: c.hostname,
port: c.port,
pos: c.pos,
active: s.active,
enter_hook: c.cmd,
})
.collect();
self.config.set_clients(clients);
let authorized_keys = self.authorized_keys.read().expect("lock").clone();
self.config.set_authorized_keys(authorized_keys);
if let Err(e) = self.config.write_back() {
log::warn!("failed to write config: {e}");
} }
} }