mirror of
https://github.com/feschber/lan-mouse.git
synced 2026-03-08 04:20:01 +03:00
Compare commits
171 Commits
v0.5.1
...
macos-clie
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
df85f0baf8 | ||
|
|
75b790ec2e | ||
|
|
9f52a2ac93 | ||
|
|
4856199153 | ||
|
|
24e7a1d07f | ||
|
|
555fbfeb79 | ||
|
|
6191216873 | ||
|
|
5b1dc4ccf8 | ||
|
|
ab1b45ff45 | ||
|
|
b071201dcb | ||
|
|
f52f19d2e3 | ||
|
|
39fed0344c | ||
|
|
6cd190191e | ||
|
|
be677d4c81 | ||
|
|
12bc0d86ca | ||
|
|
1f7a7309eb | ||
|
|
8926d8f803 | ||
|
|
9248007986 | ||
|
|
e7a1d72149 | ||
|
|
19c2c4327f | ||
|
|
fe06ca1fae | ||
|
|
266ad28c6b | ||
|
|
096567640c | ||
|
|
8f7890c9be | ||
|
|
68361b25d1 | ||
|
|
22dc33367b | ||
|
|
ec412a5e74 | ||
|
|
e1096ae86c | ||
|
|
de3167221c | ||
|
|
0509b51a61 | ||
|
|
281cb406dd | ||
|
|
06ac390dbf | ||
|
|
dcc9250b6d | ||
|
|
376ae50b45 | ||
|
|
0e2c749b29 | ||
|
|
127c3366bf | ||
|
|
00e1ded35d | ||
|
|
65fb228db5 | ||
|
|
975d4b58a5 | ||
|
|
0be85f63f7 | ||
|
|
bea7d6f8a5 | ||
|
|
55bdf1e63e | ||
|
|
84696760f0 | ||
|
|
6a4dd740c3 | ||
|
|
703465a370 | ||
|
|
ef3ebc59bd | ||
|
|
37a8f729ea | ||
|
|
9abec63313 | ||
|
|
35e626976e | ||
|
|
684735b499 | ||
|
|
abfc744e4c | ||
|
|
fb2c39e8ae | ||
|
|
82ab5ecbbd | ||
|
|
3fd2b31562 | ||
|
|
90e83cee87 | ||
|
|
4db2d37f32 | ||
|
|
7b511bb97d | ||
|
|
70a23b9fa7 | ||
|
|
b6b16063a8 | ||
|
|
9cbe1ed8d8 | ||
|
|
3528ef4fae | ||
|
|
232c048c19 | ||
|
|
1c37579ae5 | ||
|
|
460bacade5 | ||
|
|
5fd3b719d6 | ||
|
|
e6d4585bb2 | ||
|
|
1c082d5c0c | ||
|
|
cd98acbd08 | ||
|
|
11e1919588 | ||
|
|
152bceaa86 | ||
|
|
60180d841c | ||
|
|
5802a0be0b | ||
|
|
1737727d61 | ||
|
|
ba46037a1f | ||
|
|
da768b6fb8 | ||
|
|
799b45104a | ||
|
|
e9738fc024 | ||
|
|
9969f997d3 | ||
|
|
b8cc9e2197 | ||
|
|
ba6abafe75 | ||
|
|
effb9ce0fa | ||
|
|
1f0d386d4a | ||
|
|
973360a774 | ||
|
|
e21ab02a6e | ||
|
|
18a3c10f8e | ||
|
|
c76d9ef7af | ||
|
|
5318f5a02d | ||
|
|
1e4312b3ce | ||
|
|
77aa96e09a | ||
|
|
636c5924bf | ||
|
|
3e96b42067 | ||
|
|
279e582698 | ||
|
|
9edd2f7f3b | ||
|
|
43c16a537b | ||
|
|
36855a1a17 | ||
|
|
e537cdbc7e | ||
|
|
5b76c3bcda | ||
|
|
81f65dcd3d | ||
|
|
f0099ee535 | ||
|
|
633d2c346e | ||
|
|
ccb201ea53 | ||
|
|
f7edfecba9 | ||
|
|
141ea2809d | ||
|
|
058097c618 | ||
|
|
f9eeb254d3 | ||
|
|
9ca7e2378c | ||
|
|
cc7984c066 | ||
|
|
1a2645cfbc | ||
|
|
e52febf457 | ||
|
|
db96717044 | ||
|
|
be8124a190 | ||
|
|
dcee2933a2 | ||
|
|
8aaff9fb58 | ||
|
|
742b1585d7 | ||
|
|
78c9de45c7 | ||
|
|
a491c0e9e3 | ||
|
|
af02cccc2a | ||
|
|
4a6399f866 | ||
|
|
66bce9083e | ||
|
|
102b64f2b4 | ||
|
|
4b499742ad | ||
|
|
a86d74b52c | ||
|
|
c25a15e2d8 | ||
|
|
8b82325bdb | ||
|
|
5415205c83 | ||
|
|
1666fb8b7b | ||
|
|
9afe7da0dd | ||
|
|
f7c59e40c9 | ||
|
|
5be5b0ad7c | ||
|
|
8ed4520172 | ||
|
|
9e56c546cd | ||
|
|
daf8818a9f | ||
|
|
6eaa199503 | ||
|
|
8ff991aefe | ||
|
|
abf95afb9f | ||
|
|
9a75a7622e | ||
|
|
0196cfe56c | ||
|
|
097468f708 | ||
|
|
3470abc03a | ||
|
|
a7397ad4f4 | ||
|
|
9889b49f10 | ||
|
|
f4db2366b7 | ||
|
|
c9deb6eba4 | ||
|
|
5cc8cda19d | ||
|
|
8084b52cfc | ||
|
|
1f4821a16d | ||
|
|
82926d8272 | ||
|
|
006831b9f1 | ||
|
|
e5b770a799 | ||
|
|
017bc43176 | ||
|
|
36001c6fb2 | ||
|
|
2803db7073 | ||
|
|
622b04b36c | ||
|
|
61ff05c95a | ||
|
|
ecab3a360d | ||
|
|
6674af8e63 | ||
|
|
b3caba99ab | ||
|
|
fad48c2504 | ||
|
|
f28f75418c | ||
|
|
e2c47d3096 | ||
|
|
f19944515a | ||
|
|
535cd055b9 | ||
|
|
118c0dfc73 | ||
|
|
7897db6047 | ||
|
|
347256e966 | ||
|
|
0017dbc634 | ||
|
|
d90eb0cd0f | ||
|
|
2e52660714 | ||
|
|
8c276f88b7 | ||
|
|
13597b3587 | ||
|
|
b59808742a |
2
.github/FUNDING.yml
vendored
Normal file
2
.github/FUNDING.yml
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
github: [feschber]
|
||||
ko_fi: feschber
|
||||
40
.github/workflows/cachix.yml
vendored
Normal file
40
.github/workflows/cachix.yml
vendored
Normal file
@@ -0,0 +1,40 @@
|
||||
name: Binary Cache
|
||||
|
||||
on: [push, pull_request, workflow_dispatch]
|
||||
jobs:
|
||||
nix:
|
||||
strategy:
|
||||
matrix:
|
||||
os:
|
||||
- ubuntu-latest
|
||||
- macos-13
|
||||
- macos-14
|
||||
name: "Build"
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
- uses: DeterminateSystems/nix-installer-action@main
|
||||
with:
|
||||
logger: pretty
|
||||
- uses: DeterminateSystems/magic-nix-cache-action@main
|
||||
- uses: cachix/cachix-action@v14
|
||||
with:
|
||||
name: lan-mouse
|
||||
authToken: '${{ secrets.CACHIX_AUTH_TOKEN }}'
|
||||
|
||||
- name: Build lan-mouse (x86_64-linux)
|
||||
if: matrix.os == 'ubuntu-latest'
|
||||
run: nix build --print-build-logs --show-trace .#packages.x86_64-linux.lan-mouse
|
||||
|
||||
- name: Build lan-mouse (x86_64-darwin)
|
||||
if: matrix.os == 'macos-13'
|
||||
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
|
||||
|
||||
41
.github/workflows/pre-release.yml
vendored
41
.github/workflows/pre-release.yml
vendored
@@ -13,7 +13,7 @@ jobs:
|
||||
linux-release-build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
- name: install dependencies
|
||||
run: |
|
||||
sudo apt-get update
|
||||
@@ -22,7 +22,7 @@ jobs:
|
||||
- name: Release Build
|
||||
run: cargo build --release
|
||||
- name: Upload build artifact
|
||||
uses: actions/upload-artifact@v3
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: lan-mouse-linux
|
||||
path: target/release/lan-mouse
|
||||
@@ -55,14 +55,16 @@ jobs:
|
||||
# choco install msys2
|
||||
# choco install visualstudio2022-workload-vctools
|
||||
# choco install pkgconfiglite
|
||||
pipx install gvsbuild
|
||||
py -m venv .venv
|
||||
.venv\Scripts\activate.ps1
|
||||
py -m pip install gvsbuild
|
||||
# see https://github.com/wingtk/gvsbuild/pull/1004
|
||||
Move-Item "C:\Program Files\Git\usr\bin" "C:\Program Files\Git\usr\notbin"
|
||||
Move-Item "C:\Program Files\Git\bin" "C:\Program Files\Git\notbin"
|
||||
gvsbuild build --msys-dir=C:\msys64 gtk4 libadwaita librsvg
|
||||
Move-Item "C:\Program Files\Git\usr\notbin" "C:\Program Files\Git\usr\bin"
|
||||
Move-Item "C:\Program Files\Git\notbin" "C:\Program Files\Git\bin"
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
- name: Release Build
|
||||
run: cargo build --release
|
||||
- name: Create Archive
|
||||
@@ -72,15 +74,15 @@ jobs:
|
||||
Copy-Item -Path "target\release\lan-mouse.exe" -Destination "lan-mouse-windows"
|
||||
Compress-Archive -Path "lan-mouse-windows\*" -DestinationPath lan-mouse-windows.zip
|
||||
- name: Upload build artifact
|
||||
uses: actions/upload-artifact@v3
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: lan-mouse-windows
|
||||
path: lan-mouse-windows.zip
|
||||
|
||||
macos-release-build:
|
||||
runs-on: macos-latest
|
||||
runs-on: macos-13
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
- name: install dependencies
|
||||
run: brew install gtk4 libadwaita
|
||||
- name: Release Build
|
||||
@@ -88,18 +90,34 @@ jobs:
|
||||
cargo build --release
|
||||
cp target/release/lan-mouse lan-mouse-macos-intel
|
||||
- name: Upload build artifact
|
||||
uses: actions/upload-artifact@v3
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: lan-mouse-macos
|
||||
name: lan-mouse-macos-intel
|
||||
path: lan-mouse-macos-intel
|
||||
|
||||
macos-aarch64-release-build:
|
||||
runs-on: macos-14
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: install dependencies
|
||||
run: brew install gtk4 libadwaita
|
||||
- name: Release Build
|
||||
run: |
|
||||
cargo build --release
|
||||
cp target/release/lan-mouse lan-mouse-macos-aarch64
|
||||
- name: Upload build artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: lan-mouse-macos-aarch64
|
||||
path: lan-mouse-macos-aarch64
|
||||
|
||||
pre-release:
|
||||
name: "Pre Release"
|
||||
needs: [windows-release-build, linux-release-build, macos-release-build]
|
||||
runs-on: "ubuntu-latest"
|
||||
steps:
|
||||
- name: Download build artifacts
|
||||
uses: actions/download-artifact@v3
|
||||
uses: actions/download-artifact@v4
|
||||
- name: Create Release
|
||||
uses: "marvinpinto/action-automatic-releases@latest"
|
||||
with:
|
||||
@@ -109,5 +127,6 @@ jobs:
|
||||
title: "Development Build"
|
||||
files: |
|
||||
lan-mouse-linux/lan-mouse
|
||||
lan-mouse-macos/lan-mouse-macos-intel
|
||||
lan-mouse-macos-intel/lan-mouse-macos-intel
|
||||
lan-mouse-macos-aarch64/lan-mouse-macos-aarch64
|
||||
lan-mouse-windows/lan-mouse-windows.zip
|
||||
|
||||
38
.github/workflows/rust.yml
vendored
38
.github/workflows/rust.yml
vendored
@@ -15,7 +15,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
- name: install dependencies
|
||||
run: |
|
||||
sudo apt-get update
|
||||
@@ -30,7 +30,7 @@ jobs:
|
||||
- name: Clippy
|
||||
run: cargo clippy --all-features --all-targets -- --deny warnings
|
||||
- name: Upload build artifact
|
||||
uses: actions/upload-artifact@v3
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: lan-mouse
|
||||
path: target/debug/lan-mouse
|
||||
@@ -40,7 +40,7 @@ jobs:
|
||||
runs-on: windows-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: '3.11'
|
||||
@@ -66,7 +66,9 @@ jobs:
|
||||
# choco install msys2
|
||||
# choco install visualstudio2022-workload-vctools
|
||||
# choco install pkgconfiglite
|
||||
pipx install gvsbuild
|
||||
py -m venv .venv
|
||||
.venv\Scripts\activate.ps1
|
||||
py -m pip install gvsbuild
|
||||
# see https://github.com/wingtk/gvsbuild/pull/1004
|
||||
Move-Item "C:\Program Files\Git\usr\bin" "C:\Program Files\Git\usr\notbin"
|
||||
Move-Item "C:\Program Files\Git\bin" "C:\Program Files\Git\notbin"
|
||||
@@ -84,7 +86,7 @@ jobs:
|
||||
- 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@v3
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: lan-mouse-windows
|
||||
path: |
|
||||
@@ -92,9 +94,9 @@ jobs:
|
||||
target/debug/*.dll
|
||||
|
||||
build-macos:
|
||||
runs-on: macos-latest
|
||||
runs-on: macos-13
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
- name: install dependencies
|
||||
run: brew install gtk4 libadwaita
|
||||
- name: Build
|
||||
@@ -106,7 +108,27 @@ jobs:
|
||||
- name: Clippy
|
||||
run: cargo clippy --all-features --all-targets -- --deny warnings
|
||||
- name: Upload build artifact
|
||||
uses: actions/upload-artifact@v3
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: lan-mouse-macos
|
||||
path: target/debug/lan-mouse
|
||||
|
||||
build-macos-aarch64:
|
||||
runs-on: macos-14
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: install dependencies
|
||||
run: brew install gtk4 libadwaita
|
||||
- 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-macos-aarch64
|
||||
path: target/debug/lan-mouse
|
||||
|
||||
41
.github/workflows/tagged-release.yml
vendored
41
.github/workflows/tagged-release.yml
vendored
@@ -9,7 +9,7 @@ jobs:
|
||||
linux-release-build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
- name: install dependencies
|
||||
run: |
|
||||
sudo apt-get update
|
||||
@@ -18,7 +18,7 @@ jobs:
|
||||
- name: Release Build
|
||||
run: cargo build --release
|
||||
- name: Upload build artifact
|
||||
uses: actions/upload-artifact@v3
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: lan-mouse-linux
|
||||
path: target/release/lan-mouse
|
||||
@@ -51,14 +51,16 @@ jobs:
|
||||
# choco install msys2
|
||||
# choco install visualstudio2022-workload-vctools
|
||||
# choco install pkgconfiglite
|
||||
pipx install gvsbuild
|
||||
py -m venv .venv
|
||||
.venv\Scripts\activate.ps1
|
||||
py -m pip install gvsbuild
|
||||
# see https://github.com/wingtk/gvsbuild/pull/1004
|
||||
Move-Item "C:\Program Files\Git\usr\bin" "C:\Program Files\Git\usr\notbin"
|
||||
Move-Item "C:\Program Files\Git\bin" "C:\Program Files\Git\notbin"
|
||||
gvsbuild build --msys-dir=C:\msys64 gtk4 libadwaita librsvg
|
||||
Move-Item "C:\Program Files\Git\usr\notbin" "C:\Program Files\Git\usr\bin"
|
||||
Move-Item "C:\Program Files\Git\notbin" "C:\Program Files\Git\bin"
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
- name: Release Build
|
||||
run: cargo build --release
|
||||
- name: Create Archive
|
||||
@@ -68,15 +70,15 @@ jobs:
|
||||
Copy-Item -Path "target\release\lan-mouse.exe" -Destination "lan-mouse-windows"
|
||||
Compress-Archive -Path "lan-mouse-windows\*" -DestinationPath lan-mouse-windows.zip
|
||||
- name: Upload build artifact
|
||||
uses: actions/upload-artifact@v3
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: lan-mouse-windows
|
||||
path: lan-mouse-windows.zip
|
||||
|
||||
macos-release-build:
|
||||
runs-on: macos-latest
|
||||
runs-on: macos-13
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
- name: install dependencies
|
||||
run: brew install gtk4 libadwaita
|
||||
- name: Release Build
|
||||
@@ -84,18 +86,34 @@ jobs:
|
||||
cargo build --release
|
||||
cp target/release/lan-mouse lan-mouse-macos-intel
|
||||
- name: Upload build artifact
|
||||
uses: actions/upload-artifact@v3
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: lan-mouse-macos
|
||||
name: lan-mouse-macos-intel
|
||||
path: lan-mouse-macos-intel
|
||||
|
||||
macos-aarch64-release-build:
|
||||
runs-on: macos-14
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: install dependencies
|
||||
run: brew install gtk4 libadwaita
|
||||
- name: Release Build
|
||||
run: |
|
||||
cargo build --release
|
||||
cp target/release/lan-mouse lan-mouse-macos-aarch64
|
||||
- name: Upload build artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: lan-mouse-macos-aarch64
|
||||
path: lan-mouse-macos-aarch64
|
||||
|
||||
tagged-release:
|
||||
name: "Tagged Release"
|
||||
needs: [windows-release-build, linux-release-build, macos-release-build]
|
||||
runs-on: "ubuntu-latest"
|
||||
steps:
|
||||
- name: Download build artifacts
|
||||
uses: actions/download-artifact@v3
|
||||
uses: actions/download-artifact@v4
|
||||
- name: "Create Release"
|
||||
uses: "marvinpinto/action-automatic-releases@latest"
|
||||
with:
|
||||
@@ -103,5 +121,6 @@ jobs:
|
||||
prerelease: false
|
||||
files: |
|
||||
lan-mouse-linux/lan-mouse
|
||||
lan-mouse-macos/lan-mouse-macos-intel
|
||||
lan-mouse-macos-intel/lan-mouse-macos-intel
|
||||
lan-mouse-macos-aarch64/lan-mouse-macos-aarch64
|
||||
lan-mouse-windows/lan-mouse-windows.zip
|
||||
|
||||
5
.gitignore
vendored
5
.gitignore
vendored
@@ -1,4 +1,7 @@
|
||||
/target
|
||||
.gdbinit
|
||||
.idea/
|
||||
.vs/
|
||||
.vs/
|
||||
.vscode/
|
||||
.direnv/
|
||||
result
|
||||
1613
Cargo.lock
generated
1613
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
82
Cargo.toml
82
Cargo.toml
@@ -1,61 +1,65 @@
|
||||
[workspace]
|
||||
members = [
|
||||
"input-capture",
|
||||
"input-emulation",
|
||||
"input-event",
|
||||
"lan-mouse-ipc",
|
||||
"lan-mouse-cli",
|
||||
"lan-mouse-gtk",
|
||||
"lan-mouse-proto",
|
||||
]
|
||||
|
||||
[package]
|
||||
name = "lan-mouse"
|
||||
description = "Software KVM Switch / mouse & keyboard sharing software for Local Area Networks"
|
||||
version = "0.5.1"
|
||||
version = "0.9.1"
|
||||
edition = "2021"
|
||||
license = "GPL-3.0-or-later"
|
||||
repository = "https://github.com/ferdinandschober/lan-mouse"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
repository = "https://github.com/feschber/lan-mouse"
|
||||
|
||||
[profile.release]
|
||||
strip = true
|
||||
lto = "fat"
|
||||
|
||||
[dependencies]
|
||||
tempfile = "3.8"
|
||||
trust-dns-resolver = "0.23"
|
||||
memmap = "0.7"
|
||||
input-event = { path = "input-event", version = "0.2.1" }
|
||||
input-emulation = { path = "input-emulation", version = "0.2.1", default-features = false }
|
||||
input-capture = { path = "input-capture", version = "0.2.0", default-features = false }
|
||||
lan-mouse-cli = { path = "lan-mouse-cli", version = "0.1.0" }
|
||||
lan-mouse-gtk = { path = "lan-mouse-gtk", version = "0.1.0", optional = true }
|
||||
lan-mouse-ipc = { path = "lan-mouse-ipc", version = "0.1.0" }
|
||||
lan-mouse-proto = { path = "lan-mouse-proto", version = "0.1.0" }
|
||||
|
||||
hickory-resolver = "0.24.1"
|
||||
toml = "0.8"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
anyhow = "1.0.71"
|
||||
log = "0.4.20"
|
||||
env_logger = "0.10.0"
|
||||
env_logger = "0.11.3"
|
||||
serde_json = "1.0.107"
|
||||
tokio = {version = "1.32.0", features = ["io-util", "macros", "net", "rt", "sync", "signal"] }
|
||||
async-trait = "0.1.73"
|
||||
futures-core = "0.3.28"
|
||||
tokio = { version = "1.32.0", features = [
|
||||
"io-util",
|
||||
"io-std",
|
||||
"macros",
|
||||
"net",
|
||||
"process",
|
||||
"rt",
|
||||
"sync",
|
||||
"signal",
|
||||
] }
|
||||
futures = "0.3.28"
|
||||
clap = { version="4.4.11", features = ["derive"] }
|
||||
gtk = { package = "gtk4", version = "0.7.2", features = ["v4_2"], optional = true }
|
||||
adw = { package = "libadwaita", version = "0.5.2", features = ["v1_1"], optional = true }
|
||||
async-channel = { version = "2.1.1", optional = true }
|
||||
clap = { version = "4.4.11", features = ["derive"] }
|
||||
slab = "0.4.9"
|
||||
thiserror = "1.0.61"
|
||||
tokio-util = "0.7.11"
|
||||
local-channel = "0.1.5"
|
||||
|
||||
[target.'cfg(unix)'.dependencies]
|
||||
libc = "0.2.148"
|
||||
|
||||
[target.'cfg(all(unix, not(target_os="macos")))'.dependencies]
|
||||
wayland-client = { version="0.31.1", optional = true }
|
||||
wayland-protocols = { version="0.31.0", features=["client", "staging", "unstable"], optional = true }
|
||||
wayland-protocols-wlr = { version="0.2.0", features=["client"], optional = true }
|
||||
wayland-protocols-misc = { version="0.2.0", features=["client"], optional = true }
|
||||
x11 = { version = "2.21.0", features = ["xlib", "xtest"], optional = true }
|
||||
ashpd = { version = "0.6.2", default-features = false, features = ["tokio"], optional = true }
|
||||
reis = { git = "https://github.com/ids1024/reis", features = [ "tokio" ], optional = true }
|
||||
|
||||
[target.'cfg(target_os="macos")'.dependencies]
|
||||
core-graphics = { version = "0.23", features = ["highsierra"] }
|
||||
|
||||
[target.'cfg(windows)'.dependencies]
|
||||
winapi = { version = "0.3.9", features = ["winuser"] }
|
||||
|
||||
[build-dependencies]
|
||||
glib-build-tools = "0.18.0"
|
||||
|
||||
[features]
|
||||
default = ["wayland", "x11", "xdg_desktop_portal", "libei", "gtk"]
|
||||
wayland = ["dep:wayland-client", "dep:wayland-protocols", "dep:wayland-protocols-wlr", "dep:wayland-protocols-misc" ]
|
||||
x11 = ["dep:x11"]
|
||||
xdg_desktop_portal = ["dep:ashpd"]
|
||||
libei = ["dep:reis", "dep:ashpd"]
|
||||
gtk = ["dep:gtk", "dep:adw", "dep:async-channel"]
|
||||
wayland = ["input-capture/wayland", "input-emulation/wayland"]
|
||||
x11 = ["input-capture/x11", "input-emulation/x11"]
|
||||
xdg_desktop_portal = ["input-emulation/xdg_desktop_portal"]
|
||||
libei = ["input-event/libei", "input-capture/libei", "input-emulation/libei"]
|
||||
gtk = ["dep:lan-mouse-gtk"]
|
||||
|
||||
235
README.md
235
README.md
@@ -20,7 +20,7 @@ Focus lies on performance and a clean, manageable implementation that can easily
|
||||
|
||||
***blazingly fast™*** because it's written in rust.
|
||||
|
||||
For an alternative (with slightly different goals) you may check out [Input Leap](https://github.com/input-leap).
|
||||
For an alternative (with slightly different goals) you may check out [Synergy 1 Community Edition](https://github.com/symless/synergy) or [Input Leap](https://github.com/input-leap) (Synergy fork).
|
||||
|
||||
|
||||
> [!WARNING]
|
||||
@@ -39,88 +39,73 @@ For an alternative (with slightly different goals) you may check out [Input Leap
|
||||
The following table shows support for input emulation (to emulate events received from other clients) and
|
||||
input capture (to send events *to* other clients) on different operating systems:
|
||||
|
||||
| Backend | input emulation | input capture |
|
||||
| OS / Desktop Environment | input emulation | input capture |
|
||||
|---------------------------|--------------------------|--------------------------------------|
|
||||
| Wayland (wlroots) | :heavy_check_mark: | :heavy_check_mark: |
|
||||
| Wayland (KDE) | :heavy_check_mark: | :heavy_check_mark: |
|
||||
| Wayland (Gnome) | :heavy_check_mark: | WIP |
|
||||
| Wayland (Gnome) | :heavy_check_mark: | :heavy_check_mark: (starting at GNOME 45) |
|
||||
| Windows | :heavy_check_mark: | :heavy_check_mark: |
|
||||
| X11 | :heavy_check_mark: | WIP |
|
||||
| Windows | :heavy_check_mark: | WIP |
|
||||
| MacOS | ( :heavy_check_mark: ) | WIP |
|
||||
|
||||
Keycode translation is not yet implemented so on MacOS only mouse emulation works as of right now.
|
||||
| MacOS | :heavy_check_mark: | WIP |
|
||||
|
||||
> [!Important]
|
||||
> Gnome -> Sway only partially works (modifier events are not handled correctly)
|
||||
|
||||
> [!Important]
|
||||
> **Wayfire**
|
||||
>
|
||||
> If you are using [Wayfire](https://github.com/WayfireWM/wayfire), make sure to use a recent version (must be newer than October 23rd) and **add `shortcuts-inhibit` to the list of plugins in your wayfire config!**
|
||||
> Otherwise input capture will not work.
|
||||
|
||||
## Build and Run
|
||||
> [!Important]
|
||||
> The mouse cursor will be invisible when sending input to a Windows system if
|
||||
> there is no real mouse connected to the machine.
|
||||
|
||||
### Install Dependencies
|
||||
#### Macos
|
||||
## Installation
|
||||
### Install via cargo
|
||||
```sh
|
||||
brew install libadwaita
|
||||
cargo install lan-mouse
|
||||
```
|
||||
|
||||
#### Ubuntu and derivatives
|
||||
```sh
|
||||
sudo apt install libadwaita-1-dev libgtk-4-dev libx11-dev libxtst-dev
|
||||
```
|
||||
### Download from Releases
|
||||
Precompiled release binaries for Windows, MacOS and Linux are available in the [releases section](https://github.com/feschber/lan-mouse/releases).
|
||||
|
||||
#### Arch and derivatives
|
||||
```sh
|
||||
sudo pacman -S libadwaita gtk libx11 libxtst
|
||||
```
|
||||
For Windows, the depenedencies are included in the .zip file, for other operating systems see [Installing Dependencies](#installing-dependencies).
|
||||
|
||||
#### Fedora and derivatives
|
||||
```sh
|
||||
sudo dnf install libadwaita-devel libXtst-devel libX11-devel
|
||||
```
|
||||
### Arch Linux
|
||||
|
||||
#### Windows
|
||||
Follow the instructions at [gtk-rs.org](https://gtk-rs.org/gtk4-rs/stable/latest/book/installation_windows.html)
|
||||
|
||||
*TLDR:*
|
||||
|
||||
Build gtk from source
|
||||
|
||||
- The following commands should be run in an admin power shell instance:
|
||||
```sh
|
||||
# install chocolatey
|
||||
Set-ExecutionPolicy Bypass -Scope Process -Force; iex ((New-Object System.Net.WebClient).DownloadString('https://community.chocolatey.org/install.ps1'))
|
||||
|
||||
# install python 3.11 (Version is important, as 3.12 does not work currently)
|
||||
choco install python --version=3.11.0
|
||||
|
||||
# install git
|
||||
choco install git
|
||||
|
||||
# install msys2
|
||||
choco install msys2
|
||||
|
||||
# install Visual Studio 2022
|
||||
choco install visualstudio2022-workload-vctools
|
||||
```
|
||||
|
||||
- The following commands should be run in a regular power shell instance:
|
||||
Lan Mouse can be installed from the [official repositories](https://archlinux.org/packages/extra/x86_64/lan-mouse/):
|
||||
|
||||
```sh
|
||||
# install gvsbuild with python
|
||||
python -m pip install --user pipx
|
||||
python -m pipx ensurepath
|
||||
pipx install gvsbuild
|
||||
|
||||
# build gtk + libadwaita
|
||||
gvsbuild build gtk4 libadwaita librsvg
|
||||
pacman -S lan-mouse
|
||||
```
|
||||
|
||||
Make sure to add the directory `C:\gtk-build\gtk\x64\release\bin`
|
||||
[to the `PATH` environment variable]((https://learn.microsoft.com/en-us/previous-versions/office/developer/sharepoint-2010/ee537574(v=office.14))). Otherwise the project will fail to build.
|
||||
It is also available on the AUR:
|
||||
|
||||
To avoid building GTK from source, it is possible to disable
|
||||
the gtk frontend (see conditional compilation below).
|
||||
```sh
|
||||
# git version (includes latest changes)
|
||||
paru -S lan-mouse-git
|
||||
|
||||
# alternatively
|
||||
paru -S lan-mouse-bin
|
||||
```
|
||||
|
||||
### Nix
|
||||
- nixpkgs: [search.nixos.org](https://search.nixos.org/packages?channel=unstable&show=lan-mouse&from=0&size=50&sort=relevance&type=packages&query=lan-mouse)
|
||||
- flake: [README.md](./nix/README.md)
|
||||
|
||||
### Building from source with Nix
|
||||
In the root of the project to build Lan Mouse, run
|
||||
```sh
|
||||
nix-build
|
||||
```
|
||||
|
||||
You can find the executable in `result/bin/lan-mouse` .
|
||||
|
||||
### Manual Installation
|
||||
|
||||
First make sure to [install the necessary dependencies](#installing-dependencies).
|
||||
|
||||
### Build and run
|
||||
Build in release mode:
|
||||
```sh
|
||||
cargo build --release
|
||||
@@ -131,6 +116,27 @@ Run directly:
|
||||
cargo run --release
|
||||
```
|
||||
|
||||
Install the files:
|
||||
```sh
|
||||
# install lan-mouse
|
||||
sudo cp target/release/lan-mouse /usr/local/bin/
|
||||
|
||||
# install app icon
|
||||
sudo mkdir -p /usr/local/share/icons/hicolor/scalable/apps
|
||||
sudo cp lan-mouse-gtk/resources/de.feschber.LanMouse.svg /usr/local/share/icons/hicolor/scalable/apps
|
||||
|
||||
# update icon cache
|
||||
gtk-update-icon-cache /usr/local/share/icons/hicolor/
|
||||
|
||||
# install desktop entry
|
||||
sudo mkdir -p /usr/local/share/applications
|
||||
sudo cp de.feschber.LanMouse.desktop /usr/local/share/applications
|
||||
|
||||
# when using firewalld: install firewall rule
|
||||
sudo cp firewall/lan-mouse.xml /etc/firewalld/services
|
||||
# -> enable the service in firewalld settings
|
||||
```
|
||||
|
||||
### Conditional Compilation
|
||||
|
||||
Currently only x11, wayland, windows and MacOS are supported backends.
|
||||
@@ -151,6 +157,85 @@ cargo build --no-default-features --features wayland
|
||||
```
|
||||
For a detailed list of available features, checkout the [Cargo.toml](./Cargo.toml)
|
||||
|
||||
|
||||
## Installing Dependencies
|
||||
<details>
|
||||
<summary>MacOS</summary>
|
||||
|
||||
```sh
|
||||
brew install libadwaita
|
||||
```
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>Ubuntu and derivatives</summary>
|
||||
|
||||
```sh
|
||||
sudo apt install libadwaita-1-dev libgtk-4-dev libx11-dev libxtst-dev
|
||||
```
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>Arch and derivatives</summary>
|
||||
|
||||
```sh
|
||||
sudo pacman -S libadwaita gtk libx11 libxtst
|
||||
```
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>Fedora and derivatives</summary>
|
||||
|
||||
```sh
|
||||
sudo dnf install libadwaita-devel libXtst-devel libX11-devel
|
||||
```
|
||||
</details>
|
||||
<details>
|
||||
<summary>Windows</summary>
|
||||
|
||||
> [!NOTE]
|
||||
> This is only necessary when building lan-mouse from source. The windows release comes with precompiled gtk dlls.
|
||||
|
||||
- First install [Rust](https://www.rust-lang.org/tools/install).
|
||||
|
||||
- Then follow the instructions at [gtk-rs.org](https://gtk-rs.org/gtk4-rs/stable/latest/book/installation_windows.html)
|
||||
|
||||
*TLDR:*
|
||||
|
||||
Build gtk from source
|
||||
|
||||
- The following commands should be run in an **admin power shell** instance:
|
||||
```sh
|
||||
# install chocolatey
|
||||
Set-ExecutionPolicy Bypass -Scope Process -Force; iex ((New-Object System.Net.WebClient).DownloadString('https://community.chocolatey.org/install.ps1'))
|
||||
|
||||
# install gvsbuild dependencies
|
||||
choco install python git msys2 visualstudio2022-workload-vctools
|
||||
```
|
||||
|
||||
- The following commands should be run in a **regular power shell** instance:
|
||||
|
||||
```sh
|
||||
# install gvsbuild with python
|
||||
python -m pip install --user pipx
|
||||
python -m pipx ensurepath
|
||||
```
|
||||
|
||||
- Relaunch your powershell instance so the changes in the environment are reflected.
|
||||
```sh
|
||||
pipx install gvsbuild
|
||||
|
||||
# build gtk + libadwaita
|
||||
gvsbuild build gtk4 libadwaita librsvg
|
||||
```
|
||||
|
||||
- **Make sure to add the directory** `C:\gtk-build\gtk\x64\release\bin`
|
||||
[**to the `PATH` environment variable**]((https://learn.microsoft.com/en-us/previous-versions/office/developer/sharepoint-2010/ee537574(v=office.14))). Otherwise the project will fail to build.
|
||||
|
||||
To avoid building GTK from source, it is possible to disable
|
||||
the gtk frontend (see conditional compilation below).
|
||||
</details>
|
||||
|
||||
## Usage
|
||||
### Gtk Frontend
|
||||
By default the gtk frontend will open when running `lan-mouse`.
|
||||
@@ -184,6 +269,17 @@ To do so, add `--daemon` to the commandline args:
|
||||
$ cargo run --release -- --daemon
|
||||
```
|
||||
|
||||
In order to start lan-mouse with a graphical session automatically,
|
||||
the [systemd-service](service/lan-mouse.service) can be used:
|
||||
|
||||
Copy the file to `~/.config/systemd/user/` and enable the service:
|
||||
|
||||
```sh
|
||||
cp service/lan-mouse.service ~/.config/systemd/user
|
||||
systemctl --user daemon-reload
|
||||
systemctl --user enable --now lan-mouse.service
|
||||
```
|
||||
|
||||
## Configuration
|
||||
To automatically load clients on startup, the file `$XDG_CONFIG_HOME/lan-mouse/config.toml` is parsed.
|
||||
`$XDG_CONFIG_HOME` defaults to `~/.config/`.
|
||||
@@ -191,10 +287,17 @@ To automatically load clients on startup, the file `$XDG_CONFIG_HOME/lan-mouse/c
|
||||
To create this file you can copy the following example config:
|
||||
|
||||
### Example config
|
||||
> [!TIP]
|
||||
> key symbols in the release bind are named according
|
||||
> to their names in [src/scancode.rs#L172](src/scancode.rs#L172).
|
||||
> This is bound to change
|
||||
|
||||
```toml
|
||||
# example configuration
|
||||
|
||||
# configure release bind
|
||||
release_bind = [ "KeyA", "KeyS", "KeyD", "KeyF" ]
|
||||
|
||||
# optional port (defaults to 4242)
|
||||
port = 4242
|
||||
# # optional frontend -> defaults to gtk if available
|
||||
@@ -204,7 +307,9 @@ port = 4242
|
||||
# define a client on the right side with host name "iridium"
|
||||
[right]
|
||||
# hostname
|
||||
host_name = "iridium"
|
||||
hostname = "iridium"
|
||||
# activate this client immediately when lan-mouse is started
|
||||
activate_on_startup = true
|
||||
# optional list of (known) ip addresses
|
||||
ips = ["192.168.178.156"]
|
||||
|
||||
@@ -212,7 +317,7 @@ ips = ["192.168.178.156"]
|
||||
[left]
|
||||
# The hostname is optional: When no hostname is specified,
|
||||
# at least one ip address needs to be specified.
|
||||
host_name = "thorium"
|
||||
hostname = "thorium"
|
||||
# ips for ethernet and wifi
|
||||
ips = ["192.168.178.189", "192.168.178.172"]
|
||||
# optional port
|
||||
@@ -227,11 +332,11 @@ Where `left` can be either `left`, `right`, `top` or `bottom`.
|
||||
- [x] IP Address switching
|
||||
- [x] Liveness tracking Automatically ungrab mouse when client unreachable
|
||||
- [x] Liveness tracking: Automatically release keys, when server offline
|
||||
- [ ] Libei Input Capture
|
||||
- [x] MacOS KeyCode Translation
|
||||
- [x] Libei Input Capture
|
||||
- [ ] X11 Input Capture
|
||||
- [ ] Windows Input Capture
|
||||
- [ ] MacOS Input Capture
|
||||
- [ ] MacOS KeyCode Translation
|
||||
- [ ] Latency measurement and visualization
|
||||
- [ ] Bandwidth usage measurement and visualization
|
||||
- [ ] Clipboard support
|
||||
|
||||
19
build.rs
19
build.rs
@@ -1,8 +1,15 @@
|
||||
use std::process::Command;
|
||||
|
||||
fn main() {
|
||||
// composite_templates
|
||||
glib_build_tools::compile_resources(
|
||||
&["resources"],
|
||||
"resources/resources.gresource.xml",
|
||||
"lan-mouse.gresource",
|
||||
);
|
||||
// commit hash
|
||||
let git_describe = Command::new("git")
|
||||
.arg("describe")
|
||||
.arg("--always")
|
||||
.arg("--dirty")
|
||||
.arg("--tags")
|
||||
.output()
|
||||
.unwrap();
|
||||
|
||||
let git_describe = String::from_utf8(git_describe.stdout).unwrap();
|
||||
println!("cargo::rustc-env=GIT_DESCRIBE={git_describe}");
|
||||
}
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
# example configuration
|
||||
|
||||
# capture_backend = "LayerShell"
|
||||
|
||||
# release bind
|
||||
release_bind = [ "KeyA", "KeyS", "KeyD", "KeyF" ]
|
||||
|
||||
# optional port (defaults to 4242)
|
||||
port = 4242
|
||||
# optional frontend -> defaults to gtk if available
|
||||
@@ -8,7 +13,7 @@ port = 4242
|
||||
# define a client on the right side with host name "iridium"
|
||||
[right]
|
||||
# hostname
|
||||
host_name = "iridium"
|
||||
hostname = "iridium"
|
||||
# optional list of (known) ip addresses
|
||||
ips = ["192.168.178.156"]
|
||||
|
||||
@@ -16,7 +21,7 @@ ips = ["192.168.178.156"]
|
||||
[left]
|
||||
# The hostname is optional: When no hostname is specified,
|
||||
# at least one ip address needs to be specified.
|
||||
host_name = "thorium"
|
||||
hostname = "thorium"
|
||||
# ips for ethernet and wifi
|
||||
ips = ["192.168.178.189", "192.168.178.172"]
|
||||
# optional port
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
[Desktop Entry]
|
||||
Categories=Utility;
|
||||
Comment[en_US]=mouse & keyboard sharing via LAN
|
||||
Comment[en_US]=Mouse & Keyboard sharing via LAN
|
||||
Comment=Mouse & Keyboard sharing via LAN
|
||||
Comment[de_DE]=Maus- und Tastaturfreigabe über LAN
|
||||
Exec=lan-mouse
|
||||
Icon=mouse-icon.svg
|
||||
Icon=de.feschber.LanMouse
|
||||
Name[en_US]=Lan Mouse
|
||||
Name[de_DE]=Lan Maus
|
||||
Name=Lan Mouse
|
||||
StartupNotify=true
|
||||
Terminal=false
|
||||
Type=Application
|
||||
Version=0.5.1
|
||||
3
default.nix
Normal file
3
default.nix
Normal file
@@ -0,0 +1,3 @@
|
||||
{ pkgs ? import <nixpkgs> { }
|
||||
}:
|
||||
pkgs.callPackage nix/default.nix { }
|
||||
8
firewall/lan-mouse.xml
Normal file
8
firewall/lan-mouse.xml
Normal file
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- for packaging: /usr/lib/firewalld/services/lan-mouse.xml -->
|
||||
<!-- configure manually: /etc/firewalld/services/lan-mouse.xml -->
|
||||
<service>
|
||||
<short>LAN Mouse</short>
|
||||
<description>mouse and keyboard sharing via LAN</description>
|
||||
<port port="4242" protocol="udp"/>
|
||||
</service>
|
||||
48
flake.lock
generated
Normal file
48
flake.lock
generated
Normal file
@@ -0,0 +1,48 @@
|
||||
{
|
||||
"nodes": {
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1728018373,
|
||||
"narHash": "sha256-NOiTvBbRLIOe5F6RbHaAh6++BNjsb149fGZd1T4+KBg=",
|
||||
"owner": "nixos",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "bc947f541ae55e999ffdb4013441347d83b00feb",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "nixos",
|
||||
"ref": "nixos-unstable",
|
||||
"repo": "nixpkgs",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"root": {
|
||||
"inputs": {
|
||||
"nixpkgs": "nixpkgs",
|
||||
"rust-overlay": "rust-overlay"
|
||||
}
|
||||
},
|
||||
"rust-overlay": {
|
||||
"inputs": {
|
||||
"nixpkgs": [
|
||||
"nixpkgs"
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1728181869,
|
||||
"narHash": "sha256-sQXHXsjIcGEoIHkB+RO6BZdrPfB+43V1TEpyoWRI3ww=",
|
||||
"owner": "oxalica",
|
||||
"repo": "rust-overlay",
|
||||
"rev": "cd46aa3906c14790ef5cbe278d9e54f2c38f95c0",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "oxalica",
|
||||
"repo": "rust-overlay",
|
||||
"type": "github"
|
||||
}
|
||||
}
|
||||
},
|
||||
"root": "root",
|
||||
"version": 7
|
||||
}
|
||||
66
flake.nix
Normal file
66
flake.nix
Normal file
@@ -0,0 +1,66 @@
|
||||
{
|
||||
description = "Nix Flake for lan-mouse";
|
||||
inputs = {
|
||||
nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable";
|
||||
rust-overlay = {
|
||||
url = "github:oxalica/rust-overlay";
|
||||
inputs.nixpkgs.follows = "nixpkgs";
|
||||
};
|
||||
};
|
||||
outputs = {
|
||||
self,
|
||||
nixpkgs,
|
||||
rust-overlay,
|
||||
...
|
||||
}: let
|
||||
inherit (nixpkgs) lib;
|
||||
genSystems = lib.genAttrs [
|
||||
"aarch64-darwin"
|
||||
"aarch64-linux"
|
||||
"x86_64-darwin"
|
||||
"x86_64-linux"
|
||||
];
|
||||
pkgsFor = system:
|
||||
import nixpkgs {
|
||||
inherit system;
|
||||
|
||||
overlays = [
|
||||
rust-overlay.overlays.default
|
||||
];
|
||||
};
|
||||
mkRustToolchain = pkgs:
|
||||
pkgs.rust-bin.stable.latest.default.override {
|
||||
extensions = ["rust-src"];
|
||||
};
|
||||
pkgs = genSystems (system: import nixpkgs {inherit system;});
|
||||
in {
|
||||
packages = genSystems (system: rec {
|
||||
default = pkgs.${system}.callPackage ./nix {};
|
||||
lan-mouse = default;
|
||||
});
|
||||
homeManagerModules.default = import ./nix/hm-module.nix self;
|
||||
devShells = genSystems (system: let
|
||||
pkgs = pkgsFor system;
|
||||
rust = mkRustToolchain pkgs;
|
||||
in {
|
||||
default = pkgs.mkShell {
|
||||
packages = with pkgs; [
|
||||
rust
|
||||
rust-analyzer-unwrapped
|
||||
pkg-config
|
||||
xorg.libX11
|
||||
gtk4
|
||||
libadwaita
|
||||
librsvg
|
||||
xorg.libXtst
|
||||
] ++ lib.optionals stdenv.isDarwin
|
||||
(with darwin.apple_sdk_11_0.frameworks; [
|
||||
CoreGraphics
|
||||
ApplicationServices
|
||||
]);
|
||||
|
||||
RUST_SRC_PATH = "${rust}/lib/rustlib/src/rust/library";
|
||||
};
|
||||
});
|
||||
};
|
||||
}
|
||||
75
input-capture/Cargo.toml
Normal file
75
input-capture/Cargo.toml
Normal file
@@ -0,0 +1,75 @@
|
||||
[package]
|
||||
name = "input-capture"
|
||||
description = "cross-platform input-capture library used by lan-mouse"
|
||||
version = "0.2.0"
|
||||
edition = "2021"
|
||||
license = "GPL-3.0-or-later"
|
||||
repository = "https://github.com/feschber/lan-mouse"
|
||||
|
||||
[dependencies]
|
||||
futures = "0.3.28"
|
||||
futures-core = "0.3.30"
|
||||
log = "0.4.22"
|
||||
input-event = { path = "../input-event", version = "0.2.1" }
|
||||
memmap = "0.7"
|
||||
tempfile = "3.8"
|
||||
thiserror = "1.0.61"
|
||||
tokio = { version = "1.32.0", features = [
|
||||
"io-util",
|
||||
"io-std",
|
||||
"macros",
|
||||
"net",
|
||||
"process",
|
||||
"rt",
|
||||
"sync",
|
||||
"signal",
|
||||
] }
|
||||
once_cell = "1.19.0"
|
||||
async-trait = "0.1.81"
|
||||
tokio-util = "0.7.11"
|
||||
|
||||
|
||||
[target.'cfg(all(unix, not(target_os="macos")))'.dependencies]
|
||||
wayland-client = { version = "0.31.1", optional = true }
|
||||
wayland-protocols = { version = "0.32.1", features = [
|
||||
"client",
|
||||
"staging",
|
||||
"unstable",
|
||||
], optional = true }
|
||||
wayland-protocols-wlr = { version = "0.3.1", features = [
|
||||
"client",
|
||||
], optional = true }
|
||||
x11 = { version = "2.21.0", features = ["xlib", "xtest"], optional = true }
|
||||
ashpd = { version = "0.9", default-features = false, features = [
|
||||
"tokio",
|
||||
], optional = true }
|
||||
reis = { version = "0.2", features = ["tokio"], optional = true }
|
||||
|
||||
[target.'cfg(target_os="macos")'.dependencies]
|
||||
core-graphics = { version = "0.23", features = ["highsierra"] }
|
||||
core-foundation = "0.9.4"
|
||||
core-foundation-sys = "0.8.6"
|
||||
libc = "0.2.155"
|
||||
keycode = "0.4.0"
|
||||
bitflags = "2.5.0"
|
||||
|
||||
[target.'cfg(windows)'.dependencies]
|
||||
windows = { version = "0.58.0", features = [
|
||||
"Win32_System_LibraryLoader",
|
||||
"Win32_System_Threading",
|
||||
"Win32_Foundation",
|
||||
"Win32_Graphics",
|
||||
"Win32_Graphics_Gdi",
|
||||
"Win32_UI_Input_KeyboardAndMouse",
|
||||
"Win32_UI_WindowsAndMessaging",
|
||||
] }
|
||||
|
||||
[features]
|
||||
default = ["wayland", "x11", "libei"]
|
||||
wayland = [
|
||||
"dep:wayland-client",
|
||||
"dep:wayland-protocols",
|
||||
"dep:wayland-protocols-wlr",
|
||||
]
|
||||
x11 = ["dep:x11"]
|
||||
libei = ["dep:reis", "dep:ashpd"]
|
||||
86
input-capture/src/dummy.rs
Normal file
86
input-capture/src/dummy.rs
Normal file
@@ -0,0 +1,86 @@
|
||||
use std::f64::consts::PI;
|
||||
use std::pin::Pin;
|
||||
use std::task::{ready, Context, Poll};
|
||||
use std::time::Duration;
|
||||
|
||||
use async_trait::async_trait;
|
||||
use futures_core::Stream;
|
||||
use input_event::PointerEvent;
|
||||
use tokio::time::{self, Instant, Interval};
|
||||
|
||||
use super::{Capture, CaptureError, CaptureEvent, Position};
|
||||
|
||||
pub struct DummyInputCapture {
|
||||
start: Option<Instant>,
|
||||
interval: Interval,
|
||||
offset: (i32, i32),
|
||||
}
|
||||
|
||||
impl DummyInputCapture {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
start: None,
|
||||
interval: time::interval(Duration::from_millis(1)),
|
||||
offset: (0, 0),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for DummyInputCapture {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl Capture for DummyInputCapture {
|
||||
async fn create(&mut self, _pos: Position) -> Result<(), CaptureError> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn destroy(&mut self, _pos: Position) -> Result<(), CaptureError> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn release(&mut self) -> Result<(), CaptureError> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn terminate(&mut self) -> Result<(), CaptureError> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
const FREQUENCY_HZ: f64 = 1.0;
|
||||
const RADIUS: f64 = 100.0;
|
||||
|
||||
impl Stream for DummyInputCapture {
|
||||
type Item = Result<(Position, CaptureEvent), CaptureError>;
|
||||
|
||||
fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
|
||||
let current = ready!(self.interval.poll_tick(cx));
|
||||
let event = match self.start {
|
||||
None => {
|
||||
self.start.replace(current);
|
||||
CaptureEvent::Begin
|
||||
}
|
||||
Some(start) => {
|
||||
let elapsed = start.elapsed();
|
||||
let elapsed_sec_f64 = elapsed.as_secs_f64();
|
||||
let second_fraction = elapsed_sec_f64 - elapsed_sec_f64 as u64 as f64;
|
||||
let radians = second_fraction * 2. * PI * FREQUENCY_HZ;
|
||||
let offset = (radians.cos() * RADIUS * 2., (radians * 2.).sin() * RADIUS);
|
||||
let offset = (offset.0 as i32, offset.1 as i32);
|
||||
let relative_motion = (offset.0 - self.offset.0, offset.1 - self.offset.1);
|
||||
self.offset = offset;
|
||||
let (dx, dy) = (relative_motion.0 as f64, relative_motion.1 as f64);
|
||||
CaptureEvent::Input(input_event::Event::Pointer(PointerEvent::Motion {
|
||||
time: 0,
|
||||
dx,
|
||||
dy,
|
||||
}))
|
||||
}
|
||||
};
|
||||
Poll::Ready(Some(Ok((Position::Left, event))))
|
||||
}
|
||||
}
|
||||
176
input-capture/src/error.rs
Normal file
176
input-capture/src/error.rs
Normal file
@@ -0,0 +1,176 @@
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum InputCaptureError {
|
||||
#[error("error creating input-capture: `{0}`")]
|
||||
Create(#[from] CaptureCreationError),
|
||||
#[error("error while capturing input: `{0}`")]
|
||||
Capture(#[from] CaptureError),
|
||||
}
|
||||
|
||||
#[cfg(all(unix, feature = "wayland", not(target_os = "macos")))]
|
||||
use std::io;
|
||||
#[cfg(all(unix, feature = "wayland", not(target_os = "macos")))]
|
||||
use wayland_client::{
|
||||
backend::WaylandError,
|
||||
globals::{BindError, GlobalError},
|
||||
ConnectError, DispatchError,
|
||||
};
|
||||
|
||||
#[cfg(all(unix, feature = "libei", not(target_os = "macos")))]
|
||||
use ashpd::desktop::ResponseError;
|
||||
#[cfg(all(unix, feature = "libei", not(target_os = "macos")))]
|
||||
use reis::tokio::{EiConvertEventStreamError, HandshakeError};
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
use core_graphics::base::CGError;
|
||||
|
||||
#[cfg(all(unix, feature = "libei", not(target_os = "macos")))]
|
||||
#[derive(Debug, Error)]
|
||||
#[error("error in libei stream: {inner:?}")]
|
||||
pub struct ReisConvertEventStreamError {
|
||||
inner: EiConvertEventStreamError,
|
||||
}
|
||||
|
||||
#[cfg(all(unix, feature = "libei", not(target_os = "macos")))]
|
||||
impl From<EiConvertEventStreamError> for ReisConvertEventStreamError {
|
||||
fn from(e: EiConvertEventStreamError) -> Self {
|
||||
Self { inner: e }
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum CaptureError {
|
||||
#[error("activation stream closed unexpectedly")]
|
||||
ActivationClosed,
|
||||
#[error("libei stream was closed")]
|
||||
EndOfStream,
|
||||
#[error("io error: `{0}`")]
|
||||
Io(#[from] std::io::Error),
|
||||
#[cfg(all(unix, feature = "libei", not(target_os = "macos")))]
|
||||
#[error("error in libei stream: `{0}`")]
|
||||
Reis(#[from] ReisConvertEventStreamError),
|
||||
#[cfg(all(unix, feature = "libei", not(target_os = "macos")))]
|
||||
#[error("libei handshake failed: `{0}`")]
|
||||
Handshake(#[from] HandshakeError),
|
||||
#[cfg(all(unix, feature = "libei", not(target_os = "macos")))]
|
||||
#[error(transparent)]
|
||||
Portal(#[from] ashpd::Error),
|
||||
#[cfg(all(unix, feature = "libei", not(target_os = "macos")))]
|
||||
#[error("libei disconnected - reason: `{0}`")]
|
||||
Disconnected(String),
|
||||
#[cfg(target_os = "macos")]
|
||||
#[error("failed to warp mouse cursor: `{0}`")]
|
||||
WarpCursor(CGError),
|
||||
#[cfg(target_os = "macos")]
|
||||
#[error("reset_mouse_position called without a connected client")]
|
||||
ResetMouseWithoutClient,
|
||||
#[cfg(target_os = "macos")]
|
||||
#[error("core-graphics error: {0}")]
|
||||
CoreGraphics(CGError),
|
||||
#[cfg(target_os = "macos")]
|
||||
#[error("unable to map key event: {0}")]
|
||||
KeyMapError(i64),
|
||||
#[cfg(target_os = "macos")]
|
||||
#[error("Event tap disabled")]
|
||||
EventTapDisabled,
|
||||
}
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum CaptureCreationError {
|
||||
#[error("no backend available")]
|
||||
NoAvailableBackend,
|
||||
#[cfg(all(unix, feature = "libei", not(target_os = "macos")))]
|
||||
#[error("error creating input-capture-portal backend: `{0}`")]
|
||||
Libei(#[from] LibeiCaptureCreationError),
|
||||
#[cfg(all(unix, feature = "wayland", not(target_os = "macos")))]
|
||||
#[error("error creating layer-shell capture backend: `{0}`")]
|
||||
LayerShell(#[from] LayerShellCaptureCreationError),
|
||||
#[cfg(all(unix, feature = "x11", not(target_os = "macos")))]
|
||||
#[error("error creating x11 capture backend: `{0}`")]
|
||||
X11(#[from] X11InputCaptureCreationError),
|
||||
#[cfg(windows)]
|
||||
#[error("error creating windows capture backend")]
|
||||
Windows,
|
||||
#[cfg(target_os = "macos")]
|
||||
#[error("error creating macos capture backend: `{0}`")]
|
||||
MacOS(#[from] MacosCaptureCreationError),
|
||||
}
|
||||
|
||||
impl CaptureCreationError {
|
||||
/// request was intentionally denied by the user
|
||||
#[cfg(all(unix, feature = "libei", not(target_os = "macos")))]
|
||||
pub(crate) fn cancelled_by_user(&self) -> bool {
|
||||
matches!(
|
||||
self,
|
||||
CaptureCreationError::Libei(LibeiCaptureCreationError::Ashpd(ashpd::Error::Response(
|
||||
ResponseError::Cancelled
|
||||
)))
|
||||
)
|
||||
}
|
||||
#[cfg(not(all(unix, feature = "libei", not(target_os = "macos"))))]
|
||||
pub(crate) fn cancelled_by_user(&self) -> bool {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(all(unix, feature = "libei", not(target_os = "macos")))]
|
||||
#[derive(Debug, Error)]
|
||||
pub enum LibeiCaptureCreationError {
|
||||
#[error("xdg-desktop-portal: `{0}`")]
|
||||
Ashpd(#[from] ashpd::Error),
|
||||
}
|
||||
|
||||
#[cfg(all(unix, feature = "wayland", not(target_os = "macos")))]
|
||||
#[derive(Debug, Error)]
|
||||
#[error("{protocol} protocol not supported: {inner}")]
|
||||
pub struct WaylandBindError {
|
||||
inner: BindError,
|
||||
protocol: &'static str,
|
||||
}
|
||||
|
||||
#[cfg(all(unix, feature = "wayland", not(target_os = "macos")))]
|
||||
impl WaylandBindError {
|
||||
pub(crate) fn new(inner: BindError, protocol: &'static str) -> Self {
|
||||
Self { inner, protocol }
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(all(unix, feature = "wayland", not(target_os = "macos")))]
|
||||
#[derive(Debug, Error)]
|
||||
pub enum LayerShellCaptureCreationError {
|
||||
#[error(transparent)]
|
||||
Connect(#[from] ConnectError),
|
||||
#[error(transparent)]
|
||||
Global(#[from] GlobalError),
|
||||
#[error(transparent)]
|
||||
Wayland(#[from] WaylandError),
|
||||
#[error(transparent)]
|
||||
Bind(#[from] WaylandBindError),
|
||||
#[error(transparent)]
|
||||
Dispatch(#[from] DispatchError),
|
||||
#[error(transparent)]
|
||||
Io(#[from] io::Error),
|
||||
}
|
||||
|
||||
#[cfg(all(unix, feature = "x11", not(target_os = "macos")))]
|
||||
#[derive(Debug, Error)]
|
||||
pub enum X11InputCaptureCreationError {
|
||||
#[error("X11 input capture is not yet implemented :(")]
|
||||
NotImplemented,
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
#[derive(Debug, Error)]
|
||||
pub enum MacosCaptureCreationError {
|
||||
#[error("event source creation failed!")]
|
||||
EventSourceCreation,
|
||||
#[cfg(target_os = "macos")]
|
||||
#[error("event tap creation failed")]
|
||||
EventTapCreation,
|
||||
#[error("failed to set CG Cursor property")]
|
||||
CGCursorProperty,
|
||||
#[cfg(target_os = "macos")]
|
||||
#[error("failed to get display ids: {0}")]
|
||||
ActiveDisplays(CGError),
|
||||
}
|
||||
334
input-capture/src/lib.rs
Normal file
334
input-capture/src/lib.rs
Normal file
@@ -0,0 +1,334 @@
|
||||
use std::{
|
||||
collections::{HashMap, HashSet, VecDeque},
|
||||
fmt::Display,
|
||||
mem::swap,
|
||||
task::{ready, Poll},
|
||||
};
|
||||
|
||||
use async_trait::async_trait;
|
||||
use futures::StreamExt;
|
||||
use futures_core::Stream;
|
||||
|
||||
use input_event::{scancode, Event, KeyboardEvent};
|
||||
|
||||
pub use error::{CaptureCreationError, CaptureError, InputCaptureError};
|
||||
|
||||
pub mod error;
|
||||
|
||||
#[cfg(all(unix, feature = "libei", not(target_os = "macos")))]
|
||||
mod libei;
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
mod macos;
|
||||
|
||||
#[cfg(all(unix, feature = "wayland", not(target_os = "macos")))]
|
||||
mod wayland;
|
||||
|
||||
#[cfg(windows)]
|
||||
mod windows;
|
||||
|
||||
#[cfg(all(unix, feature = "x11", not(target_os = "macos")))]
|
||||
mod x11;
|
||||
|
||||
/// fallback input capture (does not produce events)
|
||||
mod dummy;
|
||||
|
||||
pub type CaptureHandle = u64;
|
||||
|
||||
#[derive(Copy, Clone, Debug, PartialEq)]
|
||||
pub enum CaptureEvent {
|
||||
/// capture on this capture handle is now active
|
||||
Begin,
|
||||
/// input event coming from capture handle
|
||||
Input(Event),
|
||||
}
|
||||
|
||||
impl Display for CaptureEvent {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
CaptureEvent::Begin => write!(f, "begin capture"),
|
||||
CaptureEvent::Input(e) => write!(f, "{e}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, Eq, Hash, PartialEq)]
|
||||
pub enum Position {
|
||||
Left,
|
||||
Right,
|
||||
Top,
|
||||
Bottom,
|
||||
}
|
||||
|
||||
impl Position {
|
||||
pub fn opposite(&self) -> Self {
|
||||
match self {
|
||||
Position::Left => Self::Right,
|
||||
Position::Right => Self::Left,
|
||||
Position::Top => Self::Bottom,
|
||||
Position::Bottom => Self::Top,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for Position {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
let pos = match self {
|
||||
Position::Left => "left",
|
||||
Position::Right => "right",
|
||||
Position::Top => "top",
|
||||
Position::Bottom => "bottom",
|
||||
};
|
||||
write!(f, "{}", pos)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub enum Backend {
|
||||
#[cfg(all(unix, feature = "libei", not(target_os = "macos")))]
|
||||
InputCapturePortal,
|
||||
#[cfg(all(unix, feature = "wayland", not(target_os = "macos")))]
|
||||
LayerShell,
|
||||
#[cfg(all(unix, feature = "x11", not(target_os = "macos")))]
|
||||
X11,
|
||||
#[cfg(windows)]
|
||||
Windows,
|
||||
#[cfg(target_os = "macos")]
|
||||
MacOs,
|
||||
Dummy,
|
||||
}
|
||||
|
||||
impl Display for Backend {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
#[cfg(all(unix, feature = "libei", not(target_os = "macos")))]
|
||||
Backend::InputCapturePortal => write!(f, "input-capture-portal"),
|
||||
#[cfg(all(unix, feature = "wayland", not(target_os = "macos")))]
|
||||
Backend::LayerShell => write!(f, "layer-shell"),
|
||||
#[cfg(all(unix, feature = "x11", not(target_os = "macos")))]
|
||||
Backend::X11 => write!(f, "X11"),
|
||||
#[cfg(windows)]
|
||||
Backend::Windows => write!(f, "windows"),
|
||||
#[cfg(target_os = "macos")]
|
||||
Backend::MacOs => write!(f, "MacOS"),
|
||||
Backend::Dummy => write!(f, "dummy"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct InputCapture {
|
||||
/// capture backend
|
||||
capture: Box<dyn Capture>,
|
||||
/// keys pressed by active capture
|
||||
pressed_keys: HashSet<scancode::Linux>,
|
||||
/// map from position to ids
|
||||
position_map: HashMap<Position, Vec<CaptureHandle>>,
|
||||
/// map from id to position
|
||||
id_map: HashMap<CaptureHandle, Position>,
|
||||
/// pending events
|
||||
pending: VecDeque<(CaptureHandle, CaptureEvent)>,
|
||||
}
|
||||
|
||||
impl InputCapture {
|
||||
/// create a new client with the given id
|
||||
pub async fn create(&mut self, id: CaptureHandle, pos: Position) -> Result<(), CaptureError> {
|
||||
if let Some(v) = self.position_map.get_mut(&pos) {
|
||||
v.push(id);
|
||||
Ok(())
|
||||
} else {
|
||||
self.position_map.insert(pos, vec![id]);
|
||||
self.id_map.insert(id, pos);
|
||||
self.capture.create(pos).await
|
||||
}
|
||||
}
|
||||
|
||||
/// destroy the client with the given id, if it exists
|
||||
pub async fn destroy(&mut self, id: CaptureHandle) -> Result<(), CaptureError> {
|
||||
if let Some(pos) = self.id_map.remove(&id) {
|
||||
let destroy = if let Some(v) = self.position_map.get_mut(&pos) {
|
||||
v.retain(|&i| i != id);
|
||||
// we were the last id registered at this position
|
||||
v.is_empty()
|
||||
} else {
|
||||
// nothing to destroy
|
||||
false
|
||||
};
|
||||
if destroy {
|
||||
self.position_map.remove(&pos);
|
||||
self.capture.destroy(pos).await?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// release mouse
|
||||
pub async fn release(&mut self) -> Result<(), CaptureError> {
|
||||
self.pressed_keys.clear();
|
||||
self.capture.release().await
|
||||
}
|
||||
|
||||
/// destroy the input capture
|
||||
pub async fn terminate(&mut self) -> Result<(), CaptureError> {
|
||||
self.capture.terminate().await
|
||||
}
|
||||
|
||||
/// creates a new [`InputCapture`]
|
||||
pub async fn new(backend: Option<Backend>) -> Result<Self, CaptureCreationError> {
|
||||
let capture = create(backend).await?;
|
||||
Ok(Self {
|
||||
capture,
|
||||
id_map: Default::default(),
|
||||
pending: Default::default(),
|
||||
position_map: Default::default(),
|
||||
pressed_keys: HashSet::new(),
|
||||
})
|
||||
}
|
||||
|
||||
/// check whether the given keys are pressed
|
||||
pub fn keys_pressed(&self, keys: &[scancode::Linux]) -> bool {
|
||||
keys.iter().all(|k| self.pressed_keys.contains(k))
|
||||
}
|
||||
|
||||
fn update_pressed_keys(&mut self, key: u32, state: u8) {
|
||||
if let Ok(scancode) = scancode::Linux::try_from(key) {
|
||||
log::debug!("key: {key}, state: {state}, scancode: {scancode:?}");
|
||||
match state {
|
||||
1 => self.pressed_keys.insert(scancode),
|
||||
_ => self.pressed_keys.remove(&scancode),
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Stream for InputCapture {
|
||||
type Item = Result<(CaptureHandle, CaptureEvent), CaptureError>;
|
||||
|
||||
fn poll_next(
|
||||
mut self: std::pin::Pin<&mut Self>,
|
||||
cx: &mut std::task::Context<'_>,
|
||||
) -> Poll<Option<Self::Item>> {
|
||||
if let Some(e) = self.pending.pop_front() {
|
||||
return Poll::Ready(Some(Ok(e)));
|
||||
}
|
||||
|
||||
// ready
|
||||
let event = ready!(self.capture.poll_next_unpin(cx));
|
||||
|
||||
// stream closed
|
||||
let event = match event {
|
||||
Some(e) => e,
|
||||
None => return Poll::Ready(None),
|
||||
};
|
||||
|
||||
// error occurred
|
||||
let (pos, event) = match event {
|
||||
Ok(e) => e,
|
||||
Err(e) => return Poll::Ready(Some(Err(e))),
|
||||
};
|
||||
|
||||
// handle key presses
|
||||
if let CaptureEvent::Input(Event::Keyboard(KeyboardEvent::Key { key, state, .. })) = event {
|
||||
self.update_pressed_keys(key, state);
|
||||
}
|
||||
|
||||
let len = self
|
||||
.position_map
|
||||
.get(&pos)
|
||||
.map(|ids| ids.len())
|
||||
.unwrap_or(0);
|
||||
|
||||
match len {
|
||||
0 => Poll::Pending,
|
||||
1 => Poll::Ready(Some(Ok((
|
||||
self.position_map.get(&pos).expect("no id")[0],
|
||||
event,
|
||||
)))),
|
||||
_ => {
|
||||
let mut position_map = HashMap::new();
|
||||
swap(&mut self.position_map, &mut position_map);
|
||||
{
|
||||
for &id in position_map.get(&pos).expect("position") {
|
||||
self.pending.push_back((id, event));
|
||||
}
|
||||
}
|
||||
swap(&mut self.position_map, &mut position_map);
|
||||
|
||||
Poll::Ready(Some(Ok(self.pending.pop_front().expect("event"))))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
trait Capture: Stream<Item = Result<(Position, CaptureEvent), CaptureError>> + Unpin {
|
||||
/// create a new client with the given id
|
||||
async fn create(&mut self, pos: Position) -> Result<(), CaptureError>;
|
||||
|
||||
/// destroy the client with the given id, if it exists
|
||||
async fn destroy(&mut self, pos: Position) -> Result<(), CaptureError>;
|
||||
|
||||
/// release mouse
|
||||
async fn release(&mut self) -> Result<(), CaptureError>;
|
||||
|
||||
/// destroy the input capture
|
||||
async fn terminate(&mut self) -> Result<(), CaptureError>;
|
||||
}
|
||||
|
||||
async fn create_backend(
|
||||
backend: Backend,
|
||||
) -> Result<
|
||||
Box<dyn Capture<Item = Result<(Position, CaptureEvent), CaptureError>>>,
|
||||
CaptureCreationError,
|
||||
> {
|
||||
match backend {
|
||||
#[cfg(all(unix, feature = "libei", not(target_os = "macos")))]
|
||||
Backend::InputCapturePortal => Ok(Box::new(libei::LibeiInputCapture::new().await?)),
|
||||
#[cfg(all(unix, feature = "wayland", not(target_os = "macos")))]
|
||||
Backend::LayerShell => Ok(Box::new(wayland::LayerShellInputCapture::new()?)),
|
||||
#[cfg(all(unix, feature = "x11", not(target_os = "macos")))]
|
||||
Backend::X11 => Ok(Box::new(x11::X11InputCapture::new()?)),
|
||||
#[cfg(windows)]
|
||||
Backend::Windows => Ok(Box::new(windows::WindowsInputCapture::new())),
|
||||
#[cfg(target_os = "macos")]
|
||||
Backend::MacOs => Ok(Box::new(macos::MacOSInputCapture::new().await?)),
|
||||
Backend::Dummy => Ok(Box::new(dummy::DummyInputCapture::new())),
|
||||
}
|
||||
}
|
||||
|
||||
async fn create(
|
||||
backend: Option<Backend>,
|
||||
) -> Result<
|
||||
Box<dyn Capture<Item = Result<(Position, CaptureEvent), CaptureError>>>,
|
||||
CaptureCreationError,
|
||||
> {
|
||||
if let Some(backend) = backend {
|
||||
let b = create_backend(backend).await;
|
||||
if b.is_ok() {
|
||||
log::info!("using capture backend: {backend}");
|
||||
}
|
||||
return b;
|
||||
}
|
||||
|
||||
for backend in [
|
||||
#[cfg(all(unix, feature = "libei", not(target_os = "macos")))]
|
||||
Backend::InputCapturePortal,
|
||||
#[cfg(all(unix, feature = "wayland", not(target_os = "macos")))]
|
||||
Backend::LayerShell,
|
||||
#[cfg(all(unix, feature = "x11", not(target_os = "macos")))]
|
||||
Backend::X11,
|
||||
#[cfg(windows)]
|
||||
Backend::Windows,
|
||||
#[cfg(target_os = "macos")]
|
||||
Backend::MacOs,
|
||||
] {
|
||||
match create_backend(backend).await {
|
||||
Ok(b) => {
|
||||
log::info!("using capture backend: {backend}");
|
||||
return Ok(b);
|
||||
}
|
||||
Err(e) if e.cancelled_by_user() => return Err(e),
|
||||
Err(e) => log::warn!("{backend} input capture backend unavailable: {e}"),
|
||||
}
|
||||
}
|
||||
Err(CaptureCreationError::NoAvailableBackend)
|
||||
}
|
||||
636
input-capture/src/libei.rs
Normal file
636
input-capture/src/libei.rs
Normal file
@@ -0,0 +1,636 @@
|
||||
use ashpd::{
|
||||
desktop::{
|
||||
input_capture::{Activated, Barrier, BarrierID, Capabilities, InputCapture, Region, Zones},
|
||||
Session,
|
||||
},
|
||||
enumflags2::BitFlags,
|
||||
};
|
||||
use async_trait::async_trait;
|
||||
use futures::{FutureExt, StreamExt};
|
||||
use reis::{
|
||||
ei,
|
||||
event::{DeviceCapability, EiEvent},
|
||||
tokio::{EiConvertEventStream, EiEventStream},
|
||||
};
|
||||
use std::{
|
||||
cell::Cell,
|
||||
collections::HashMap,
|
||||
io,
|
||||
os::unix::net::UnixStream,
|
||||
pin::Pin,
|
||||
rc::Rc,
|
||||
sync::Arc,
|
||||
task::{Context, Poll},
|
||||
};
|
||||
use tokio::{
|
||||
sync::{
|
||||
mpsc::{self, Receiver, Sender},
|
||||
Notify,
|
||||
},
|
||||
task::JoinHandle,
|
||||
};
|
||||
use tokio_util::sync::CancellationToken;
|
||||
|
||||
use futures_core::Stream;
|
||||
use once_cell::sync::Lazy;
|
||||
|
||||
use input_event::Event;
|
||||
|
||||
use crate::CaptureEvent;
|
||||
|
||||
use super::{
|
||||
error::{CaptureError, LibeiCaptureCreationError, ReisConvertEventStreamError},
|
||||
Capture as LanMouseInputCapture, Position,
|
||||
};
|
||||
|
||||
/* there is a bug in xdg-remote-desktop-portal-gnome / mutter that
|
||||
* prevents receiving further events after a session has been disabled once.
|
||||
* Therefore the session needs to be recreated when the barriers are updated */
|
||||
|
||||
/// events that necessitate restarting the capture session
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
enum LibeiNotifyEvent {
|
||||
Create(Position),
|
||||
Destroy(Position),
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub struct LibeiInputCapture<'a> {
|
||||
input_capture: Pin<Box<InputCapture<'a>>>,
|
||||
capture_task: JoinHandle<Result<(), CaptureError>>,
|
||||
event_rx: Receiver<(Position, CaptureEvent)>,
|
||||
notify_capture: Sender<LibeiNotifyEvent>,
|
||||
notify_release: Arc<Notify>,
|
||||
cancellation_token: CancellationToken,
|
||||
terminated: bool,
|
||||
}
|
||||
|
||||
static INTERFACES: Lazy<HashMap<&'static str, u32>> = Lazy::new(|| {
|
||||
let mut m = HashMap::new();
|
||||
m.insert("ei_connection", 1);
|
||||
m.insert("ei_callback", 1);
|
||||
m.insert("ei_pingpong", 1);
|
||||
m.insert("ei_seat", 1);
|
||||
m.insert("ei_device", 2);
|
||||
m.insert("ei_pointer", 1);
|
||||
m.insert("ei_pointer_absolute", 1);
|
||||
m.insert("ei_scroll", 1);
|
||||
m.insert("ei_button", 1);
|
||||
m.insert("ei_keyboard", 1);
|
||||
m.insert("ei_touchscreen", 1);
|
||||
m
|
||||
});
|
||||
|
||||
/// returns (start pos, end pos), inclusive
|
||||
fn pos_to_barrier(r: &Region, pos: Position) -> (i32, i32, i32, i32) {
|
||||
let (x, y) = (r.x_offset(), r.y_offset());
|
||||
let (w, h) = (r.width() as i32, r.height() as i32);
|
||||
match pos {
|
||||
Position::Left => (x, y, x, y + h - 1),
|
||||
Position::Right => (x + w, y, x + w, y + h - 1),
|
||||
Position::Top => (x, y, x + w - 1, y),
|
||||
Position::Bottom => (x, y + h, x + w - 1, y + h),
|
||||
}
|
||||
}
|
||||
|
||||
/// Ashpd does not expose fields
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
struct ICBarrier {
|
||||
barrier_id: BarrierID,
|
||||
position: (i32, i32, i32, i32),
|
||||
}
|
||||
|
||||
impl ICBarrier {
|
||||
fn new(barrier_id: BarrierID, position: (i32, i32, i32, i32)) -> Self {
|
||||
Self {
|
||||
barrier_id,
|
||||
position,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ICBarrier> for Barrier {
|
||||
fn from(barrier: ICBarrier) -> Self {
|
||||
Barrier::new(barrier.barrier_id, barrier.position)
|
||||
}
|
||||
}
|
||||
|
||||
fn select_barriers(
|
||||
zones: &Zones,
|
||||
clients: &[Position],
|
||||
next_barrier_id: &mut u32,
|
||||
) -> (Vec<ICBarrier>, HashMap<BarrierID, Position>) {
|
||||
let mut pos_for_barrier = HashMap::new();
|
||||
let mut barriers: Vec<ICBarrier> = vec![];
|
||||
|
||||
for pos in clients {
|
||||
let mut client_barriers = zones
|
||||
.regions()
|
||||
.iter()
|
||||
.map(|r| {
|
||||
let id = *next_barrier_id;
|
||||
*next_barrier_id = id + 1;
|
||||
let position = pos_to_barrier(r, *pos);
|
||||
pos_for_barrier.insert(id, *pos);
|
||||
ICBarrier::new(id, position)
|
||||
})
|
||||
.collect();
|
||||
barriers.append(&mut client_barriers);
|
||||
}
|
||||
(barriers, pos_for_barrier)
|
||||
}
|
||||
|
||||
async fn update_barriers(
|
||||
input_capture: &InputCapture<'_>,
|
||||
session: &Session<'_, InputCapture<'_>>,
|
||||
active_clients: &[Position],
|
||||
next_barrier_id: &mut u32,
|
||||
) -> Result<(Vec<ICBarrier>, HashMap<BarrierID, Position>), ashpd::Error> {
|
||||
let zones = input_capture.zones(session).await?.response()?;
|
||||
log::debug!("zones: {zones:?}");
|
||||
|
||||
let (barriers, id_map) = select_barriers(&zones, active_clients, next_barrier_id);
|
||||
log::debug!("barriers: {barriers:?}");
|
||||
log::debug!("client for barrier id: {id_map:?}");
|
||||
|
||||
let ashpd_barriers: Vec<Barrier> = barriers.iter().copied().map(|b| b.into()).collect();
|
||||
let response = input_capture
|
||||
.set_pointer_barriers(session, &ashpd_barriers, zones.zone_set())
|
||||
.await?;
|
||||
let response = response.response()?;
|
||||
log::debug!("{response:?}");
|
||||
Ok((barriers, id_map))
|
||||
}
|
||||
|
||||
async fn create_session<'a>(
|
||||
input_capture: &'a InputCapture<'a>,
|
||||
) -> std::result::Result<(Session<'_, InputCapture<'_>>, BitFlags<Capabilities>), ashpd::Error> {
|
||||
log::debug!("creating input capture session");
|
||||
input_capture
|
||||
.create_session(
|
||||
&ashpd::WindowIdentifier::default(),
|
||||
Capabilities::Keyboard | Capabilities::Pointer | Capabilities::Touchscreen,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn connect_to_eis(
|
||||
input_capture: &InputCapture<'_>,
|
||||
session: &Session<'_, InputCapture<'_>>,
|
||||
) -> Result<(ei::Context, EiConvertEventStream), CaptureError> {
|
||||
log::debug!("connect_to_eis");
|
||||
let fd = input_capture.connect_to_eis(session).await?;
|
||||
|
||||
// create unix stream from fd
|
||||
let stream = UnixStream::from(fd);
|
||||
stream.set_nonblocking(true)?;
|
||||
|
||||
// create ei context
|
||||
let context = ei::Context::new(stream)?;
|
||||
let mut event_stream = EiEventStream::new(context.clone())?;
|
||||
let response = reis::tokio::ei_handshake(
|
||||
&mut event_stream,
|
||||
"de.feschber.LanMouse",
|
||||
ei::handshake::ContextType::Receiver,
|
||||
&INTERFACES,
|
||||
)
|
||||
.await?;
|
||||
let event_stream = EiConvertEventStream::new(event_stream, response.serial);
|
||||
|
||||
Ok((context, event_stream))
|
||||
}
|
||||
|
||||
async fn libei_event_handler(
|
||||
mut ei_event_stream: EiConvertEventStream,
|
||||
context: ei::Context,
|
||||
event_tx: Sender<(Position, CaptureEvent)>,
|
||||
release_session: Arc<Notify>,
|
||||
current_pos: Rc<Cell<Option<Position>>>,
|
||||
) -> Result<(), CaptureError> {
|
||||
loop {
|
||||
let ei_event = ei_event_stream
|
||||
.next()
|
||||
.await
|
||||
.ok_or(CaptureError::EndOfStream)?
|
||||
.map_err(ReisConvertEventStreamError::from)?;
|
||||
log::trace!("from ei: {ei_event:?}");
|
||||
let client = current_pos.get();
|
||||
handle_ei_event(ei_event, client, &context, &event_tx, &release_session).await?;
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> LibeiInputCapture<'a> {
|
||||
pub async fn new() -> std::result::Result<Self, LibeiCaptureCreationError> {
|
||||
let input_capture = Box::pin(InputCapture::new().await?);
|
||||
let input_capture_ptr = input_capture.as_ref().get_ref() as *const InputCapture<'static>;
|
||||
let first_session = Some(create_session(unsafe { &*input_capture_ptr }).await?);
|
||||
|
||||
let (event_tx, event_rx) = mpsc::channel(1);
|
||||
let (notify_capture, notify_rx) = mpsc::channel(1);
|
||||
let notify_release = Arc::new(Notify::new());
|
||||
|
||||
let cancellation_token = CancellationToken::new();
|
||||
|
||||
let capture = do_capture(
|
||||
input_capture_ptr,
|
||||
notify_rx,
|
||||
notify_release.clone(),
|
||||
first_session,
|
||||
event_tx,
|
||||
cancellation_token.clone(),
|
||||
);
|
||||
let capture_task = tokio::task::spawn_local(capture);
|
||||
|
||||
let producer = Self {
|
||||
input_capture,
|
||||
event_rx,
|
||||
capture_task,
|
||||
notify_capture,
|
||||
notify_release,
|
||||
cancellation_token,
|
||||
terminated: false,
|
||||
};
|
||||
|
||||
Ok(producer)
|
||||
}
|
||||
}
|
||||
|
||||
async fn do_capture(
|
||||
input_capture: *const InputCapture<'static>,
|
||||
mut capture_event: Receiver<LibeiNotifyEvent>,
|
||||
notify_release: Arc<Notify>,
|
||||
session: Option<(Session<'_, InputCapture<'_>>, BitFlags<Capabilities>)>,
|
||||
event_tx: Sender<(Position, CaptureEvent)>,
|
||||
cancellation_token: CancellationToken,
|
||||
) -> Result<(), CaptureError> {
|
||||
let mut session = session.map(|s| s.0);
|
||||
|
||||
/* safety: libei_task does not outlive Self */
|
||||
let input_capture = unsafe { &*input_capture };
|
||||
let mut active_clients: Vec<Position> = vec![];
|
||||
let mut next_barrier_id = 1u32;
|
||||
|
||||
let mut zones_changed = input_capture.receive_zones_changed().await?;
|
||||
|
||||
loop {
|
||||
// do capture session
|
||||
let cancel_session = CancellationToken::new();
|
||||
let cancel_update = CancellationToken::new();
|
||||
|
||||
let mut capture_event_occured: Option<LibeiNotifyEvent> = None;
|
||||
let mut zones_have_changed = false;
|
||||
|
||||
// kill session if clients need to be updated
|
||||
let handle_session_update_request = async {
|
||||
tokio::select! {
|
||||
_ = cancellation_token.cancelled() => {
|
||||
log::debug!("cancelled")
|
||||
}, /* exit requested */
|
||||
_ = cancel_update.cancelled() => {
|
||||
log::debug!("update task cancelled");
|
||||
}, /* session exited */
|
||||
_ = zones_changed.next() => {
|
||||
log::debug!("zones changed!");
|
||||
zones_have_changed = true
|
||||
}, /* zones have changed */
|
||||
e = capture_event.recv() => if let Some(e) = e { /* clients changed */
|
||||
log::debug!("capture event: {e:?}");
|
||||
capture_event_occured.replace(e);
|
||||
},
|
||||
}
|
||||
// kill session (might already be dead!)
|
||||
log::debug!("=> cancelling session");
|
||||
cancel_session.cancel();
|
||||
};
|
||||
|
||||
if !active_clients.is_empty() {
|
||||
// create session
|
||||
let mut session = match session.take() {
|
||||
Some(s) => s,
|
||||
None => create_session(input_capture).await?.0,
|
||||
};
|
||||
|
||||
let capture_session = do_capture_session(
|
||||
input_capture,
|
||||
&mut session,
|
||||
&event_tx,
|
||||
&active_clients,
|
||||
&mut next_barrier_id,
|
||||
¬ify_release,
|
||||
(cancel_session.clone(), cancel_update.clone()),
|
||||
);
|
||||
|
||||
let (capture_result, ()) = tokio::join!(capture_session, handle_session_update_request);
|
||||
log::debug!("capture session + session_update task done!");
|
||||
|
||||
// disable capture
|
||||
log::debug!("disabling input capture");
|
||||
if let Err(e) = input_capture.disable(&session).await {
|
||||
log::warn!("input_capture.disable(&session) {e}");
|
||||
}
|
||||
if let Err(e) = session.close().await {
|
||||
log::warn!("session.close(): {e}");
|
||||
}
|
||||
|
||||
// propagate error from capture session
|
||||
capture_result?;
|
||||
} else {
|
||||
handle_session_update_request.await;
|
||||
}
|
||||
|
||||
// update clients if requested
|
||||
if let Some(event) = capture_event_occured.take() {
|
||||
match event {
|
||||
LibeiNotifyEvent::Create(p) => active_clients.push(p),
|
||||
LibeiNotifyEvent::Destroy(p) => active_clients.retain(|&pos| pos != p),
|
||||
}
|
||||
}
|
||||
|
||||
// break
|
||||
if cancellation_token.is_cancelled() {
|
||||
break Ok(());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn do_capture_session(
|
||||
input_capture: &InputCapture<'_>,
|
||||
session: &mut Session<'_, InputCapture<'_>>,
|
||||
event_tx: &Sender<(Position, CaptureEvent)>,
|
||||
active_clients: &[Position],
|
||||
next_barrier_id: &mut u32,
|
||||
notify_release: &Notify,
|
||||
cancel: (CancellationToken, CancellationToken),
|
||||
) -> Result<(), CaptureError> {
|
||||
let (cancel_session, cancel_update) = cancel;
|
||||
// current client
|
||||
let current_pos = Rc::new(Cell::new(None));
|
||||
|
||||
// connect to eis server
|
||||
let (context, ei_event_stream) = connect_to_eis(input_capture, session).await?;
|
||||
|
||||
// set barriers
|
||||
let (barriers, pos_for_barrier_id) =
|
||||
update_barriers(input_capture, session, active_clients, next_barrier_id).await?;
|
||||
|
||||
log::debug!("enabling session");
|
||||
input_capture.enable(session).await?;
|
||||
|
||||
// cancellation token to release session
|
||||
let release_session = Arc::new(Notify::new());
|
||||
|
||||
// async event task
|
||||
let cancel_ei_handler = CancellationToken::new();
|
||||
let event_chan = event_tx.clone();
|
||||
let pos = current_pos.clone();
|
||||
let cancel_session_clone = cancel_session.clone();
|
||||
let release_session_clone = release_session.clone();
|
||||
let cancel_ei_handler_clone = cancel_ei_handler.clone();
|
||||
let ei_task = async move {
|
||||
tokio::select! {
|
||||
r = libei_event_handler(
|
||||
ei_event_stream,
|
||||
context,
|
||||
event_chan,
|
||||
release_session_clone,
|
||||
pos,
|
||||
) => {
|
||||
log::debug!("libei exited: {r:?} cancelling session task");
|
||||
cancel_session_clone.cancel();
|
||||
}
|
||||
_ = cancel_ei_handler_clone.cancelled() => {},
|
||||
}
|
||||
Ok::<(), CaptureError>(())
|
||||
};
|
||||
|
||||
let capture_session_task = async {
|
||||
// receiver for activation tokens
|
||||
let mut activated = input_capture.receive_activated().await?;
|
||||
let mut ei_devices_changed = false;
|
||||
loop {
|
||||
tokio::select! {
|
||||
activated = activated.next() => {
|
||||
let activated = activated.ok_or(CaptureError::ActivationClosed)?;
|
||||
log::debug!("activated: {activated:?}");
|
||||
|
||||
// get barrier id from activation
|
||||
let barrier_id = match activated.barrier_id() {
|
||||
Some(bid) => bid,
|
||||
// workaround for KDE plasma not reporting barrier ids
|
||||
None => find_corresponding_client(&barriers, activated.cursor_position().expect("no cursor position reported by compositor")),
|
||||
};
|
||||
|
||||
// find client corresponding to barrier
|
||||
let pos = *pos_for_barrier_id.get(&barrier_id).expect("invalid barrier id");
|
||||
current_pos.replace(Some(pos));
|
||||
|
||||
// client entered => send event
|
||||
event_tx.send((pos, CaptureEvent::Begin)).await.expect("no channel");
|
||||
|
||||
tokio::select! {
|
||||
_ = notify_release.notified() => { /* capture release */
|
||||
log::debug!("release session requested");
|
||||
},
|
||||
_ = release_session.notified() => { /* release session */
|
||||
log::debug!("ei devices changed");
|
||||
ei_devices_changed = true;
|
||||
},
|
||||
_ = cancel_session.cancelled() => { /* kill session notify */
|
||||
log::debug!("session cancel requested");
|
||||
break
|
||||
},
|
||||
}
|
||||
|
||||
release_capture(input_capture, session, activated, pos).await?;
|
||||
|
||||
}
|
||||
_ = notify_release.notified() => { /* capture release -> we are not capturing anyway, so ignore */
|
||||
log::debug!("release session requested");
|
||||
},
|
||||
_ = release_session.notified() => { /* release session */
|
||||
log::debug!("ei devices changed");
|
||||
ei_devices_changed = true;
|
||||
},
|
||||
_ = cancel_session.cancelled() => { /* kill session notify */
|
||||
log::debug!("session cancel requested");
|
||||
break
|
||||
},
|
||||
}
|
||||
if ei_devices_changed {
|
||||
/* for whatever reason, GNOME seems to kill the session
|
||||
* as soon as devices are added or removed, so we need
|
||||
* to cancel */
|
||||
break;
|
||||
}
|
||||
}
|
||||
// cancel libei task
|
||||
log::debug!("session exited: killing libei task");
|
||||
cancel_ei_handler.cancel();
|
||||
Ok::<(), CaptureError>(())
|
||||
};
|
||||
|
||||
let (a, b) = tokio::join!(ei_task, capture_session_task);
|
||||
|
||||
cancel_update.cancel();
|
||||
|
||||
log::debug!("both session and ei task finished!");
|
||||
a?;
|
||||
b?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn release_capture<'a>(
|
||||
input_capture: &InputCapture<'a>,
|
||||
session: &Session<'a, InputCapture<'a>>,
|
||||
activated: Activated,
|
||||
current_pos: Position,
|
||||
) -> Result<(), CaptureError> {
|
||||
if let Some(activation_id) = activated.activation_id() {
|
||||
log::debug!("releasing input capture {activation_id}");
|
||||
}
|
||||
let (x, y) = activated
|
||||
.cursor_position()
|
||||
.expect("compositor did not report cursor position!");
|
||||
log::debug!("client entered @ ({x}, {y})");
|
||||
let (dx, dy) = match current_pos {
|
||||
// offset cursor position to not enter again immediately
|
||||
Position::Left => (1., 0.),
|
||||
Position::Right => (-1., 0.),
|
||||
Position::Top => (0., 1.),
|
||||
Position::Bottom => (0., -1.),
|
||||
};
|
||||
// release 1px to the right of the entered zone
|
||||
let cursor_position = (x as f64 + dx, y as f64 + dy);
|
||||
input_capture
|
||||
.release(session, activated.activation_id(), Some(cursor_position))
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn find_corresponding_client(barriers: &[ICBarrier], pos: (f32, f32)) -> BarrierID {
|
||||
barriers
|
||||
.iter()
|
||||
.copied()
|
||||
.min_by_key(|b| {
|
||||
let (x1, y1, x2, y2) = b.position;
|
||||
let (x1, y1, x2, y2) = (x1 as f32, y1 as f32, x2 as f32, y2 as f32);
|
||||
distance_to_line(((x1, y1), (x2, y2)), pos) as i32
|
||||
})
|
||||
.expect("could not find barrier corresponding to client")
|
||||
.barrier_id
|
||||
}
|
||||
|
||||
fn distance_to_line(line: ((f32, f32), (f32, f32)), p: (f32, f32)) -> f32 {
|
||||
let ((x1, y1), (x2, y2)) = line;
|
||||
let (x0, y0) = p;
|
||||
/*
|
||||
* we use the fact that for the triangle spanned by the line and p,
|
||||
* the height of the triangle is the desired distance and can be calculated by
|
||||
* h = 2A / b with b being the line_length and
|
||||
*/
|
||||
let double_triangle_area = ((y2 - y1) * x0 - (x2 - x1) * y0 + x2 * y1 - y2 * x1).abs();
|
||||
let line_length = ((y2 - y1).powf(2.0) + (x2 - x1).powf(2.0)).sqrt();
|
||||
let distance = double_triangle_area / line_length;
|
||||
log::debug!("distance to line({line:?}, {p:?}) = {distance}");
|
||||
distance
|
||||
}
|
||||
|
||||
static ALL_CAPABILITIES: &[DeviceCapability] = &[
|
||||
DeviceCapability::Pointer,
|
||||
DeviceCapability::PointerAbsolute,
|
||||
DeviceCapability::Keyboard,
|
||||
DeviceCapability::Touch,
|
||||
DeviceCapability::Scroll,
|
||||
DeviceCapability::Button,
|
||||
];
|
||||
|
||||
async fn handle_ei_event(
|
||||
ei_event: EiEvent,
|
||||
current_client: Option<Position>,
|
||||
context: &ei::Context,
|
||||
event_tx: &Sender<(Position, CaptureEvent)>,
|
||||
release_session: &Notify,
|
||||
) -> Result<(), CaptureError> {
|
||||
match ei_event {
|
||||
EiEvent::SeatAdded(s) => {
|
||||
s.seat.bind_capabilities(ALL_CAPABILITIES);
|
||||
context.flush().map_err(|e| io::Error::new(e.kind(), e))?;
|
||||
}
|
||||
EiEvent::SeatRemoved(_) | /* EiEvent::DeviceAdded(_) | */ EiEvent::DeviceRemoved(_) => {
|
||||
log::debug!("releasing session: {ei_event:?}");
|
||||
release_session.notify_waiters();
|
||||
}
|
||||
EiEvent::DevicePaused(_) | EiEvent::DeviceResumed(_) => {}
|
||||
EiEvent::DeviceStartEmulating(_) => log::debug!("START EMULATING"),
|
||||
EiEvent::DeviceStopEmulating(_) => log::debug!("STOP EMULATING"),
|
||||
EiEvent::Disconnected(d) => {
|
||||
return Err(CaptureError::Disconnected(format!("{:?}", d.reason)))
|
||||
}
|
||||
_ => {
|
||||
if let Some(pos) = current_client {
|
||||
for event in Event::from_ei_event(ei_event) {
|
||||
event_tx.send((pos, CaptureEvent::Input(event))).await.expect("no channel");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl<'a> LanMouseInputCapture for LibeiInputCapture<'a> {
|
||||
async fn create(&mut self, pos: Position) -> Result<(), CaptureError> {
|
||||
let _ = self
|
||||
.notify_capture
|
||||
.send(LibeiNotifyEvent::Create(pos))
|
||||
.await;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn destroy(&mut self, pos: Position) -> Result<(), CaptureError> {
|
||||
let _ = self
|
||||
.notify_capture
|
||||
.send(LibeiNotifyEvent::Destroy(pos))
|
||||
.await;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn release(&mut self) -> Result<(), CaptureError> {
|
||||
self.notify_release.notify_waiters();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn terminate(&mut self) -> Result<(), CaptureError> {
|
||||
self.cancellation_token.cancel();
|
||||
let task = &mut self.capture_task;
|
||||
log::debug!("waiting for capture to terminate...");
|
||||
let res = task.await.expect("libei task panic");
|
||||
log::debug!("done!");
|
||||
self.terminated = true;
|
||||
res
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Drop for LibeiInputCapture<'a> {
|
||||
fn drop(&mut self) {
|
||||
if !self.terminated {
|
||||
/* this workaround is needed until async drop is stabilized */
|
||||
panic!("LibeiInputCapture dropped without being terminated!");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Stream for LibeiInputCapture<'a> {
|
||||
type Item = Result<(Position, CaptureEvent), CaptureError>;
|
||||
|
||||
fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
|
||||
match self.capture_task.poll_unpin(cx) {
|
||||
Poll::Ready(r) => match r.expect("failed to join") {
|
||||
Ok(()) => Poll::Ready(None),
|
||||
Err(e) => Poll::Ready(Some(Err(e))),
|
||||
},
|
||||
Poll::Pending => self.event_rx.poll_recv(cx).map(|e| e.map(Result::Ok)),
|
||||
}
|
||||
}
|
||||
}
|
||||
618
input-capture/src/macos.rs
Normal file
618
input-capture/src/macos.rs
Normal file
@@ -0,0 +1,618 @@
|
||||
use super::{error::MacosCaptureCreationError, Capture, CaptureError, CaptureEvent, Position};
|
||||
use async_trait::async_trait;
|
||||
use bitflags::bitflags;
|
||||
use core_foundation::base::{kCFAllocatorDefault, CFRelease};
|
||||
use core_foundation::date::CFTimeInterval;
|
||||
use core_foundation::number::{kCFBooleanTrue, CFBooleanRef};
|
||||
use core_foundation::runloop::{kCFRunLoopCommonModes, CFRunLoop, CFRunLoopSource};
|
||||
use core_foundation::string::{kCFStringEncodingUTF8, CFStringCreateWithCString, CFStringRef};
|
||||
use core_graphics::base::{kCGErrorSuccess, CGError};
|
||||
use core_graphics::display::{CGDisplay, CGPoint};
|
||||
use core_graphics::event::{
|
||||
CGEvent, CGEventFlags, CGEventTap, CGEventTapLocation, CGEventTapOptions, CGEventTapPlacement,
|
||||
CGEventTapProxy, CGEventType, EventField,
|
||||
};
|
||||
use core_graphics::event_source::{CGEventSource, CGEventSourceStateID};
|
||||
use futures_core::Stream;
|
||||
use input_event::{Event, KeyboardEvent, PointerEvent, BTN_LEFT, BTN_MIDDLE, BTN_RIGHT};
|
||||
use keycode::{KeyMap, KeyMapping};
|
||||
use libc::c_void;
|
||||
use once_cell::unsync::Lazy;
|
||||
use std::collections::HashSet;
|
||||
use std::ffi::{c_char, CString};
|
||||
use std::pin::Pin;
|
||||
use std::sync::Arc;
|
||||
use std::task::{ready, Context, Poll};
|
||||
use std::thread::{self};
|
||||
use tokio::sync::mpsc::{self, Receiver, Sender};
|
||||
use tokio::sync::{oneshot, Mutex};
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
struct Bounds {
|
||||
xmin: f64,
|
||||
xmax: f64,
|
||||
ymin: f64,
|
||||
ymax: f64,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct InputCaptureState {
|
||||
active_clients: Lazy<HashSet<Position>>,
|
||||
current_pos: Option<Position>,
|
||||
bounds: Bounds,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
enum ProducerEvent {
|
||||
Release,
|
||||
Create(Position),
|
||||
Destroy(Position),
|
||||
Grab(Position),
|
||||
EventTapDisabled,
|
||||
}
|
||||
|
||||
impl InputCaptureState {
|
||||
fn new() -> Result<Self, MacosCaptureCreationError> {
|
||||
let mut res = Self {
|
||||
active_clients: Lazy::new(HashSet::new),
|
||||
current_pos: None,
|
||||
bounds: Bounds::default(),
|
||||
};
|
||||
res.update_bounds()?;
|
||||
Ok(res)
|
||||
}
|
||||
|
||||
fn crossed(&mut self, event: &CGEvent) -> Option<Position> {
|
||||
let location = event.location();
|
||||
let relative_x = event.get_double_value_field(EventField::MOUSE_EVENT_DELTA_X);
|
||||
let relative_y = event.get_double_value_field(EventField::MOUSE_EVENT_DELTA_Y);
|
||||
|
||||
for &position in self.active_clients.iter() {
|
||||
if (position == Position::Left && (location.x + relative_x) <= self.bounds.xmin)
|
||||
|| (position == Position::Right && (location.x + relative_x) >= self.bounds.xmax)
|
||||
|| (position == Position::Top && (location.y + relative_y) <= self.bounds.ymin)
|
||||
|| (position == Position::Bottom && (location.y + relative_y) >= self.bounds.ymax)
|
||||
{
|
||||
log::debug!("Crossed barrier into position: {position:?}");
|
||||
return Some(position);
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
// Get the max bounds of all displays
|
||||
fn update_bounds(&mut self) -> Result<(), MacosCaptureCreationError> {
|
||||
let active_ids =
|
||||
CGDisplay::active_displays().map_err(MacosCaptureCreationError::ActiveDisplays)?;
|
||||
active_ids.iter().for_each(|d| {
|
||||
let bounds = CGDisplay::new(*d).bounds();
|
||||
self.bounds.xmin = self.bounds.xmin.min(bounds.origin.x);
|
||||
self.bounds.xmax = self.bounds.xmax.max(bounds.origin.x + bounds.size.width);
|
||||
self.bounds.ymin = self.bounds.ymin.min(bounds.origin.y);
|
||||
self.bounds.ymax = self.bounds.ymax.max(bounds.origin.y + bounds.size.height);
|
||||
});
|
||||
|
||||
log::debug!("Updated displays bounds: {0:?}", self.bounds);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// We can't disable mouse movement when in a client so we need to reset the cursor position
|
||||
// to the edge of the screen, the cursor will be hidden but we dont want it to appear in a
|
||||
// random location when we exit the client
|
||||
fn reset_mouse_position(&self, event: &CGEvent) -> Result<(), CaptureError> {
|
||||
if let Some(pos) = self.current_pos {
|
||||
let location = event.location();
|
||||
let edge_offset = 1.0;
|
||||
|
||||
// After the cursor is warped no event is produced but the next event
|
||||
// will carry the delta from the warp so only half the delta is needed to move the cursor
|
||||
let delta_y = event.get_double_value_field(EventField::MOUSE_EVENT_DELTA_Y) / 2.0;
|
||||
let delta_x = event.get_double_value_field(EventField::MOUSE_EVENT_DELTA_X) / 2.0;
|
||||
|
||||
let mut new_x = location.x + delta_x;
|
||||
let mut new_y = location.y + delta_y;
|
||||
|
||||
match pos {
|
||||
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(
|
||||
&mut self,
|
||||
producer_event: ProducerEvent,
|
||||
) -> Result<(), CaptureError> {
|
||||
log::debug!("handling event: {producer_event:?}");
|
||||
match producer_event {
|
||||
ProducerEvent::Release => {
|
||||
if self.current_pos.is_some() {
|
||||
CGDisplay::show_cursor(&CGDisplay::main())
|
||||
.map_err(CaptureError::CoreGraphics)?;
|
||||
self.current_pos = None;
|
||||
}
|
||||
}
|
||||
ProducerEvent::Grab(pos) => {
|
||||
if self.current_pos.is_none() {
|
||||
CGDisplay::hide_cursor(&CGDisplay::main())
|
||||
.map_err(CaptureError::CoreGraphics)?;
|
||||
self.current_pos = Some(pos);
|
||||
}
|
||||
}
|
||||
ProducerEvent::Create(p) => {
|
||||
self.active_clients.insert(p);
|
||||
}
|
||||
ProducerEvent::Destroy(p) => {
|
||||
if let Some(current) = self.current_pos {
|
||||
if current == p {
|
||||
CGDisplay::show_cursor(&CGDisplay::main())
|
||||
.map_err(CaptureError::CoreGraphics)?;
|
||||
self.current_pos = None;
|
||||
};
|
||||
}
|
||||
self.active_clients.remove(&p);
|
||||
}
|
||||
ProducerEvent::EventTapDisabled => return Err(CaptureError::EventTapDisabled),
|
||||
};
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn get_events(
|
||||
ev_type: &CGEventType,
|
||||
ev: &CGEvent,
|
||||
result: &mut Vec<CaptureEvent>,
|
||||
) -> Result<(), CaptureError> {
|
||||
fn map_pointer_event(ev: &CGEvent) -> PointerEvent {
|
||||
PointerEvent::Motion {
|
||||
time: 0,
|
||||
dx: ev.get_double_value_field(EventField::MOUSE_EVENT_DELTA_X),
|
||||
dy: ev.get_double_value_field(EventField::MOUSE_EVENT_DELTA_Y),
|
||||
}
|
||||
}
|
||||
|
||||
fn map_key(ev: &CGEvent) -> Result<u32, CaptureError> {
|
||||
let code = ev.get_integer_value_field(EventField::KEYBOARD_EVENT_KEYCODE);
|
||||
match KeyMap::from_key_mapping(KeyMapping::Mac(code as u16)) {
|
||||
Ok(k) => Ok(k.evdev as u32),
|
||||
Err(()) => Err(CaptureError::KeyMapError(code)),
|
||||
}
|
||||
}
|
||||
|
||||
match ev_type {
|
||||
CGEventType::KeyDown => {
|
||||
let k = map_key(ev)?;
|
||||
result.push(CaptureEvent::Input(Event::Keyboard(KeyboardEvent::Key {
|
||||
time: 0,
|
||||
key: k,
|
||||
state: 1,
|
||||
})));
|
||||
}
|
||||
CGEventType::KeyUp => {
|
||||
let k = map_key(ev)?;
|
||||
result.push(CaptureEvent::Input(Event::Keyboard(KeyboardEvent::Key {
|
||||
time: 0,
|
||||
key: k,
|
||||
state: 0,
|
||||
})));
|
||||
}
|
||||
CGEventType::FlagsChanged => {
|
||||
let mut mods = XMods::empty();
|
||||
let mut mods_locked = XMods::empty();
|
||||
let cg_flags = ev.get_flags();
|
||||
|
||||
if cg_flags.contains(CGEventFlags::CGEventFlagShift) {
|
||||
mods |= XMods::ShiftMask;
|
||||
}
|
||||
if cg_flags.contains(CGEventFlags::CGEventFlagControl) {
|
||||
mods |= XMods::ControlMask;
|
||||
}
|
||||
if cg_flags.contains(CGEventFlags::CGEventFlagAlternate) {
|
||||
mods |= XMods::Mod1Mask;
|
||||
}
|
||||
if cg_flags.contains(CGEventFlags::CGEventFlagCommand) {
|
||||
mods |= XMods::Mod4Mask;
|
||||
}
|
||||
if cg_flags.contains(CGEventFlags::CGEventFlagAlphaShift) {
|
||||
mods |= XMods::LockMask;
|
||||
mods_locked |= XMods::LockMask;
|
||||
}
|
||||
|
||||
let modifier_event = KeyboardEvent::Modifiers {
|
||||
depressed: mods.bits(),
|
||||
latched: 0,
|
||||
locked: mods_locked.bits(),
|
||||
group: 0,
|
||||
};
|
||||
|
||||
result.push(CaptureEvent::Input(Event::Keyboard(modifier_event)));
|
||||
}
|
||||
CGEventType::MouseMoved => {
|
||||
result.push(CaptureEvent::Input(Event::Pointer(map_pointer_event(ev))))
|
||||
}
|
||||
CGEventType::LeftMouseDragged => {
|
||||
result.push(CaptureEvent::Input(Event::Pointer(map_pointer_event(ev))))
|
||||
}
|
||||
CGEventType::RightMouseDragged => {
|
||||
result.push(CaptureEvent::Input(Event::Pointer(map_pointer_event(ev))))
|
||||
}
|
||||
CGEventType::OtherMouseDragged => {
|
||||
result.push(CaptureEvent::Input(Event::Pointer(map_pointer_event(ev))))
|
||||
}
|
||||
CGEventType::LeftMouseDown => {
|
||||
result.push(CaptureEvent::Input(Event::Pointer(PointerEvent::Button {
|
||||
time: 0,
|
||||
button: BTN_LEFT,
|
||||
state: 1,
|
||||
})))
|
||||
}
|
||||
CGEventType::LeftMouseUp => {
|
||||
result.push(CaptureEvent::Input(Event::Pointer(PointerEvent::Button {
|
||||
time: 0,
|
||||
button: BTN_LEFT,
|
||||
state: 0,
|
||||
})))
|
||||
}
|
||||
CGEventType::RightMouseDown => {
|
||||
result.push(CaptureEvent::Input(Event::Pointer(PointerEvent::Button {
|
||||
time: 0,
|
||||
button: BTN_RIGHT,
|
||||
state: 1,
|
||||
})))
|
||||
}
|
||||
CGEventType::RightMouseUp => {
|
||||
result.push(CaptureEvent::Input(Event::Pointer(PointerEvent::Button {
|
||||
time: 0,
|
||||
button: BTN_RIGHT,
|
||||
state: 0,
|
||||
})))
|
||||
}
|
||||
CGEventType::OtherMouseDown => {
|
||||
result.push(CaptureEvent::Input(Event::Pointer(PointerEvent::Button {
|
||||
time: 0,
|
||||
button: BTN_MIDDLE,
|
||||
state: 1,
|
||||
})))
|
||||
}
|
||||
CGEventType::OtherMouseUp => {
|
||||
result.push(CaptureEvent::Input(Event::Pointer(PointerEvent::Button {
|
||||
time: 0,
|
||||
button: BTN_MIDDLE,
|
||||
state: 0,
|
||||
})))
|
||||
}
|
||||
CGEventType::ScrollWheel => {
|
||||
let v = ev.get_integer_value_field(EventField::SCROLL_WHEEL_EVENT_POINT_DELTA_AXIS_1);
|
||||
let h = ev.get_integer_value_field(EventField::SCROLL_WHEEL_EVENT_POINT_DELTA_AXIS_2);
|
||||
if v != 0 {
|
||||
result.push(CaptureEvent::Input(Event::Pointer(PointerEvent::Axis {
|
||||
time: 0,
|
||||
axis: 0, // Vertical
|
||||
value: v as f64,
|
||||
})));
|
||||
}
|
||||
if h != 0 {
|
||||
result.push(CaptureEvent::Input(Event::Pointer(PointerEvent::Axis {
|
||||
time: 0,
|
||||
axis: 1, // Horizontal
|
||||
value: h as f64,
|
||||
})));
|
||||
}
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn create_event_tap<'a>(
|
||||
client_state: Arc<Mutex<InputCaptureState>>,
|
||||
notify_tx: Sender<ProducerEvent>,
|
||||
event_tx: Sender<(Position, CaptureEvent)>,
|
||||
) -> Result<CGEventTap<'a>, MacosCaptureCreationError> {
|
||||
let cg_events_of_interest: Vec<CGEventType> = vec![
|
||||
CGEventType::LeftMouseDown,
|
||||
CGEventType::LeftMouseUp,
|
||||
CGEventType::RightMouseDown,
|
||||
CGEventType::RightMouseUp,
|
||||
CGEventType::OtherMouseDown,
|
||||
CGEventType::OtherMouseUp,
|
||||
CGEventType::MouseMoved,
|
||||
CGEventType::LeftMouseDragged,
|
||||
CGEventType::RightMouseDragged,
|
||||
CGEventType::OtherMouseDragged,
|
||||
CGEventType::ScrollWheel,
|
||||
CGEventType::KeyDown,
|
||||
CGEventType::KeyUp,
|
||||
CGEventType::FlagsChanged,
|
||||
];
|
||||
|
||||
let event_tap_callback =
|
||||
move |_proxy: CGEventTapProxy, event_type: CGEventType, cg_ev: &CGEvent| {
|
||||
log::trace!("Got event from tap: {event_type:?}");
|
||||
let mut state = client_state.blocking_lock();
|
||||
let mut pos = None;
|
||||
let mut res_events = vec![];
|
||||
|
||||
if matches!(
|
||||
event_type,
|
||||
CGEventType::TapDisabledByTimeout | CGEventType::TapDisabledByUserInput
|
||||
) {
|
||||
log::error!("CGEventTap disabled");
|
||||
notify_tx
|
||||
.blocking_send(ProducerEvent::EventTapDisabled)
|
||||
.unwrap_or_else(|e| {
|
||||
log::error!("Failed to send notification: {e}");
|
||||
});
|
||||
}
|
||||
|
||||
// Are we in a client?
|
||||
if let Some(current_pos) = state.current_pos {
|
||||
pos = Some(current_pos);
|
||||
get_events(&event_type, cg_ev, &mut res_events).unwrap_or_else(|e| {
|
||||
log::error!("Failed to get events: {e}");
|
||||
});
|
||||
|
||||
// Keep (hidden) cursor at the edge of the screen
|
||||
if matches!(event_type, CGEventType::MouseMoved) {
|
||||
state.reset_mouse_position(cg_ev).unwrap_or_else(|e| {
|
||||
log::error!("Failed to reset mouse position: {e}");
|
||||
})
|
||||
}
|
||||
}
|
||||
// Did we cross a barrier?
|
||||
else if matches!(event_type, CGEventType::MouseMoved) {
|
||||
if let Some(new_pos) = state.crossed(cg_ev) {
|
||||
pos = Some(new_pos);
|
||||
res_events.push(CaptureEvent::Begin);
|
||||
notify_tx
|
||||
.blocking_send(ProducerEvent::Grab(new_pos))
|
||||
.expect("Failed to send notification");
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(pos) = pos {
|
||||
res_events.iter().for_each(|e| {
|
||||
event_tx
|
||||
.blocking_send((pos, *e))
|
||||
.expect("Failed to send event");
|
||||
});
|
||||
// Returning None should stop the event from being processed
|
||||
// but core fundation still returns the event
|
||||
cg_ev.set_type(CGEventType::Null);
|
||||
}
|
||||
Some(cg_ev.to_owned())
|
||||
};
|
||||
|
||||
let tap = CGEventTap::new(
|
||||
CGEventTapLocation::Session,
|
||||
CGEventTapPlacement::HeadInsertEventTap,
|
||||
CGEventTapOptions::Default,
|
||||
cg_events_of_interest,
|
||||
event_tap_callback,
|
||||
)
|
||||
.map_err(|_| MacosCaptureCreationError::EventTapCreation)?;
|
||||
|
||||
let tap_source: CFRunLoopSource = tap
|
||||
.mach_port
|
||||
.create_runloop_source(0)
|
||||
.expect("Failed creating loop source");
|
||||
|
||||
unsafe {
|
||||
CFRunLoop::get_current().add_source(&tap_source, kCFRunLoopCommonModes);
|
||||
}
|
||||
|
||||
Ok(tap)
|
||||
}
|
||||
|
||||
fn event_tap_thread(
|
||||
client_state: Arc<Mutex<InputCaptureState>>,
|
||||
event_tx: Sender<(Position, CaptureEvent)>,
|
||||
notify_tx: Sender<ProducerEvent>,
|
||||
ready: std::sync::mpsc::Sender<Result<(), MacosCaptureCreationError>>,
|
||||
exit: oneshot::Sender<Result<(), &'static str>>,
|
||||
) {
|
||||
let _tap = match create_event_tap(client_state, notify_tx, event_tx) {
|
||||
Err(e) => {
|
||||
ready.send(Err(e)).expect("channel closed");
|
||||
return;
|
||||
}
|
||||
Ok(tap) => {
|
||||
ready.send(Ok(())).expect("channel closed");
|
||||
tap
|
||||
}
|
||||
};
|
||||
CFRunLoop::run_current();
|
||||
|
||||
let _ = exit.send(Err("tap thread exited"));
|
||||
}
|
||||
|
||||
pub struct MacOSInputCapture {
|
||||
event_rx: Receiver<(Position, CaptureEvent)>,
|
||||
notify_tx: Sender<ProducerEvent>,
|
||||
}
|
||||
|
||||
impl MacOSInputCapture {
|
||||
pub async fn new() -> Result<Self, MacosCaptureCreationError> {
|
||||
let state = Arc::new(Mutex::new(InputCaptureState::new()?));
|
||||
let (event_tx, event_rx) = mpsc::channel(32);
|
||||
let (notify_tx, mut notify_rx) = mpsc::channel(32);
|
||||
let (ready_tx, ready_rx) = std::sync::mpsc::channel();
|
||||
let (tap_exit_tx, mut tap_exit_rx) = oneshot::channel();
|
||||
|
||||
unsafe {
|
||||
configure_cf_settings()?;
|
||||
}
|
||||
|
||||
log::info!("Enabling CGEvent tap");
|
||||
let event_tap_thread_state = state.clone();
|
||||
let event_tap_notify = notify_tx.clone();
|
||||
thread::spawn(move || {
|
||||
event_tap_thread(
|
||||
event_tap_thread_state,
|
||||
event_tx,
|
||||
event_tap_notify,
|
||||
ready_tx,
|
||||
tap_exit_tx,
|
||||
)
|
||||
});
|
||||
|
||||
// wait for event tap creation result
|
||||
ready_rx.recv().expect("channel closed")?;
|
||||
|
||||
let _tap_task: tokio::task::JoinHandle<()> = tokio::task::spawn_local(async move {
|
||||
loop {
|
||||
tokio::select! {
|
||||
producer_event = notify_rx.recv() => {
|
||||
let producer_event = producer_event.expect("channel closed");
|
||||
let mut state = state.lock().await;
|
||||
state.handle_producer_event(producer_event).await.unwrap_or_else(|e| {
|
||||
log::error!("Failed to handle producer event: {e}");
|
||||
})
|
||||
}
|
||||
|
||||
res = &mut tap_exit_rx => {
|
||||
if let Err(e) = res.expect("channel closed") {
|
||||
log::error!("Tap thread failed: {:?}", e);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
Ok(Self {
|
||||
event_rx,
|
||||
notify_tx,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl Capture for MacOSInputCapture {
|
||||
async fn create(&mut self, pos: Position) -> Result<(), CaptureError> {
|
||||
let notify_tx = self.notify_tx.clone();
|
||||
tokio::task::spawn_local(async move {
|
||||
log::debug!("creating capture, {pos}");
|
||||
let _ = notify_tx.send(ProducerEvent::Create(pos)).await;
|
||||
log::debug!("done !");
|
||||
});
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn destroy(&mut self, pos: Position) -> Result<(), CaptureError> {
|
||||
let notify_tx = self.notify_tx.clone();
|
||||
tokio::task::spawn_local(async move {
|
||||
log::debug!("destroying capture {pos}");
|
||||
let _ = notify_tx.send(ProducerEvent::Destroy(pos)).await;
|
||||
log::debug!("done !");
|
||||
});
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn release(&mut self) -> Result<(), CaptureError> {
|
||||
let notify_tx = self.notify_tx.clone();
|
||||
tokio::task::spawn_local(async move {
|
||||
log::debug!("notifying Release");
|
||||
let _ = notify_tx.send(ProducerEvent::Release).await;
|
||||
});
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn terminate(&mut self) -> Result<(), CaptureError> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl Stream for MacOSInputCapture {
|
||||
type Item = Result<(Position, CaptureEvent), CaptureError>;
|
||||
|
||||
fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
|
||||
match ready!(self.event_rx.poll_recv(cx)) {
|
||||
None => Poll::Ready(None),
|
||||
Some(e) => Poll::Ready(Some(Ok(e))),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type CGSConnectionID = u32;
|
||||
|
||||
#[link(name = "ApplicationServices", kind = "framework")]
|
||||
extern "C" {
|
||||
fn CGSSetConnectionProperty(
|
||||
cid: CGSConnectionID,
|
||||
targetCID: CGSConnectionID,
|
||||
key: CFStringRef,
|
||||
value: CFBooleanRef,
|
||||
) -> CGError;
|
||||
fn _CGSDefaultConnection() -> CGSConnectionID;
|
||||
}
|
||||
|
||||
extern "C" {
|
||||
fn CGEventSourceSetLocalEventsSuppressionInterval(
|
||||
event_source: CGEventSource,
|
||||
seconds: CFTimeInterval,
|
||||
);
|
||||
}
|
||||
|
||||
unsafe fn configure_cf_settings() -> Result<(), MacosCaptureCreationError> {
|
||||
// When we warp the cursor using CGWarpMouseCursorPosition local events are suppressed for a short time
|
||||
// this leeds to the cursor not flowing when crossing back from a clinet, set this to to 0 stops the warp
|
||||
// from working, set a low value by trial and error, 0.05s seems good. 0.25s is the default
|
||||
let event_source = CGEventSource::new(CGEventSourceStateID::CombinedSessionState)
|
||||
.map_err(|_| MacosCaptureCreationError::EventSourceCreation)?;
|
||||
CGEventSourceSetLocalEventsSuppressionInterval(event_source, 0.05);
|
||||
|
||||
// This is a private settings that allows the cursor to be hidden while in the background.
|
||||
// It is used by Barrier and other apps.
|
||||
let key = CString::new("SetsCursorInBackground").unwrap();
|
||||
let cf_key = CFStringCreateWithCString(
|
||||
kCFAllocatorDefault,
|
||||
key.as_ptr() as *const c_char,
|
||||
kCFStringEncodingUTF8,
|
||||
);
|
||||
if CGSSetConnectionProperty(
|
||||
_CGSDefaultConnection(),
|
||||
_CGSDefaultConnection(),
|
||||
cf_key,
|
||||
kCFBooleanTrue,
|
||||
) != kCGErrorSuccess
|
||||
{
|
||||
return Err(MacosCaptureCreationError::CGCursorProperty);
|
||||
}
|
||||
CFRelease(cf_key as *const c_void);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// From X11/X.h
|
||||
bitflags! {
|
||||
#[repr(C)]
|
||||
#[derive(Clone, Copy, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd)]
|
||||
struct XMods: u32 {
|
||||
const ShiftMask = (1<<0);
|
||||
const LockMask = (1<<1);
|
||||
const ControlMask = (1<<2);
|
||||
const Mod1Mask = (1<<3);
|
||||
const Mod2Mask = (1<<4);
|
||||
const Mod3Mask = (1<<5);
|
||||
const Mod4Mask = (1<<6);
|
||||
const Mod5Mask = (1<<7);
|
||||
}
|
||||
}
|
||||
@@ -1,16 +1,10 @@
|
||||
use crate::{
|
||||
client::{ClientEvent, ClientHandle, Position},
|
||||
producer::EventProducer,
|
||||
};
|
||||
|
||||
use anyhow::{anyhow, Result};
|
||||
use async_trait::async_trait;
|
||||
use futures_core::Stream;
|
||||
use memmap::MmapOptions;
|
||||
use std::{
|
||||
collections::VecDeque,
|
||||
env,
|
||||
io::{self, ErrorKind},
|
||||
os::fd::{AsFd, OwnedFd, RawFd},
|
||||
os::fd::{AsFd, RawFd},
|
||||
pin::Pin,
|
||||
task::{ready, Context, Poll},
|
||||
};
|
||||
@@ -19,8 +13,8 @@ use tokio::io::unix::AsyncFd;
|
||||
use std::{
|
||||
fs::File,
|
||||
io::{BufWriter, Write},
|
||||
os::unix::prelude::{AsRawFd, FromRawFd},
|
||||
rc::Rc,
|
||||
os::unix::prelude::AsRawFd,
|
||||
sync::Arc,
|
||||
};
|
||||
|
||||
use wayland_protocols::{
|
||||
@@ -54,26 +48,34 @@ use wayland_client::{
|
||||
delegate_noop,
|
||||
globals::{registry_queue_init, GlobalListContents},
|
||||
protocol::{
|
||||
wl_buffer, wl_compositor, wl_keyboard,
|
||||
wl_buffer, wl_compositor,
|
||||
wl_keyboard::{self, WlKeyboard},
|
||||
wl_output::{self, WlOutput},
|
||||
wl_pointer, wl_region, wl_registry, wl_seat, wl_shm, wl_shm_pool, wl_surface,
|
||||
wl_pointer::{self, WlPointer},
|
||||
wl_region, wl_registry, wl_seat, wl_shm, wl_shm_pool,
|
||||
wl_surface::WlSurface,
|
||||
},
|
||||
Connection, Dispatch, DispatchError, EventQueue, QueueHandle, WEnum,
|
||||
};
|
||||
|
||||
use tempfile;
|
||||
use input_event::{Event, KeyboardEvent, PointerEvent};
|
||||
|
||||
use crate::event::{Event, KeyboardEvent, PointerEvent};
|
||||
use crate::{CaptureError, CaptureEvent};
|
||||
|
||||
use super::{
|
||||
error::{LayerShellCaptureCreationError, WaylandBindError},
|
||||
Capture, Position,
|
||||
};
|
||||
|
||||
struct Globals {
|
||||
compositor: wl_compositor::WlCompositor,
|
||||
pointer_constraints: ZwpPointerConstraintsV1,
|
||||
relative_pointer_manager: ZwpRelativePointerManagerV1,
|
||||
shortcut_inhibit_manager: ZwpKeyboardShortcutsInhibitManagerV1,
|
||||
shortcut_inhibit_manager: Option<ZwpKeyboardShortcutsInhibitManagerV1>,
|
||||
seat: wl_seat::WlSeat,
|
||||
shm: wl_shm::WlShm,
|
||||
layer_shell: ZwlrLayerShellV1,
|
||||
outputs: Vec<wl_output::WlOutput>,
|
||||
outputs: Vec<WlOutput>,
|
||||
xdg_output_manager: ZxdgOutputManagerV1,
|
||||
}
|
||||
|
||||
@@ -95,17 +97,20 @@ impl OutputInfo {
|
||||
}
|
||||
|
||||
struct State {
|
||||
pointer: Option<WlPointer>,
|
||||
keyboard: Option<WlKeyboard>,
|
||||
pointer_lock: Option<ZwpLockedPointerV1>,
|
||||
rel_pointer: Option<ZwpRelativePointerV1>,
|
||||
shortcut_inhibitor: Option<ZwpKeyboardShortcutsInhibitorV1>,
|
||||
client_for_window: Vec<(Rc<Window>, ClientHandle)>,
|
||||
focused: Option<(Rc<Window>, ClientHandle)>,
|
||||
active_windows: Vec<Arc<Window>>,
|
||||
focused: Option<Arc<Window>>,
|
||||
g: Globals,
|
||||
wayland_fd: OwnedFd,
|
||||
wayland_fd: RawFd,
|
||||
read_guard: Option<ReadEventsGuard>,
|
||||
qh: QueueHandle<Self>,
|
||||
pending_events: VecDeque<(ClientHandle, Event)>,
|
||||
pending_events: VecDeque<(Position, CaptureEvent)>,
|
||||
output_info: Vec<(WlOutput, OutputInfo)>,
|
||||
scroll_discrete_pending: bool,
|
||||
}
|
||||
|
||||
struct Inner {
|
||||
@@ -115,15 +120,15 @@ struct Inner {
|
||||
|
||||
impl AsRawFd for Inner {
|
||||
fn as_raw_fd(&self) -> RawFd {
|
||||
self.state.wayland_fd.as_raw_fd()
|
||||
self.state.wayland_fd
|
||||
}
|
||||
}
|
||||
|
||||
pub struct WaylandEventProducer(AsyncFd<Inner>);
|
||||
pub struct LayerShellInputCapture(AsyncFd<Inner>);
|
||||
|
||||
struct Window {
|
||||
buffer: wl_buffer::WlBuffer,
|
||||
surface: wl_surface::WlSurface,
|
||||
surface: WlSurface,
|
||||
layer_surface: ZwlrLayerSurfaceV1,
|
||||
pos: Position,
|
||||
}
|
||||
@@ -136,6 +141,7 @@ impl Window {
|
||||
pos: Position,
|
||||
size: (i32, i32),
|
||||
) -> Window {
|
||||
log::debug!("creating window output: {output:?}, size: {size:?}");
|
||||
let g = &state.g;
|
||||
|
||||
let (width, height) = match pos {
|
||||
@@ -217,6 +223,7 @@ fn get_edges(outputs: &[(WlOutput, OutputInfo)], pos: Position) -> Vec<(WlOutput
|
||||
fn get_output_configuration(state: &State, pos: Position) -> Vec<(WlOutput, OutputInfo)> {
|
||||
// get all output edges corresponding to the position
|
||||
let edges = get_edges(&state.output_info, pos);
|
||||
log::debug!("edges: {edges:?}");
|
||||
let opposite_edges = get_edges(&state.output_info, pos.opposite());
|
||||
|
||||
// remove those edges that are at the same position
|
||||
@@ -249,65 +256,47 @@ fn draw(f: &mut File, (width, height): (u32, u32)) {
|
||||
}
|
||||
}
|
||||
|
||||
impl WaylandEventProducer {
|
||||
pub fn new() -> Result<Self> {
|
||||
let conn = match Connection::connect_to_env() {
|
||||
Ok(c) => c,
|
||||
Err(e) => return Err(anyhow!("could not connect to wayland compositor: {e:?}")),
|
||||
};
|
||||
|
||||
let (g, mut queue) = match registry_queue_init::<State>(&conn) {
|
||||
Ok(q) => q,
|
||||
Err(e) => return Err(anyhow!("failed to initialize wl_registry: {e:?}")),
|
||||
};
|
||||
impl LayerShellInputCapture {
|
||||
pub fn new() -> std::result::Result<Self, LayerShellCaptureCreationError> {
|
||||
let conn = Connection::connect_to_env()?;
|
||||
let (g, mut queue) = registry_queue_init::<State>(&conn)?;
|
||||
|
||||
let qh = queue.handle();
|
||||
|
||||
let compositor: wl_compositor::WlCompositor = match g.bind(&qh, 4..=5, ()) {
|
||||
Ok(compositor) => compositor,
|
||||
Err(_) => return Err(anyhow!("wl_compositor >= v4 not supported")),
|
||||
};
|
||||
|
||||
let xdg_output_manager: ZxdgOutputManagerV1 = match g.bind(&qh, 1..=3, ()) {
|
||||
Ok(xdg_output_manager) => xdg_output_manager,
|
||||
Err(_) => return Err(anyhow!("xdg_output not supported!")),
|
||||
};
|
||||
|
||||
let shm: wl_shm::WlShm = match g.bind(&qh, 1..=1, ()) {
|
||||
Ok(wl_shm) => wl_shm,
|
||||
Err(_) => return Err(anyhow!("wl_shm v1 not supported")),
|
||||
};
|
||||
|
||||
let layer_shell: ZwlrLayerShellV1 = match g.bind(&qh, 3..=4, ()) {
|
||||
Ok(layer_shell) => layer_shell,
|
||||
Err(_) => return Err(anyhow!("zwlr_layer_shell_v1 >= v3 not supported - required to display a surface at the edge of the screen")),
|
||||
};
|
||||
|
||||
let seat: wl_seat::WlSeat = match g.bind(&qh, 7..=8, ()) {
|
||||
Ok(wl_seat) => wl_seat,
|
||||
Err(_) => return Err(anyhow!("wl_seat >= v7 not supported")),
|
||||
};
|
||||
|
||||
let pointer_constraints: ZwpPointerConstraintsV1 = match g.bind(&qh, 1..=1, ()) {
|
||||
Ok(pointer_constraints) => pointer_constraints,
|
||||
Err(_) => return Err(anyhow!("zwp_pointer_constraints_v1 not supported")),
|
||||
};
|
||||
|
||||
let relative_pointer_manager: ZwpRelativePointerManagerV1 = match g.bind(&qh, 1..=1, ()) {
|
||||
Ok(relative_pointer_manager) => relative_pointer_manager,
|
||||
Err(_) => return Err(anyhow!("zwp_relative_pointer_manager_v1 not supported")),
|
||||
};
|
||||
|
||||
let shortcut_inhibit_manager: ZwpKeyboardShortcutsInhibitManagerV1 =
|
||||
match g.bind(&qh, 1..=1, ()) {
|
||||
Ok(shortcut_inhibit_manager) => shortcut_inhibit_manager,
|
||||
Err(_) => {
|
||||
return Err(anyhow!(
|
||||
"zwp_keyboard_shortcuts_inhibit_manager_v1 not supported"
|
||||
))
|
||||
}
|
||||
};
|
||||
let compositor: wl_compositor::WlCompositor = g
|
||||
.bind(&qh, 4..=5, ())
|
||||
.map_err(|e| WaylandBindError::new(e, "wl_compositor 4..=5"))?;
|
||||
let xdg_output_manager: ZxdgOutputManagerV1 = g
|
||||
.bind(&qh, 1..=3, ())
|
||||
.map_err(|e| WaylandBindError::new(e, "xdg_output_manager 1..=3"))?;
|
||||
let shm: wl_shm::WlShm = g
|
||||
.bind(&qh, 1..=1, ())
|
||||
.map_err(|e| WaylandBindError::new(e, "wl_shm"))?;
|
||||
let layer_shell: ZwlrLayerShellV1 = g
|
||||
.bind(&qh, 3..=4, ())
|
||||
.map_err(|e| WaylandBindError::new(e, "wlr_layer_shell 3..=4"))?;
|
||||
let seat: wl_seat::WlSeat = g
|
||||
.bind(&qh, 7..=8, ())
|
||||
.map_err(|e| WaylandBindError::new(e, "wl_seat 7..=8"))?;
|
||||
|
||||
let pointer_constraints: ZwpPointerConstraintsV1 = g
|
||||
.bind(&qh, 1..=1, ())
|
||||
.map_err(|e| WaylandBindError::new(e, "zwp_pointer_constraints_v1"))?;
|
||||
let relative_pointer_manager: ZwpRelativePointerManagerV1 = g
|
||||
.bind(&qh, 1..=1, ())
|
||||
.map_err(|e| WaylandBindError::new(e, "zwp_relative_pointer_manager_v1"))?;
|
||||
let shortcut_inhibit_manager: Result<
|
||||
ZwpKeyboardShortcutsInhibitManagerV1,
|
||||
WaylandBindError,
|
||||
> = g
|
||||
.bind(&qh, 1..=1, ())
|
||||
.map_err(|e| WaylandBindError::new(e, "zwp_keyboard_shortcuts_inhibit_manager_v1"));
|
||||
// layer-shell backend still works without this protocol so we make it an optional dependency
|
||||
if let Err(e) = &shortcut_inhibit_manager {
|
||||
log::warn!("shortcut_inhibit_manager not supported: {e}\nkeybinds handled by the compositor will not be passed
|
||||
to the client");
|
||||
}
|
||||
let shortcut_inhibit_manager = shortcut_inhibit_manager.ok();
|
||||
let outputs = vec![];
|
||||
|
||||
let g = Globals {
|
||||
@@ -325,23 +314,23 @@ impl WaylandEventProducer {
|
||||
// flush outgoing events
|
||||
queue.flush()?;
|
||||
|
||||
// prepare reading wayland events
|
||||
let read_guard = queue.prepare_read().unwrap(); // there can not yet be events to dispatch
|
||||
let wayland_fd = read_guard.connection_fd().try_clone_to_owned().unwrap();
|
||||
std::mem::drop(read_guard);
|
||||
let wayland_fd = queue.as_fd().as_raw_fd();
|
||||
|
||||
let mut state = State {
|
||||
pointer: None,
|
||||
keyboard: None,
|
||||
g,
|
||||
pointer_lock: None,
|
||||
rel_pointer: None,
|
||||
shortcut_inhibitor: None,
|
||||
client_for_window: Vec::new(),
|
||||
active_windows: Vec::new(),
|
||||
focused: None,
|
||||
qh,
|
||||
wayland_fd,
|
||||
read_guard: None,
|
||||
pending_events: VecDeque::new(),
|
||||
output_info: vec![],
|
||||
scroll_discrete_pending: false,
|
||||
};
|
||||
|
||||
// dispatch registry to () again, in order to read all wl_outputs
|
||||
@@ -381,23 +370,18 @@ impl WaylandEventProducer {
|
||||
|
||||
let inner = AsyncFd::new(Inner { queue, state })?;
|
||||
|
||||
Ok(WaylandEventProducer(inner))
|
||||
Ok(LayerShellInputCapture(inner))
|
||||
}
|
||||
|
||||
fn add_client(&mut self, handle: ClientHandle, pos: Position) {
|
||||
self.0.get_mut().state.add_client(handle, pos);
|
||||
fn add_client(&mut self, pos: Position) {
|
||||
self.0.get_mut().state.add_client(pos);
|
||||
}
|
||||
|
||||
fn delete_client(&mut self, handle: ClientHandle) {
|
||||
fn delete_client(&mut self, pos: Position) {
|
||||
let inner = self.0.get_mut();
|
||||
// remove all windows corresponding to this client
|
||||
while let Some(i) = inner
|
||||
.state
|
||||
.client_for_window
|
||||
.iter()
|
||||
.position(|(_, c)| *c == handle)
|
||||
{
|
||||
inner.state.client_for_window.remove(i);
|
||||
while let Some(i) = inner.state.active_windows.iter().position(|w| w.pos == pos) {
|
||||
inner.state.active_windows.remove(i);
|
||||
inner.state.focused = None;
|
||||
}
|
||||
}
|
||||
@@ -406,12 +390,12 @@ impl WaylandEventProducer {
|
||||
impl State {
|
||||
fn grab(
|
||||
&mut self,
|
||||
surface: &wl_surface::WlSurface,
|
||||
pointer: &wl_pointer::WlPointer,
|
||||
surface: &WlSurface,
|
||||
pointer: &WlPointer,
|
||||
serial: u32,
|
||||
qh: &QueueHandle<State>,
|
||||
) {
|
||||
let (window, _) = self.focused.as_ref().unwrap();
|
||||
let window = self.focused.as_ref().unwrap();
|
||||
|
||||
// hide the cursor
|
||||
pointer.set_cursor(serial, None, 0, 0);
|
||||
@@ -444,19 +428,17 @@ impl State {
|
||||
}
|
||||
|
||||
// capture modifier keys
|
||||
if self.shortcut_inhibitor.is_none() {
|
||||
self.shortcut_inhibitor = Some(self.g.shortcut_inhibit_manager.inhibit_shortcuts(
|
||||
surface,
|
||||
&self.g.seat,
|
||||
qh,
|
||||
(),
|
||||
));
|
||||
if let Some(shortcut_inhibit_manager) = &self.g.shortcut_inhibit_manager {
|
||||
if self.shortcut_inhibitor.is_none() {
|
||||
self.shortcut_inhibitor =
|
||||
Some(shortcut_inhibit_manager.inhibit_shortcuts(surface, &self.g.seat, qh, ()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn ungrab(&mut self) {
|
||||
// get focused client
|
||||
let (window, _client) = match self.focused.as_ref() {
|
||||
let window = match self.focused.as_ref() {
|
||||
Some(focused) => focused,
|
||||
None => return,
|
||||
};
|
||||
@@ -486,26 +468,23 @@ impl State {
|
||||
}
|
||||
}
|
||||
|
||||
fn add_client(&mut self, client: ClientHandle, pos: Position) {
|
||||
fn add_client(&mut self, pos: Position) {
|
||||
let outputs = get_output_configuration(self, pos);
|
||||
|
||||
log::debug!("outputs: {outputs:?}");
|
||||
outputs.iter().for_each(|(o, i)| {
|
||||
let window = Window::new(self, &self.qh, o, pos, i.size);
|
||||
let window = Rc::new(window);
|
||||
self.client_for_window.push((window, client));
|
||||
let window = Arc::new(window);
|
||||
self.active_windows.push(window);
|
||||
});
|
||||
}
|
||||
|
||||
fn update_windows(&mut self) {
|
||||
log::debug!("updating windows");
|
||||
log::debug!("output info: {:?}", self.output_info);
|
||||
let clients: Vec<_> = self
|
||||
.client_for_window
|
||||
.drain(..)
|
||||
.map(|(w, c)| (c, w.pos))
|
||||
.collect();
|
||||
for (client, pos) in clients {
|
||||
self.add_client(client, pos);
|
||||
let clients: Vec<_> = self.active_windows.drain(..).map(|w| w.pos).collect();
|
||||
for pos in clients {
|
||||
self.add_client(pos);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -577,30 +556,34 @@ impl Inner {
|
||||
}
|
||||
}
|
||||
|
||||
impl EventProducer for WaylandEventProducer {
|
||||
fn notify(&mut self, client_event: ClientEvent) -> io::Result<()> {
|
||||
match client_event {
|
||||
ClientEvent::Create(handle, pos) => {
|
||||
self.add_client(handle, pos);
|
||||
}
|
||||
ClientEvent::Destroy(handle) => {
|
||||
self.delete_client(handle);
|
||||
}
|
||||
}
|
||||
#[async_trait]
|
||||
impl Capture for LayerShellInputCapture {
|
||||
async fn create(&mut self, pos: Position) -> Result<(), CaptureError> {
|
||||
self.add_client(pos);
|
||||
let inner = self.0.get_mut();
|
||||
inner.flush_events()
|
||||
Ok(inner.flush_events()?)
|
||||
}
|
||||
|
||||
fn release(&mut self) -> io::Result<()> {
|
||||
async fn destroy(&mut self, pos: Position) -> Result<(), CaptureError> {
|
||||
self.delete_client(pos);
|
||||
let inner = self.0.get_mut();
|
||||
Ok(inner.flush_events()?)
|
||||
}
|
||||
|
||||
async fn release(&mut self) -> Result<(), CaptureError> {
|
||||
log::debug!("releasing pointer");
|
||||
let inner = self.0.get_mut();
|
||||
inner.state.ungrab();
|
||||
inner.flush_events()
|
||||
Ok(inner.flush_events()?)
|
||||
}
|
||||
|
||||
async fn terminate(&mut self) -> Result<(), CaptureError> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl Stream for WaylandEventProducer {
|
||||
type Item = io::Result<(ClientHandle, Event)>;
|
||||
impl Stream for LayerShellInputCapture {
|
||||
type Item = Result<(Position, CaptureEvent), CaptureError>;
|
||||
|
||||
fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
|
||||
if let Some(event) = self.0.get_mut().state.pending_events.pop_front() {
|
||||
@@ -618,7 +601,7 @@ impl Stream for WaylandEventProducer {
|
||||
// prepare next read
|
||||
match inner.prepare_read() {
|
||||
Ok(_) => {}
|
||||
Err(e) => return Poll::Ready(Some(Err(e))),
|
||||
Err(e) => return Poll::Ready(Some(Err(e.into()))),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -628,14 +611,14 @@ impl Stream for WaylandEventProducer {
|
||||
// flush outgoing events
|
||||
if let Err(e) = inner.flush_events() {
|
||||
if e.kind() != ErrorKind::WouldBlock {
|
||||
return Poll::Ready(Some(Err(e)));
|
||||
return Poll::Ready(Some(Err(e.into())));
|
||||
}
|
||||
}
|
||||
|
||||
// prepare for the next read
|
||||
match inner.prepare_read() {
|
||||
Ok(_) => {}
|
||||
Err(e) => return Poll::Ready(Some(Err(e))),
|
||||
Err(e) => return Poll::Ready(Some(Err(e.into()))),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -654,7 +637,7 @@ impl Stream for WaylandEventProducer {
|
||||
|
||||
impl Dispatch<wl_seat::WlSeat, ()> for State {
|
||||
fn event(
|
||||
_: &mut Self,
|
||||
state: &mut Self,
|
||||
seat: &wl_seat::WlSeat,
|
||||
event: <wl_seat::WlSeat as wayland_client::Proxy>::Event,
|
||||
_: &(),
|
||||
@@ -665,21 +648,21 @@ impl Dispatch<wl_seat::WlSeat, ()> for State {
|
||||
capabilities: WEnum::Value(capabilities),
|
||||
} = event
|
||||
{
|
||||
if capabilities.contains(wl_seat::Capability::Pointer) {
|
||||
seat.get_pointer(qh, ());
|
||||
if capabilities.contains(wl_seat::Capability::Pointer) && state.pointer.is_none() {
|
||||
state.pointer.replace(seat.get_pointer(qh, ()));
|
||||
}
|
||||
if capabilities.contains(wl_seat::Capability::Keyboard) {
|
||||
if capabilities.contains(wl_seat::Capability::Keyboard) && state.keyboard.is_none() {
|
||||
seat.get_keyboard(qh, ());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Dispatch<wl_pointer::WlPointer, ()> for State {
|
||||
impl Dispatch<WlPointer, ()> for State {
|
||||
fn event(
|
||||
app: &mut Self,
|
||||
pointer: &wl_pointer::WlPointer,
|
||||
event: <wl_pointer::WlPointer as wayland_client::Proxy>::Event,
|
||||
pointer: &WlPointer,
|
||||
event: <WlPointer as wayland_client::Proxy>::Event,
|
||||
_: &(),
|
||||
_: &Connection,
|
||||
qh: &QueueHandle<Self>,
|
||||
@@ -693,23 +676,20 @@ impl Dispatch<wl_pointer::WlPointer, ()> for State {
|
||||
} => {
|
||||
// get client corresponding to the focused surface
|
||||
{
|
||||
if let Some((window, client)) = app
|
||||
.client_for_window
|
||||
.iter()
|
||||
.find(|(w, _c)| w.surface == surface)
|
||||
{
|
||||
app.focused = Some((window.clone(), *client));
|
||||
if let Some(window) = app.active_windows.iter().find(|w| w.surface == surface) {
|
||||
app.focused = Some(window.clone());
|
||||
app.grab(&surface, pointer, serial, qh);
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
}
|
||||
let (_, client) = app
|
||||
.client_for_window
|
||||
let pos = app
|
||||
.active_windows
|
||||
.iter()
|
||||
.find(|(w, _c)| w.surface == surface)
|
||||
.find(|w| w.surface == surface)
|
||||
.map(|w| w.pos)
|
||||
.unwrap();
|
||||
app.pending_events.push_back((*client, Event::Enter()));
|
||||
app.pending_events.push_back((pos, CaptureEvent::Begin));
|
||||
}
|
||||
wl_pointer::Event::Leave { .. } => {
|
||||
/* There are rare cases, where when a window is opened in
|
||||
@@ -719,6 +699,9 @@ impl Dispatch<wl_pointer::WlPointer, ()> for State {
|
||||
* it is impossible to grab it again (since the pointer
|
||||
* lock, relative pointer,... objects are still in place)
|
||||
*/
|
||||
if app.pointer_lock.is_some() {
|
||||
log::warn!("compositor released mouse");
|
||||
}
|
||||
app.ungrab();
|
||||
}
|
||||
wl_pointer::Event::Button {
|
||||
@@ -727,25 +710,43 @@ impl Dispatch<wl_pointer::WlPointer, ()> for State {
|
||||
button,
|
||||
state,
|
||||
} => {
|
||||
let (_, client) = app.focused.as_ref().unwrap();
|
||||
let window = app.focused.as_ref().unwrap();
|
||||
app.pending_events.push_back((
|
||||
*client,
|
||||
Event::Pointer(PointerEvent::Button {
|
||||
window.pos,
|
||||
CaptureEvent::Input(Event::Pointer(PointerEvent::Button {
|
||||
time,
|
||||
button,
|
||||
state: u32::from(state),
|
||||
}),
|
||||
})),
|
||||
));
|
||||
}
|
||||
wl_pointer::Event::Axis { time, axis, value } => {
|
||||
let (_, client) = app.focused.as_ref().unwrap();
|
||||
let window = app.focused.as_ref().unwrap();
|
||||
if app.scroll_discrete_pending {
|
||||
// each axisvalue120 event is coupled with
|
||||
// a corresponding axis event, which needs to
|
||||
// be ignored to not duplicate the scrolling
|
||||
app.scroll_discrete_pending = false;
|
||||
} else {
|
||||
app.pending_events.push_back((
|
||||
window.pos,
|
||||
CaptureEvent::Input(Event::Pointer(PointerEvent::Axis {
|
||||
time,
|
||||
axis: u32::from(axis) as u8,
|
||||
value,
|
||||
})),
|
||||
));
|
||||
}
|
||||
}
|
||||
wl_pointer::Event::AxisValue120 { axis, value120 } => {
|
||||
let window = app.focused.as_ref().unwrap();
|
||||
app.scroll_discrete_pending = true;
|
||||
app.pending_events.push_back((
|
||||
*client,
|
||||
Event::Pointer(PointerEvent::Axis {
|
||||
time,
|
||||
window.pos,
|
||||
CaptureEvent::Input(Event::Pointer(PointerEvent::AxisDiscrete120 {
|
||||
axis: u32::from(axis) as u8,
|
||||
value,
|
||||
}),
|
||||
value: value120,
|
||||
})),
|
||||
));
|
||||
}
|
||||
wl_pointer::Event::Frame {} => {
|
||||
@@ -758,19 +759,16 @@ impl Dispatch<wl_pointer::WlPointer, ()> for State {
|
||||
}
|
||||
}
|
||||
|
||||
impl Dispatch<wl_keyboard::WlKeyboard, ()> for State {
|
||||
impl Dispatch<WlKeyboard, ()> for State {
|
||||
fn event(
|
||||
app: &mut Self,
|
||||
_: &wl_keyboard::WlKeyboard,
|
||||
_: &WlKeyboard,
|
||||
event: wl_keyboard::Event,
|
||||
_: &(),
|
||||
_: &Connection,
|
||||
_: &QueueHandle<Self>,
|
||||
) {
|
||||
let (_window, client) = match &app.focused {
|
||||
Some(focused) => (Some(&focused.0), Some(&focused.1)),
|
||||
None => (None, None),
|
||||
};
|
||||
let window = &app.focused;
|
||||
match event {
|
||||
wl_keyboard::Event::Key {
|
||||
serial: _,
|
||||
@@ -778,14 +776,14 @@ impl Dispatch<wl_keyboard::WlKeyboard, ()> for State {
|
||||
key,
|
||||
state,
|
||||
} => {
|
||||
if let Some(client) = client {
|
||||
if let Some(window) = window {
|
||||
app.pending_events.push_back((
|
||||
*client,
|
||||
Event::Keyboard(KeyboardEvent::Key {
|
||||
window.pos,
|
||||
CaptureEvent::Input(Event::Keyboard(KeyboardEvent::Key {
|
||||
time,
|
||||
key,
|
||||
state: u32::from(state) as u8,
|
||||
}),
|
||||
})),
|
||||
));
|
||||
}
|
||||
}
|
||||
@@ -796,27 +794,18 @@ impl Dispatch<wl_keyboard::WlKeyboard, ()> for State {
|
||||
mods_locked,
|
||||
group,
|
||||
} => {
|
||||
if let Some(client) = client {
|
||||
if let Some(window) = window {
|
||||
app.pending_events.push_back((
|
||||
*client,
|
||||
Event::Keyboard(KeyboardEvent::Modifiers {
|
||||
mods_depressed,
|
||||
mods_latched,
|
||||
mods_locked,
|
||||
window.pos,
|
||||
CaptureEvent::Input(Event::Keyboard(KeyboardEvent::Modifiers {
|
||||
depressed: mods_depressed,
|
||||
latched: mods_latched,
|
||||
locked: mods_locked,
|
||||
group,
|
||||
}),
|
||||
})),
|
||||
));
|
||||
}
|
||||
}
|
||||
wl_keyboard::Event::Keymap {
|
||||
format: _,
|
||||
fd,
|
||||
size: _,
|
||||
} => {
|
||||
let fd = unsafe { &File::from_raw_fd(fd.as_raw_fd()) };
|
||||
let _mmap = unsafe { MmapOptions::new().map_copy(fd).unwrap() };
|
||||
// TODO keymap
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
@@ -834,21 +823,16 @@ impl Dispatch<ZwpRelativePointerV1, ()> for State {
|
||||
if let zwp_relative_pointer_v1::Event::RelativeMotion {
|
||||
utime_hi,
|
||||
utime_lo,
|
||||
dx: _,
|
||||
dy: _,
|
||||
dx_unaccel: surface_x,
|
||||
dy_unaccel: surface_y,
|
||||
dx_unaccel: dx,
|
||||
dy_unaccel: dy,
|
||||
..
|
||||
} = event
|
||||
{
|
||||
if let Some((_window, client)) = &app.focused {
|
||||
if let Some(window) = &app.focused {
|
||||
let time = (((utime_hi as u64) << 32 | utime_lo as u64) / 1000) as u32;
|
||||
app.pending_events.push_back((
|
||||
*client,
|
||||
Event::Pointer(PointerEvent::Motion {
|
||||
time,
|
||||
relative_x: surface_x,
|
||||
relative_y: surface_y,
|
||||
}),
|
||||
window.pos,
|
||||
CaptureEvent::Input(Event::Pointer(PointerEvent::Motion { time, dx, dy })),
|
||||
));
|
||||
}
|
||||
}
|
||||
@@ -865,10 +849,10 @@ impl Dispatch<ZwlrLayerSurfaceV1, ()> for State {
|
||||
_: &QueueHandle<Self>,
|
||||
) {
|
||||
if let zwlr_layer_surface_v1::Event::Configure { serial, .. } = event {
|
||||
if let Some((window, _client)) = app
|
||||
.client_for_window
|
||||
if let Some(window) = app
|
||||
.active_windows
|
||||
.iter()
|
||||
.find(|(w, _c)| &w.layer_surface == layer_surface)
|
||||
.find(|w| &w.layer_surface == layer_surface)
|
||||
{
|
||||
// client corresponding to the layer_surface
|
||||
let surface = &window.surface;
|
||||
@@ -914,7 +898,7 @@ impl Dispatch<wl_registry::WlRegistry, ()> for State {
|
||||
state
|
||||
.g
|
||||
.outputs
|
||||
.push(registry.bind::<wl_output::WlOutput, _, _>(name, 4, qh, ()))
|
||||
.push(registry.bind::<WlOutput, _, _>(name, 4, qh, ()))
|
||||
}
|
||||
}
|
||||
wl_registry::Event::GlobalRemove { .. } => {}
|
||||
@@ -959,11 +943,11 @@ impl Dispatch<ZxdgOutputV1, WlOutput> for State {
|
||||
}
|
||||
}
|
||||
|
||||
impl Dispatch<wl_output::WlOutput, ()> for State {
|
||||
impl Dispatch<WlOutput, ()> for State {
|
||||
fn event(
|
||||
state: &mut Self,
|
||||
_proxy: &wl_output::WlOutput,
|
||||
event: <wl_output::WlOutput as wayland_client::Proxy>::Event,
|
||||
_proxy: &WlOutput,
|
||||
event: <WlOutput as wayland_client::Proxy>::Event,
|
||||
_data: &(),
|
||||
_conn: &Connection,
|
||||
_qhandle: &QueueHandle<Self>,
|
||||
@@ -987,6 +971,6 @@ delegate_noop!(State: ZwpPointerConstraintsV1);
|
||||
delegate_noop!(State: ignore ZxdgOutputManagerV1);
|
||||
delegate_noop!(State: ignore wl_shm::WlShm);
|
||||
delegate_noop!(State: ignore wl_buffer::WlBuffer);
|
||||
delegate_noop!(State: ignore wl_surface::WlSurface);
|
||||
delegate_noop!(State: ignore WlSurface);
|
||||
delegate_noop!(State: ignore ZwpKeyboardShortcutsInhibitorV1);
|
||||
delegate_noop!(State: ignore ZwpLockedPointerV1);
|
||||
632
input-capture/src/windows.rs
Normal file
632
input-capture/src/windows.rs
Normal file
@@ -0,0 +1,632 @@
|
||||
use async_trait::async_trait;
|
||||
use core::task::{Context, Poll};
|
||||
use futures::Stream;
|
||||
use once_cell::unsync::Lazy;
|
||||
|
||||
use std::collections::HashSet;
|
||||
use std::ptr::{addr_of, addr_of_mut};
|
||||
|
||||
use futures::executor::block_on;
|
||||
use std::default::Default;
|
||||
use std::sync::atomic::{AtomicBool, AtomicU32, Ordering};
|
||||
use std::sync::{mpsc, Mutex};
|
||||
use std::task::ready;
|
||||
use std::{pin::Pin, thread};
|
||||
use tokio::sync::mpsc::{channel, Receiver, Sender};
|
||||
use windows::core::{w, PCWSTR};
|
||||
use windows::Win32::Foundation::{FALSE, HINSTANCE, HWND, LPARAM, LRESULT, RECT, WPARAM};
|
||||
use windows::Win32::Graphics::Gdi::{
|
||||
EnumDisplayDevicesW, EnumDisplaySettingsW, DEVMODEW, DISPLAY_DEVICEW,
|
||||
DISPLAY_DEVICE_ATTACHED_TO_DESKTOP, ENUM_CURRENT_SETTINGS,
|
||||
};
|
||||
use windows::Win32::System::LibraryLoader::GetModuleHandleW;
|
||||
use windows::Win32::System::Threading::GetCurrentThreadId;
|
||||
|
||||
use windows::Win32::UI::WindowsAndMessaging::{
|
||||
CallNextHookEx, CreateWindowExW, DispatchMessageW, GetMessageW, PostThreadMessageW,
|
||||
RegisterClassW, SetWindowsHookExW, TranslateMessage, EDD_GET_DEVICE_INTERFACE_NAME, HHOOK,
|
||||
HMENU, HOOKPROC, KBDLLHOOKSTRUCT, LLKHF_EXTENDED, MSG, MSLLHOOKSTRUCT, WH_KEYBOARD_LL,
|
||||
WH_MOUSE_LL, WINDOW_STYLE, WM_DISPLAYCHANGE, WM_KEYDOWN, WM_KEYUP, WM_LBUTTONDOWN,
|
||||
WM_LBUTTONUP, WM_MBUTTONDOWN, WM_MBUTTONUP, WM_MOUSEMOVE, WM_MOUSEWHEEL, WM_RBUTTONDOWN,
|
||||
WM_RBUTTONUP, WM_SYSKEYDOWN, WM_SYSKEYUP, WM_USER, WM_XBUTTONDOWN, WM_XBUTTONUP, WNDCLASSW,
|
||||
WNDPROC,
|
||||
};
|
||||
|
||||
use input_event::{
|
||||
scancode::{self, Linux},
|
||||
Event, KeyboardEvent, PointerEvent, BTN_BACK, BTN_FORWARD, BTN_LEFT, BTN_MIDDLE, BTN_RIGHT,
|
||||
};
|
||||
|
||||
use super::{Capture, CaptureError, CaptureEvent, Position};
|
||||
|
||||
enum Request {
|
||||
Create(Position),
|
||||
Destroy(Position),
|
||||
}
|
||||
|
||||
pub struct WindowsInputCapture {
|
||||
event_rx: Receiver<(Position, CaptureEvent)>,
|
||||
msg_thread: Option<std::thread::JoinHandle<()>>,
|
||||
}
|
||||
|
||||
enum EventType {
|
||||
Request = 0,
|
||||
Release = 1,
|
||||
Exit = 2,
|
||||
}
|
||||
|
||||
unsafe fn signal_message_thread(event_type: EventType) {
|
||||
if let Some(event_tid) = get_event_tid() {
|
||||
PostThreadMessageW(event_tid, WM_USER, WPARAM(event_type as usize), LPARAM(0)).unwrap();
|
||||
} else {
|
||||
panic!();
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl Capture for WindowsInputCapture {
|
||||
async fn create(&mut self, pos: Position) -> Result<(), CaptureError> {
|
||||
unsafe {
|
||||
{
|
||||
let mut requests = REQUEST_BUFFER.lock().unwrap();
|
||||
requests.push(Request::Create(pos));
|
||||
}
|
||||
signal_message_thread(EventType::Request);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn destroy(&mut self, pos: Position) -> Result<(), CaptureError> {
|
||||
unsafe {
|
||||
{
|
||||
let mut requests = REQUEST_BUFFER.lock().unwrap();
|
||||
requests.push(Request::Destroy(pos));
|
||||
}
|
||||
signal_message_thread(EventType::Request);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn release(&mut self) -> Result<(), CaptureError> {
|
||||
unsafe { signal_message_thread(EventType::Release) };
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn terminate(&mut self) -> Result<(), CaptureError> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
static mut REQUEST_BUFFER: Mutex<Vec<Request>> = Mutex::new(Vec::new());
|
||||
static mut ACTIVE_CLIENT: Option<Position> = None;
|
||||
static mut CLIENTS: Lazy<HashSet<Position>> = Lazy::new(HashSet::new);
|
||||
static mut EVENT_TX: Option<Sender<(Position, CaptureEvent)>> = None;
|
||||
static mut EVENT_THREAD_ID: AtomicU32 = AtomicU32::new(0);
|
||||
unsafe fn set_event_tid(tid: u32) {
|
||||
EVENT_THREAD_ID.store(tid, Ordering::SeqCst);
|
||||
}
|
||||
unsafe fn get_event_tid() -> Option<u32> {
|
||||
match EVENT_THREAD_ID.load(Ordering::SeqCst) {
|
||||
0 => None,
|
||||
id => Some(id),
|
||||
}
|
||||
}
|
||||
|
||||
static mut ENTRY_POINT: (i32, i32) = (0, 0);
|
||||
|
||||
fn to_mouse_event(wparam: WPARAM, lparam: LPARAM) -> Option<PointerEvent> {
|
||||
let mouse_low_level: MSLLHOOKSTRUCT = unsafe { *(lparam.0 as *const MSLLHOOKSTRUCT) };
|
||||
match wparam {
|
||||
WPARAM(p) if p == WM_LBUTTONDOWN as usize => Some(PointerEvent::Button {
|
||||
time: 0,
|
||||
button: BTN_LEFT,
|
||||
state: 1,
|
||||
}),
|
||||
WPARAM(p) if p == WM_MBUTTONDOWN as usize => Some(PointerEvent::Button {
|
||||
time: 0,
|
||||
button: BTN_MIDDLE,
|
||||
state: 1,
|
||||
}),
|
||||
WPARAM(p) if p == WM_RBUTTONDOWN as usize => Some(PointerEvent::Button {
|
||||
time: 0,
|
||||
button: BTN_RIGHT,
|
||||
state: 1,
|
||||
}),
|
||||
WPARAM(p) if p == WM_LBUTTONUP as usize => Some(PointerEvent::Button {
|
||||
time: 0,
|
||||
button: BTN_LEFT,
|
||||
state: 0,
|
||||
}),
|
||||
WPARAM(p) if p == WM_MBUTTONUP as usize => Some(PointerEvent::Button {
|
||||
time: 0,
|
||||
button: BTN_MIDDLE,
|
||||
state: 0,
|
||||
}),
|
||||
WPARAM(p) if p == WM_RBUTTONUP as usize => Some(PointerEvent::Button {
|
||||
time: 0,
|
||||
button: BTN_RIGHT,
|
||||
state: 0,
|
||||
}),
|
||||
WPARAM(p) if p == WM_MOUSEMOVE as usize => unsafe {
|
||||
let (x, y) = (mouse_low_level.pt.x, mouse_low_level.pt.y);
|
||||
let (ex, ey) = ENTRY_POINT;
|
||||
let (dx, dy) = (x - ex, y - ey);
|
||||
let (dx, dy) = (dx as f64, dy as f64);
|
||||
Some(PointerEvent::Motion { time: 0, dx, dy })
|
||||
},
|
||||
WPARAM(p) if p == WM_MOUSEWHEEL as usize => Some(PointerEvent::AxisDiscrete120 {
|
||||
axis: 0,
|
||||
value: -(mouse_low_level.mouseData as i32 >> 16),
|
||||
}),
|
||||
WPARAM(p) if p == WM_XBUTTONDOWN as usize || p == WM_XBUTTONUP as usize => {
|
||||
let hb = mouse_low_level.mouseData >> 16;
|
||||
let button = match hb {
|
||||
1 => BTN_BACK,
|
||||
2 => BTN_FORWARD,
|
||||
_ => {
|
||||
log::warn!("unknown mouse button");
|
||||
return None;
|
||||
}
|
||||
};
|
||||
Some(PointerEvent::Button {
|
||||
time: 0,
|
||||
button,
|
||||
state: if p == WM_XBUTTONDOWN as usize { 1 } else { 0 },
|
||||
})
|
||||
}
|
||||
w => {
|
||||
log::warn!("unknown mouse event: {w:?}");
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
unsafe fn to_key_event(wparam: WPARAM, lparam: LPARAM) -> Option<KeyboardEvent> {
|
||||
let kybrdllhookstruct: KBDLLHOOKSTRUCT = *(lparam.0 as *const KBDLLHOOKSTRUCT);
|
||||
let mut scan_code = kybrdllhookstruct.scanCode;
|
||||
log::trace!("scan_code: {scan_code}");
|
||||
if kybrdllhookstruct.flags.contains(LLKHF_EXTENDED) {
|
||||
scan_code |= 0xE000;
|
||||
}
|
||||
let Ok(win_scan_code) = scancode::Windows::try_from(scan_code) else {
|
||||
log::warn!("failed to translate to windows scancode: {scan_code}");
|
||||
return None;
|
||||
};
|
||||
log::trace!("windows_scan: {win_scan_code:?}");
|
||||
let Ok(linux_scan_code): Result<Linux, ()> = win_scan_code.try_into() else {
|
||||
log::warn!("failed to translate into linux scancode: {win_scan_code:?}");
|
||||
return None;
|
||||
};
|
||||
log::trace!("windows_scan: {linux_scan_code:?}");
|
||||
let scan_code = linux_scan_code as u32;
|
||||
match wparam {
|
||||
WPARAM(p) if p == WM_KEYDOWN as usize => Some(KeyboardEvent::Key {
|
||||
time: 0,
|
||||
key: scan_code,
|
||||
state: 1,
|
||||
}),
|
||||
WPARAM(p) if p == WM_KEYUP as usize => Some(KeyboardEvent::Key {
|
||||
time: 0,
|
||||
key: scan_code,
|
||||
state: 0,
|
||||
}),
|
||||
WPARAM(p) if p == WM_SYSKEYDOWN as usize => Some(KeyboardEvent::Key {
|
||||
time: 0,
|
||||
key: scan_code,
|
||||
state: 1,
|
||||
}),
|
||||
WPARAM(p) if p == WM_SYSKEYUP as usize => Some(KeyboardEvent::Key {
|
||||
time: 0,
|
||||
key: scan_code,
|
||||
state: 1,
|
||||
}),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
///
|
||||
/// clamp point to display bounds
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `prev_point`: coordinates, the cursor was before entering, within bounds of a display
|
||||
/// * `entry_point`: point to clamp
|
||||
///
|
||||
/// returns: (i32, i32), the corrected entry point
|
||||
///
|
||||
fn clamp_to_display_bounds(prev_point: (i32, i32), point: (i32, i32)) -> (i32, i32) {
|
||||
/* find display where movement came from */
|
||||
let display_regions = unsafe { get_display_regions() };
|
||||
let display = display_regions
|
||||
.iter()
|
||||
.find(|&d| is_within_dp_region(prev_point, d))
|
||||
.unwrap();
|
||||
|
||||
/* clamp to bounds (inclusive) */
|
||||
let (x, y) = point;
|
||||
let (min_x, max_x) = (display.left, display.right - 1);
|
||||
let (min_y, max_y) = (display.top, display.bottom - 1);
|
||||
(x.clamp(min_x, max_x), y.clamp(min_y, max_y))
|
||||
}
|
||||
|
||||
unsafe fn send_blocking(event: CaptureEvent) {
|
||||
if let Some(active) = ACTIVE_CLIENT {
|
||||
block_on(async move {
|
||||
let _ = EVENT_TX.as_ref().unwrap().send((active, event)).await;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
unsafe fn check_client_activation(wparam: WPARAM, lparam: LPARAM) -> bool {
|
||||
if wparam.0 != WM_MOUSEMOVE as usize {
|
||||
return ACTIVE_CLIENT.is_some();
|
||||
}
|
||||
let mouse_low_level: MSLLHOOKSTRUCT = *(lparam.0 as *const MSLLHOOKSTRUCT);
|
||||
static mut PREV_POS: Option<(i32, i32)> = None;
|
||||
let curr_pos = (mouse_low_level.pt.x, mouse_low_level.pt.y);
|
||||
let prev_pos = PREV_POS.unwrap_or(curr_pos);
|
||||
PREV_POS.replace(curr_pos);
|
||||
|
||||
/* next event is the first actual event */
|
||||
let ret = ACTIVE_CLIENT.is_some();
|
||||
|
||||
/* client already active, no need to check */
|
||||
if ACTIVE_CLIENT.is_some() {
|
||||
return ret;
|
||||
}
|
||||
|
||||
/* check if a client was activated */
|
||||
let Some(pos) = entered_barrier(prev_pos, curr_pos, get_display_regions()) else {
|
||||
return ret;
|
||||
};
|
||||
|
||||
/* check if a client is registered for the barrier */
|
||||
if !CLIENTS.contains(&pos) {
|
||||
return ret;
|
||||
}
|
||||
|
||||
/* update active client and entry point */
|
||||
ACTIVE_CLIENT.replace(pos);
|
||||
ENTRY_POINT = clamp_to_display_bounds(prev_pos, curr_pos);
|
||||
|
||||
/* notify main thread */
|
||||
log::debug!("ENTERED @ {prev_pos:?} -> {curr_pos:?}");
|
||||
send_blocking(CaptureEvent::Begin);
|
||||
|
||||
ret
|
||||
}
|
||||
|
||||
unsafe extern "system" fn mouse_proc(ncode: i32, wparam: WPARAM, lparam: LPARAM) -> LRESULT {
|
||||
let active = check_client_activation(wparam, lparam);
|
||||
|
||||
/* no client was active */
|
||||
if !active {
|
||||
return CallNextHookEx(HHOOK::default(), ncode, wparam, lparam);
|
||||
}
|
||||
|
||||
/* get active client if any */
|
||||
let Some(pos) = ACTIVE_CLIENT else {
|
||||
return LRESULT(1);
|
||||
};
|
||||
|
||||
/* convert to lan-mouse event */
|
||||
let Some(pointer_event) = to_mouse_event(wparam, lparam) else {
|
||||
return LRESULT(1);
|
||||
};
|
||||
let event = (pos, CaptureEvent::Input(Event::Pointer(pointer_event)));
|
||||
|
||||
/* notify mainthread (drop events if sending too fast) */
|
||||
if let Err(e) = EVENT_TX.as_ref().unwrap().try_send(event) {
|
||||
log::warn!("e: {e}");
|
||||
}
|
||||
|
||||
/* don't pass event to applications */
|
||||
LRESULT(1)
|
||||
}
|
||||
|
||||
unsafe extern "system" fn kybrd_proc(ncode: i32, wparam: WPARAM, lparam: LPARAM) -> LRESULT {
|
||||
/* get active client if any */
|
||||
let Some(client) = ACTIVE_CLIENT else {
|
||||
return CallNextHookEx(HHOOK::default(), ncode, wparam, lparam);
|
||||
};
|
||||
|
||||
/* convert to key event */
|
||||
let Some(key_event) = to_key_event(wparam, lparam) else {
|
||||
return LRESULT(1);
|
||||
};
|
||||
let event = (client, CaptureEvent::Input(Event::Keyboard(key_event)));
|
||||
|
||||
if let Err(e) = EVENT_TX.as_ref().unwrap().try_send(event) {
|
||||
log::warn!("e: {e}");
|
||||
}
|
||||
|
||||
/* don't pass event to applications */
|
||||
LRESULT(1)
|
||||
}
|
||||
|
||||
unsafe extern "system" fn window_proc(
|
||||
_hwnd: HWND,
|
||||
uint: u32,
|
||||
_wparam: WPARAM,
|
||||
_lparam: LPARAM,
|
||||
) -> LRESULT {
|
||||
match uint {
|
||||
x if x == WM_DISPLAYCHANGE => {
|
||||
log::debug!("display resolution changed");
|
||||
DISPLAY_RESOLUTION_CHANGED = true;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
LRESULT(1)
|
||||
}
|
||||
|
||||
fn enumerate_displays() -> Vec<RECT> {
|
||||
unsafe {
|
||||
let mut display_rects = vec![];
|
||||
let mut devices = vec![];
|
||||
for i in 0.. {
|
||||
let mut device: DISPLAY_DEVICEW = std::mem::zeroed();
|
||||
device.cb = std::mem::size_of::<DISPLAY_DEVICEW>() as u32;
|
||||
let ret = EnumDisplayDevicesW(None, i, &mut device, EDD_GET_DEVICE_INTERFACE_NAME);
|
||||
if ret == FALSE {
|
||||
break;
|
||||
}
|
||||
if device.StateFlags & DISPLAY_DEVICE_ATTACHED_TO_DESKTOP != 0 {
|
||||
devices.push(device.DeviceName);
|
||||
}
|
||||
}
|
||||
for device in devices {
|
||||
let mut dev_mode: DEVMODEW = std::mem::zeroed();
|
||||
dev_mode.dmSize = std::mem::size_of::<DEVMODEW>() as u16;
|
||||
let ret = EnumDisplaySettingsW(
|
||||
PCWSTR::from_raw(&device as *const _),
|
||||
ENUM_CURRENT_SETTINGS,
|
||||
&mut dev_mode,
|
||||
);
|
||||
if ret == FALSE {
|
||||
log::warn!("no display mode");
|
||||
}
|
||||
|
||||
let pos = dev_mode.Anonymous1.Anonymous2.dmPosition;
|
||||
let (x, y) = (pos.x, pos.y);
|
||||
let (width, height) = (dev_mode.dmPelsWidth, dev_mode.dmPelsHeight);
|
||||
|
||||
display_rects.push(RECT {
|
||||
left: x,
|
||||
right: x + width as i32,
|
||||
top: y,
|
||||
bottom: y + height as i32,
|
||||
});
|
||||
}
|
||||
display_rects
|
||||
}
|
||||
}
|
||||
|
||||
static mut DISPLAY_RESOLUTION_CHANGED: bool = true;
|
||||
|
||||
unsafe fn get_display_regions() -> &'static Vec<RECT> {
|
||||
static mut DISPLAYS: Vec<RECT> = vec![];
|
||||
if DISPLAY_RESOLUTION_CHANGED {
|
||||
DISPLAYS = enumerate_displays();
|
||||
DISPLAY_RESOLUTION_CHANGED = false;
|
||||
log::debug!("displays: {DISPLAYS:?}");
|
||||
}
|
||||
&*addr_of!(DISPLAYS)
|
||||
}
|
||||
|
||||
fn is_within_dp_region(point: (i32, i32), display: &RECT) -> bool {
|
||||
[
|
||||
Position::Left,
|
||||
Position::Right,
|
||||
Position::Top,
|
||||
Position::Bottom,
|
||||
]
|
||||
.iter()
|
||||
.all(|&pos| is_within_dp_boundary(point, display, pos))
|
||||
}
|
||||
fn is_within_dp_boundary(point: (i32, i32), display: &RECT, pos: Position) -> bool {
|
||||
let (x, y) = point;
|
||||
match pos {
|
||||
Position::Left => display.left <= x,
|
||||
Position::Right => display.right > x,
|
||||
Position::Top => display.top <= y,
|
||||
Position::Bottom => display.bottom > y,
|
||||
}
|
||||
}
|
||||
|
||||
/// returns whether the given position is within the display bounds with respect to the given
|
||||
/// barrier position
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `x`:
|
||||
/// * `y`:
|
||||
/// * `displays`:
|
||||
/// * `pos`:
|
||||
///
|
||||
/// returns: bool
|
||||
///
|
||||
fn in_bounds(point: (i32, i32), displays: &[RECT], pos: Position) -> bool {
|
||||
displays
|
||||
.iter()
|
||||
.any(|d| is_within_dp_boundary(point, d, pos))
|
||||
}
|
||||
|
||||
fn in_display_region(point: (i32, i32), displays: &[RECT]) -> bool {
|
||||
displays.iter().any(|d| is_within_dp_region(point, d))
|
||||
}
|
||||
|
||||
fn moved_across_boundary(
|
||||
prev_pos: (i32, i32),
|
||||
curr_pos: (i32, i32),
|
||||
displays: &[RECT],
|
||||
pos: Position,
|
||||
) -> bool {
|
||||
/* was within bounds, but is not anymore */
|
||||
in_display_region(prev_pos, displays) && !in_bounds(curr_pos, displays, pos)
|
||||
}
|
||||
|
||||
fn entered_barrier(
|
||||
prev_pos: (i32, i32),
|
||||
curr_pos: (i32, i32),
|
||||
displays: &[RECT],
|
||||
) -> Option<Position> {
|
||||
[
|
||||
Position::Left,
|
||||
Position::Right,
|
||||
Position::Top,
|
||||
Position::Bottom,
|
||||
]
|
||||
.into_iter()
|
||||
.find(|&pos| moved_across_boundary(prev_pos, curr_pos, displays, pos))
|
||||
}
|
||||
|
||||
fn get_msg() -> Option<MSG> {
|
||||
unsafe {
|
||||
let mut msg = std::mem::zeroed();
|
||||
let ret = GetMessageW(addr_of_mut!(msg), HWND::default(), 0, 0);
|
||||
match ret.0 {
|
||||
0 => None,
|
||||
x if x > 0 => Some(msg),
|
||||
_ => panic!("error in GetMessageW"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static WINDOW_CLASS_REGISTERED: AtomicBool = AtomicBool::new(false);
|
||||
|
||||
fn message_thread(ready_tx: mpsc::Sender<()>) {
|
||||
unsafe {
|
||||
set_event_tid(GetCurrentThreadId());
|
||||
ready_tx.send(()).expect("channel closed");
|
||||
let mouse_proc: HOOKPROC = Some(mouse_proc);
|
||||
let kybrd_proc: HOOKPROC = Some(kybrd_proc);
|
||||
let window_proc: WNDPROC = Some(window_proc);
|
||||
|
||||
/* register hooks */
|
||||
let _ = SetWindowsHookExW(WH_MOUSE_LL, mouse_proc, HINSTANCE::default(), 0).unwrap();
|
||||
let _ = SetWindowsHookExW(WH_KEYBOARD_LL, kybrd_proc, HINSTANCE::default(), 0).unwrap();
|
||||
|
||||
let instance = GetModuleHandleW(None).unwrap();
|
||||
let window_class: WNDCLASSW = WNDCLASSW {
|
||||
lpfnWndProc: window_proc,
|
||||
hInstance: instance.into(),
|
||||
lpszClassName: w!("lan-mouse-message-window-class"),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
if WINDOW_CLASS_REGISTERED
|
||||
.compare_exchange(false, true, Ordering::SeqCst, Ordering::SeqCst)
|
||||
.is_ok()
|
||||
{
|
||||
/* register window class if not yet done so */
|
||||
let ret = RegisterClassW(&window_class);
|
||||
if ret == 0 {
|
||||
panic!("RegisterClassW");
|
||||
}
|
||||
}
|
||||
|
||||
/* window is used ro receive WM_DISPLAYCHANGE messages */
|
||||
CreateWindowExW(
|
||||
Default::default(),
|
||||
w!("lan-mouse-message-window-class"),
|
||||
w!("lan-mouse-msg-window"),
|
||||
WINDOW_STYLE::default(),
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
HWND::default(),
|
||||
HMENU::default(),
|
||||
instance,
|
||||
None,
|
||||
)
|
||||
.expect("CreateWindowExW");
|
||||
|
||||
/* run message loop */
|
||||
loop {
|
||||
// mouse / keybrd proc do not actually return a message
|
||||
let Some(msg) = get_msg() else {
|
||||
break;
|
||||
};
|
||||
if msg.hwnd.0.is_null() {
|
||||
/* messages sent via PostThreadMessage */
|
||||
match msg.wParam.0 {
|
||||
x if x == EventType::Exit as usize => break,
|
||||
x if x == EventType::Release as usize => {
|
||||
ACTIVE_CLIENT.take();
|
||||
}
|
||||
x if x == EventType::Request as usize => {
|
||||
let requests = {
|
||||
let mut res = vec![];
|
||||
let mut requests = REQUEST_BUFFER.lock().unwrap();
|
||||
for request in requests.drain(..) {
|
||||
res.push(request);
|
||||
}
|
||||
res
|
||||
};
|
||||
|
||||
for request in requests {
|
||||
update_clients(request)
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
} else {
|
||||
/* other messages for window_procs */
|
||||
let _ = TranslateMessage(&msg);
|
||||
DispatchMessageW(&msg);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn update_clients(request: Request) {
|
||||
match request {
|
||||
Request::Create(pos) => {
|
||||
unsafe { CLIENTS.insert(pos) };
|
||||
}
|
||||
Request::Destroy(pos) => unsafe {
|
||||
if let Some(active_pos) = ACTIVE_CLIENT {
|
||||
if pos == active_pos {
|
||||
let _ = ACTIVE_CLIENT.take();
|
||||
}
|
||||
}
|
||||
CLIENTS.remove(&pos);
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
impl WindowsInputCapture {
|
||||
pub(crate) fn new() -> Self {
|
||||
unsafe {
|
||||
let (tx, rx) = channel(10);
|
||||
EVENT_TX.replace(tx);
|
||||
let (ready_tx, ready_rx) = mpsc::channel();
|
||||
let msg_thread = Some(thread::spawn(|| message_thread(ready_tx)));
|
||||
/* wait for thread to set its id */
|
||||
ready_rx.recv().expect("channel closed");
|
||||
Self {
|
||||
msg_thread,
|
||||
event_rx: rx,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Stream for WindowsInputCapture {
|
||||
type Item = Result<(Position, CaptureEvent), CaptureError>;
|
||||
fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
|
||||
match ready!(self.event_rx.poll_recv(cx)) {
|
||||
None => Poll::Ready(None),
|
||||
Some(e) => Poll::Ready(Some(Ok(e))),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for WindowsInputCapture {
|
||||
fn drop(&mut self) {
|
||||
unsafe { signal_message_thread(EventType::Exit) };
|
||||
let _ = self.msg_thread.take().unwrap().join();
|
||||
}
|
||||
}
|
||||
44
input-capture/src/x11.rs
Normal file
44
input-capture/src/x11.rs
Normal file
@@ -0,0 +1,44 @@
|
||||
use std::task::Poll;
|
||||
|
||||
use async_trait::async_trait;
|
||||
use futures_core::Stream;
|
||||
|
||||
use super::{error::X11InputCaptureCreationError, Capture, CaptureError, CaptureEvent, Position};
|
||||
|
||||
pub struct X11InputCapture {}
|
||||
|
||||
impl X11InputCapture {
|
||||
pub fn new() -> std::result::Result<Self, X11InputCaptureCreationError> {
|
||||
Err(X11InputCaptureCreationError::NotImplemented)
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl Capture for X11InputCapture {
|
||||
async fn create(&mut self, _pos: Position) -> Result<(), CaptureError> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn destroy(&mut self, _pos: Position) -> Result<(), CaptureError> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn release(&mut self) -> Result<(), CaptureError> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn terminate(&mut self) -> Result<(), CaptureError> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl Stream for X11InputCapture {
|
||||
type Item = Result<(Position, CaptureEvent), CaptureError>;
|
||||
|
||||
fn poll_next(
|
||||
self: std::pin::Pin<&mut Self>,
|
||||
_cx: &mut std::task::Context<'_>,
|
||||
) -> std::task::Poll<Option<Self::Item>> {
|
||||
Poll::Pending
|
||||
}
|
||||
}
|
||||
72
input-emulation/Cargo.toml
Normal file
72
input-emulation/Cargo.toml
Normal file
@@ -0,0 +1,72 @@
|
||||
[package]
|
||||
name = "input-emulation"
|
||||
description = "cross-platform input emulation library used by lan-mouse"
|
||||
version = "0.2.1"
|
||||
edition = "2021"
|
||||
license = "GPL-3.0-or-later"
|
||||
repository = "https://github.com/feschber/lan-mouse"
|
||||
|
||||
[dependencies]
|
||||
async-trait = "0.1.80"
|
||||
futures = "0.3.28"
|
||||
log = "0.4.22"
|
||||
input-event = { path = "../input-event", version = "0.2.1" }
|
||||
thiserror = "1.0.61"
|
||||
tokio = { version = "1.32.0", features = [
|
||||
"io-util",
|
||||
"io-std",
|
||||
"macros",
|
||||
"net",
|
||||
"process",
|
||||
"rt",
|
||||
"sync",
|
||||
"signal",
|
||||
] }
|
||||
once_cell = "1.19.0"
|
||||
|
||||
[target.'cfg(all(unix, not(target_os="macos")))'.dependencies]
|
||||
wayland-client = { version = "0.31.1", optional = true }
|
||||
wayland-protocols = { version = "0.32.1", features = [
|
||||
"client",
|
||||
"staging",
|
||||
"unstable",
|
||||
], optional = true }
|
||||
wayland-protocols-wlr = { version = "0.3.1", features = [
|
||||
"client",
|
||||
], optional = true }
|
||||
wayland-protocols-misc = { version = "0.3.1", features = [
|
||||
"client",
|
||||
], optional = true }
|
||||
x11 = { version = "2.21.0", features = ["xlib", "xtest"], optional = true }
|
||||
ashpd = { version = "0.9", default-features = false, features = [
|
||||
"tokio",
|
||||
], optional = true }
|
||||
reis = { version = "0.2", features = ["tokio"], optional = true }
|
||||
|
||||
[target.'cfg(target_os="macos")'.dependencies]
|
||||
bitflags = "2.5.0"
|
||||
core-graphics = { version = "0.23", features = ["highsierra"] }
|
||||
keycode = "0.4.0"
|
||||
|
||||
[target.'cfg(windows)'.dependencies]
|
||||
windows = { version = "0.58.0", features = [
|
||||
"Win32_System_LibraryLoader",
|
||||
"Win32_System_Threading",
|
||||
"Win32_Foundation",
|
||||
"Win32_Graphics",
|
||||
"Win32_Graphics_Gdi",
|
||||
"Win32_UI_Input_KeyboardAndMouse",
|
||||
"Win32_UI_WindowsAndMessaging",
|
||||
] }
|
||||
|
||||
[features]
|
||||
default = ["wayland", "x11", "xdg_desktop_portal", "libei"]
|
||||
wayland = [
|
||||
"dep:wayland-client",
|
||||
"dep:wayland-protocols",
|
||||
"dep:wayland-protocols-wlr",
|
||||
"dep:wayland-protocols-misc",
|
||||
]
|
||||
x11 = ["dep:x11"]
|
||||
xdg_desktop_portal = ["dep:ashpd"]
|
||||
libei = ["dep:reis", "dep:ashpd"]
|
||||
32
input-emulation/src/dummy.rs
Normal file
32
input-emulation/src/dummy.rs
Normal file
@@ -0,0 +1,32 @@
|
||||
use async_trait::async_trait;
|
||||
use input_event::Event;
|
||||
|
||||
use crate::error::EmulationError;
|
||||
|
||||
use super::{Emulation, EmulationHandle};
|
||||
|
||||
#[derive(Default)]
|
||||
pub(crate) struct DummyEmulation;
|
||||
|
||||
impl DummyEmulation {
|
||||
pub(crate) fn new() -> Self {
|
||||
Self {}
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl Emulation for DummyEmulation {
|
||||
async fn consume(
|
||||
&mut self,
|
||||
event: Event,
|
||||
client_handle: EmulationHandle,
|
||||
) -> Result<(), EmulationError> {
|
||||
log::info!("received event: ({client_handle}) {event}");
|
||||
Ok(())
|
||||
}
|
||||
async fn create(&mut self, _: EmulationHandle) {}
|
||||
async fn destroy(&mut self, _: EmulationHandle) {}
|
||||
async fn terminate(&mut self) {
|
||||
/* nothing to do */
|
||||
}
|
||||
}
|
||||
179
input-emulation/src/error.rs
Normal file
179
input-emulation/src/error.rs
Normal file
@@ -0,0 +1,179 @@
|
||||
#[derive(Debug, Error)]
|
||||
pub enum InputEmulationError {
|
||||
#[error("error creating input-emulation: `{0}`")]
|
||||
Create(#[from] EmulationCreationError),
|
||||
#[error("error emulating input: `{0}`")]
|
||||
Emulate(#[from] EmulationError),
|
||||
}
|
||||
|
||||
#[cfg(all(unix, feature = "libei", not(target_os = "macos")))]
|
||||
use ashpd::{desktop::ResponseError, Error::Response};
|
||||
#[cfg(all(unix, feature = "libei", not(target_os = "macos")))]
|
||||
use reis::tokio::EiConvertEventStreamError;
|
||||
use std::io;
|
||||
use thiserror::Error;
|
||||
|
||||
#[cfg(all(unix, feature = "wayland", not(target_os = "macos")))]
|
||||
use wayland_client::{
|
||||
backend::WaylandError,
|
||||
globals::{BindError, GlobalError},
|
||||
ConnectError, DispatchError,
|
||||
};
|
||||
|
||||
#[cfg(all(unix, feature = "libei", not(target_os = "macos")))]
|
||||
use reis::tokio::HandshakeError;
|
||||
|
||||
#[cfg(all(unix, feature = "libei", not(target_os = "macos")))]
|
||||
#[derive(Debug, Error)]
|
||||
#[error("error in libei stream: {inner:?}")]
|
||||
pub struct ReisConvertStreamError {
|
||||
inner: EiConvertEventStreamError,
|
||||
}
|
||||
|
||||
#[cfg(all(unix, feature = "libei", not(target_os = "macos")))]
|
||||
impl From<EiConvertEventStreamError> for ReisConvertStreamError {
|
||||
fn from(e: EiConvertEventStreamError) -> Self {
|
||||
Self { inner: e }
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum EmulationError {
|
||||
#[error("event stream closed")]
|
||||
EndOfStream,
|
||||
#[cfg(all(unix, feature = "libei", not(target_os = "macos")))]
|
||||
#[error("libei error flushing events: `{0}`")]
|
||||
Libei(#[from] reis::event::Error),
|
||||
#[cfg(all(unix, feature = "libei", not(target_os = "macos")))]
|
||||
#[error("")]
|
||||
LibeiConvertStream(#[from] ReisConvertStreamError),
|
||||
#[cfg(all(unix, feature = "wayland", not(target_os = "macos")))]
|
||||
#[error("wayland error: `{0}`")]
|
||||
Wayland(#[from] wayland_client::backend::WaylandError),
|
||||
#[cfg(all(
|
||||
unix,
|
||||
any(feature = "xdg_desktop_portal", feature = "libei"),
|
||||
not(target_os = "macos")
|
||||
))]
|
||||
#[error("xdg-desktop-portal: `{0}`")]
|
||||
Ashpd(#[from] ashpd::Error),
|
||||
#[error("io error: `{0}`")]
|
||||
Io(#[from] io::Error),
|
||||
}
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum EmulationCreationError {
|
||||
#[cfg(all(unix, feature = "wayland", not(target_os = "macos")))]
|
||||
#[error("wlroots backend: `{0}`")]
|
||||
Wlroots(#[from] WlrootsEmulationCreationError),
|
||||
#[cfg(all(unix, feature = "libei", not(target_os = "macos")))]
|
||||
#[error("libei backend: `{0}`")]
|
||||
Libei(#[from] LibeiEmulationCreationError),
|
||||
#[cfg(all(unix, feature = "xdg_desktop_portal", not(target_os = "macos")))]
|
||||
#[error("xdg-desktop-portal: `{0}`")]
|
||||
Xdp(#[from] XdpEmulationCreationError),
|
||||
#[cfg(all(unix, feature = "x11", not(target_os = "macos")))]
|
||||
#[error("x11: `{0}`")]
|
||||
X11(#[from] X11EmulationCreationError),
|
||||
#[cfg(target_os = "macos")]
|
||||
#[error("macos: `{0}`")]
|
||||
MacOs(#[from] MacOSEmulationCreationError),
|
||||
#[cfg(windows)]
|
||||
#[error("windows: `{0}`")]
|
||||
Windows(#[from] WindowsEmulationCreationError),
|
||||
#[error("capture error")]
|
||||
NoAvailableBackend,
|
||||
}
|
||||
|
||||
impl EmulationCreationError {
|
||||
/// request was intentionally denied by the user
|
||||
pub(crate) fn cancelled_by_user(&self) -> bool {
|
||||
#[cfg(all(unix, feature = "libei", not(target_os = "macos")))]
|
||||
if matches!(
|
||||
self,
|
||||
EmulationCreationError::Libei(LibeiEmulationCreationError::Ashpd(Response(
|
||||
ResponseError::Cancelled,
|
||||
)))
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
#[cfg(all(unix, feature = "xdg_desktop_portal", not(target_os = "macos")))]
|
||||
if matches!(
|
||||
self,
|
||||
EmulationCreationError::Xdp(XdpEmulationCreationError::Ashpd(Response(
|
||||
ResponseError::Cancelled,
|
||||
)))
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(all(unix, feature = "wayland", not(target_os = "macos")))]
|
||||
#[derive(Debug, Error)]
|
||||
pub enum WlrootsEmulationCreationError {
|
||||
#[error(transparent)]
|
||||
Connect(#[from] ConnectError),
|
||||
#[error(transparent)]
|
||||
Global(#[from] GlobalError),
|
||||
#[error(transparent)]
|
||||
Wayland(#[from] WaylandError),
|
||||
#[error(transparent)]
|
||||
Bind(#[from] WaylandBindError),
|
||||
#[error(transparent)]
|
||||
Dispatch(#[from] DispatchError),
|
||||
#[error(transparent)]
|
||||
Io(#[from] std::io::Error),
|
||||
}
|
||||
|
||||
#[cfg(all(unix, feature = "wayland", not(target_os = "macos")))]
|
||||
#[derive(Debug, Error)]
|
||||
#[error("wayland protocol \"{protocol}\" not supported: {inner}")]
|
||||
pub struct WaylandBindError {
|
||||
inner: BindError,
|
||||
protocol: &'static str,
|
||||
}
|
||||
|
||||
#[cfg(all(unix, feature = "wayland", not(target_os = "macos")))]
|
||||
impl WaylandBindError {
|
||||
pub(crate) fn new(inner: BindError, protocol: &'static str) -> Self {
|
||||
Self { inner, protocol }
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(all(unix, feature = "libei", not(target_os = "macos")))]
|
||||
#[derive(Debug, Error)]
|
||||
pub enum LibeiEmulationCreationError {
|
||||
#[error(transparent)]
|
||||
Ashpd(#[from] ashpd::Error),
|
||||
#[error(transparent)]
|
||||
Io(#[from] std::io::Error),
|
||||
#[error(transparent)]
|
||||
Handshake(#[from] HandshakeError),
|
||||
}
|
||||
|
||||
#[cfg(all(unix, feature = "xdg_desktop_portal", not(target_os = "macos")))]
|
||||
#[derive(Debug, Error)]
|
||||
pub enum XdpEmulationCreationError {
|
||||
#[error(transparent)]
|
||||
Ashpd(#[from] ashpd::Error),
|
||||
}
|
||||
|
||||
#[cfg(all(unix, feature = "x11", not(target_os = "macos")))]
|
||||
#[derive(Debug, Error)]
|
||||
pub enum X11EmulationCreationError {
|
||||
#[error("could not open display")]
|
||||
OpenDisplay,
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
#[derive(Debug, Error)]
|
||||
pub enum MacOSEmulationCreationError {
|
||||
#[error("could not create event source")]
|
||||
EventSourceCreation,
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
#[derive(Debug, Error)]
|
||||
pub enum WindowsEmulationCreationError {}
|
||||
240
input-emulation/src/lib.rs
Normal file
240
input-emulation/src/lib.rs
Normal file
@@ -0,0 +1,240 @@
|
||||
use async_trait::async_trait;
|
||||
use std::{
|
||||
collections::{HashMap, HashSet},
|
||||
fmt::Display,
|
||||
};
|
||||
|
||||
use input_event::{Event, KeyboardEvent};
|
||||
|
||||
pub use self::error::{EmulationCreationError, EmulationError, InputEmulationError};
|
||||
|
||||
#[cfg(windows)]
|
||||
mod windows;
|
||||
|
||||
#[cfg(all(unix, feature = "x11", not(target_os = "macos")))]
|
||||
mod x11;
|
||||
|
||||
#[cfg(all(unix, feature = "wayland", not(target_os = "macos")))]
|
||||
mod wlroots;
|
||||
|
||||
#[cfg(all(unix, feature = "xdg_desktop_portal", not(target_os = "macos")))]
|
||||
mod xdg_desktop_portal;
|
||||
|
||||
#[cfg(all(unix, feature = "libei", not(target_os = "macos")))]
|
||||
mod libei;
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
mod macos;
|
||||
|
||||
/// fallback input emulation (logs events)
|
||||
mod dummy;
|
||||
mod error;
|
||||
|
||||
pub type EmulationHandle = u64;
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub enum Backend {
|
||||
#[cfg(all(unix, feature = "wayland", not(target_os = "macos")))]
|
||||
Wlroots,
|
||||
#[cfg(all(unix, feature = "libei", not(target_os = "macos")))]
|
||||
Libei,
|
||||
#[cfg(all(unix, feature = "xdg_desktop_portal", not(target_os = "macos")))]
|
||||
Xdp,
|
||||
#[cfg(all(unix, feature = "x11", not(target_os = "macos")))]
|
||||
X11,
|
||||
#[cfg(windows)]
|
||||
Windows,
|
||||
#[cfg(target_os = "macos")]
|
||||
MacOs,
|
||||
Dummy,
|
||||
}
|
||||
|
||||
impl Display for Backend {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
#[cfg(all(unix, feature = "wayland", not(target_os = "macos")))]
|
||||
Backend::Wlroots => write!(f, "wlroots"),
|
||||
#[cfg(all(unix, feature = "libei", not(target_os = "macos")))]
|
||||
Backend::Libei => write!(f, "libei"),
|
||||
#[cfg(all(unix, feature = "xdg_desktop_portal", not(target_os = "macos")))]
|
||||
Backend::Xdp => write!(f, "xdg-desktop-portal"),
|
||||
#[cfg(all(unix, feature = "x11", not(target_os = "macos")))]
|
||||
Backend::X11 => write!(f, "X11"),
|
||||
#[cfg(windows)]
|
||||
Backend::Windows => write!(f, "windows"),
|
||||
#[cfg(target_os = "macos")]
|
||||
Backend::MacOs => write!(f, "macos"),
|
||||
Backend::Dummy => write!(f, "dummy"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct InputEmulation {
|
||||
emulation: Box<dyn Emulation>,
|
||||
handles: HashSet<EmulationHandle>,
|
||||
pressed_keys: HashMap<EmulationHandle, HashSet<u32>>,
|
||||
}
|
||||
|
||||
impl InputEmulation {
|
||||
async fn with_backend(backend: Backend) -> Result<InputEmulation, EmulationCreationError> {
|
||||
let emulation: Box<dyn Emulation> = match backend {
|
||||
#[cfg(all(unix, feature = "wayland", not(target_os = "macos")))]
|
||||
Backend::Wlroots => Box::new(wlroots::WlrootsEmulation::new()?),
|
||||
#[cfg(all(unix, feature = "libei", not(target_os = "macos")))]
|
||||
Backend::Libei => Box::new(libei::LibeiEmulation::new().await?),
|
||||
#[cfg(all(unix, feature = "x11", not(target_os = "macos")))]
|
||||
Backend::X11 => Box::new(x11::X11Emulation::new()?),
|
||||
#[cfg(all(unix, feature = "xdg_desktop_portal", not(target_os = "macos")))]
|
||||
Backend::Xdp => Box::new(xdg_desktop_portal::DesktopPortalEmulation::new().await?),
|
||||
#[cfg(windows)]
|
||||
Backend::Windows => Box::new(windows::WindowsEmulation::new()?),
|
||||
#[cfg(target_os = "macos")]
|
||||
Backend::MacOs => Box::new(macos::MacOSEmulation::new()?),
|
||||
Backend::Dummy => Box::new(dummy::DummyEmulation::new()),
|
||||
};
|
||||
Ok(Self {
|
||||
emulation,
|
||||
handles: HashSet::new(),
|
||||
pressed_keys: HashMap::new(),
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn new(backend: Option<Backend>) -> Result<InputEmulation, EmulationCreationError> {
|
||||
if let Some(backend) = backend {
|
||||
let b = Self::with_backend(backend).await;
|
||||
if b.is_ok() {
|
||||
log::info!("using emulation backend: {backend}");
|
||||
}
|
||||
return b;
|
||||
}
|
||||
|
||||
for backend in [
|
||||
#[cfg(all(unix, feature = "wayland", not(target_os = "macos")))]
|
||||
Backend::Wlroots,
|
||||
#[cfg(all(unix, feature = "libei", not(target_os = "macos")))]
|
||||
Backend::Libei,
|
||||
#[cfg(all(unix, feature = "xdg_desktop_portal", not(target_os = "macos")))]
|
||||
Backend::Xdp,
|
||||
#[cfg(all(unix, feature = "x11", not(target_os = "macos")))]
|
||||
Backend::X11,
|
||||
#[cfg(windows)]
|
||||
Backend::Windows,
|
||||
#[cfg(target_os = "macos")]
|
||||
Backend::MacOs,
|
||||
Backend::Dummy,
|
||||
] {
|
||||
match Self::with_backend(backend).await {
|
||||
Ok(b) => {
|
||||
log::info!("using emulation backend: {backend}");
|
||||
return Ok(b);
|
||||
}
|
||||
Err(e) if e.cancelled_by_user() => return Err(e),
|
||||
Err(e) => log::warn!("{e}"),
|
||||
}
|
||||
}
|
||||
|
||||
Err(EmulationCreationError::NoAvailableBackend)
|
||||
}
|
||||
|
||||
pub async fn consume(
|
||||
&mut self,
|
||||
event: Event,
|
||||
handle: EmulationHandle,
|
||||
) -> Result<(), EmulationError> {
|
||||
match event {
|
||||
Event::Keyboard(KeyboardEvent::Key { key, state, .. }) => {
|
||||
// prevent double pressed / released keys
|
||||
if self.update_pressed_keys(handle, key, state) {
|
||||
self.emulation.consume(event, handle).await?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
_ => self.emulation.consume(event, handle).await,
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn create(&mut self, handle: EmulationHandle) -> bool {
|
||||
if self.handles.insert(handle) {
|
||||
self.pressed_keys.insert(handle, HashSet::new());
|
||||
self.emulation.create(handle).await;
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn destroy(&mut self, handle: EmulationHandle) {
|
||||
let _ = self.release_keys(handle).await;
|
||||
if self.handles.remove(&handle) {
|
||||
self.pressed_keys.remove(&handle);
|
||||
self.emulation.destroy(handle).await
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn terminate(&mut self) {
|
||||
for handle in self.handles.iter().cloned().collect::<Vec<_>>() {
|
||||
self.destroy(handle).await
|
||||
}
|
||||
self.emulation.terminate().await
|
||||
}
|
||||
|
||||
pub async fn release_keys(&mut self, handle: EmulationHandle) -> Result<(), EmulationError> {
|
||||
if let Some(keys) = self.pressed_keys.get_mut(&handle) {
|
||||
let keys = keys.drain().collect::<Vec<_>>();
|
||||
for key in keys {
|
||||
let event = Event::Keyboard(KeyboardEvent::Key {
|
||||
time: 0,
|
||||
key,
|
||||
state: 0,
|
||||
});
|
||||
self.emulation.consume(event, handle).await?;
|
||||
if let Ok(key) = input_event::scancode::Linux::try_from(key) {
|
||||
log::warn!("releasing stuck key: {key:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let event = Event::Keyboard(KeyboardEvent::Modifiers {
|
||||
depressed: 0,
|
||||
latched: 0,
|
||||
locked: 0,
|
||||
group: 0,
|
||||
});
|
||||
self.emulation.consume(event, handle).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn has_pressed_keys(&self, handle: EmulationHandle) -> bool {
|
||||
self.pressed_keys
|
||||
.get(&handle)
|
||||
.is_some_and(|p| !p.is_empty())
|
||||
}
|
||||
|
||||
/// update the pressed_keys for the given handle
|
||||
/// returns whether the event should be processed
|
||||
fn update_pressed_keys(&mut self, handle: EmulationHandle, key: u32, state: u8) -> bool {
|
||||
let Some(pressed_keys) = self.pressed_keys.get_mut(&handle) else {
|
||||
return false;
|
||||
};
|
||||
|
||||
if state == 0 {
|
||||
// currently pressed => can release
|
||||
pressed_keys.remove(&key)
|
||||
} else {
|
||||
// currently not pressed => can press
|
||||
pressed_keys.insert(key)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
trait Emulation: Send {
|
||||
async fn consume(
|
||||
&mut self,
|
||||
event: Event,
|
||||
handle: EmulationHandle,
|
||||
) -> Result<(), EmulationError>;
|
||||
async fn create(&mut self, handle: EmulationHandle);
|
||||
async fn destroy(&mut self, handle: EmulationHandle);
|
||||
async fn terminate(&mut self);
|
||||
}
|
||||
370
input-emulation/src/libei.rs
Normal file
370
input-emulation/src/libei.rs
Normal file
@@ -0,0 +1,370 @@
|
||||
use futures::{future, StreamExt};
|
||||
use once_cell::sync::Lazy;
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
io,
|
||||
os::{fd::OwnedFd, unix::net::UnixStream},
|
||||
sync::{
|
||||
atomic::{AtomicBool, AtomicU32, Ordering},
|
||||
Arc, Mutex, RwLock,
|
||||
},
|
||||
time::{SystemTime, UNIX_EPOCH},
|
||||
};
|
||||
use tokio::task::JoinHandle;
|
||||
|
||||
use ashpd::{
|
||||
desktop::{
|
||||
remote_desktop::{DeviceType, RemoteDesktop},
|
||||
PersistMode, Session,
|
||||
},
|
||||
WindowIdentifier,
|
||||
};
|
||||
use async_trait::async_trait;
|
||||
|
||||
use reis::{
|
||||
ei::{
|
||||
self, button::ButtonState, handshake::ContextType, keyboard::KeyState, Button, Keyboard,
|
||||
Pointer, Scroll,
|
||||
},
|
||||
event::{DeviceCapability, DeviceEvent, EiEvent, SeatEvent},
|
||||
tokio::{ei_handshake, EiConvertEventStream, EiEventStream},
|
||||
};
|
||||
|
||||
use input_event::{Event, KeyboardEvent, PointerEvent};
|
||||
|
||||
use crate::error::{EmulationError, ReisConvertStreamError};
|
||||
|
||||
use super::{error::LibeiEmulationCreationError, Emulation, EmulationHandle};
|
||||
|
||||
static INTERFACES: Lazy<HashMap<&'static str, u32>> = Lazy::new(|| {
|
||||
let mut m = HashMap::new();
|
||||
m.insert("ei_connection", 1);
|
||||
m.insert("ei_callback", 1);
|
||||
m.insert("ei_pingpong", 1);
|
||||
m.insert("ei_seat", 1);
|
||||
m.insert("ei_device", 2);
|
||||
m.insert("ei_pointer", 1);
|
||||
m.insert("ei_pointer_absolute", 1);
|
||||
m.insert("ei_scroll", 1);
|
||||
m.insert("ei_button", 1);
|
||||
m.insert("ei_keyboard", 1);
|
||||
m.insert("ei_touchscreen", 1);
|
||||
m
|
||||
});
|
||||
|
||||
#[derive(Clone, Default)]
|
||||
struct Devices {
|
||||
pointer: Arc<RwLock<Option<(ei::Device, ei::Pointer)>>>,
|
||||
scroll: Arc<RwLock<Option<(ei::Device, ei::Scroll)>>>,
|
||||
button: Arc<RwLock<Option<(ei::Device, ei::Button)>>>,
|
||||
keyboard: Arc<RwLock<Option<(ei::Device, ei::Keyboard)>>>,
|
||||
}
|
||||
|
||||
pub(crate) struct LibeiEmulation<'a> {
|
||||
context: ei::Context,
|
||||
devices: Devices,
|
||||
ei_task: JoinHandle<()>,
|
||||
error: Arc<Mutex<Option<EmulationError>>>,
|
||||
libei_error: Arc<AtomicBool>,
|
||||
serial: AtomicU32,
|
||||
_remote_desktop: RemoteDesktop<'a>,
|
||||
session: Session<'a, RemoteDesktop<'a>>,
|
||||
}
|
||||
|
||||
async fn get_ei_fd<'a>(
|
||||
) -> Result<(RemoteDesktop<'a>, Session<'a, RemoteDesktop<'a>>, OwnedFd), ashpd::Error> {
|
||||
let remote_desktop = RemoteDesktop::new().await?;
|
||||
|
||||
log::debug!("creating session ...");
|
||||
let session = remote_desktop.create_session().await?;
|
||||
|
||||
log::debug!("selecting devices ...");
|
||||
remote_desktop
|
||||
.select_devices(
|
||||
&session,
|
||||
DeviceType::Keyboard | DeviceType::Pointer,
|
||||
None,
|
||||
PersistMode::ExplicitlyRevoked,
|
||||
)
|
||||
.await?;
|
||||
|
||||
log::info!("requesting permission for input emulation");
|
||||
let _devices = remote_desktop
|
||||
.start(&session, &WindowIdentifier::default())
|
||||
.await?
|
||||
.response()?;
|
||||
|
||||
let fd = remote_desktop.connect_to_eis(&session).await?;
|
||||
Ok((remote_desktop, session, fd))
|
||||
}
|
||||
|
||||
impl<'a> LibeiEmulation<'a> {
|
||||
pub(crate) async fn new() -> Result<Self, LibeiEmulationCreationError> {
|
||||
let (_remote_desktop, session, eifd) = get_ei_fd().await?;
|
||||
let stream = UnixStream::from(eifd);
|
||||
stream.set_nonblocking(true)?;
|
||||
let context = ei::Context::new(stream)?;
|
||||
let mut events = EiEventStream::new(context.clone())?;
|
||||
let handshake = ei_handshake(
|
||||
&mut events,
|
||||
"de.feschber.LanMouse",
|
||||
ContextType::Sender,
|
||||
&INTERFACES,
|
||||
)
|
||||
.await?;
|
||||
let events = EiConvertEventStream::new(events, handshake.serial);
|
||||
let devices = Devices::default();
|
||||
let libei_error = Arc::new(AtomicBool::default());
|
||||
let error = Arc::new(Mutex::new(None));
|
||||
let ei_handler = ei_task(
|
||||
events,
|
||||
context.clone(),
|
||||
devices.clone(),
|
||||
libei_error.clone(),
|
||||
error.clone(),
|
||||
);
|
||||
let ei_task = tokio::task::spawn_local(ei_handler);
|
||||
|
||||
let serial = AtomicU32::new(handshake.serial);
|
||||
|
||||
Ok(Self {
|
||||
context,
|
||||
devices,
|
||||
ei_task,
|
||||
error,
|
||||
libei_error,
|
||||
serial,
|
||||
_remote_desktop,
|
||||
session,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Drop for LibeiEmulation<'a> {
|
||||
fn drop(&mut self) {
|
||||
self.ei_task.abort();
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl<'a> Emulation for LibeiEmulation<'a> {
|
||||
async fn consume(
|
||||
&mut self,
|
||||
event: Event,
|
||||
_handle: EmulationHandle,
|
||||
) -> Result<(), EmulationError> {
|
||||
let now = SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.unwrap()
|
||||
.as_micros() as u64;
|
||||
if self.libei_error.load(Ordering::SeqCst) {
|
||||
// don't break sending additional events but signal error
|
||||
if let Some(e) = self.error.lock().unwrap().take() {
|
||||
return Err(e);
|
||||
}
|
||||
}
|
||||
match event {
|
||||
Event::Pointer(p) => match p {
|
||||
PointerEvent::Motion { time: _, dx, dy } => {
|
||||
let pointer_device = self.devices.pointer.read().unwrap();
|
||||
if let Some((d, p)) = pointer_device.as_ref() {
|
||||
p.motion_relative(dx as f32, dy as f32);
|
||||
d.frame(self.serial.load(Ordering::SeqCst), now);
|
||||
}
|
||||
}
|
||||
PointerEvent::Button {
|
||||
time: _,
|
||||
button,
|
||||
state,
|
||||
} => {
|
||||
let button_device = self.devices.button.read().unwrap();
|
||||
if let Some((d, b)) = button_device.as_ref() {
|
||||
b.button(
|
||||
button,
|
||||
match state {
|
||||
0 => ButtonState::Released,
|
||||
_ => ButtonState::Press,
|
||||
},
|
||||
);
|
||||
d.frame(self.serial.load(Ordering::SeqCst), now);
|
||||
}
|
||||
}
|
||||
PointerEvent::Axis {
|
||||
time: _,
|
||||
axis,
|
||||
value,
|
||||
} => {
|
||||
let scroll_device = self.devices.scroll.read().unwrap();
|
||||
if let Some((d, s)) = scroll_device.as_ref() {
|
||||
match axis {
|
||||
0 => s.scroll(0., value as f32),
|
||||
_ => s.scroll(value as f32, 0.),
|
||||
}
|
||||
d.frame(self.serial.load(Ordering::SeqCst), now);
|
||||
}
|
||||
}
|
||||
PointerEvent::AxisDiscrete120 { axis, value } => {
|
||||
let scroll_device = self.devices.scroll.read().unwrap();
|
||||
if let Some((d, s)) = scroll_device.as_ref() {
|
||||
match axis {
|
||||
0 => s.scroll_discrete(0, value),
|
||||
_ => s.scroll_discrete(value, 0),
|
||||
}
|
||||
d.frame(self.serial.load(Ordering::SeqCst), now);
|
||||
}
|
||||
}
|
||||
},
|
||||
Event::Keyboard(k) => match k {
|
||||
KeyboardEvent::Key {
|
||||
time: _,
|
||||
key,
|
||||
state,
|
||||
} => {
|
||||
let keyboard_device = self.devices.keyboard.read().unwrap();
|
||||
if let Some((d, k)) = keyboard_device.as_ref() {
|
||||
k.key(
|
||||
key,
|
||||
match state {
|
||||
0 => KeyState::Released,
|
||||
_ => KeyState::Press,
|
||||
},
|
||||
);
|
||||
d.frame(self.serial.load(Ordering::SeqCst), now);
|
||||
}
|
||||
}
|
||||
KeyboardEvent::Modifiers { .. } => {}
|
||||
},
|
||||
}
|
||||
self.context
|
||||
.flush()
|
||||
.map_err(|e| io::Error::new(e.kind(), e))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn create(&mut self, _: EmulationHandle) {}
|
||||
async fn destroy(&mut self, _: EmulationHandle) {}
|
||||
|
||||
async fn terminate(&mut self) {
|
||||
let _ = self.session.close().await;
|
||||
self.ei_task.abort();
|
||||
}
|
||||
}
|
||||
|
||||
async fn ei_task(
|
||||
mut events: EiConvertEventStream,
|
||||
context: ei::Context,
|
||||
devices: Devices,
|
||||
libei_error: Arc<AtomicBool>,
|
||||
error: Arc<Mutex<Option<EmulationError>>>,
|
||||
) {
|
||||
loop {
|
||||
match ei_event_handler(&mut events, &context, &devices).await {
|
||||
Ok(()) => {}
|
||||
Err(e) => {
|
||||
libei_error.store(true, Ordering::SeqCst);
|
||||
error.lock().unwrap().replace(e);
|
||||
// wait for termination -> otherwise we will loop forever
|
||||
future::pending::<()>().await;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn ei_event_handler(
|
||||
events: &mut EiConvertEventStream,
|
||||
context: &ei::Context,
|
||||
devices: &Devices,
|
||||
) -> Result<(), EmulationError> {
|
||||
loop {
|
||||
let event = events
|
||||
.next()
|
||||
.await
|
||||
.ok_or(EmulationError::EndOfStream)?
|
||||
.map_err(ReisConvertStreamError::from)?;
|
||||
const CAPABILITIES: &[DeviceCapability] = &[
|
||||
DeviceCapability::Pointer,
|
||||
DeviceCapability::PointerAbsolute,
|
||||
DeviceCapability::Keyboard,
|
||||
DeviceCapability::Touch,
|
||||
DeviceCapability::Scroll,
|
||||
DeviceCapability::Button,
|
||||
];
|
||||
log::debug!("{event:?}");
|
||||
match event {
|
||||
EiEvent::Disconnected(e) => {
|
||||
log::debug!("ei disconnected: {e:?}");
|
||||
return Err(EmulationError::EndOfStream);
|
||||
}
|
||||
EiEvent::SeatAdded(e) => {
|
||||
e.seat().bind_capabilities(CAPABILITIES);
|
||||
}
|
||||
EiEvent::SeatRemoved(e) => {
|
||||
log::debug!("seat removed: {:?}", e.seat());
|
||||
}
|
||||
EiEvent::DeviceAdded(e) => {
|
||||
let device_type = e.device().device_type();
|
||||
log::debug!("device added: {device_type:?}");
|
||||
e.device().device();
|
||||
let device = e.device();
|
||||
if let Some(pointer) = e.device().interface::<Pointer>() {
|
||||
devices
|
||||
.pointer
|
||||
.write()
|
||||
.unwrap()
|
||||
.replace((device.device().clone(), pointer));
|
||||
}
|
||||
if let Some(keyboard) = e.device().interface::<Keyboard>() {
|
||||
devices
|
||||
.keyboard
|
||||
.write()
|
||||
.unwrap()
|
||||
.replace((device.device().clone(), keyboard));
|
||||
}
|
||||
if let Some(scroll) = e.device().interface::<Scroll>() {
|
||||
devices
|
||||
.scroll
|
||||
.write()
|
||||
.unwrap()
|
||||
.replace((device.device().clone(), scroll));
|
||||
}
|
||||
if let Some(button) = e.device().interface::<Button>() {
|
||||
devices
|
||||
.button
|
||||
.write()
|
||||
.unwrap()
|
||||
.replace((device.device().clone(), button));
|
||||
}
|
||||
}
|
||||
EiEvent::DeviceRemoved(e) => {
|
||||
log::debug!("device removed: {:?}", e.device().device_type());
|
||||
}
|
||||
EiEvent::DevicePaused(e) => {
|
||||
log::debug!("device paused: {:?}", e.device().device_type());
|
||||
}
|
||||
EiEvent::DeviceResumed(e) => {
|
||||
log::debug!("device resumed: {:?}", e.device().device_type());
|
||||
e.device().device().start_emulating(0, 0);
|
||||
}
|
||||
EiEvent::KeyboardModifiers(e) => {
|
||||
log::debug!("modifiers: {e:?}");
|
||||
}
|
||||
// only for receiver context
|
||||
// EiEvent::Frame(_) => { },
|
||||
// EiEvent::DeviceStartEmulating(_) => { },
|
||||
// EiEvent::DeviceStopEmulating(_) => { },
|
||||
// EiEvent::PointerMotion(_) => { },
|
||||
// EiEvent::PointerMotionAbsolute(_) => { },
|
||||
// EiEvent::Button(_) => { },
|
||||
// EiEvent::ScrollDelta(_) => { },
|
||||
// EiEvent::ScrollStop(_) => { },
|
||||
// EiEvent::ScrollCancel(_) => { },
|
||||
// EiEvent::ScrollDiscrete(_) => { },
|
||||
// EiEvent::KeyboardKey(_) => { },
|
||||
// EiEvent::TouchDown(_) => { },
|
||||
// EiEvent::TouchUp(_) => { },
|
||||
// EiEvent::TouchMotion(_) => { },
|
||||
_ => unreachable!("unexpected ei event"),
|
||||
}
|
||||
context.flush().map_err(|e| io::Error::new(e.kind(), e))?;
|
||||
}
|
||||
}
|
||||
495
input-emulation/src/macos.rs
Normal file
495
input-emulation/src/macos.rs
Normal file
@@ -0,0 +1,495 @@
|
||||
use super::{error::EmulationError, Emulation, EmulationHandle};
|
||||
use async_trait::async_trait;
|
||||
use bitflags::bitflags;
|
||||
use core_graphics::base::CGFloat;
|
||||
use core_graphics::display::{
|
||||
CGDirectDisplayID, CGDisplayBounds, CGGetDisplaysWithRect, CGPoint, CGRect, CGSize,
|
||||
};
|
||||
use core_graphics::event::{
|
||||
CGEvent, CGEventFlags, CGEventTapLocation, CGEventType, CGKeyCode, CGMouseButton, EventField,
|
||||
ScrollEventUnit,
|
||||
};
|
||||
use core_graphics::event_source::{CGEventSource, CGEventSourceStateID};
|
||||
use input_event::{scancode, Event, KeyboardEvent, PointerEvent};
|
||||
use keycode::{KeyMap, KeyMapping};
|
||||
use std::cell::Cell;
|
||||
use std::ops::{Index, IndexMut};
|
||||
use std::rc::Rc;
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
use tokio::{sync::Notify, task::JoinHandle};
|
||||
|
||||
use super::error::MacOSEmulationCreationError;
|
||||
|
||||
const DEFAULT_REPEAT_DELAY: Duration = Duration::from_millis(500);
|
||||
const DEFAULT_REPEAT_INTERVAL: Duration = Duration::from_millis(32);
|
||||
|
||||
pub(crate) struct MacOSEmulation {
|
||||
event_source: CGEventSource,
|
||||
repeat_task: Option<JoinHandle<()>>,
|
||||
button_state: ButtonState,
|
||||
modifier_state: Rc<Cell<XMods>>,
|
||||
notify_repeat_task: Arc<Notify>,
|
||||
}
|
||||
|
||||
struct ButtonState {
|
||||
left: bool,
|
||||
right: bool,
|
||||
center: bool,
|
||||
}
|
||||
|
||||
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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
unsafe impl Send for MacOSEmulation {}
|
||||
|
||||
impl MacOSEmulation {
|
||||
pub(crate) fn new() -> Result<Self, MacOSEmulationCreationError> {
|
||||
let event_source = CGEventSource::new(CGEventSourceStateID::CombinedSessionState)
|
||||
.map_err(|_| MacOSEmulationCreationError::EventSourceCreation)?;
|
||||
let button_state = ButtonState {
|
||||
left: false,
|
||||
right: false,
|
||||
center: false,
|
||||
};
|
||||
Ok(Self {
|
||||
event_source,
|
||||
button_state,
|
||||
repeat_task: None,
|
||||
notify_repeat_task: Arc::new(Notify::new()),
|
||||
modifier_state: Rc::new(Cell::new(XMods::empty())),
|
||||
})
|
||||
}
|
||||
|
||||
fn get_mouse_location(&self) -> Option<CGPoint> {
|
||||
let event: CGEvent = CGEvent::new(self.event_source.clone()).ok()?;
|
||||
Some(event.location())
|
||||
}
|
||||
|
||||
async fn spawn_repeat_task(&mut self, key: u16) {
|
||||
// there can only be one repeating key and it's
|
||||
// always the last to be pressed
|
||||
self.cancel_repeat_task().await;
|
||||
let event_source = self.event_source.clone();
|
||||
let notify = self.notify_repeat_task.clone();
|
||||
let modifiers = self.modifier_state.clone();
|
||||
let repeat_task = tokio::task::spawn_local(async move {
|
||||
let stop = tokio::select! {
|
||||
_ = tokio::time::sleep(DEFAULT_REPEAT_DELAY) => false,
|
||||
_ = notify.notified() => true,
|
||||
};
|
||||
if !stop {
|
||||
loop {
|
||||
key_event(event_source.clone(), key, 1, modifiers.get());
|
||||
tokio::select! {
|
||||
_ = tokio::time::sleep(DEFAULT_REPEAT_INTERVAL) => {},
|
||||
_ = notify.notified() => break,
|
||||
}
|
||||
}
|
||||
}
|
||||
// release key when cancelled
|
||||
update_modifiers(&modifiers, key as u32, 0);
|
||||
key_event(event_source.clone(), key, 0, modifiers.get());
|
||||
});
|
||||
self.repeat_task = Some(repeat_task);
|
||||
}
|
||||
|
||||
async fn cancel_repeat_task(&mut self) {
|
||||
if let Some(task) = self.repeat_task.take() {
|
||||
self.notify_repeat_task.notify_waiters();
|
||||
let _ = task.await;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn key_event(event_source: CGEventSource, key: u16, state: u8, modifiers: XMods) {
|
||||
let event = match CGEvent::new_keyboard_event(event_source, key, state != 0) {
|
||||
Ok(e) => e,
|
||||
Err(_) => {
|
||||
log::warn!("unable to create key event");
|
||||
return;
|
||||
}
|
||||
};
|
||||
event.set_flags(to_cgevent_flags(modifiers));
|
||||
event.post(CGEventTapLocation::HID);
|
||||
log::trace!("key event: {key} {state}");
|
||||
}
|
||||
|
||||
fn modifier_event(event_source: CGEventSource, depressed: XMods) {
|
||||
let Ok(event) = CGEvent::new(event_source) else {
|
||||
log::warn!("could not create CGEvent");
|
||||
return;
|
||||
};
|
||||
let flags = to_cgevent_flags(depressed);
|
||||
event.set_type(CGEventType::FlagsChanged);
|
||||
event.set_flags(flags);
|
||||
event.post(CGEventTapLocation::HID);
|
||||
log::trace!("modifiers updated: {depressed:?}");
|
||||
}
|
||||
|
||||
fn get_display_at_point(x: CGFloat, y: CGFloat) -> Option<CGDirectDisplayID> {
|
||||
let mut displays: [CGDirectDisplayID; 16] = [0; 16];
|
||||
let mut display_count: u32 = 0;
|
||||
let rect = CGRect::new(&CGPoint::new(x, y), &CGSize::new(0.0, 0.0));
|
||||
|
||||
let error = unsafe {
|
||||
CGGetDisplaysWithRect(
|
||||
rect,
|
||||
1,
|
||||
displays.as_mut_ptr(),
|
||||
&mut display_count as *mut u32,
|
||||
)
|
||||
};
|
||||
|
||||
if error != 0 {
|
||||
log::warn!("error getting displays at point ({}, {}): {}", x, y, error);
|
||||
return Option::None;
|
||||
}
|
||||
|
||||
if display_count == 0 {
|
||||
log::debug!("no displays found at point ({}, {})", x, y);
|
||||
return Option::None;
|
||||
}
|
||||
|
||||
return displays.first().copied();
|
||||
}
|
||||
|
||||
fn get_display_bounds(display: CGDirectDisplayID) -> (CGFloat, CGFloat, CGFloat, CGFloat) {
|
||||
unsafe {
|
||||
let bounds = CGDisplayBounds(display);
|
||||
let min_x = bounds.origin.x;
|
||||
let max_x = bounds.origin.x + bounds.size.width;
|
||||
let min_y = bounds.origin.y;
|
||||
let max_y = bounds.origin.y + bounds.size.height;
|
||||
(min_x as f64, min_y as f64, max_x as f64, max_y as f64)
|
||||
}
|
||||
}
|
||||
|
||||
fn clamp_to_screen_space(
|
||||
current_x: CGFloat,
|
||||
current_y: CGFloat,
|
||||
dx: CGFloat,
|
||||
dy: CGFloat,
|
||||
) -> (CGFloat, CGFloat) {
|
||||
// Check which display the mouse is currently on
|
||||
// Determine what the location of the mouse would be after applying the move
|
||||
// Get the display at the new location
|
||||
// If the point is not on a display
|
||||
// Clamp the mouse to the current display
|
||||
// Else If the point is on a display
|
||||
// Clamp the mouse to the new display
|
||||
let current_display = match get_display_at_point(current_x, current_y) {
|
||||
Some(display) => display,
|
||||
None => {
|
||||
log::warn!("could not get current display!");
|
||||
return (current_x, current_y);
|
||||
}
|
||||
};
|
||||
|
||||
let new_x = current_x + dx;
|
||||
let new_y = current_y + dy;
|
||||
|
||||
let final_display = get_display_at_point(new_x, new_y).unwrap_or(current_display);
|
||||
let (min_x, min_y, max_x, max_y) = get_display_bounds(final_display);
|
||||
|
||||
(
|
||||
new_x.clamp(min_x, max_x - 1.),
|
||||
new_y.clamp(min_y, max_y - 1.),
|
||||
)
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl Emulation for MacOSEmulation {
|
||||
async fn consume(
|
||||
&mut self,
|
||||
event: Event,
|
||||
_handle: EmulationHandle,
|
||||
) -> Result<(), EmulationError> {
|
||||
match event {
|
||||
Event::Pointer(pointer_event) => match pointer_event {
|
||||
PointerEvent::Motion { time: _, dx, dy } => {
|
||||
let mut mouse_location = match self.get_mouse_location() {
|
||||
Some(l) => l,
|
||||
None => {
|
||||
log::warn!("could not get mouse location!");
|
||||
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;
|
||||
|
||||
let mut event_type = CGEventType::MouseMoved;
|
||||
if self.button_state.left {
|
||||
event_type = CGEventType::LeftMouseDragged
|
||||
} else if self.button_state.right {
|
||||
event_type = CGEventType::RightMouseDragged
|
||||
} else if self.button_state.center {
|
||||
event_type = CGEventType::OtherMouseDragged
|
||||
};
|
||||
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,
|
||||
} => {
|
||||
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;
|
||||
|
||||
let location = self.get_mouse_location().unwrap();
|
||||
let event = match CGEvent::new_mouse_event(
|
||||
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 {
|
||||
KeyboardEvent::Key {
|
||||
time: _,
|
||||
key,
|
||||
state,
|
||||
} => {
|
||||
let code = match KeyMap::from_key_mapping(KeyMapping::Evdev(key as u16)) {
|
||||
Ok(k) => k.mac as CGKeyCode,
|
||||
Err(_) => {
|
||||
log::warn!("unable to map key event");
|
||||
return Ok(());
|
||||
}
|
||||
};
|
||||
match state {
|
||||
// pressed
|
||||
1 => self.spawn_repeat_task(code).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 {
|
||||
depressed,
|
||||
latched,
|
||||
locked,
|
||||
group,
|
||||
} => {
|
||||
set_modifiers(&self.modifier_state, depressed, latched, locked, group);
|
||||
modifier_event(self.event_source.clone(), self.modifier_state.get());
|
||||
}
|
||||
},
|
||||
}
|
||||
// FIXME
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn create(&mut self, _handle: EmulationHandle) {}
|
||||
|
||||
async fn destroy(&mut self, _handle: EmulationHandle) {}
|
||||
|
||||
async fn terminate(&mut self) {}
|
||||
}
|
||||
|
||||
fn update_modifiers(modifiers: &Cell<XMods>, key: u32, state: u8) -> bool {
|
||||
if let Ok(key) = scancode::Linux::try_from(key) {
|
||||
let mask = match key {
|
||||
scancode::Linux::KeyLeftShift | scancode::Linux::KeyRightShift => XMods::ShiftMask,
|
||||
scancode::Linux::KeyCapsLock => XMods::LockMask,
|
||||
scancode::Linux::KeyLeftCtrl | scancode::Linux::KeyRightCtrl => XMods::ControlMask,
|
||||
scancode::Linux::KeyLeftAlt | scancode::Linux::KeyRightalt => XMods::Mod1Mask,
|
||||
scancode::Linux::KeyLeftMeta | scancode::Linux::KeyRightmeta => XMods::Mod4Mask,
|
||||
_ => XMods::empty(),
|
||||
};
|
||||
// unchanged
|
||||
if mask.is_empty() {
|
||||
return false;
|
||||
}
|
||||
let mut mods = modifiers.get();
|
||||
match state {
|
||||
1 => mods.insert(mask),
|
||||
_ => mods.remove(mask),
|
||||
}
|
||||
modifiers.set(mods);
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
fn set_modifiers(
|
||||
active_modifiers: &Cell<XMods>,
|
||||
depressed: u32,
|
||||
latched: u32,
|
||||
locked: u32,
|
||||
group: u32,
|
||||
) {
|
||||
let depressed = XMods::from_bits(depressed).unwrap_or_default();
|
||||
let _latched = XMods::from_bits(latched).unwrap_or_default();
|
||||
let _locked = XMods::from_bits(locked).unwrap_or_default();
|
||||
let _group = XMods::from_bits(group).unwrap_or_default();
|
||||
|
||||
// we only care about the depressed modifiers for now
|
||||
active_modifiers.replace(depressed);
|
||||
}
|
||||
|
||||
fn to_cgevent_flags(depressed: XMods) -> CGEventFlags {
|
||||
let mut flags = CGEventFlags::empty();
|
||||
if depressed.contains(XMods::ShiftMask) {
|
||||
flags |= CGEventFlags::CGEventFlagShift;
|
||||
}
|
||||
if depressed.contains(XMods::LockMask) {
|
||||
flags |= CGEventFlags::CGEventFlagAlphaShift;
|
||||
}
|
||||
if depressed.contains(XMods::ControlMask) {
|
||||
flags |= CGEventFlags::CGEventFlagControl;
|
||||
}
|
||||
if depressed.contains(XMods::Mod1Mask) {
|
||||
flags |= CGEventFlags::CGEventFlagAlternate;
|
||||
}
|
||||
if depressed.contains(XMods::Mod4Mask) {
|
||||
flags |= CGEventFlags::CGEventFlagCommand;
|
||||
}
|
||||
flags
|
||||
}
|
||||
|
||||
// From X11/X.h
|
||||
bitflags! {
|
||||
#[repr(C)]
|
||||
#[derive(Clone, Copy, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd)]
|
||||
struct XMods: u32 {
|
||||
const ShiftMask = (1<<0);
|
||||
const LockMask = (1<<1);
|
||||
const ControlMask = (1<<2);
|
||||
const Mod1Mask = (1<<3);
|
||||
const Mod2Mask = (1<<4);
|
||||
const Mod3Mask = (1<<5);
|
||||
const Mod4Mask = (1<<6);
|
||||
const Mod5Mask = (1<<7);
|
||||
}
|
||||
}
|
||||
@@ -1,52 +1,46 @@
|
||||
use crate::{
|
||||
consumer::EventConsumer,
|
||||
event::{KeyboardEvent, PointerEvent},
|
||||
scancode,
|
||||
};
|
||||
use anyhow::Result;
|
||||
use async_trait::async_trait;
|
||||
use std::time::Duration;
|
||||
use tokio::task::AbortHandle;
|
||||
use winapi::um::winuser::{SendInput, KEYEVENTF_EXTENDEDKEY};
|
||||
use winapi::{
|
||||
self,
|
||||
um::winuser::{
|
||||
INPUT, INPUT_KEYBOARD, INPUT_MOUSE, KEYBDINPUT, KEYEVENTF_KEYUP, KEYEVENTF_SCANCODE,
|
||||
LPINPUT, MOUSEEVENTF_HWHEEL, MOUSEEVENTF_LEFTDOWN, MOUSEEVENTF_LEFTUP,
|
||||
MOUSEEVENTF_MIDDLEDOWN, MOUSEEVENTF_MIDDLEUP, MOUSEEVENTF_MOVE, MOUSEEVENTF_RIGHTDOWN,
|
||||
MOUSEEVENTF_RIGHTUP, MOUSEEVENTF_WHEEL, MOUSEINPUT,
|
||||
},
|
||||
use super::error::{EmulationError, WindowsEmulationCreationError};
|
||||
use input_event::{
|
||||
scancode, Event, KeyboardEvent, PointerEvent, BTN_BACK, BTN_FORWARD, BTN_LEFT, BTN_MIDDLE,
|
||||
BTN_RIGHT,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
client::{ClientEvent, ClientHandle},
|
||||
event::Event,
|
||||
use async_trait::async_trait;
|
||||
use std::ops::BitOrAssign;
|
||||
use std::time::Duration;
|
||||
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::{
|
||||
INPUT, INPUT_KEYBOARD, INPUT_MOUSE, KEYBDINPUT, KEYEVENTF_KEYUP, KEYEVENTF_SCANCODE,
|
||||
MOUSEEVENTF_HWHEEL, MOUSEEVENTF_LEFTDOWN, MOUSEEVENTF_LEFTUP, MOUSEEVENTF_MIDDLEDOWN,
|
||||
MOUSEEVENTF_MIDDLEUP, MOUSEEVENTF_MOVE, MOUSEEVENTF_RIGHTDOWN, MOUSEEVENTF_RIGHTUP,
|
||||
MOUSEEVENTF_WHEEL, MOUSEINPUT,
|
||||
};
|
||||
use windows::Win32::UI::WindowsAndMessaging::{XBUTTON1, XBUTTON2};
|
||||
|
||||
use super::{Emulation, EmulationHandle};
|
||||
|
||||
const DEFAULT_REPEAT_DELAY: Duration = Duration::from_millis(500);
|
||||
const DEFAULT_REPEAT_INTERVAL: Duration = Duration::from_millis(32);
|
||||
|
||||
pub struct WindowsConsumer {
|
||||
pub(crate) struct WindowsEmulation {
|
||||
repeat_task: Option<AbortHandle>,
|
||||
}
|
||||
|
||||
impl WindowsConsumer {
|
||||
pub fn new() -> Result<Self> {
|
||||
impl WindowsEmulation {
|
||||
pub(crate) fn new() -> Result<Self, WindowsEmulationCreationError> {
|
||||
Ok(Self { repeat_task: None })
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl EventConsumer for WindowsConsumer {
|
||||
async fn consume(&mut self, event: Event, _: ClientHandle) {
|
||||
impl Emulation for WindowsEmulation {
|
||||
async fn consume(&mut self, event: Event, _: EmulationHandle) -> Result<(), EmulationError> {
|
||||
match event {
|
||||
Event::Pointer(pointer_event) => match pointer_event {
|
||||
PointerEvent::Motion {
|
||||
time: _,
|
||||
relative_x,
|
||||
relative_y,
|
||||
} => {
|
||||
rel_mouse(relative_x as i32, relative_y as i32);
|
||||
PointerEvent::Motion { time: _, dx, dy } => {
|
||||
rel_mouse(dx as i32, dy as i32);
|
||||
}
|
||||
PointerEvent::Button {
|
||||
time: _,
|
||||
@@ -57,8 +51,8 @@ impl EventConsumer for WindowsConsumer {
|
||||
time: _,
|
||||
axis,
|
||||
value,
|
||||
} => scroll(axis, value),
|
||||
PointerEvent::Frame {} => {}
|
||||
} => scroll(axis, value as i32),
|
||||
PointerEvent::AxisDiscrete120 { axis, value } => scroll(axis, value),
|
||||
},
|
||||
Event::Keyboard(keyboard_event) => match keyboard_event {
|
||||
KeyboardEvent::Key {
|
||||
@@ -76,18 +70,19 @@ impl EventConsumer for WindowsConsumer {
|
||||
}
|
||||
KeyboardEvent::Modifiers { .. } => {}
|
||||
},
|
||||
_ => {}
|
||||
}
|
||||
// FIXME
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn notify(&mut self, _: ClientEvent) {
|
||||
// nothing to do
|
||||
}
|
||||
async fn create(&mut self, _handle: EmulationHandle) {}
|
||||
|
||||
async fn destroy(&mut self) {}
|
||||
async fn destroy(&mut self, _handle: EmulationHandle) {}
|
||||
|
||||
async fn terminate(&mut self) {}
|
||||
}
|
||||
|
||||
impl WindowsConsumer {
|
||||
impl WindowsEmulation {
|
||||
async fn spawn_repeat_task(&mut self, key: u32) {
|
||||
// there can only be one repeating key and it's
|
||||
// always the last to be pressed
|
||||
@@ -108,21 +103,30 @@ impl WindowsConsumer {
|
||||
}
|
||||
}
|
||||
|
||||
fn send_mouse_input(mi: MOUSEINPUT) {
|
||||
fn send_input_safe(input: INPUT) {
|
||||
unsafe {
|
||||
let mut input = INPUT {
|
||||
type_: INPUT_MOUSE,
|
||||
u: std::mem::transmute(mi),
|
||||
};
|
||||
|
||||
SendInput(
|
||||
1_u32,
|
||||
&mut input as LPINPUT,
|
||||
std::mem::size_of::<INPUT>() as i32,
|
||||
);
|
||||
loop {
|
||||
/* retval = number of successfully submitted events */
|
||||
if SendInput(&[input], std::mem::size_of::<INPUT>() as i32) > 0 {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn send_mouse_input(mi: MOUSEINPUT) {
|
||||
send_input_safe(INPUT {
|
||||
r#type: INPUT_MOUSE,
|
||||
Anonymous: INPUT_0 { mi },
|
||||
});
|
||||
}
|
||||
|
||||
fn send_keyboard_input(ki: KEYBDINPUT) {
|
||||
send_input_safe(INPUT {
|
||||
r#type: INPUT_KEYBOARD,
|
||||
Anonymous: INPUT_0 { ki },
|
||||
});
|
||||
}
|
||||
fn rel_mouse(dx: i32, dy: i32) {
|
||||
let mi = MOUSEINPUT {
|
||||
dx,
|
||||
@@ -138,23 +142,32 @@ fn rel_mouse(dx: i32, dy: i32) {
|
||||
fn mouse_button(button: u32, state: u32) {
|
||||
let dw_flags = match state {
|
||||
0 => match button {
|
||||
0x110 => MOUSEEVENTF_LEFTUP,
|
||||
0x111 => MOUSEEVENTF_RIGHTUP,
|
||||
0x112 => MOUSEEVENTF_MIDDLEUP,
|
||||
BTN_LEFT => MOUSEEVENTF_LEFTUP,
|
||||
BTN_RIGHT => MOUSEEVENTF_RIGHTUP,
|
||||
BTN_MIDDLE => MOUSEEVENTF_MIDDLEUP,
|
||||
BTN_BACK => MOUSEEVENTF_XUP,
|
||||
BTN_FORWARD => MOUSEEVENTF_XUP,
|
||||
_ => return,
|
||||
},
|
||||
1 => match button {
|
||||
0x110 => MOUSEEVENTF_LEFTDOWN,
|
||||
0x111 => MOUSEEVENTF_RIGHTDOWN,
|
||||
0x112 => MOUSEEVENTF_MIDDLEDOWN,
|
||||
BTN_LEFT => MOUSEEVENTF_LEFTDOWN,
|
||||
BTN_RIGHT => MOUSEEVENTF_RIGHTDOWN,
|
||||
BTN_MIDDLE => MOUSEEVENTF_MIDDLEDOWN,
|
||||
BTN_BACK => MOUSEEVENTF_XDOWN,
|
||||
BTN_FORWARD => MOUSEEVENTF_XDOWN,
|
||||
_ => return,
|
||||
},
|
||||
_ => return,
|
||||
};
|
||||
let mouse_data = match button {
|
||||
BTN_BACK => XBUTTON1 as u32,
|
||||
BTN_FORWARD => XBUTTON2 as u32,
|
||||
_ => 0,
|
||||
};
|
||||
let mi = MOUSEINPUT {
|
||||
dx: 0,
|
||||
dy: 0, // no movement
|
||||
mouseData: 0,
|
||||
mouseData: mouse_data,
|
||||
dwFlags: dw_flags,
|
||||
time: 0,
|
||||
dwExtraInfo: 0,
|
||||
@@ -162,7 +175,7 @@ fn mouse_button(button: u32, state: u32) {
|
||||
send_mouse_input(mi);
|
||||
}
|
||||
|
||||
fn scroll(axis: u8, value: f64) {
|
||||
fn scroll(axis: u8, value: i32) {
|
||||
let event_type = match axis {
|
||||
0 => MOUSEEVENTF_WHEEL,
|
||||
1 => MOUSEEVENTF_HWHEEL,
|
||||
@@ -171,7 +184,7 @@ fn scroll(axis: u8, value: f64) {
|
||||
let mi = MOUSEINPUT {
|
||||
dx: 0,
|
||||
dy: 0,
|
||||
mouseData: (-value * 15.0) as i32 as u32,
|
||||
mouseData: -value as u32,
|
||||
dwFlags: event_type,
|
||||
time: 0,
|
||||
dwExtraInfo: 0,
|
||||
@@ -186,33 +199,23 @@ fn key_event(key: u32, state: u8) {
|
||||
};
|
||||
let extended = scancode > 0xff;
|
||||
let scancode = scancode & 0xff;
|
||||
let mut flags = KEYEVENTF_SCANCODE;
|
||||
if extended {
|
||||
flags.bitor_assign(KEYEVENTF_EXTENDEDKEY);
|
||||
}
|
||||
if state == 0 {
|
||||
flags.bitor_assign(KEYEVENTF_KEYUP);
|
||||
}
|
||||
let ki = KEYBDINPUT {
|
||||
wVk: 0,
|
||||
wVk: Default::default(),
|
||||
wScan: scancode,
|
||||
dwFlags: KEYEVENTF_SCANCODE
|
||||
| if extended { KEYEVENTF_EXTENDEDKEY } else { 0 }
|
||||
| match state {
|
||||
0 => KEYEVENTF_KEYUP,
|
||||
1 => 0u32,
|
||||
_ => return,
|
||||
},
|
||||
dwFlags: flags,
|
||||
time: 0,
|
||||
dwExtraInfo: 0,
|
||||
};
|
||||
send_keyboard_input(ki);
|
||||
}
|
||||
|
||||
fn send_keyboard_input(ki: KEYBDINPUT) {
|
||||
unsafe {
|
||||
let mut input = INPUT {
|
||||
type_: INPUT_KEYBOARD,
|
||||
u: std::mem::zeroed(),
|
||||
};
|
||||
*input.u.ki_mut() = ki;
|
||||
SendInput(1_u32, &mut input, std::mem::size_of::<INPUT>() as i32);
|
||||
}
|
||||
}
|
||||
|
||||
fn linux_keycode_to_windows_scancode(linux_keycode: u32) -> Option<u16> {
|
||||
let linux_scancode = match scancode::Linux::try_from(linux_keycode) {
|
||||
Ok(s) => s,
|
||||
@@ -1,5 +1,6 @@
|
||||
use crate::client::{ClientEvent, ClientHandle};
|
||||
use crate::consumer::EventConsumer;
|
||||
use crate::error::EmulationError;
|
||||
|
||||
use super::{error::WlrootsEmulationCreationError, Emulation};
|
||||
use async_trait::async_trait;
|
||||
use std::collections::HashMap;
|
||||
use std::io;
|
||||
@@ -7,7 +8,6 @@ use std::os::fd::{AsFd, OwnedFd};
|
||||
use wayland_client::backend::WaylandError;
|
||||
use wayland_client::WEnum;
|
||||
|
||||
use anyhow::{anyhow, Result};
|
||||
use wayland_client::protocol::wl_keyboard::{self, WlKeyboard};
|
||||
use wayland_client::protocol::wl_pointer::{Axis, ButtonState};
|
||||
use wayland_client::protocol::wl_seat::WlSeat;
|
||||
@@ -28,11 +28,14 @@ use wayland_client::{
|
||||
Connection, Dispatch, EventQueue, QueueHandle,
|
||||
};
|
||||
|
||||
use crate::event::{Event, KeyboardEvent, PointerEvent};
|
||||
use input_event::{Event, KeyboardEvent, PointerEvent};
|
||||
|
||||
use super::error::WaylandBindError;
|
||||
use super::EmulationHandle;
|
||||
|
||||
struct State {
|
||||
keymap: Option<(u32, OwnedFd, u32)>,
|
||||
input_for_client: HashMap<ClientHandle, VirtualInput>,
|
||||
input_for_client: HashMap<EmulationHandle, VirtualInput>,
|
||||
seat: wl_seat::WlSeat,
|
||||
qh: QueueHandle<Self>,
|
||||
vpm: VpManager,
|
||||
@@ -40,29 +43,32 @@ struct State {
|
||||
}
|
||||
|
||||
// App State, implements Dispatch event handlers
|
||||
pub(crate) struct WlrootsConsumer {
|
||||
pub(crate) struct WlrootsEmulation {
|
||||
last_flush_failed: bool,
|
||||
state: State,
|
||||
queue: EventQueue<State>,
|
||||
}
|
||||
|
||||
impl WlrootsConsumer {
|
||||
pub fn new() -> Result<Self> {
|
||||
impl WlrootsEmulation {
|
||||
pub(crate) fn new() -> Result<Self, WlrootsEmulationCreationError> {
|
||||
let conn = Connection::connect_to_env()?;
|
||||
let (globals, queue) = registry_queue_init::<State>(&conn)?;
|
||||
let qh = queue.handle();
|
||||
|
||||
let seat: wl_seat::WlSeat = match globals.bind(&qh, 7..=8, ()) {
|
||||
Ok(wl_seat) => wl_seat,
|
||||
Err(_) => return Err(anyhow!("wl_seat >= v7 not supported")),
|
||||
};
|
||||
let seat: wl_seat::WlSeat = globals
|
||||
.bind(&qh, 7..=8, ())
|
||||
.map_err(|e| WaylandBindError::new(e, "wl_seat 7..=8"))?;
|
||||
|
||||
let vpm: VpManager = globals.bind(&qh, 1..=1, ())?;
|
||||
let vkm: VkManager = globals.bind(&qh, 1..=1, ())?;
|
||||
let vpm: VpManager = globals
|
||||
.bind(&qh, 1..=1, ())
|
||||
.map_err(|e| WaylandBindError::new(e, "wlr-virtual-pointer-unstable-v1"))?;
|
||||
let vkm: VkManager = globals
|
||||
.bind(&qh, 1..=1, ())
|
||||
.map_err(|e| WaylandBindError::new(e, "virtual-keyboard-unstable-v1"))?;
|
||||
|
||||
let input_for_client: HashMap<ClientHandle, VirtualInput> = HashMap::new();
|
||||
let input_for_client: HashMap<EmulationHandle, VirtualInput> = HashMap::new();
|
||||
|
||||
let mut consumer = WlrootsConsumer {
|
||||
let mut emulate = WlrootsEmulation {
|
||||
last_flush_failed: false,
|
||||
state: State {
|
||||
keymap: None,
|
||||
@@ -74,21 +80,18 @@ impl WlrootsConsumer {
|
||||
},
|
||||
queue,
|
||||
};
|
||||
while consumer.state.keymap.is_none() {
|
||||
consumer
|
||||
.queue
|
||||
.blocking_dispatch(&mut consumer.state)
|
||||
.unwrap();
|
||||
while emulate.state.keymap.is_none() {
|
||||
emulate.queue.blocking_dispatch(&mut emulate.state)?;
|
||||
}
|
||||
// let fd = unsafe { &File::from_raw_fd(consumer.state.keymap.unwrap().1.as_raw_fd()) };
|
||||
// let fd = unsafe { &File::from_raw_fd(emulate.state.keymap.unwrap().1.as_raw_fd()) };
|
||||
// let mmap = unsafe { MmapOptions::new().map_copy(fd).unwrap() };
|
||||
// log::debug!("{:?}", &mmap[..100]);
|
||||
Ok(consumer)
|
||||
Ok(emulate)
|
||||
}
|
||||
}
|
||||
|
||||
impl State {
|
||||
fn add_client(&mut self, client: ClientHandle) {
|
||||
fn add_client(&mut self, client: EmulationHandle) {
|
||||
let pointer: Vp = self.vpm.create_virtual_pointer(None, &self.qh, ());
|
||||
let keyboard: Vk = self.vkm.create_virtual_keyboard(&self.seat, &self.qh, ());
|
||||
|
||||
@@ -103,56 +106,68 @@ impl State {
|
||||
|
||||
self.input_for_client.insert(client, vinput);
|
||||
}
|
||||
|
||||
fn destroy_client(&mut self, handle: EmulationHandle) {
|
||||
if let Some(input) = self.input_for_client.remove(&handle) {
|
||||
input.pointer.destroy();
|
||||
input.keyboard.destroy();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl EventConsumer for WlrootsConsumer {
|
||||
async fn consume(&mut self, event: Event, client_handle: ClientHandle) {
|
||||
if let Some(virtual_input) = self.state.input_for_client.get(&client_handle) {
|
||||
impl Emulation for WlrootsEmulation {
|
||||
async fn consume(
|
||||
&mut self,
|
||||
event: Event,
|
||||
handle: EmulationHandle,
|
||||
) -> Result<(), EmulationError> {
|
||||
if let Some(virtual_input) = self.state.input_for_client.get(&handle) {
|
||||
if self.last_flush_failed {
|
||||
if let Err(WaylandError::Io(e)) = self.queue.flush() {
|
||||
if e.kind() == io::ErrorKind::WouldBlock {
|
||||
match self.queue.flush() {
|
||||
Err(WaylandError::Io(e)) if e.kind() == io::ErrorKind::WouldBlock => {
|
||||
/*
|
||||
* outgoing buffer is full - sending more events
|
||||
* will overwhelm the output buffer and leave the
|
||||
* wayland connection in a broken state
|
||||
*/
|
||||
log::warn!(
|
||||
"can't keep up, discarding event: ({client_handle}) - {event:?}"
|
||||
);
|
||||
return;
|
||||
log::warn!("can't keep up, discarding event: ({handle}) - {event:?}");
|
||||
return Ok(());
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
virtual_input.consume_event(event).unwrap();
|
||||
virtual_input
|
||||
.consume_event(event)
|
||||
.unwrap_or_else(|_| panic!("failed to convert event: {event:?}"));
|
||||
match self.queue.flush() {
|
||||
Err(WaylandError::Io(e)) if e.kind() == io::ErrorKind::WouldBlock => {
|
||||
self.last_flush_failed = true;
|
||||
log::warn!("can't keep up, retrying ...");
|
||||
}
|
||||
Err(WaylandError::Io(e)) => {
|
||||
log::error!("{e}")
|
||||
}
|
||||
Err(WaylandError::Protocol(e)) => {
|
||||
panic!("wayland protocol violation: {e}")
|
||||
}
|
||||
Ok(()) => {
|
||||
self.last_flush_failed = false;
|
||||
log::warn!("can't keep up, discarding event: ({handle}) - {event:?}");
|
||||
}
|
||||
Err(WaylandError::Protocol(e)) => panic!("wayland protocol violation: {e}"),
|
||||
Ok(()) => self.last_flush_failed = false,
|
||||
Err(e) => Err(e)?,
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn notify(&mut self, client_event: ClientEvent) {
|
||||
if let ClientEvent::Create(client, _) = client_event {
|
||||
self.state.add_client(client);
|
||||
if let Err(e) = self.queue.flush() {
|
||||
log::error!("{}", e);
|
||||
}
|
||||
async fn create(&mut self, handle: EmulationHandle) {
|
||||
self.state.add_client(handle);
|
||||
if let Err(e) = self.queue.flush() {
|
||||
log::error!("{}", e);
|
||||
}
|
||||
}
|
||||
|
||||
async fn destroy(&mut self) {}
|
||||
async fn destroy(&mut self, handle: EmulationHandle) {
|
||||
self.state.destroy_client(handle);
|
||||
if let Err(e) = self.queue.flush() {
|
||||
log::error!("{}", e);
|
||||
}
|
||||
}
|
||||
async fn terminate(&mut self) {
|
||||
/* nothing to do */
|
||||
}
|
||||
}
|
||||
|
||||
struct VirtualInput {
|
||||
@@ -165,11 +180,7 @@ impl VirtualInput {
|
||||
match event {
|
||||
Event::Pointer(e) => {
|
||||
match e {
|
||||
PointerEvent::Motion {
|
||||
time,
|
||||
relative_x,
|
||||
relative_y,
|
||||
} => self.pointer.motion(time, relative_x, relative_y),
|
||||
PointerEvent::Motion { time, dx, dy } => self.pointer.motion(time, dx, dy),
|
||||
PointerEvent::Button {
|
||||
time,
|
||||
button,
|
||||
@@ -183,7 +194,12 @@ impl VirtualInput {
|
||||
self.pointer.axis(time, axis, value);
|
||||
self.pointer.frame();
|
||||
}
|
||||
PointerEvent::Frame {} => self.pointer.frame(),
|
||||
PointerEvent::AxisDiscrete120 { axis, value } => {
|
||||
let axis: Axis = (axis as u32).try_into()?;
|
||||
self.pointer
|
||||
.axis_discrete(0, axis, value as f64 / 6., value / 120);
|
||||
self.pointer.frame();
|
||||
}
|
||||
}
|
||||
self.pointer.frame();
|
||||
}
|
||||
@@ -192,16 +208,15 @@ impl VirtualInput {
|
||||
self.keyboard.key(time, key, state as u32);
|
||||
}
|
||||
KeyboardEvent::Modifiers {
|
||||
mods_depressed,
|
||||
mods_latched,
|
||||
mods_locked,
|
||||
depressed: mods_depressed,
|
||||
latched: mods_latched,
|
||||
locked: mods_locked,
|
||||
group,
|
||||
} => {
|
||||
self.keyboard
|
||||
.modifiers(mods_depressed, mods_latched, mods_locked, group);
|
||||
}
|
||||
},
|
||||
_ => {}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@@ -1,4 +1,3 @@
|
||||
use anyhow::{anyhow, Result};
|
||||
use async_trait::async_trait;
|
||||
use std::ptr;
|
||||
use x11::{
|
||||
@@ -6,26 +5,26 @@ use x11::{
|
||||
xtest,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
client::ClientHandle,
|
||||
consumer::EventConsumer,
|
||||
event::{
|
||||
Event, KeyboardEvent, PointerEvent, BTN_BACK, BTN_FORWARD, BTN_LEFT, BTN_MIDDLE, BTN_RIGHT,
|
||||
},
|
||||
use input_event::{
|
||||
Event, KeyboardEvent, PointerEvent, BTN_BACK, BTN_FORWARD, BTN_LEFT, BTN_MIDDLE, BTN_RIGHT,
|
||||
};
|
||||
|
||||
pub struct X11Consumer {
|
||||
use crate::error::EmulationError;
|
||||
|
||||
use super::{error::X11EmulationCreationError, Emulation, EmulationHandle};
|
||||
|
||||
pub(crate) struct X11Emulation {
|
||||
display: *mut xlib::Display,
|
||||
}
|
||||
|
||||
unsafe impl Send for X11Consumer {}
|
||||
unsafe impl Send for X11Emulation {}
|
||||
|
||||
impl X11Consumer {
|
||||
pub fn new() -> Result<Self> {
|
||||
impl X11Emulation {
|
||||
pub(crate) fn new() -> Result<Self, X11EmulationCreationError> {
|
||||
let display = unsafe {
|
||||
match xlib::XOpenDisplay(ptr::null()) {
|
||||
d if d == ptr::null::<xlib::Display>() as *mut xlib::Display => {
|
||||
Err(anyhow!("could not open display"))
|
||||
Err(X11EmulationCreationError::OpenDisplay)
|
||||
}
|
||||
display => Ok(display),
|
||||
}
|
||||
@@ -91,7 +90,7 @@ impl X11Consumer {
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for X11Consumer {
|
||||
impl Drop for X11Emulation {
|
||||
fn drop(&mut self) {
|
||||
unsafe {
|
||||
XCloseDisplay(self.display);
|
||||
@@ -100,16 +99,12 @@ impl Drop for X11Consumer {
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl EventConsumer for X11Consumer {
|
||||
async fn consume(&mut self, event: Event, _: ClientHandle) {
|
||||
impl Emulation for X11Emulation {
|
||||
async fn consume(&mut self, event: Event, _: EmulationHandle) -> Result<(), EmulationError> {
|
||||
match event {
|
||||
Event::Pointer(pointer_event) => match pointer_event {
|
||||
PointerEvent::Motion {
|
||||
time: _,
|
||||
relative_x,
|
||||
relative_y,
|
||||
} => {
|
||||
self.relative_motion(relative_x as i32, relative_y as i32);
|
||||
PointerEvent::Motion { time: _, dx, dy } => {
|
||||
self.relative_motion(dx as i32, dy as i32);
|
||||
}
|
||||
PointerEvent::Button {
|
||||
time: _,
|
||||
@@ -125,7 +120,9 @@ impl EventConsumer for X11Consumer {
|
||||
} => {
|
||||
self.emulate_scroll(axis, value);
|
||||
}
|
||||
PointerEvent::Frame {} => {}
|
||||
PointerEvent::AxisDiscrete120 { axis, value } => {
|
||||
self.emulate_scroll(axis, value as f64);
|
||||
}
|
||||
},
|
||||
Event::Keyboard(KeyboardEvent::Key {
|
||||
time: _,
|
||||
@@ -139,11 +136,19 @@ impl EventConsumer for X11Consumer {
|
||||
unsafe {
|
||||
xlib::XFlush(self.display);
|
||||
}
|
||||
// FIXME
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn notify(&mut self, _: crate::client::ClientEvent) {
|
||||
async fn create(&mut self, _: EmulationHandle) {
|
||||
// for our purposes it does not matter what client sent the event
|
||||
}
|
||||
|
||||
async fn destroy(&mut self) {}
|
||||
async fn destroy(&mut self, _: EmulationHandle) {
|
||||
// for our purposes it does not matter what client sent the event
|
||||
}
|
||||
|
||||
async fn terminate(&mut self) {
|
||||
/* nothing to do */
|
||||
}
|
||||
}
|
||||
165
input-emulation/src/xdg_desktop_portal.rs
Normal file
165
input-emulation/src/xdg_desktop_portal.rs
Normal file
@@ -0,0 +1,165 @@
|
||||
use ashpd::{
|
||||
desktop::{
|
||||
remote_desktop::{Axis, DeviceType, KeyState, RemoteDesktop},
|
||||
PersistMode, Session,
|
||||
},
|
||||
zbus::AsyncDrop,
|
||||
WindowIdentifier,
|
||||
};
|
||||
use async_trait::async_trait;
|
||||
|
||||
use futures::FutureExt;
|
||||
use input_event::{
|
||||
Event::{Keyboard, Pointer},
|
||||
KeyboardEvent, PointerEvent,
|
||||
};
|
||||
|
||||
use crate::error::EmulationError;
|
||||
|
||||
use super::{error::XdpEmulationCreationError, Emulation, EmulationHandle};
|
||||
|
||||
pub(crate) struct DesktopPortalEmulation<'a> {
|
||||
proxy: RemoteDesktop<'a>,
|
||||
session: Session<'a, RemoteDesktop<'a>>,
|
||||
}
|
||||
|
||||
impl<'a> DesktopPortalEmulation<'a> {
|
||||
pub(crate) async fn new() -> Result<DesktopPortalEmulation<'a>, XdpEmulationCreationError> {
|
||||
log::debug!("connecting to org.freedesktop.portal.RemoteDesktop portal ...");
|
||||
let proxy = RemoteDesktop::new().await?;
|
||||
|
||||
// retry when user presses the cancel button
|
||||
log::debug!("creating session ...");
|
||||
let session = proxy.create_session().await?;
|
||||
|
||||
log::debug!("selecting devices ...");
|
||||
proxy
|
||||
.select_devices(
|
||||
&session,
|
||||
DeviceType::Keyboard | DeviceType::Pointer,
|
||||
None,
|
||||
PersistMode::ExplicitlyRevoked,
|
||||
)
|
||||
.await?;
|
||||
|
||||
log::info!("requesting permission for input emulation");
|
||||
let _devices = proxy
|
||||
.start(&session, &WindowIdentifier::default())
|
||||
.await?
|
||||
.response()?;
|
||||
|
||||
log::debug!("started session");
|
||||
let session = session;
|
||||
|
||||
Ok(Self { proxy, session })
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl<'a> Emulation for DesktopPortalEmulation<'a> {
|
||||
async fn consume(
|
||||
&mut self,
|
||||
event: input_event::Event,
|
||||
_client: EmulationHandle,
|
||||
) -> Result<(), EmulationError> {
|
||||
match event {
|
||||
Pointer(p) => match p {
|
||||
PointerEvent::Motion { time: _, dx, dy } => {
|
||||
self.proxy
|
||||
.notify_pointer_motion(&self.session, dx, dy)
|
||||
.await?;
|
||||
}
|
||||
PointerEvent::Button {
|
||||
time: _,
|
||||
button,
|
||||
state,
|
||||
} => {
|
||||
let state = match state {
|
||||
0 => KeyState::Released,
|
||||
_ => KeyState::Pressed,
|
||||
};
|
||||
self.proxy
|
||||
.notify_pointer_button(&self.session, button as i32, state)
|
||||
.await?;
|
||||
}
|
||||
PointerEvent::AxisDiscrete120 { axis, value } => {
|
||||
let axis = match axis {
|
||||
0 => Axis::Vertical,
|
||||
_ => Axis::Horizontal,
|
||||
};
|
||||
self.proxy
|
||||
.notify_pointer_axis_discrete(&self.session, axis, value / 120)
|
||||
.await?;
|
||||
}
|
||||
PointerEvent::Axis {
|
||||
time: _,
|
||||
axis,
|
||||
value,
|
||||
} => {
|
||||
let axis = match axis {
|
||||
0 => Axis::Vertical,
|
||||
_ => Axis::Horizontal,
|
||||
};
|
||||
let (dx, dy) = match axis {
|
||||
Axis::Vertical => (0., value),
|
||||
Axis::Horizontal => (value, 0.),
|
||||
};
|
||||
self.proxy
|
||||
.notify_pointer_axis(&self.session, dx, dy, true)
|
||||
.await?;
|
||||
}
|
||||
},
|
||||
Keyboard(k) => {
|
||||
match k {
|
||||
KeyboardEvent::Key {
|
||||
time: _,
|
||||
key,
|
||||
state,
|
||||
} => {
|
||||
let state = match state {
|
||||
0 => KeyState::Released,
|
||||
_ => KeyState::Pressed,
|
||||
};
|
||||
self.proxy
|
||||
.notify_keyboard_keycode(&self.session, key as i32, state)
|
||||
.await?;
|
||||
}
|
||||
KeyboardEvent::Modifiers { .. } => {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn create(&mut self, _client: EmulationHandle) {}
|
||||
async fn destroy(&mut self, _client: EmulationHandle) {}
|
||||
async fn terminate(&mut self) {
|
||||
if let Err(e) = self.session.close().await {
|
||||
log::warn!("session.close(): {e}");
|
||||
};
|
||||
if let Err(e) = self.session.receive_closed().await {
|
||||
log::warn!("session.receive_closed(): {e}");
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> AsyncDrop for DesktopPortalEmulation<'a> {
|
||||
#[doc = r" Perform the async cleanup."]
|
||||
#[must_use]
|
||||
#[allow(clippy::type_complexity, clippy::type_repetition_in_bounds)]
|
||||
fn async_drop<'async_trait>(
|
||||
self,
|
||||
) -> ::core::pin::Pin<
|
||||
Box<dyn ::core::future::Future<Output = ()> + ::core::marker::Send + 'async_trait>,
|
||||
>
|
||||
where
|
||||
Self: 'async_trait,
|
||||
{
|
||||
async move {
|
||||
let _ = self.session.close().await;
|
||||
}
|
||||
.boxed()
|
||||
}
|
||||
}
|
||||
21
input-event/Cargo.toml
Normal file
21
input-event/Cargo.toml
Normal file
@@ -0,0 +1,21 @@
|
||||
[package]
|
||||
name = "input-event"
|
||||
description = "cross-platform input-event types for input-capture / input-emulation"
|
||||
version = "0.2.1"
|
||||
edition = "2021"
|
||||
license = "GPL-3.0-or-later"
|
||||
repository = "https://github.com/feschber/lan-mouse"
|
||||
|
||||
[dependencies]
|
||||
futures-core = "0.3.30"
|
||||
log = "0.4.22"
|
||||
num_enum = "0.7.2"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
thiserror = "1.0.61"
|
||||
|
||||
[target.'cfg(all(unix, not(target_os="macos")))'.dependencies]
|
||||
reis = { version = "0.2.0", optional = true }
|
||||
|
||||
[features]
|
||||
default = ["libei"]
|
||||
libei = ["dep:reis"]
|
||||
1
input-event/src/error.rs
Normal file
1
input-event/src/error.rs
Normal file
@@ -0,0 +1 @@
|
||||
|
||||
119
input-event/src/lib.rs
Normal file
119
input-event/src/lib.rs
Normal file
@@ -0,0 +1,119 @@
|
||||
use std::fmt::{self, Display};
|
||||
|
||||
pub mod error;
|
||||
pub mod scancode;
|
||||
|
||||
#[cfg(all(unix, feature = "libei", not(target_os = "macos")))]
|
||||
mod libei;
|
||||
|
||||
// FIXME
|
||||
pub const BTN_LEFT: u32 = 0x110;
|
||||
pub const BTN_RIGHT: u32 = 0x111;
|
||||
pub const BTN_MIDDLE: u32 = 0x112;
|
||||
pub const BTN_BACK: u32 = 0x113;
|
||||
pub const BTN_FORWARD: u32 = 0x114;
|
||||
|
||||
#[derive(Debug, PartialEq, Clone, Copy)]
|
||||
pub enum PointerEvent {
|
||||
/// relative motion event
|
||||
Motion { time: u32, dx: f64, dy: f64 },
|
||||
/// mouse button event
|
||||
Button { time: u32, button: u32, state: u32 },
|
||||
/// axis event, scroll event for touchpads
|
||||
Axis { time: u32, axis: u8, value: f64 },
|
||||
/// discrete axis event, scroll event for mice - 120 = one scroll tick
|
||||
AxisDiscrete120 { axis: u8, value: i32 },
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Clone, Copy)]
|
||||
pub enum KeyboardEvent {
|
||||
/// a key press / release event
|
||||
Key { time: u32, key: u32, state: u8 },
|
||||
/// modifiers changed state
|
||||
Modifiers {
|
||||
depressed: u32,
|
||||
latched: u32,
|
||||
locked: u32,
|
||||
group: u32,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Debug, Clone, Copy)]
|
||||
pub enum Event {
|
||||
/// pointer event (motion / button / axis)
|
||||
Pointer(PointerEvent),
|
||||
/// keyboard events (key / modifiers)
|
||||
Keyboard(KeyboardEvent),
|
||||
}
|
||||
|
||||
impl Display for PointerEvent {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
PointerEvent::Motion { time: _, dx, dy } => write!(f, "motion({dx},{dy})"),
|
||||
PointerEvent::Button {
|
||||
time: _,
|
||||
button,
|
||||
state,
|
||||
} => {
|
||||
let str = match *button {
|
||||
BTN_LEFT => Some("left"),
|
||||
BTN_RIGHT => Some("right"),
|
||||
BTN_MIDDLE => Some("middle"),
|
||||
BTN_FORWARD => Some("forward"),
|
||||
BTN_BACK => Some("back"),
|
||||
_ => None,
|
||||
};
|
||||
if let Some(button) = str {
|
||||
write!(f, "button({button}, {state})")
|
||||
} else {
|
||||
write!(f, "button({button}, {state}")
|
||||
}
|
||||
}
|
||||
PointerEvent::Axis {
|
||||
time: _,
|
||||
axis,
|
||||
value,
|
||||
} => write!(f, "scroll({axis}, {value})"),
|
||||
PointerEvent::AxisDiscrete120 { axis, value } => {
|
||||
write!(f, "scroll-120 ({axis}, {value})")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for KeyboardEvent {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
KeyboardEvent::Key {
|
||||
time: _,
|
||||
key,
|
||||
state,
|
||||
} => {
|
||||
let scan = scancode::Linux::try_from(*key);
|
||||
if let Ok(scan) = scan {
|
||||
write!(f, "key({scan:?}, {state})")
|
||||
} else {
|
||||
write!(f, "key({key}, {state})")
|
||||
}
|
||||
}
|
||||
KeyboardEvent::Modifiers {
|
||||
depressed: mods_depressed,
|
||||
latched: mods_latched,
|
||||
locked: mods_locked,
|
||||
group,
|
||||
} => write!(
|
||||
f,
|
||||
"modifiers({mods_depressed},{mods_latched},{mods_locked},{group})"
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for Event {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
Event::Pointer(p) => write!(f, "{}", p),
|
||||
Event::Keyboard(k) => write!(f, "{}", k),
|
||||
}
|
||||
}
|
||||
}
|
||||
146
input-event/src/libei.rs
Normal file
146
input-event/src/libei.rs
Normal file
@@ -0,0 +1,146 @@
|
||||
use reis::{
|
||||
ei::{button::ButtonState, keyboard::KeyState},
|
||||
event::EiEvent,
|
||||
};
|
||||
|
||||
use crate::{Event, KeyboardEvent, PointerEvent};
|
||||
|
||||
impl Event {
|
||||
pub fn from_ei_event(ei_event: EiEvent) -> impl Iterator<Item = Self> {
|
||||
to_input_events(ei_event).into_iter()
|
||||
}
|
||||
}
|
||||
|
||||
enum Events {
|
||||
None,
|
||||
One(Event),
|
||||
Two(Event, Event),
|
||||
}
|
||||
|
||||
impl Events {
|
||||
fn into_iter(self) -> impl Iterator<Item = Event> {
|
||||
EventIterator::new(self)
|
||||
}
|
||||
}
|
||||
|
||||
struct EventIterator {
|
||||
events: [Option<Event>; 2],
|
||||
pos: usize,
|
||||
}
|
||||
|
||||
impl EventIterator {
|
||||
fn new(events: Events) -> Self {
|
||||
let events = match events {
|
||||
Events::None => [None, None],
|
||||
Events::One(e) => [Some(e), None],
|
||||
Events::Two(e, f) => [Some(e), Some(f)],
|
||||
};
|
||||
Self { events, pos: 0 }
|
||||
}
|
||||
}
|
||||
|
||||
impl Iterator for EventIterator {
|
||||
type Item = Event;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
let res = if self.pos >= self.events.len() {
|
||||
None
|
||||
} else {
|
||||
self.events[self.pos]
|
||||
};
|
||||
self.pos += 1;
|
||||
res
|
||||
}
|
||||
}
|
||||
|
||||
fn to_input_events(ei_event: EiEvent) -> Events {
|
||||
match ei_event {
|
||||
EiEvent::KeyboardModifiers(mods) => {
|
||||
let modifier_event = KeyboardEvent::Modifiers {
|
||||
depressed: mods.depressed,
|
||||
latched: mods.latched,
|
||||
locked: mods.locked,
|
||||
group: mods.group,
|
||||
};
|
||||
Events::One(Event::Keyboard(modifier_event))
|
||||
}
|
||||
EiEvent::Frame(_) => Events::None, /* FIXME */
|
||||
EiEvent::PointerMotion(motion) => {
|
||||
let motion_event = PointerEvent::Motion {
|
||||
time: motion.time as u32,
|
||||
dx: motion.dx as f64,
|
||||
dy: motion.dy as f64,
|
||||
};
|
||||
Events::One(Event::Pointer(motion_event))
|
||||
}
|
||||
EiEvent::PointerMotionAbsolute(_) => Events::None,
|
||||
EiEvent::Button(button) => {
|
||||
let button_event = PointerEvent::Button {
|
||||
time: button.time as u32,
|
||||
button: button.button,
|
||||
state: match button.state {
|
||||
ButtonState::Released => 0,
|
||||
ButtonState::Press => 1,
|
||||
},
|
||||
};
|
||||
Events::One(Event::Pointer(button_event))
|
||||
}
|
||||
EiEvent::ScrollDelta(delta) => {
|
||||
let dy = Event::Pointer(PointerEvent::Axis {
|
||||
time: 0,
|
||||
axis: 0,
|
||||
value: delta.dy as f64,
|
||||
});
|
||||
let dx = Event::Pointer(PointerEvent::Axis {
|
||||
time: 0,
|
||||
axis: 1,
|
||||
value: delta.dx as f64,
|
||||
});
|
||||
if delta.dy != 0. && delta.dx != 0. {
|
||||
Events::Two(dy, dx)
|
||||
} else if delta.dy != 0. {
|
||||
Events::One(dy)
|
||||
} else if delta.dx != 0. {
|
||||
Events::One(dx)
|
||||
} else {
|
||||
Events::None
|
||||
}
|
||||
}
|
||||
EiEvent::ScrollStop(_) => Events::None, /* TODO */
|
||||
EiEvent::ScrollCancel(_) => Events::None, /* TODO */
|
||||
EiEvent::ScrollDiscrete(scroll) => {
|
||||
let dy = Event::Pointer(PointerEvent::AxisDiscrete120 {
|
||||
axis: 0,
|
||||
value: scroll.discrete_dy,
|
||||
});
|
||||
let dx = Event::Pointer(PointerEvent::AxisDiscrete120 {
|
||||
axis: 1,
|
||||
value: scroll.discrete_dx,
|
||||
});
|
||||
if scroll.discrete_dy != 0 && scroll.discrete_dx != 0 {
|
||||
Events::Two(dy, dx)
|
||||
} else if scroll.discrete_dy != 0 {
|
||||
Events::One(dy)
|
||||
} else if scroll.discrete_dx != 0 {
|
||||
Events::One(dx)
|
||||
} else {
|
||||
Events::None
|
||||
}
|
||||
}
|
||||
EiEvent::KeyboardKey(key) => {
|
||||
let key_event = KeyboardEvent::Key {
|
||||
key: key.key,
|
||||
state: match key.state {
|
||||
KeyState::Press => 1,
|
||||
KeyState::Released => 0,
|
||||
},
|
||||
time: key.time as u32,
|
||||
};
|
||||
Events::One(Event::Keyboard(key_event))
|
||||
}
|
||||
EiEvent::TouchDown(_) => Events::None, /* TODO */
|
||||
EiEvent::TouchUp(_) => Events::None, /* TODO */
|
||||
EiEvent::TouchMotion(_) => Events::None, /* TODO */
|
||||
_ => Events::None,
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,13 @@
|
||||
use num_enum::TryFromPrimitive;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/*
|
||||
* https://learn.microsoft.com/en-us/windows/win32/inputdev/about-keyboard-input
|
||||
* https://download.microsoft.com/download/1/6/1/161ba512-40e2-4cc9-843a-923143f3456c/translate.pdf
|
||||
* https://kbd-project.org/docs/scancodes/scancodes-1.html
|
||||
*/
|
||||
#[repr(u32)]
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
#[derive(Debug, Clone, Copy, TryFromPrimitive)]
|
||||
pub enum Windows {
|
||||
Shutdown = 0xE05E,
|
||||
SystemSleep = 0xE05F,
|
||||
@@ -117,15 +122,15 @@ pub enum Windows {
|
||||
KeyF21 = 0x006C,
|
||||
KeyF22 = 0x006D,
|
||||
KeyF23 = 0x006E,
|
||||
KeyF24 = 0x0076,
|
||||
KeyF24 = 0x0076, // KeyLANG5
|
||||
KeypadComma = 0x007E,
|
||||
KeyInternational1 = 0x0073,
|
||||
KeyInternational2 = 0x0070,
|
||||
KeyInternational3 = 0x007D,
|
||||
KeyInternational3 = 0x007D, // typo in doc -> its Int'l 3 not Int'l 2
|
||||
#[allow(dead_code)]
|
||||
KeyInternational4 = 0x0079, // FIXME unused
|
||||
KeyInternational4 = 0x0079,
|
||||
#[allow(dead_code)]
|
||||
KeyInternational5 = 0x007B, // FIXME unused
|
||||
KeyInternational5 = 0x007B,
|
||||
// KeyInternational6 = 0x005C,
|
||||
KeyLANG1 = 0x0072,
|
||||
KeyLANG2 = 0x0071,
|
||||
@@ -138,6 +143,7 @@ pub enum Windows {
|
||||
KeyLeftGUI = 0xE05B,
|
||||
KeyRightCtrl = 0xE01D,
|
||||
KeyRightShift = 0x0036,
|
||||
KeyFakeRightShift = 0xE036,
|
||||
KeyRightAlt = 0xE038,
|
||||
KeyRightGUI = 0xE05C,
|
||||
KeyScanNextTrack = 0xE019,
|
||||
@@ -165,7 +171,7 @@ pub enum Windows {
|
||||
* https://github.com/torvalds/linux/blob/master/include/uapi/linux/input-event-codes.h
|
||||
*/
|
||||
#[repr(u32)]
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, Copy, Eq, Hash, PartialEq, TryFromPrimitive)]
|
||||
#[allow(dead_code)]
|
||||
pub enum Linux {
|
||||
KeyReserved = 0,
|
||||
@@ -210,7 +216,7 @@ pub enum Linux {
|
||||
KeySemicolon = 39,
|
||||
KeyApostrophe = 40,
|
||||
KeyGrave = 41,
|
||||
KeyLeftshift = 42,
|
||||
KeyLeftShift = 42,
|
||||
KeyBackslash = 43,
|
||||
KeyZ = 44,
|
||||
KeyX = 45,
|
||||
@@ -224,7 +230,7 @@ pub enum Linux {
|
||||
KeySlash = 53,
|
||||
KeyRightShift = 54,
|
||||
KeyKpAsterisk = 55,
|
||||
KeyLeftalt = 56,
|
||||
KeyLeftAlt = 56,
|
||||
KeySpace = 57,
|
||||
KeyCapsLock = 58,
|
||||
KeyF1 = 59,
|
||||
@@ -290,11 +296,11 @@ pub enum Linux {
|
||||
KeyPause = 119,
|
||||
KeyScale = 120, /* AL Compiz Scale (Expose) */
|
||||
KeyKpcomma = 121,
|
||||
KeyHangeul = 122,
|
||||
KeyHanguel = 122,
|
||||
// KEY_HANGUEL = KeyHangeul,
|
||||
KeyHanja = 123,
|
||||
KeyYen = 124,
|
||||
KeyLeftmeta = 125,
|
||||
KeyLeftMeta = 125,
|
||||
KeyRightmeta = 126,
|
||||
KeyCompose = 127,
|
||||
KeyStop = 128, /* AC Stop */
|
||||
@@ -426,18 +432,6 @@ pub enum Linux {
|
||||
KeyCount = 249,
|
||||
}
|
||||
|
||||
impl TryFrom<u32> for Linux {
|
||||
type Error = ();
|
||||
|
||||
fn try_from(value: u32) -> Result<Self, Self::Error> {
|
||||
if value >= Self::KeyCount as u32 {
|
||||
return Err(());
|
||||
}
|
||||
let code: Linux = unsafe { std::mem::transmute(value) };
|
||||
Ok(code)
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<Linux> for Windows {
|
||||
type Error = ();
|
||||
|
||||
@@ -485,7 +479,7 @@ impl TryFrom<Linux> for Windows {
|
||||
Linux::KeySemicolon => Ok(Self::KeySemiColon),
|
||||
Linux::KeyApostrophe => Ok(Self::KeyApostrophe),
|
||||
Linux::KeyGrave => Ok(Self::KeyGrave),
|
||||
Linux::KeyLeftshift => Ok(Self::KeyLeftShift),
|
||||
Linux::KeyLeftShift => Ok(Self::KeyLeftShift),
|
||||
Linux::KeyBackslash => Ok(Self::KeyBackslash),
|
||||
Linux::KeyZ => Ok(Self::KeyZ),
|
||||
Linux::KeyX => Ok(Self::KeyX),
|
||||
@@ -499,7 +493,7 @@ impl TryFrom<Linux> for Windows {
|
||||
Linux::KeySlash => Ok(Self::KeySlash),
|
||||
Linux::KeyRightShift => Ok(Self::KeyRightShift),
|
||||
Linux::KeyKpAsterisk => Ok(Self::KeypadStar),
|
||||
Linux::KeyLeftalt => Ok(Self::KeyLeftAlt),
|
||||
Linux::KeyLeftAlt => Ok(Self::KeyLeftAlt),
|
||||
Linux::KeySpace => Ok(Self::KeySpace),
|
||||
Linux::KeyCapsLock => Ok(Self::KeyCapsLock),
|
||||
Linux::KeyF1 => Ok(Self::KeyF1),
|
||||
@@ -527,16 +521,16 @@ impl TryFrom<Linux> for Windows {
|
||||
Linux::KeyKp3 => Ok(Self::Keypad3PageDn),
|
||||
Linux::KeyKp0 => Ok(Self::Keypad0Insert),
|
||||
Linux::KeyKpDot => Ok(Self::KeypadDot),
|
||||
Linux::KeyZenkakuhankaku => Ok(Self::KeyLANG1), // TODO unsure
|
||||
Linux::Key102nd => Ok(Self::KeyNonUSSlashBar), // TODO unsure
|
||||
Linux::KeyZenkakuhankaku => Ok(Self::KeyF24), // KeyLANG5
|
||||
Linux::Key102nd => Ok(Self::KeyNonUSSlashBar), // TODO unsure
|
||||
Linux::KeyF11 => Ok(Self::KeyF11),
|
||||
Linux::KeyF12 => Ok(Self::KeyF12),
|
||||
Linux::KeyRo => Ok(Self::ErrorRollOver), // TODO unsure
|
||||
Linux::KeyKatakana => Ok(Self::KeyLANG1), // TODO unsure
|
||||
Linux::KeyHiragana => Ok(Self::KeyLANG2), // TODO unsure
|
||||
Linux::KeyHenkan => Ok(Self::KeyLANG3), // TODO unsure
|
||||
Linux::KeyKatakanahiragana => Ok(Self::KeyLANG4), // TODO unsure
|
||||
Linux::KeyMuhenkan => Ok(Self::KeyLANG4), // TODO unsure
|
||||
Linux::KeyRo => Ok(Self::KeyInternational1),
|
||||
Linux::KeyKatakana => Ok(Self::KeyLANG3),
|
||||
Linux::KeyHiragana => Ok(Self::KeyLANG4),
|
||||
Linux::KeyHenkan => Ok(Self::KeyInternational4),
|
||||
Linux::KeyKatakanahiragana => Ok(Self::KeyInternational2),
|
||||
Linux::KeyMuhenkan => Ok(Self::KeyInternational5),
|
||||
Linux::KeyKpJpComma => Ok(Self::KeypadComma),
|
||||
Linux::KeyKpEnter => Ok(Self::KeypadEnter),
|
||||
Linux::KeyRightCtrl => Ok(Self::KeyRightCtrl),
|
||||
@@ -564,10 +558,10 @@ impl TryFrom<Linux> for Windows {
|
||||
Linux::KeyPause => Ok(Self::KeyPause),
|
||||
Linux::KeyScale => Err(()), // TODO
|
||||
Linux::KeyKpcomma => Ok(Self::KeypadComma),
|
||||
Linux::KeyHangeul => Ok(Self::KeyInternational1), // TODO unsure
|
||||
Linux::KeyHanja => Ok(Self::KeyInternational2), // TODO unsure
|
||||
Linux::KeyYen => Ok(Self::KeyInternational3), // TODO unsure
|
||||
Linux::KeyLeftmeta => Ok(Self::KeyLeftGUI),
|
||||
Linux::KeyHanguel => Ok(Self::KeyLANG1), // FIXME should be 00F2?
|
||||
Linux::KeyHanja => Ok(Self::KeyLANG2), // FIXME should be 00F1?
|
||||
Linux::KeyYen => Ok(Self::KeyInternational3),
|
||||
Linux::KeyLeftMeta => Ok(Self::KeyLeftGUI),
|
||||
Linux::KeyRightmeta => Ok(Self::KeyRightGUI),
|
||||
Linux::KeyCompose => Ok(Self::KeyApplication),
|
||||
Linux::KeyStop => Ok(Self::ACStop),
|
||||
@@ -696,3 +690,162 @@ impl TryFrom<Linux> for Windows {
|
||||
}
|
||||
}
|
||||
}
|
||||
impl TryFrom<Windows> for Linux {
|
||||
type Error = ();
|
||||
|
||||
fn try_from(value: Windows) -> Result<Self, Self::Error> {
|
||||
match value {
|
||||
Windows::Shutdown => Ok(Self::KeyPower),
|
||||
Windows::SystemSleep => Ok(Self::KeySleep),
|
||||
Windows::SystemWakeUp => Ok(Self::KeyWakeup),
|
||||
Windows::ErrorRollOver => Ok(Self::KeyRo),
|
||||
Windows::KeyA => Ok(Self::KeyA),
|
||||
Windows::KeyB => Ok(Self::KeyB),
|
||||
Windows::KeyC => Ok(Self::KeyC),
|
||||
Windows::KeyD => Ok(Self::KeyD),
|
||||
Windows::KeyE => Ok(Self::KeyE),
|
||||
Windows::KeyF => Ok(Self::KeyF),
|
||||
Windows::KeyG => Ok(Self::KeyG),
|
||||
Windows::KeyH => Ok(Self::KeyH),
|
||||
Windows::KeyI => Ok(Self::KeyI),
|
||||
Windows::KeyJ => Ok(Self::KeyJ),
|
||||
Windows::KeyK => Ok(Self::KeyK),
|
||||
Windows::KeyL => Ok(Self::KeyL),
|
||||
Windows::KeyM => Ok(Self::KeyM),
|
||||
Windows::KeyN => Ok(Self::KeyN),
|
||||
Windows::KeyO => Ok(Self::KeyO),
|
||||
Windows::KeyP => Ok(Self::KeyP),
|
||||
Windows::KeyQ => Ok(Self::KeyQ),
|
||||
Windows::KeyR => Ok(Self::KeyR),
|
||||
Windows::KeyS => Ok(Self::KeyS),
|
||||
Windows::KeyT => Ok(Self::KeyT),
|
||||
Windows::KeyU => Ok(Self::KeyU),
|
||||
Windows::KeyV => Ok(Self::KeyV),
|
||||
Windows::KeyW => Ok(Self::KeyW),
|
||||
Windows::KeyX => Ok(Self::KeyX),
|
||||
Windows::KeyY => Ok(Self::KeyY),
|
||||
Windows::KeyZ => Ok(Self::KeyZ),
|
||||
Windows::Key1 => Ok(Self::Key1),
|
||||
Windows::Key2 => Ok(Self::Key2),
|
||||
Windows::Key3 => Ok(Self::Key3),
|
||||
Windows::Key4 => Ok(Self::Key4),
|
||||
Windows::Key5 => Ok(Self::Key5),
|
||||
Windows::Key6 => Ok(Self::Key6),
|
||||
Windows::Key7 => Ok(Self::Key7),
|
||||
Windows::Key8 => Ok(Self::Key8),
|
||||
Windows::Key9 => Ok(Self::Key9),
|
||||
Windows::Key0 => Ok(Self::Key0),
|
||||
Windows::KeyEnter => Ok(Self::KeyEnter),
|
||||
Windows::KeyEsc => Ok(Self::KeyEsc),
|
||||
Windows::KeyDelete => Ok(Self::KeyBackspace),
|
||||
Windows::KeyTab => Ok(Self::KeyTab),
|
||||
Windows::KeySpace => Ok(Self::KeySpace),
|
||||
Windows::KeyMinus => Ok(Self::KeyMinus),
|
||||
Windows::KeyEqual => Ok(Self::KeyEqual),
|
||||
Windows::KeyLeftBrace => Ok(Self::KeyLeftbrace),
|
||||
Windows::KeyRightBrace => Ok(Self::KeyRightbrace),
|
||||
Windows::KeyBackslash => Ok(Self::KeyBackslash),
|
||||
Windows::KeySemiColon => Ok(Self::KeySemicolon),
|
||||
Windows::KeyApostrophe => Ok(Self::KeyApostrophe),
|
||||
Windows::KeyGrave => Ok(Self::KeyGrave),
|
||||
Windows::KeyComma => Ok(Self::KeyComma),
|
||||
Windows::KeyDot => Ok(Self::KeyDot),
|
||||
Windows::KeySlash => Ok(Self::KeySlash),
|
||||
Windows::KeyCapsLock => Ok(Self::KeyCapsLock),
|
||||
Windows::KeyF1 => Ok(Self::KeyF1),
|
||||
Windows::KeyF2 => Ok(Self::KeyF2),
|
||||
Windows::KeyF3 => Ok(Self::KeyF3),
|
||||
Windows::KeyF4 => Ok(Self::KeyF4),
|
||||
Windows::KeyF5 => Ok(Self::KeyF5),
|
||||
Windows::KeyF6 => Ok(Self::KeyF6),
|
||||
Windows::KeyF7 => Ok(Self::KeyF7),
|
||||
Windows::KeyF8 => Ok(Self::KeyF8),
|
||||
Windows::KeyF9 => Ok(Self::KeyF9),
|
||||
Windows::KeyF10 => Ok(Self::KeyF10),
|
||||
Windows::KeyF11 => Ok(Self::KeyF11),
|
||||
Windows::KeyF12 => Ok(Self::KeyF12),
|
||||
Windows::KeyPrintScreen => Ok(Self::KeySysrq),
|
||||
Windows::KeyScrollLock => Ok(Self::KeyScrollLock),
|
||||
Windows::KeyPause => Ok(Self::KeyPause),
|
||||
Windows::KeyInsert => Ok(Self::KeyInsert),
|
||||
Windows::KeyHome => Ok(Self::KeyHome),
|
||||
Windows::KeyPageUp => Ok(Self::KeyPageup),
|
||||
Windows::KeyDeleteForward => Ok(Self::KeyDelete),
|
||||
Windows::KeyEnd => Ok(Self::KeyEnd),
|
||||
Windows::KeyPageDown => Ok(Self::KeyPagedown),
|
||||
Windows::KeyRight => Ok(Self::KeyRight),
|
||||
Windows::KeyLeft => Ok(Self::KeyLeft),
|
||||
Windows::KeyDown => Ok(Self::KeyDown),
|
||||
Windows::KeyUp => Ok(Self::KeyUp),
|
||||
Windows::KeypadNumLock => Ok(Self::KeyNumlock),
|
||||
Windows::KeypadSlash => Ok(Self::KeyKpslash),
|
||||
Windows::KeypadStar => Ok(Self::KeyKpAsterisk),
|
||||
Windows::KeypadDash => Ok(Self::KeyKpMinus),
|
||||
Windows::KeypadPlus => Ok(Self::KeyKpplus),
|
||||
Windows::KeypadEnter => Ok(Self::KeyKpEnter),
|
||||
Windows::Keypad1End => Ok(Self::KeyKp1),
|
||||
Windows::Keypad2DownArrow => Ok(Self::KeyKp2),
|
||||
Windows::Keypad3PageDn => Ok(Self::KeyKp3),
|
||||
Windows::Keypad4LeftArrow => Ok(Self::KeyKp4),
|
||||
Windows::Keypad5 => Ok(Self::KeyKp5),
|
||||
Windows::Keypad6RightArrow => Ok(Self::KeyKp6),
|
||||
Windows::Keypad7Home => Ok(Self::KeyKp7),
|
||||
Windows::Keypad8UpArrow => Ok(Self::KeyKp8),
|
||||
Windows::Keypad9PageUp => Ok(Self::KeyKp9),
|
||||
Windows::Keypad0Insert => Ok(Self::KeyKp0),
|
||||
Windows::KeypadDot => Ok(Self::KeyKpDot),
|
||||
Windows::KeyNonUSSlashBar => Ok(Self::Key102nd),
|
||||
Windows::KeyApplication => Ok(Self::KeyMenu),
|
||||
Windows::KeypadEquals => Ok(Self::KeyKpequal),
|
||||
Windows::KeyF13 => Ok(Self::KeyF13),
|
||||
Windows::KeyF14 => Ok(Self::KeyF14),
|
||||
Windows::KeyF15 => Ok(Self::KeyF15),
|
||||
Windows::KeyF16 => Ok(Self::KeyF16),
|
||||
Windows::KeyF17 => Ok(Self::KeyF17),
|
||||
Windows::KeyF18 => Ok(Self::KeyF18),
|
||||
Windows::KeyF19 => Ok(Self::KeyF19),
|
||||
Windows::KeyF20 => Ok(Self::KeyF20),
|
||||
Windows::KeyF21 => Ok(Self::KeyF21),
|
||||
Windows::KeyF22 => Ok(Self::KeyF22),
|
||||
Windows::KeyF23 => Ok(Self::KeyF23),
|
||||
Windows::KeyF24 => Ok(Self::KeyF24),
|
||||
Windows::KeypadComma => Ok(Self::KeyKpcomma),
|
||||
Windows::KeyInternational1 => Ok(Self::KeyRo),
|
||||
Windows::KeyInternational2 => Ok(Self::KeyKatakanahiragana),
|
||||
Windows::KeyInternational3 => Ok(Self::KeyYen),
|
||||
Windows::KeyInternational4 => Ok(Self::KeyHenkan),
|
||||
Windows::KeyInternational5 => Ok(Self::KeyMuhenkan),
|
||||
Windows::KeyLANG1 => Ok(Self::KeyHanguel),
|
||||
Windows::KeyLANG2 => Ok(Self::KeyHanja),
|
||||
Windows::KeyLANG3 => Ok(Self::KeyKatakana),
|
||||
Windows::KeyLANG4 => Ok(Self::KeyHiragana),
|
||||
Windows::KeyLeftCtrl => Ok(Self::KeyLeftCtrl),
|
||||
Windows::KeyLeftShift => Ok(Self::KeyLeftShift),
|
||||
Windows::KeyLeftAlt => Ok(Self::KeyLeftAlt),
|
||||
Windows::KeyLeftGUI => Ok(Self::KeyLeftMeta),
|
||||
Windows::KeyRightCtrl => Ok(Self::KeyRightCtrl),
|
||||
Windows::KeyRightShift => Ok(Self::KeyRightShift),
|
||||
Windows::KeyFakeRightShift => Ok(Self::KeyRightShift),
|
||||
Windows::KeyRightAlt => Ok(Self::KeyRightalt),
|
||||
Windows::KeyRightGUI => Ok(Self::KeyRightmeta),
|
||||
Windows::KeyScanNextTrack => Ok(Self::KeyNextsong),
|
||||
Windows::KeyScanPreviousTrack => Ok(Self::KeyPrevioussong),
|
||||
Windows::KeyStop => Ok(Self::KeyStopcd),
|
||||
Windows::KeyPlayPause => Ok(Self::KeyPlaypause),
|
||||
Windows::KeyMute => Ok(Self::KeyMute),
|
||||
Windows::KeyVolumeUp => Ok(Self::KeyVolumeUp),
|
||||
Windows::KeyVolumeDown => Ok(Self::KeyVolumeDown),
|
||||
Windows::ALConsumerControlConfiguration => Err(()),
|
||||
Windows::ALEmailReader => Ok(Self::KeyMail),
|
||||
Windows::ALCalculator => Ok(Self::KeyCalc),
|
||||
Windows::ALLocalMachineBrowser => Ok(Self::KeyFile),
|
||||
Windows::ACSearch => Ok(Self::KeyWww),
|
||||
Windows::ACHome => Ok(Self::KeyHomepage),
|
||||
Windows::ACBack => Ok(Self::KeyBack),
|
||||
Windows::ACForward => Ok(Self::KeyForward),
|
||||
Windows::ACStop => Ok(Self::KeyStop),
|
||||
Windows::ACRefresh => Ok(Self::KeyRefresh),
|
||||
Windows::ACBookmarks => Ok(Self::KeyBookmarks),
|
||||
}
|
||||
}
|
||||
}
|
||||
18
lan-mouse-cli/Cargo.toml
Normal file
18
lan-mouse-cli/Cargo.toml
Normal file
@@ -0,0 +1,18 @@
|
||||
[package]
|
||||
name = "lan-mouse-cli"
|
||||
description = "CLI Frontend for lan-mouse"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
license = "GPL-3.0-or-later"
|
||||
repository = "https://github.com/feschber/lan-mouse"
|
||||
|
||||
[dependencies]
|
||||
futures = "0.3.30"
|
||||
lan-mouse-ipc = { path = "../lan-mouse-ipc", version = "0.1.0" }
|
||||
tokio = { version = "1.32.0", features = [
|
||||
"io-util",
|
||||
"io-std",
|
||||
"macros",
|
||||
"net",
|
||||
"rt",
|
||||
] }
|
||||
153
lan-mouse-cli/src/command.rs
Normal file
153
lan-mouse-cli/src/command.rs
Normal file
@@ -0,0 +1,153 @@
|
||||
use std::{
|
||||
fmt::Display,
|
||||
str::{FromStr, SplitWhitespace},
|
||||
};
|
||||
|
||||
use lan_mouse_ipc::{ClientHandle, Position};
|
||||
|
||||
pub(super) enum CommandType {
|
||||
NoCommand,
|
||||
Help,
|
||||
Connect,
|
||||
Disconnect,
|
||||
Activate,
|
||||
Deactivate,
|
||||
List,
|
||||
SetHost,
|
||||
SetPort,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(super) struct InvalidCommand {
|
||||
cmd: String,
|
||||
}
|
||||
|
||||
impl Display for InvalidCommand {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "invalid command: \"{}\"", self.cmd)
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for CommandType {
|
||||
type Err = InvalidCommand;
|
||||
|
||||
fn from_str(s: &str) -> std::prelude::v1::Result<Self, Self::Err> {
|
||||
match s {
|
||||
"connect" => Ok(Self::Connect),
|
||||
"disconnect" => Ok(Self::Disconnect),
|
||||
"activate" => Ok(Self::Activate),
|
||||
"deactivate" => Ok(Self::Deactivate),
|
||||
"list" => Ok(Self::List),
|
||||
"set-host" => Ok(Self::SetHost),
|
||||
"set-port" => Ok(Self::SetPort),
|
||||
"help" => Ok(Self::Help),
|
||||
_ => Err(InvalidCommand { cmd: s.to_string() }),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(super) enum Command {
|
||||
None,
|
||||
Help,
|
||||
Connect(Position, String, Option<u16>),
|
||||
Disconnect(ClientHandle),
|
||||
Activate(ClientHandle),
|
||||
Deactivate(ClientHandle),
|
||||
List,
|
||||
SetHost(ClientHandle, String),
|
||||
SetPort(ClientHandle, Option<u16>),
|
||||
}
|
||||
|
||||
impl CommandType {
|
||||
pub(super) fn usage(&self) -> &'static str {
|
||||
match self {
|
||||
CommandType::Help => "help",
|
||||
CommandType::NoCommand => "",
|
||||
CommandType::Connect => "connect left|right|top|bottom <host> [<port>]",
|
||||
CommandType::Disconnect => "disconnect <id>",
|
||||
CommandType::Activate => "activate <id>",
|
||||
CommandType::Deactivate => "deactivate <id>",
|
||||
CommandType::List => "list",
|
||||
CommandType::SetHost => "set-host <id> <host>",
|
||||
CommandType::SetPort => "set-port <id> <host>",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) enum CommandParseError {
|
||||
Usage(CommandType),
|
||||
Invalid(InvalidCommand),
|
||||
}
|
||||
|
||||
impl Display for CommandParseError {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Self::Usage(cmd) => write!(f, "usage: {}", cmd.usage()),
|
||||
Self::Invalid(cmd) => write!(f, "{}", cmd),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for Command {
|
||||
type Err = CommandParseError;
|
||||
|
||||
fn from_str(cmd: &str) -> Result<Self, Self::Err> {
|
||||
let mut args = cmd.split_whitespace();
|
||||
let cmd_type: CommandType = match args.next() {
|
||||
Some(c) => c.parse().map_err(CommandParseError::Invalid),
|
||||
None => Ok(CommandType::NoCommand),
|
||||
}?;
|
||||
match cmd_type {
|
||||
CommandType::Help => Ok(Command::Help),
|
||||
CommandType::NoCommand => Ok(Command::None),
|
||||
CommandType::Connect => parse_connect_cmd(args),
|
||||
CommandType::Disconnect => parse_disconnect_cmd(args),
|
||||
CommandType::Activate => parse_activate_cmd(args),
|
||||
CommandType::Deactivate => parse_deactivate_cmd(args),
|
||||
CommandType::List => Ok(Command::List),
|
||||
CommandType::SetHost => parse_set_host(args),
|
||||
CommandType::SetPort => parse_set_port(args),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_connect_cmd(mut args: SplitWhitespace<'_>) -> Result<Command, CommandParseError> {
|
||||
const USAGE: CommandParseError = CommandParseError::Usage(CommandType::Connect);
|
||||
let pos = args.next().ok_or(USAGE)?.parse().map_err(|_| USAGE)?;
|
||||
let host = args.next().ok_or(USAGE)?.to_string();
|
||||
let port = args.next().and_then(|p| p.parse().ok());
|
||||
Ok(Command::Connect(pos, host, port))
|
||||
}
|
||||
|
||||
fn parse_disconnect_cmd(mut args: SplitWhitespace<'_>) -> Result<Command, CommandParseError> {
|
||||
const USAGE: CommandParseError = CommandParseError::Usage(CommandType::Disconnect);
|
||||
let id = args.next().ok_or(USAGE)?.parse().map_err(|_| USAGE)?;
|
||||
Ok(Command::Disconnect(id))
|
||||
}
|
||||
|
||||
fn parse_activate_cmd(mut args: SplitWhitespace<'_>) -> Result<Command, CommandParseError> {
|
||||
const USAGE: CommandParseError = CommandParseError::Usage(CommandType::Activate);
|
||||
let id = args.next().ok_or(USAGE)?.parse().map_err(|_| USAGE)?;
|
||||
Ok(Command::Activate(id))
|
||||
}
|
||||
|
||||
fn parse_deactivate_cmd(mut args: SplitWhitespace<'_>) -> Result<Command, CommandParseError> {
|
||||
const USAGE: CommandParseError = CommandParseError::Usage(CommandType::Deactivate);
|
||||
let id = args.next().ok_or(USAGE)?.parse().map_err(|_| USAGE)?;
|
||||
Ok(Command::Deactivate(id))
|
||||
}
|
||||
|
||||
fn parse_set_host(mut args: SplitWhitespace<'_>) -> Result<Command, CommandParseError> {
|
||||
const USAGE: CommandParseError = CommandParseError::Usage(CommandType::SetHost);
|
||||
let id = args.next().ok_or(USAGE)?.parse().map_err(|_| USAGE)?;
|
||||
let host = args.next().ok_or(USAGE)?.parse().map_err(|_| USAGE)?;
|
||||
Ok(Command::SetHost(id, host))
|
||||
}
|
||||
|
||||
fn parse_set_port(mut args: SplitWhitespace<'_>) -> Result<Command, CommandParseError> {
|
||||
const USAGE: CommandParseError = CommandParseError::Usage(CommandType::SetPort);
|
||||
let id = args.next().ok_or(USAGE)?.parse().map_err(|_| USAGE)?;
|
||||
let port = args.next().and_then(|p| p.parse().ok());
|
||||
Ok(Command::SetPort(id, port))
|
||||
}
|
||||
309
lan-mouse-cli/src/lib.rs
Normal file
309
lan-mouse-cli/src/lib.rs
Normal file
@@ -0,0 +1,309 @@
|
||||
use futures::StreamExt;
|
||||
use tokio::{
|
||||
io::{AsyncBufReadExt, BufReader},
|
||||
task::LocalSet,
|
||||
};
|
||||
|
||||
use std::io::{self, Write};
|
||||
|
||||
use self::command::{Command, CommandType};
|
||||
|
||||
use lan_mouse_ipc::{
|
||||
AsyncFrontendEventReader, AsyncFrontendRequestWriter, ClientConfig, ClientHandle, ClientState,
|
||||
FrontendEvent, FrontendRequest, IpcError, DEFAULT_PORT,
|
||||
};
|
||||
|
||||
mod command;
|
||||
|
||||
pub fn run() -> Result<(), IpcError> {
|
||||
let runtime = tokio::runtime::Builder::new_current_thread()
|
||||
.enable_io()
|
||||
.enable_time()
|
||||
.build()?;
|
||||
runtime.block_on(LocalSet::new().run_until(async move {
|
||||
let (rx, tx) = lan_mouse_ipc::connect_async().await?;
|
||||
let mut cli = Cli::new(rx, tx);
|
||||
cli.run().await
|
||||
}))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
struct Cli {
|
||||
clients: Vec<(ClientHandle, ClientConfig, ClientState)>,
|
||||
changed: Option<ClientHandle>,
|
||||
rx: AsyncFrontendEventReader,
|
||||
tx: AsyncFrontendRequestWriter,
|
||||
}
|
||||
|
||||
impl Cli {
|
||||
fn new(rx: AsyncFrontendEventReader, tx: AsyncFrontendRequestWriter) -> Cli {
|
||||
Self {
|
||||
clients: vec![],
|
||||
changed: None,
|
||||
rx,
|
||||
tx,
|
||||
}
|
||||
}
|
||||
|
||||
async fn run(&mut self) -> Result<(), IpcError> {
|
||||
let stdin = tokio::io::stdin();
|
||||
let stdin = BufReader::new(stdin);
|
||||
let mut stdin = stdin.lines();
|
||||
|
||||
/* initial state sync */
|
||||
self.clients = loop {
|
||||
match self.rx.next().await {
|
||||
Some(Ok(e)) => {
|
||||
if let FrontendEvent::Enumerate(clients) = e {
|
||||
break clients;
|
||||
}
|
||||
}
|
||||
Some(Err(e)) => return Err(e),
|
||||
None => return Ok(()),
|
||||
}
|
||||
};
|
||||
|
||||
loop {
|
||||
prompt()?;
|
||||
tokio::select! {
|
||||
line = stdin.next_line() => {
|
||||
let Some(line) = line? else {
|
||||
break Ok(());
|
||||
};
|
||||
let cmd: Command = match line.parse() {
|
||||
Ok(cmd) => cmd,
|
||||
Err(e) => {
|
||||
eprintln!("{e}");
|
||||
continue;
|
||||
}
|
||||
};
|
||||
self.execute(cmd).await?;
|
||||
}
|
||||
event = self.rx.next() => {
|
||||
if let Some(event) = event {
|
||||
self.handle_event(event?);
|
||||
} else {
|
||||
break Ok(());
|
||||
}
|
||||
}
|
||||
}
|
||||
if let Some(handle) = self.changed.take() {
|
||||
self.update_client(handle).await?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn update_client(&mut self, handle: ClientHandle) -> Result<(), IpcError> {
|
||||
self.tx.request(FrontendRequest::GetState(handle)).await?;
|
||||
while let Some(Ok(event)) = self.rx.next().await {
|
||||
self.handle_event(event.clone());
|
||||
if let FrontendEvent::State(_, _, _) | FrontendEvent::NoSuchClient(_) = event {
|
||||
break;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn execute(&mut self, cmd: Command) -> Result<(), IpcError> {
|
||||
match cmd {
|
||||
Command::None => {}
|
||||
Command::Connect(pos, host, port) => {
|
||||
let request = FrontendRequest::Create;
|
||||
self.tx.request(request).await?;
|
||||
let handle = loop {
|
||||
if let Some(Ok(event)) = self.rx.next().await {
|
||||
match event {
|
||||
FrontendEvent::Created(h, c, s) => {
|
||||
self.clients.push((h, c, s));
|
||||
break h;
|
||||
}
|
||||
_ => {
|
||||
self.handle_event(event);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
for request in [
|
||||
FrontendRequest::UpdateHostname(handle, Some(host.clone())),
|
||||
FrontendRequest::UpdatePort(handle, port.unwrap_or(DEFAULT_PORT)),
|
||||
FrontendRequest::UpdatePosition(handle, pos),
|
||||
] {
|
||||
self.tx.request(request).await?;
|
||||
}
|
||||
self.update_client(handle).await?;
|
||||
}
|
||||
Command::Disconnect(id) => {
|
||||
self.tx.request(FrontendRequest::Delete(id)).await?;
|
||||
loop {
|
||||
if let Some(Ok(event)) = self.rx.next().await {
|
||||
self.handle_event(event.clone());
|
||||
if let FrontendEvent::Deleted(_) = event {
|
||||
self.handle_event(event);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Command::Activate(id) => {
|
||||
self.tx.request(FrontendRequest::Activate(id, true)).await?;
|
||||
self.update_client(id).await?;
|
||||
}
|
||||
Command::Deactivate(id) => {
|
||||
self.tx
|
||||
.request(FrontendRequest::Activate(id, false))
|
||||
.await?;
|
||||
self.update_client(id).await?;
|
||||
}
|
||||
Command::List => {
|
||||
self.tx.request(FrontendRequest::Enumerate()).await?;
|
||||
while let Some(e) = self.rx.next().await {
|
||||
let event = e?;
|
||||
self.handle_event(event.clone());
|
||||
if let FrontendEvent::Enumerate(_) = event {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
Command::SetHost(handle, host) => {
|
||||
let request = FrontendRequest::UpdateHostname(handle, Some(host.clone()));
|
||||
self.tx.request(request).await?;
|
||||
self.update_client(handle).await?;
|
||||
}
|
||||
Command::SetPort(handle, port) => {
|
||||
let request = FrontendRequest::UpdatePort(handle, port.unwrap_or(DEFAULT_PORT));
|
||||
self.tx.request(request).await?;
|
||||
self.update_client(handle).await?;
|
||||
}
|
||||
Command::Help => {
|
||||
for cmd_type in [
|
||||
CommandType::List,
|
||||
CommandType::Connect,
|
||||
CommandType::Disconnect,
|
||||
CommandType::Activate,
|
||||
CommandType::Deactivate,
|
||||
CommandType::SetHost,
|
||||
CommandType::SetPort,
|
||||
] {
|
||||
eprintln!("{}", cmd_type.usage());
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn find_mut(
|
||||
&mut self,
|
||||
handle: ClientHandle,
|
||||
) -> Option<&mut (ClientHandle, ClientConfig, ClientState)> {
|
||||
self.clients.iter_mut().find(|(h, _, _)| *h == handle)
|
||||
}
|
||||
|
||||
fn remove(
|
||||
&mut self,
|
||||
handle: ClientHandle,
|
||||
) -> Option<(ClientHandle, ClientConfig, ClientState)> {
|
||||
let idx = self.clients.iter().position(|(h, _, _)| *h == handle);
|
||||
idx.map(|i| self.clients.swap_remove(i))
|
||||
}
|
||||
|
||||
fn handle_event(&mut self, event: FrontendEvent) {
|
||||
match event {
|
||||
FrontendEvent::Changed(h) => self.changed = Some(h),
|
||||
FrontendEvent::Created(h, c, s) => {
|
||||
eprint!("client added ({h}): ");
|
||||
print_config(&c);
|
||||
eprint!(" ");
|
||||
print_state(&s);
|
||||
eprintln!();
|
||||
self.clients.push((h, c, s));
|
||||
}
|
||||
FrontendEvent::NoSuchClient(h) => {
|
||||
eprintln!("no such client: {h}");
|
||||
}
|
||||
FrontendEvent::State(h, c, s) => {
|
||||
if let Some((_, config, state)) = self.find_mut(h) {
|
||||
let old_host = config.hostname.clone().unwrap_or("\"\"".into());
|
||||
let new_host = c.hostname.clone().unwrap_or("\"\"".into());
|
||||
if old_host != new_host {
|
||||
eprintln!(
|
||||
"client {h}: hostname updated ({} -> {})",
|
||||
old_host, new_host
|
||||
);
|
||||
}
|
||||
if config.port != c.port {
|
||||
eprintln!("client {h} changed port: {} -> {}", config.port, c.port);
|
||||
}
|
||||
if config.fix_ips != c.fix_ips {
|
||||
eprintln!("client {h} ips updated: {:?}", c.fix_ips)
|
||||
}
|
||||
*config = c;
|
||||
if state.active ^ s.active {
|
||||
eprintln!(
|
||||
"client {h} {}",
|
||||
if s.active { "activated" } else { "deactivated" }
|
||||
);
|
||||
}
|
||||
*state = s;
|
||||
}
|
||||
}
|
||||
FrontendEvent::Deleted(h) => {
|
||||
if let Some((h, c, _)) = self.remove(h) {
|
||||
eprint!("client {h} removed (");
|
||||
print_config(&c);
|
||||
eprintln!(")");
|
||||
}
|
||||
}
|
||||
FrontendEvent::PortChanged(p, e) => {
|
||||
if let Some(e) = e {
|
||||
eprintln!("failed to change port: {e}");
|
||||
} else {
|
||||
eprintln!("changed port to {p}");
|
||||
}
|
||||
}
|
||||
FrontendEvent::Enumerate(clients) => {
|
||||
self.clients = clients;
|
||||
self.print_clients();
|
||||
}
|
||||
FrontendEvent::Error(e) => {
|
||||
eprintln!("ERROR: {e}");
|
||||
}
|
||||
FrontendEvent::CaptureStatus(s) => {
|
||||
eprintln!("capture status: {s:?}")
|
||||
}
|
||||
FrontendEvent::EmulationStatus(s) => {
|
||||
eprintln!("emulation status: {s:?}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn print_clients(&mut self) {
|
||||
for (h, c, s) in self.clients.iter() {
|
||||
eprint!("client {h}: ");
|
||||
print_config(c);
|
||||
eprint!(" ");
|
||||
print_state(s);
|
||||
eprintln!();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn prompt() -> io::Result<()> {
|
||||
eprint!("lan-mouse > ");
|
||||
std::io::stderr().flush()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn print_config(c: &ClientConfig) {
|
||||
eprint!(
|
||||
"{}:{} ({}), ips: {:?}",
|
||||
c.hostname.clone().unwrap_or("(no hostname)".into()),
|
||||
c.port,
|
||||
c.pos,
|
||||
c.fix_ips
|
||||
);
|
||||
}
|
||||
|
||||
fn print_state(s: &ClientState) {
|
||||
eprint!("active: {}, dns: {:?}", s.active, s.ips);
|
||||
}
|
||||
18
lan-mouse-gtk/Cargo.toml
Normal file
18
lan-mouse-gtk/Cargo.toml
Normal file
@@ -0,0 +1,18 @@
|
||||
[package]
|
||||
name = "lan-mouse-gtk"
|
||||
description = "GTK4 / Libadwaita Frontend for lan-mouse"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
license = "GPL-3.0-or-later"
|
||||
repository = "https://github.com/feschber/lan-mouse"
|
||||
|
||||
[dependencies]
|
||||
gtk = { package = "gtk4", version = "0.9.0", features = ["v4_2"] }
|
||||
adw = { package = "libadwaita", version = "0.7.0", features = ["v1_1"] }
|
||||
async-channel = { version = "2.1.1" }
|
||||
hostname = "0.4.0"
|
||||
log = "0.4.20"
|
||||
lan-mouse-ipc = { path = "../lan-mouse-ipc", version = "0.1.0" }
|
||||
|
||||
[build-dependencies]
|
||||
glib-build-tools = { version = "0.20.0" }
|
||||
8
lan-mouse-gtk/build.rs
Normal file
8
lan-mouse-gtk/build.rs
Normal file
@@ -0,0 +1,8 @@
|
||||
fn main() {
|
||||
// composite_templates
|
||||
glib_build_tools::compile_resources(
|
||||
&["resources"],
|
||||
"resources/resources.gresource.xml",
|
||||
"lan-mouse.gresource",
|
||||
);
|
||||
}
|
||||
@@ -11,6 +11,20 @@
|
||||
<property name="tooltip-text" translatable="yes">enable</property>
|
||||
</object>
|
||||
</child>
|
||||
<child type="suffix">
|
||||
<object class="GtkButton" id="dns_button">
|
||||
<signal name="clicked" handler="handle_request_dns" swapped="true"/>
|
||||
<!--<property name="icon-name">network-wired-disconnected-symbolic</property>-->
|
||||
<property name="icon-name">network-wired-symbolic</property>
|
||||
<property name="valign">center</property>
|
||||
<property name="halign">end</property>
|
||||
<property name="tooltip-text" translatable="yes">resolve host</property>
|
||||
</object>
|
||||
</child>
|
||||
<child type="suffix">
|
||||
<object class="GtkSpinner" id="dns_loading_indicator">
|
||||
</object>
|
||||
</child>
|
||||
<!-- host -->
|
||||
<child>
|
||||
<object class="AdwActionRow">
|
||||
@@ -30,6 +44,7 @@
|
||||
<child>
|
||||
<object class="GtkEntry" id="port">
|
||||
<!-- <property name="title" translatable="yes">port</property> -->
|
||||
<property name="max-width-chars">5</property>
|
||||
<property name="input_purpose">GTK_INPUT_PURPOSE_NUMBER</property>
|
||||
<property name="xalign">0.5</property>
|
||||
<property name="valign">center</property>
|
||||
@@ -66,6 +81,7 @@
|
||||
<property name="valign">center</property>
|
||||
<property name="halign">center</property>
|
||||
<property name="name">delete-button</property>
|
||||
<style><class name="error"/></style>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
|
Before Width: | Height: | Size: 7.6 KiB After Width: | Height: | Size: 7.6 KiB |
@@ -3,10 +3,8 @@
|
||||
<gresource prefix="/de/feschber/LanMouse">
|
||||
<file compressed="true" preprocess="xml-stripblanks">window.ui</file>
|
||||
<file compressed="true" preprocess="xml-stripblanks">client_row.ui</file>
|
||||
<file compressed="true">style.css</file>
|
||||
<file compressed="true">style-dark.css</file>
|
||||
</gresource>
|
||||
<gresource prefix="/de/feschber/LanMouse/icons">
|
||||
<file compressed="true" preprocess="xml-stripblanks">mouse-icon.svg</file>
|
||||
<file compressed="true" preprocess="xml-stripblanks">de.feschber.LanMouse.svg</file>
|
||||
</gresource>
|
||||
</gresources>
|
||||
227
lan-mouse-gtk/resources/window.ui
Normal file
227
lan-mouse-gtk/resources/window.ui
Normal file
@@ -0,0 +1,227 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<interface>
|
||||
<requires lib="gtk" version="4.0"/>
|
||||
<requires lib="libadwaita" version="1.0"/>
|
||||
<menu id="main-menu">
|
||||
<item>
|
||||
<attribute name="label" translatable="yes">_Close window</attribute>
|
||||
<attribute name="action">window.close</attribute>
|
||||
</item>
|
||||
</menu>
|
||||
<template class="LanMouseWindow" parent="AdwApplicationWindow">
|
||||
<property name="width-request">600</property>
|
||||
<property name="height-request">700</property>
|
||||
<property name="title" translatable="yes">Lan Mouse</property>
|
||||
<property name="show-menubar">True</property>
|
||||
<property name="content">
|
||||
<object class="GtkBox">
|
||||
<property name="orientation">vertical</property>
|
||||
<child type="top">
|
||||
<object class="AdwHeaderBar">
|
||||
<child type ="end">
|
||||
<object class="GtkMenuButton">
|
||||
<property name="icon-name">open-menu-symbolic</property>
|
||||
<property name="menu-model">main-menu</property>
|
||||
</object>
|
||||
</child>
|
||||
<style>
|
||||
<class name="flat"/>
|
||||
</style>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="AdwToastOverlay" id="toast_overlay">
|
||||
<child>
|
||||
<object class="AdwStatusPage">
|
||||
<property name="title" translatable="yes">Lan Mouse</property>
|
||||
<property name="description" translatable="yes">easily use your mouse and keyboard on multiple computers</property>
|
||||
<property name="icon-name">de.feschber.LanMouse</property>
|
||||
<property name="child">
|
||||
<object class="AdwClamp">
|
||||
<property name="maximum-size">600</property>
|
||||
<property name="tightening-threshold">0</property>
|
||||
<property name="child">
|
||||
<object class="GtkBox">
|
||||
<property name="orientation">vertical</property>
|
||||
<property name="spacing">12</property>
|
||||
<child>
|
||||
<object class="AdwPreferencesGroup" id="capture_emulation_group">
|
||||
<property name="title" translatable="yes">Capture / Emulation Status</property>
|
||||
<child>
|
||||
<object class="AdwActionRow" id="capture_status_row">
|
||||
<property name="title">input capture is disabled</property>
|
||||
<property name="subtitle">required for outgoing and incoming connections</property>
|
||||
<property name="icon-name">dialog-warning-symbolic</property>
|
||||
<child>
|
||||
<object class="GtkButton" id="input_capture_button">
|
||||
<property name="child">
|
||||
<object class="AdwButtonContent">
|
||||
<property name="icon-name">object-rotate-right-symbolic</property>
|
||||
<property name="label" translatable="yes">Reenable</property>
|
||||
</object>
|
||||
</property>
|
||||
<signal name="clicked" handler="handle_capture" swapped="true"/>
|
||||
<property name="valign">center</property>
|
||||
<style>
|
||||
<class name="circular"/>
|
||||
<class name="flat"/>
|
||||
</style>
|
||||
</object>
|
||||
</child>
|
||||
<style>
|
||||
<class name="warning"/>
|
||||
</style>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="AdwActionRow" id="emulation_status_row">
|
||||
<property name="title">input emulation is disabled</property>
|
||||
<property name="subtitle">required for incoming connections</property>
|
||||
<property name="icon-name">dialog-warning-symbolic</property>
|
||||
<child>
|
||||
<object class="GtkButton" id="input_emulation_button">
|
||||
<property name="child">
|
||||
<object class="AdwButtonContent">
|
||||
<property name="icon-name">object-rotate-right-symbolic</property>
|
||||
<property name="label" translatable="yes">Reenable</property>
|
||||
</object>
|
||||
</property>
|
||||
<property name="valign">center</property>
|
||||
<signal name="clicked" handler="handle_emulation" swapped="true"/>
|
||||
<style>
|
||||
<class name="circular"/>
|
||||
<class name="flat"/>
|
||||
</style>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
</child>
|
||||
<style>
|
||||
<class name="warning"/>
|
||||
</style>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="AdwPreferencesGroup">
|
||||
<property name="title" translatable="yes">General</property>
|
||||
<!--
|
||||
<child>
|
||||
<object class="AdwActionRow">
|
||||
<property name="title" translatable="yes">enable</property>
|
||||
<child type="suffix">
|
||||
<object class="GtkSwitch">
|
||||
<property name="valign">center</property>
|
||||
<property name="tooltip-text" translatable="yes">enable</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
-->
|
||||
<child>
|
||||
<object class="AdwActionRow">
|
||||
<property name="title">port</property>
|
||||
<child>
|
||||
<object class="GtkEntry" id="port_entry">
|
||||
<property name="max-width-chars">5</property>
|
||||
<signal name="activate" handler="handle_port_edit_apply" swapped="true"/>
|
||||
<signal name="changed" handler="handle_port_changed" swapped="true"/>
|
||||
<!-- <signal name="delete-text" handler="handle_port_changed" swapped="true"/> -->
|
||||
<!-- <property name="title" translatable="yes">port</property> -->
|
||||
<property name="placeholder-text">4242</property>
|
||||
<property name="width-chars">5</property>
|
||||
<property name="xalign">0.5</property>
|
||||
<property name="valign">center</property>
|
||||
<!-- <property name="show-apply-button">True</property> -->
|
||||
<property name="input-purpose">GTK_INPUT_PURPOSE_DIGITS</property>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkButton" id="port_edit_apply">
|
||||
<signal name="clicked" handler="handle_port_edit_apply" swapped="true"/>
|
||||
<property name="icon-name">object-select-symbolic</property>
|
||||
<property name="valign">center</property>
|
||||
<property name="visible">false</property>
|
||||
<property name="name">port-edit-apply</property>
|
||||
<style><class name="success"/></style>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkButton" id="port_edit_cancel">
|
||||
<signal name="clicked" handler="handle_port_edit_cancel" swapped="true"/>
|
||||
<property name="icon-name">process-stop-symbolic</property>
|
||||
<property name="valign">center</property>
|
||||
<property name="visible">false</property>
|
||||
<property name="name">port-edit-cancel</property>
|
||||
<style><class name="error"/></style>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="AdwActionRow">
|
||||
<property name="title">hostname</property>
|
||||
<child>
|
||||
<object class="GtkLabel" id="hostname_label">
|
||||
<property name="label"><span font_style="italic" font_weight="light" foreground="darkgrey">could not determine hostname</span></property>
|
||||
<property name="use-markup">true</property>
|
||||
<property name="valign">center</property>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkButton" id="copy-hostname-button">
|
||||
<property name="icon-name">edit-copy-symbolic</property>
|
||||
<property name="valign">center</property>
|
||||
<signal name="clicked" handler="handle_copy_hostname" swapped="true"/>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="AdwPreferencesGroup">
|
||||
<property name="title" translatable="yes">Connections</property>
|
||||
<property name="header-suffix">
|
||||
<object class="GtkButton">
|
||||
<signal name="clicked" handler="handle_add_client_pressed" swapped="true"/>
|
||||
<property name="child">
|
||||
<object class="AdwButtonContent">
|
||||
<property name="icon-name">list-add-symbolic</property>
|
||||
<property name="label" translatable="yes">Add</property>
|
||||
</object>
|
||||
</property>
|
||||
<style>
|
||||
<class name="flat"/>
|
||||
</style>
|
||||
</object>
|
||||
</property>
|
||||
<child>
|
||||
<object class="GtkListBox" id="client_list">
|
||||
<property name="selection-mode">none</property>
|
||||
<child type="placeholder">
|
||||
<object class="AdwActionRow" id="client_placeholder">
|
||||
<property name="title">No connections!</property>
|
||||
<property name="subtitle">add a new client via the + button</property>
|
||||
</object>
|
||||
</child>
|
||||
<style>
|
||||
<class name="boxed-list" />
|
||||
</style>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</property>
|
||||
</object>
|
||||
</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</property>
|
||||
</template>
|
||||
</interface>
|
||||
46
lan-mouse-gtk/src/client_object.rs
Normal file
46
lan-mouse-gtk/src/client_object.rs
Normal file
@@ -0,0 +1,46 @@
|
||||
mod imp;
|
||||
|
||||
use adw::subclass::prelude::*;
|
||||
use gtk::glib::{self, Object};
|
||||
|
||||
use lan_mouse_ipc::{ClientConfig, ClientHandle, ClientState};
|
||||
|
||||
glib::wrapper! {
|
||||
pub struct ClientObject(ObjectSubclass<imp::ClientObject>);
|
||||
}
|
||||
|
||||
impl ClientObject {
|
||||
pub fn new(handle: ClientHandle, client: ClientConfig, state: ClientState) -> Self {
|
||||
Object::builder()
|
||||
.property("handle", handle)
|
||||
.property("hostname", client.hostname)
|
||||
.property("port", client.port as u32)
|
||||
.property("position", client.pos.to_string())
|
||||
.property("active", state.active)
|
||||
.property(
|
||||
"ips",
|
||||
state
|
||||
.ips
|
||||
.iter()
|
||||
.map(|ip| ip.to_string())
|
||||
.collect::<Vec<_>>(),
|
||||
)
|
||||
.property("resolving", state.resolving)
|
||||
.build()
|
||||
}
|
||||
|
||||
pub fn get_data(&self) -> ClientData {
|
||||
self.imp().data.borrow().clone()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default, Clone)]
|
||||
pub struct ClientData {
|
||||
pub handle: ClientHandle,
|
||||
pub hostname: Option<String>,
|
||||
pub port: u32,
|
||||
pub active: bool,
|
||||
pub position: String,
|
||||
pub resolving: bool,
|
||||
pub ips: Vec<String>,
|
||||
}
|
||||
@@ -5,7 +5,7 @@ use gtk::glib;
|
||||
use gtk::prelude::*;
|
||||
use gtk::subclass::prelude::*;
|
||||
|
||||
use crate::client::ClientHandle;
|
||||
use lan_mouse_ipc::ClientHandle;
|
||||
|
||||
use super::ClientData;
|
||||
|
||||
@@ -17,6 +17,8 @@ pub struct ClientObject {
|
||||
#[property(name = "port", get, set, type = u32, member = port, maximum = u16::MAX as u32)]
|
||||
#[property(name = "active", get, set, type = bool, member = active)]
|
||||
#[property(name = "position", get, set, type = String, member = position)]
|
||||
#[property(name = "resolving", get, set, type = bool, member = resolving)]
|
||||
#[property(name = "ips", get, set, type = Vec<String>, member = ips)]
|
||||
pub data: RefCell<ClientData>,
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ use adw::prelude::*;
|
||||
use adw::subclass::prelude::*;
|
||||
use gtk::glib::{self, Object};
|
||||
|
||||
use crate::config::DEFAULT_PORT;
|
||||
use lan_mouse_ipc::DEFAULT_PORT;
|
||||
|
||||
use super::ClientObject;
|
||||
|
||||
@@ -28,6 +28,12 @@ impl ClientRow {
|
||||
.sync_create()
|
||||
.build();
|
||||
|
||||
let switch_position_binding = client_object
|
||||
.bind_property("active", &self.imp().enable_switch.get(), "active")
|
||||
.bidirectional()
|
||||
.sync_create()
|
||||
.build();
|
||||
|
||||
let hostname_binding = client_object
|
||||
.bind_property("hostname", &self.imp().hostname.get(), "text")
|
||||
.transform_to(|_, v: Option<String>| {
|
||||
@@ -103,12 +109,36 @@ impl ClientRow {
|
||||
.sync_create()
|
||||
.build();
|
||||
|
||||
let resolve_binding = client_object
|
||||
.bind_property(
|
||||
"resolving",
|
||||
&self.imp().dns_loading_indicator.get(),
|
||||
"spinning",
|
||||
)
|
||||
.sync_create()
|
||||
.build();
|
||||
|
||||
let ip_binding = client_object
|
||||
.bind_property("ips", &self.imp().dns_button.get(), "tooltip-text")
|
||||
.transform_to(|_, ips: Vec<String>| {
|
||||
if ips.is_empty() {
|
||||
Some("no ip addresses associated with this client".into())
|
||||
} else {
|
||||
Some(ips.join("\n"))
|
||||
}
|
||||
})
|
||||
.sync_create()
|
||||
.build();
|
||||
|
||||
bindings.push(active_binding);
|
||||
bindings.push(switch_position_binding);
|
||||
bindings.push(hostname_binding);
|
||||
bindings.push(title_binding);
|
||||
bindings.push(port_binding);
|
||||
bindings.push(subtitle_binding);
|
||||
bindings.push(position_binding);
|
||||
bindings.push(resolve_binding);
|
||||
bindings.push(ip_binding);
|
||||
}
|
||||
|
||||
pub fn unbind(&self) {
|
||||
@@ -4,7 +4,9 @@ use adw::subclass::prelude::*;
|
||||
use adw::{prelude::*, ActionRow, ComboRow};
|
||||
use glib::{subclass::InitializingObject, Binding};
|
||||
use gtk::glib::clone;
|
||||
use gtk::glib::subclass::Signal;
|
||||
use gtk::{glib, Button, CompositeTemplate, Switch};
|
||||
use std::sync::OnceLock;
|
||||
|
||||
#[derive(CompositeTemplate, Default)]
|
||||
#[template(resource = "/de/feschber/LanMouse/client_row.ui")]
|
||||
@@ -12,6 +14,8 @@ pub struct ClientRow {
|
||||
#[template_child]
|
||||
pub enable_switch: TemplateChild<gtk::Switch>,
|
||||
#[template_child]
|
||||
pub dns_button: TemplateChild<gtk::Button>,
|
||||
#[template_child]
|
||||
pub hostname: TemplateChild<gtk::Entry>,
|
||||
#[template_child]
|
||||
pub port: TemplateChild<gtk::Entry>,
|
||||
@@ -21,6 +25,8 @@ pub struct ClientRow {
|
||||
pub delete_row: TemplateChild<ActionRow>,
|
||||
#[template_child]
|
||||
pub delete_button: TemplateChild<gtk::Button>,
|
||||
#[template_child]
|
||||
pub dns_loading_indicator: TemplateChild<gtk::Spinner>,
|
||||
pub bindings: RefCell<Vec<Binding>>,
|
||||
}
|
||||
|
||||
@@ -28,6 +34,8 @@ pub struct ClientRow {
|
||||
impl ObjectSubclass for ClientRow {
|
||||
// `NAME` needs to match `class` attribute of template
|
||||
const NAME: &'static str = "ClientRow";
|
||||
const ABSTRACT: bool = false;
|
||||
|
||||
type Type = super::ClientRow;
|
||||
type ParentType = adw::ExpanderRow;
|
||||
|
||||
@@ -44,33 +52,47 @@ impl ObjectSubclass for ClientRow {
|
||||
impl ObjectImpl for ClientRow {
|
||||
fn constructed(&self) {
|
||||
self.parent_constructed();
|
||||
self.delete_button
|
||||
.connect_clicked(clone!(@weak self as row => move |button| {
|
||||
self.delete_button.connect_clicked(clone!(
|
||||
#[weak(rename_to = row)]
|
||||
self,
|
||||
move |button| {
|
||||
row.handle_client_delete(button);
|
||||
}));
|
||||
}
|
||||
));
|
||||
}
|
||||
|
||||
fn signals() -> &'static [glib::subclass::Signal] {
|
||||
static SIGNALS: OnceLock<Vec<Signal>> = OnceLock::new();
|
||||
SIGNALS.get_or_init(|| {
|
||||
vec![
|
||||
Signal::builder("request-dns").build(),
|
||||
Signal::builder("request-update")
|
||||
.param_types([bool::static_type()])
|
||||
.build(),
|
||||
Signal::builder("request-delete").build(),
|
||||
]
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[gtk::template_callbacks]
|
||||
impl ClientRow {
|
||||
#[template_callback]
|
||||
fn handle_client_set_state(&self, state: bool, switch: &Switch) -> bool {
|
||||
let idx = self.obj().index() as u32;
|
||||
switch
|
||||
.activate_action("win.request-client-update", Some(&idx.to_variant()))
|
||||
.unwrap();
|
||||
switch.set_state(state);
|
||||
|
||||
fn handle_client_set_state(&self, state: bool, _switch: &Switch) -> bool {
|
||||
log::debug!("state change -> requesting update");
|
||||
self.obj().emit_by_name::<()>("request-update", &[&state]);
|
||||
true // dont run default handler
|
||||
}
|
||||
|
||||
#[template_callback]
|
||||
fn handle_client_delete(&self, button: &Button) {
|
||||
log::debug!("delete button pressed");
|
||||
let idx = self.obj().index() as u32;
|
||||
button
|
||||
.activate_action("win.request-client-delete", Some(&idx.to_variant()))
|
||||
.unwrap();
|
||||
fn handle_request_dns(&self, _: Button) {
|
||||
self.obj().emit_by_name::<()>("request-dns", &[]);
|
||||
}
|
||||
|
||||
#[template_callback]
|
||||
fn handle_client_delete(&self, _button: &Button) {
|
||||
log::debug!("delete button pressed -> requesting delete");
|
||||
self.obj().emit_by_name::<()>("request-delete", &[]);
|
||||
}
|
||||
}
|
||||
|
||||
141
lan-mouse-gtk/src/lib.rs
Normal file
141
lan-mouse-gtk/src/lib.rs
Normal file
@@ -0,0 +1,141 @@
|
||||
mod client_object;
|
||||
mod client_row;
|
||||
mod window;
|
||||
|
||||
use std::{env, process, str};
|
||||
|
||||
use window::Window;
|
||||
|
||||
use lan_mouse_ipc::{FrontendEvent, FrontendRequest};
|
||||
|
||||
use adw::Application;
|
||||
use gtk::{
|
||||
gdk::Display, glib::clone, prelude::*, subclass::prelude::ObjectSubclassIsExt, IconTheme,
|
||||
};
|
||||
use gtk::{gio, glib, prelude::ApplicationExt};
|
||||
|
||||
use self::client_object::ClientObject;
|
||||
|
||||
pub fn run() -> glib::ExitCode {
|
||||
log::debug!("running gtk frontend");
|
||||
#[cfg(windows)]
|
||||
let ret = std::thread::Builder::new()
|
||||
.stack_size(8 * 1024 * 1024) // https://gitlab.gnome.org/GNOME/gtk/-/commit/52dbb3f372b2c3ea339e879689c1de535ba2c2c3 -> caused crash on windows
|
||||
.name("gtk".into())
|
||||
.spawn(gtk_main)
|
||||
.unwrap()
|
||||
.join()
|
||||
.unwrap();
|
||||
#[cfg(not(windows))]
|
||||
let ret = gtk_main();
|
||||
|
||||
if ret == glib::ExitCode::FAILURE {
|
||||
log::error!("frontend exited with failure");
|
||||
} else {
|
||||
log::info!("frontend exited successfully");
|
||||
}
|
||||
|
||||
ret
|
||||
}
|
||||
|
||||
fn gtk_main() -> glib::ExitCode {
|
||||
gio::resources_register_include!("lan-mouse.gresource").expect("Failed to register resources.");
|
||||
|
||||
let app = Application::builder()
|
||||
.application_id("de.feschber.LanMouse")
|
||||
.build();
|
||||
|
||||
app.connect_startup(|_| load_icons());
|
||||
app.connect_activate(build_ui);
|
||||
|
||||
let args: Vec<&'static str> = vec![];
|
||||
app.run_with_args(&args)
|
||||
}
|
||||
|
||||
fn load_icons() {
|
||||
let display = &Display::default().expect("Could not connect to a display.");
|
||||
let icon_theme = IconTheme::for_display(display);
|
||||
icon_theme.add_resource_path("/de/feschber/LanMouse/icons");
|
||||
}
|
||||
|
||||
fn build_ui(app: &Application) {
|
||||
log::debug!("connecting to lan-mouse-socket");
|
||||
let (mut frontend_rx, frontend_tx) = match lan_mouse_ipc::connect() {
|
||||
Ok(conn) => conn,
|
||||
Err(e) => {
|
||||
log::error!("{e}");
|
||||
process::exit(1);
|
||||
}
|
||||
};
|
||||
log::debug!("connected to lan-mouse-socket");
|
||||
|
||||
let (sender, receiver) = async_channel::bounded(10);
|
||||
|
||||
gio::spawn_blocking(move || {
|
||||
while let Some(e) = frontend_rx.next_event() {
|
||||
match e {
|
||||
Ok(e) => sender.send_blocking(e).unwrap(),
|
||||
Err(e) => {
|
||||
log::error!("{e}");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
let window = Window::new(app, frontend_tx);
|
||||
|
||||
glib::spawn_future_local(clone!(
|
||||
#[weak]
|
||||
window,
|
||||
async move {
|
||||
loop {
|
||||
let notify = receiver.recv().await.unwrap_or_else(|_| process::exit(1));
|
||||
match notify {
|
||||
FrontendEvent::Changed(handle) => {
|
||||
window.request(FrontendRequest::GetState(handle));
|
||||
}
|
||||
FrontendEvent::Created(handle, client, state) => {
|
||||
window.new_client(handle, client, state);
|
||||
}
|
||||
FrontendEvent::Deleted(client) => {
|
||||
window.delete_client(client);
|
||||
}
|
||||
FrontendEvent::State(handle, config, state) => {
|
||||
window.update_client_config(handle, config);
|
||||
window.update_client_state(handle, state);
|
||||
}
|
||||
FrontendEvent::NoSuchClient(_) => {}
|
||||
FrontendEvent::Error(e) => {
|
||||
window.show_toast(e.as_str());
|
||||
}
|
||||
FrontendEvent::Enumerate(clients) => {
|
||||
for (handle, client, state) in clients {
|
||||
if window.client_idx(handle).is_some() {
|
||||
window.update_client_config(handle, client);
|
||||
window.update_client_state(handle, state);
|
||||
} else {
|
||||
window.new_client(handle, client, state);
|
||||
}
|
||||
}
|
||||
}
|
||||
FrontendEvent::PortChanged(port, msg) => {
|
||||
match msg {
|
||||
None => window.show_toast(format!("port changed: {port}").as_str()),
|
||||
Some(msg) => window.show_toast(msg.as_str()),
|
||||
}
|
||||
window.imp().set_port(port);
|
||||
}
|
||||
FrontendEvent::CaptureStatus(s) => {
|
||||
window.set_capture(s.into());
|
||||
}
|
||||
FrontendEvent::EmulationStatus(s) => {
|
||||
window.set_emulation(s.into());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
));
|
||||
|
||||
window.present();
|
||||
}
|
||||
322
lan-mouse-gtk/src/window.rs
Normal file
322
lan-mouse-gtk/src/window.rs
Normal file
@@ -0,0 +1,322 @@
|
||||
mod imp;
|
||||
|
||||
use adw::prelude::*;
|
||||
use adw::subclass::prelude::*;
|
||||
use glib::{clone, Object};
|
||||
use gtk::{
|
||||
gio,
|
||||
glib::{self, closure_local},
|
||||
ListBox, NoSelection,
|
||||
};
|
||||
|
||||
use lan_mouse_ipc::{
|
||||
ClientConfig, ClientHandle, ClientState, FrontendRequest, FrontendRequestWriter, Position,
|
||||
DEFAULT_PORT,
|
||||
};
|
||||
|
||||
use super::{client_object::ClientObject, client_row::ClientRow};
|
||||
|
||||
glib::wrapper! {
|
||||
pub struct Window(ObjectSubclass<imp::Window>)
|
||||
@extends adw::ApplicationWindow, gtk::Window, gtk::Widget,
|
||||
@implements gio::ActionGroup, gio::ActionMap, gtk::Accessible, gtk::Buildable,
|
||||
gtk::ConstraintTarget, gtk::Native, gtk::Root, gtk::ShortcutManager;
|
||||
}
|
||||
|
||||
impl Window {
|
||||
pub(crate) fn new(app: &adw::Application, conn: FrontendRequestWriter) -> Self {
|
||||
let window: Self = Object::builder().property("application", app).build();
|
||||
window
|
||||
.imp()
|
||||
.frontend_request_writer
|
||||
.borrow_mut()
|
||||
.replace(conn);
|
||||
window
|
||||
}
|
||||
|
||||
pub fn clients(&self) -> gio::ListStore {
|
||||
self.imp()
|
||||
.clients
|
||||
.borrow()
|
||||
.clone()
|
||||
.expect("Could not get clients")
|
||||
}
|
||||
|
||||
fn client_by_idx(&self, idx: u32) -> Option<ClientObject> {
|
||||
self.clients().item(idx).map(|o| o.downcast().unwrap())
|
||||
}
|
||||
|
||||
fn setup_clients(&self) {
|
||||
let model = gio::ListStore::new::<ClientObject>();
|
||||
self.imp().clients.replace(Some(model));
|
||||
|
||||
let selection_model = NoSelection::new(Some(self.clients()));
|
||||
self.imp().client_list.bind_model(
|
||||
Some(&selection_model),
|
||||
clone!(
|
||||
#[weak(rename_to = window)]
|
||||
self,
|
||||
#[upgrade_or_panic]
|
||||
move |obj| {
|
||||
let client_object = obj
|
||||
.downcast_ref()
|
||||
.expect("Expected object of type `ClientObject`.");
|
||||
let row = window.create_client_row(client_object);
|
||||
row.connect_closure(
|
||||
"request-update",
|
||||
false,
|
||||
closure_local!(
|
||||
#[strong]
|
||||
window,
|
||||
move |row: ClientRow, active: bool| {
|
||||
if let Some(client) = window.client_by_idx(row.index() as u32) {
|
||||
window.request_client_activate(&client, active);
|
||||
window.request_client_update(&client);
|
||||
window.request_client_state(&client);
|
||||
}
|
||||
}
|
||||
),
|
||||
);
|
||||
row.connect_closure(
|
||||
"request-delete",
|
||||
false,
|
||||
closure_local!(
|
||||
#[strong]
|
||||
window,
|
||||
move |row: ClientRow| {
|
||||
if let Some(client) = window.client_by_idx(row.index() as u32) {
|
||||
window.request_client_delete(&client);
|
||||
}
|
||||
}
|
||||
),
|
||||
);
|
||||
row.connect_closure(
|
||||
"request-dns",
|
||||
false,
|
||||
closure_local!(
|
||||
#[strong]
|
||||
window,
|
||||
move |row: ClientRow| {
|
||||
if let Some(client) = window.client_by_idx(row.index() as u32) {
|
||||
window.request_client_update(&client);
|
||||
window.request_dns(&client);
|
||||
window.request_client_state(&client);
|
||||
}
|
||||
}
|
||||
),
|
||||
);
|
||||
row.upcast()
|
||||
}
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// workaround for a bug in libadwaita that shows an ugly line beneath
|
||||
/// the last element if a placeholder is set.
|
||||
/// https://gitlab.gnome.org/GNOME/gtk/-/merge_requests/6308
|
||||
pub fn set_placeholder_visible(&self, visible: bool) {
|
||||
let placeholder = self.imp().client_placeholder.get();
|
||||
self.imp().client_list.set_placeholder(match visible {
|
||||
true => Some(&placeholder),
|
||||
false => None,
|
||||
});
|
||||
}
|
||||
|
||||
fn setup_icon(&self) {
|
||||
self.set_icon_name(Some("de.feschber.LanMouse"));
|
||||
}
|
||||
|
||||
fn create_client_row(&self, client_object: &ClientObject) -> ClientRow {
|
||||
let row = ClientRow::new(client_object);
|
||||
row.bind(client_object);
|
||||
row
|
||||
}
|
||||
|
||||
pub fn new_client(&self, handle: ClientHandle, client: ClientConfig, state: ClientState) {
|
||||
let client = ClientObject::new(handle, client, state.clone());
|
||||
self.clients().append(&client);
|
||||
self.set_placeholder_visible(false);
|
||||
self.update_dns_state(handle, !state.ips.is_empty());
|
||||
}
|
||||
|
||||
pub fn client_idx(&self, handle: ClientHandle) -> Option<usize> {
|
||||
self.clients().iter::<ClientObject>().position(|c| {
|
||||
if let Ok(c) = c {
|
||||
c.handle() == handle
|
||||
} else {
|
||||
false
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
pub fn delete_client(&self, handle: ClientHandle) {
|
||||
let Some(idx) = self.client_idx(handle) else {
|
||||
log::warn!("could not find client with handle {handle}");
|
||||
return;
|
||||
};
|
||||
|
||||
self.clients().remove(idx as u32);
|
||||
if self.clients().n_items() == 0 {
|
||||
self.set_placeholder_visible(true);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn update_client_config(&self, handle: ClientHandle, client: ClientConfig) {
|
||||
let Some(idx) = self.client_idx(handle) else {
|
||||
log::warn!("could not find client with handle {}", handle);
|
||||
return;
|
||||
};
|
||||
let client_object = self.clients().item(idx as u32).unwrap();
|
||||
let client_object: &ClientObject = client_object.downcast_ref().unwrap();
|
||||
let data = client_object.get_data();
|
||||
|
||||
/* only change if it actually has changed, otherwise
|
||||
* the update signal is triggered */
|
||||
if data.hostname != client.hostname {
|
||||
client_object.set_hostname(client.hostname.unwrap_or("".into()));
|
||||
}
|
||||
if data.port != client.port as u32 {
|
||||
client_object.set_port(client.port as u32);
|
||||
}
|
||||
if data.position != client.pos.to_string() {
|
||||
client_object.set_position(client.pos.to_string());
|
||||
}
|
||||
}
|
||||
|
||||
pub fn update_client_state(&self, handle: ClientHandle, state: ClientState) {
|
||||
let Some(idx) = self.client_idx(handle) else {
|
||||
log::warn!("could not find client with handle {}", handle);
|
||||
return;
|
||||
};
|
||||
let client_object = self.clients().item(idx as u32).unwrap();
|
||||
let client_object: &ClientObject = client_object.downcast_ref().unwrap();
|
||||
let data = client_object.get_data();
|
||||
|
||||
if state.active != data.active {
|
||||
client_object.set_active(state.active);
|
||||
log::debug!("set active to {}", state.active);
|
||||
}
|
||||
|
||||
if state.resolving != data.resolving {
|
||||
client_object.set_resolving(state.resolving);
|
||||
log::debug!("resolving {}: {}", data.handle, state.resolving);
|
||||
}
|
||||
|
||||
self.update_dns_state(handle, !state.ips.is_empty());
|
||||
let ips = state
|
||||
.ips
|
||||
.into_iter()
|
||||
.map(|ip| ip.to_string())
|
||||
.collect::<Vec<_>>();
|
||||
client_object.set_ips(ips);
|
||||
}
|
||||
|
||||
pub fn update_dns_state(&self, handle: ClientHandle, resolved: bool) {
|
||||
let Some(idx) = self.client_idx(handle) else {
|
||||
log::warn!("could not find client with handle {}", handle);
|
||||
return;
|
||||
};
|
||||
let list_box: ListBox = self.imp().client_list.get();
|
||||
let row = list_box.row_at_index(idx as i32).unwrap();
|
||||
let client_row: ClientRow = row.downcast().expect("expected ClientRow Object");
|
||||
if resolved {
|
||||
client_row.imp().dns_button.set_css_classes(&["success"])
|
||||
} else {
|
||||
client_row.imp().dns_button.set_css_classes(&["warning"])
|
||||
}
|
||||
}
|
||||
|
||||
pub fn request_port_change(&self) {
|
||||
let port = self
|
||||
.imp()
|
||||
.port_entry
|
||||
.get()
|
||||
.text()
|
||||
.as_str()
|
||||
.parse::<u16>()
|
||||
.unwrap_or(DEFAULT_PORT);
|
||||
self.request(FrontendRequest::ChangePort(port));
|
||||
}
|
||||
|
||||
pub fn request_capture(&self) {
|
||||
self.request(FrontendRequest::EnableCapture);
|
||||
}
|
||||
|
||||
pub fn request_emulation(&self) {
|
||||
self.request(FrontendRequest::EnableEmulation);
|
||||
}
|
||||
|
||||
pub fn request_client_state(&self, client: &ClientObject) {
|
||||
self.request_client_state_for(client.handle());
|
||||
}
|
||||
|
||||
pub fn request_client_state_for(&self, handle: ClientHandle) {
|
||||
self.request(FrontendRequest::GetState(handle));
|
||||
}
|
||||
|
||||
pub fn request_client_create(&self) {
|
||||
self.request(FrontendRequest::Create);
|
||||
}
|
||||
|
||||
pub fn request_dns(&self, client: &ClientObject) {
|
||||
self.request(FrontendRequest::ResolveDns(client.get_data().handle));
|
||||
}
|
||||
|
||||
pub fn request_client_update(&self, client: &ClientObject) {
|
||||
let handle = client.handle();
|
||||
let data = client.get_data();
|
||||
let position = Position::try_from(data.position.as_str()).expect("invalid position");
|
||||
let hostname = data.hostname;
|
||||
let port = data.port as u16;
|
||||
|
||||
for event in [
|
||||
FrontendRequest::UpdateHostname(handle, hostname),
|
||||
FrontendRequest::UpdatePosition(handle, position),
|
||||
FrontendRequest::UpdatePort(handle, port),
|
||||
] {
|
||||
self.request(event);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn request_client_activate(&self, client: &ClientObject, active: bool) {
|
||||
self.request(FrontendRequest::Activate(client.handle(), active));
|
||||
}
|
||||
|
||||
pub fn request_client_delete(&self, client: &ClientObject) {
|
||||
self.request(FrontendRequest::Delete(client.handle()));
|
||||
}
|
||||
|
||||
pub fn request(&self, request: FrontendRequest) {
|
||||
let mut requester = self.imp().frontend_request_writer.borrow_mut();
|
||||
let requester = requester.as_mut().unwrap();
|
||||
if let Err(e) = requester.request(request) {
|
||||
log::error!("error sending message: {e}");
|
||||
};
|
||||
}
|
||||
|
||||
pub fn show_toast(&self, msg: &str) {
|
||||
let toast = adw::Toast::new(msg);
|
||||
let toast_overlay = &self.imp().toast_overlay;
|
||||
toast_overlay.add_toast(toast);
|
||||
}
|
||||
|
||||
pub fn set_capture(&self, active: bool) {
|
||||
self.imp().capture_active.replace(active);
|
||||
self.update_capture_emulation_status();
|
||||
}
|
||||
|
||||
pub fn set_emulation(&self, active: bool) {
|
||||
self.imp().emulation_active.replace(active);
|
||||
self.update_capture_emulation_status();
|
||||
}
|
||||
|
||||
fn update_capture_emulation_status(&self) {
|
||||
let capture = self.imp().capture_active.get();
|
||||
let emulation = self.imp().emulation_active.get();
|
||||
self.imp().capture_status_row.set_visible(!capture);
|
||||
self.imp().emulation_status_row.set_visible(!emulation);
|
||||
self.imp()
|
||||
.capture_emulation_group
|
||||
.set_visible(!capture || !emulation);
|
||||
}
|
||||
}
|
||||
@@ -1,19 +1,12 @@
|
||||
use std::cell::{Cell, RefCell};
|
||||
|
||||
#[cfg(windows)]
|
||||
use std::net::TcpStream;
|
||||
#[cfg(unix)]
|
||||
use std::os::unix::net::UnixStream;
|
||||
|
||||
use adw::subclass::prelude::*;
|
||||
use adw::{
|
||||
prelude::{EditableExt, WidgetExt},
|
||||
ActionRow, ToastOverlay,
|
||||
};
|
||||
use adw::{prelude::*, ActionRow, PreferencesGroup, ToastOverlay};
|
||||
use glib::subclass::InitializingObject;
|
||||
use gtk::{gio, glib, Button, CompositeTemplate, Entry, ListBox};
|
||||
use gtk::glib::clone;
|
||||
use gtk::{gdk, gio, glib, Button, CompositeTemplate, Entry, Label, ListBox};
|
||||
|
||||
use crate::config::DEFAULT_PORT;
|
||||
use lan_mouse_ipc::{FrontendRequestWriter, DEFAULT_PORT};
|
||||
|
||||
#[derive(CompositeTemplate, Default)]
|
||||
#[template(resource = "/de/feschber/LanMouse/window.ui")]
|
||||
@@ -29,19 +22,32 @@ pub struct Window {
|
||||
#[template_child]
|
||||
pub port_entry: TemplateChild<Entry>,
|
||||
#[template_child]
|
||||
pub hostname_label: TemplateChild<Label>,
|
||||
#[template_child]
|
||||
pub toast_overlay: TemplateChild<ToastOverlay>,
|
||||
#[template_child]
|
||||
pub capture_emulation_group: TemplateChild<PreferencesGroup>,
|
||||
#[template_child]
|
||||
pub capture_status_row: TemplateChild<ActionRow>,
|
||||
#[template_child]
|
||||
pub emulation_status_row: TemplateChild<ActionRow>,
|
||||
#[template_child]
|
||||
pub input_emulation_button: TemplateChild<Button>,
|
||||
#[template_child]
|
||||
pub input_capture_button: TemplateChild<Button>,
|
||||
pub clients: RefCell<Option<gio::ListStore>>,
|
||||
#[cfg(unix)]
|
||||
pub stream: RefCell<Option<UnixStream>>,
|
||||
#[cfg(windows)]
|
||||
pub stream: RefCell<Option<TcpStream>>,
|
||||
pub frontend_request_writer: RefCell<Option<FrontendRequestWriter>>,
|
||||
pub port: Cell<u16>,
|
||||
pub capture_active: Cell<bool>,
|
||||
pub emulation_active: Cell<bool>,
|
||||
}
|
||||
|
||||
#[glib::object_subclass]
|
||||
impl ObjectSubclass for Window {
|
||||
// `NAME` needs to match `class` attribute of template
|
||||
const NAME: &'static str = "LanMouseWindow";
|
||||
const ABSTRACT: bool = false;
|
||||
|
||||
type Type = super::Window;
|
||||
type ParentType = adw::ApplicationWindow;
|
||||
|
||||
@@ -62,6 +68,26 @@ impl Window {
|
||||
self.obj().request_client_create();
|
||||
}
|
||||
|
||||
#[template_callback]
|
||||
fn handle_copy_hostname(&self, button: &Button) {
|
||||
if let Ok(hostname) = hostname::get() {
|
||||
let display = gdk::Display::default().unwrap();
|
||||
let clipboard = display.clipboard();
|
||||
clipboard.set_text(hostname.to_str().expect("hostname: invalid utf8"));
|
||||
button.set_icon_name("emblem-ok-symbolic");
|
||||
button.set_css_classes(&["success"]);
|
||||
glib::spawn_future_local(clone!(
|
||||
#[weak]
|
||||
button,
|
||||
async move {
|
||||
glib::timeout_future_seconds(1).await;
|
||||
button.set_icon_name("edit-copy-symbolic");
|
||||
button.set_css_classes(&[]);
|
||||
}
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
#[template_callback]
|
||||
fn handle_port_changed(&self, _entry: &Entry) {
|
||||
self.port_edit_apply.set_visible(true);
|
||||
@@ -82,6 +108,16 @@ impl Window {
|
||||
self.port_edit_cancel.set_visible(false);
|
||||
}
|
||||
|
||||
#[template_callback]
|
||||
fn handle_emulation(&self) {
|
||||
self.obj().request_emulation();
|
||||
}
|
||||
|
||||
#[template_callback]
|
||||
fn handle_capture(&self) {
|
||||
self.obj().request_capture();
|
||||
}
|
||||
|
||||
pub fn set_port(&self, port: u16) {
|
||||
self.port.set(port);
|
||||
if port == DEFAULT_PORT {
|
||||
@@ -96,6 +132,10 @@ impl Window {
|
||||
|
||||
impl ObjectImpl for Window {
|
||||
fn constructed(&self) {
|
||||
if let Ok(hostname) = hostname::get() {
|
||||
self.hostname_label
|
||||
.set_text(hostname.to_str().expect("hostname: invalid utf8"));
|
||||
}
|
||||
self.parent_constructed();
|
||||
self.set_port(DEFAULT_PORT);
|
||||
let obj = self.obj();
|
||||
16
lan-mouse-ipc/Cargo.toml
Normal file
16
lan-mouse-ipc/Cargo.toml
Normal file
@@ -0,0 +1,16 @@
|
||||
[package]
|
||||
name = "lan-mouse-ipc"
|
||||
description = "library for communication between lan-mouse service and frontends"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
license = "GPL-3.0-or-later"
|
||||
repository = "https://github.com/feschber/lan-mouse"
|
||||
|
||||
[dependencies]
|
||||
futures = "0.3.30"
|
||||
log = "0.4.22"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0.107"
|
||||
thiserror = "1.0.63"
|
||||
tokio = { version = "1.32.0", features = ["net", "io-util", "time"] }
|
||||
tokio-stream = { version = "0.1.15", features = ["io-util"] }
|
||||
89
lan-mouse-ipc/src/connect.rs
Normal file
89
lan-mouse-ipc/src/connect.rs
Normal file
@@ -0,0 +1,89 @@
|
||||
use crate::{ConnectionError, FrontendEvent, FrontendRequest, IpcError};
|
||||
use std::{
|
||||
cmp::min,
|
||||
io::{self, prelude::*, BufReader, LineWriter, Lines},
|
||||
thread,
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
#[cfg(unix)]
|
||||
use std::os::unix::net::UnixStream;
|
||||
|
||||
#[cfg(windows)]
|
||||
use std::net::TcpStream;
|
||||
|
||||
pub struct FrontendEventReader {
|
||||
#[cfg(unix)]
|
||||
lines: Lines<BufReader<UnixStream>>,
|
||||
#[cfg(windows)]
|
||||
lines: Lines<BufReader<TcpStream>>,
|
||||
}
|
||||
|
||||
pub struct FrontendRequestWriter {
|
||||
#[cfg(unix)]
|
||||
line_writer: LineWriter<UnixStream>,
|
||||
#[cfg(windows)]
|
||||
line_writer: LineWriter<TcpStream>,
|
||||
}
|
||||
|
||||
impl FrontendEventReader {
|
||||
pub fn next_event(&mut self) -> Option<Result<FrontendEvent, IpcError>> {
|
||||
match self.lines.next()? {
|
||||
Err(e) => Some(Err(e.into())),
|
||||
Ok(l) => Some(serde_json::from_str(l.as_str()).map_err(|e| e.into())),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl FrontendRequestWriter {
|
||||
pub fn request(&mut self, request: FrontendRequest) -> Result<(), io::Error> {
|
||||
let mut json = serde_json::to_string(&request).unwrap();
|
||||
log::debug!("requesting: {json}");
|
||||
json.push('\n');
|
||||
self.line_writer.write_all(json.as_bytes())?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
pub fn connect() -> Result<(FrontendEventReader, FrontendRequestWriter), ConnectionError> {
|
||||
let rx = wait_for_service()?;
|
||||
let tx = rx.try_clone()?;
|
||||
let buf_reader = BufReader::new(rx);
|
||||
let lines = buf_reader.lines();
|
||||
let line_writer = LineWriter::new(tx);
|
||||
let reader = FrontendEventReader { lines };
|
||||
let writer = FrontendRequestWriter { line_writer };
|
||||
Ok((reader, writer))
|
||||
}
|
||||
|
||||
/// wait for the lan-mouse socket to come online
|
||||
#[cfg(unix)]
|
||||
fn wait_for_service() -> Result<UnixStream, ConnectionError> {
|
||||
let socket_path = crate::default_socket_path()?;
|
||||
let mut duration = Duration::from_millis(10);
|
||||
loop {
|
||||
if let Ok(stream) = UnixStream::connect(&socket_path) {
|
||||
break Ok(stream);
|
||||
}
|
||||
// a signaling mechanism or inotify could be used to
|
||||
// improve this
|
||||
thread::sleep(exponential_back_off(&mut duration));
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
fn wait_for_service() -> Result<TcpStream, ConnectionError> {
|
||||
let mut duration = Duration::from_millis(10);
|
||||
loop {
|
||||
if let Ok(stream) = TcpStream::connect("127.0.0.1:5252") {
|
||||
break Ok(stream);
|
||||
}
|
||||
thread::sleep(exponential_back_off(&mut duration));
|
||||
}
|
||||
}
|
||||
|
||||
fn exponential_back_off(duration: &mut Duration) -> Duration {
|
||||
let new = duration.saturating_mul(2);
|
||||
*duration = min(new, Duration::from_secs(1));
|
||||
*duration
|
||||
}
|
||||
104
lan-mouse-ipc/src/connect_async.rs
Normal file
104
lan-mouse-ipc/src/connect_async.rs
Normal file
@@ -0,0 +1,104 @@
|
||||
use crate::{ConnectionError, FrontendEvent, FrontendRequest, IpcError};
|
||||
use std::{
|
||||
cmp::min,
|
||||
io,
|
||||
task::{ready, Poll},
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
use futures::{Stream, StreamExt};
|
||||
use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader, ReadHalf, WriteHalf};
|
||||
use tokio_stream::wrappers::LinesStream;
|
||||
|
||||
#[cfg(unix)]
|
||||
use tokio::net::UnixStream;
|
||||
|
||||
#[cfg(windows)]
|
||||
use tokio::net::TcpStream;
|
||||
|
||||
pub struct AsyncFrontendEventReader {
|
||||
#[cfg(unix)]
|
||||
lines_stream: LinesStream<BufReader<ReadHalf<UnixStream>>>,
|
||||
#[cfg(windows)]
|
||||
lines_stream: LinesStream<BufReader<ReadHalf<TcpStream>>>,
|
||||
}
|
||||
|
||||
pub struct AsyncFrontendRequestWriter {
|
||||
#[cfg(unix)]
|
||||
tx: WriteHalf<UnixStream>,
|
||||
#[cfg(windows)]
|
||||
tx: WriteHalf<TcpStream>,
|
||||
}
|
||||
|
||||
impl Stream for AsyncFrontendEventReader {
|
||||
type Item = Result<FrontendEvent, IpcError>;
|
||||
|
||||
fn poll_next(
|
||||
mut self: std::pin::Pin<&mut Self>,
|
||||
cx: &mut std::task::Context<'_>,
|
||||
) -> std::task::Poll<Option<Self::Item>> {
|
||||
let line = ready!(self.lines_stream.poll_next_unpin(cx));
|
||||
let event = line.map(|l| {
|
||||
l.map_err(Into::<IpcError>::into)
|
||||
.and_then(|l| serde_json::from_str(l.as_str()).map_err(|e| e.into()))
|
||||
});
|
||||
Poll::Ready(event)
|
||||
}
|
||||
}
|
||||
|
||||
impl AsyncFrontendRequestWriter {
|
||||
pub async fn request(&mut self, request: FrontendRequest) -> Result<(), io::Error> {
|
||||
let mut json = serde_json::to_string(&request).unwrap();
|
||||
log::debug!("requesting: {json}");
|
||||
json.push('\n');
|
||||
self.tx.write_all(json.as_bytes()).await?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn connect_async(
|
||||
) -> Result<(AsyncFrontendEventReader, AsyncFrontendRequestWriter), ConnectionError> {
|
||||
let stream = wait_for_service().await?;
|
||||
#[cfg(unix)]
|
||||
let (rx, tx): (ReadHalf<UnixStream>, WriteHalf<UnixStream>) = tokio::io::split(stream);
|
||||
#[cfg(windows)]
|
||||
let (rx, tx): (ReadHalf<TcpStream>, WriteHalf<TcpStream>) = tokio::io::split(stream);
|
||||
let buf_reader = BufReader::new(rx);
|
||||
let lines = buf_reader.lines();
|
||||
let lines_stream = LinesStream::new(lines);
|
||||
let reader = AsyncFrontendEventReader { lines_stream };
|
||||
let writer = AsyncFrontendRequestWriter { tx };
|
||||
Ok((reader, writer))
|
||||
}
|
||||
|
||||
/// wait for the lan-mouse socket to come online
|
||||
#[cfg(unix)]
|
||||
async fn wait_for_service() -> Result<UnixStream, ConnectionError> {
|
||||
let socket_path = crate::default_socket_path()?;
|
||||
let mut duration = Duration::from_millis(10);
|
||||
loop {
|
||||
if let Ok(stream) = UnixStream::connect(&socket_path).await {
|
||||
break Ok(stream);
|
||||
}
|
||||
// a signaling mechanism or inotify could be used to
|
||||
// improve this
|
||||
tokio::time::sleep(exponential_back_off(&mut duration)).await;
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
async fn wait_for_service() -> Result<TcpStream, ConnectionError> {
|
||||
let mut duration = Duration::from_millis(10);
|
||||
loop {
|
||||
if let Ok(stream) = TcpStream::connect("127.0.0.1:5252").await {
|
||||
break Ok(stream);
|
||||
}
|
||||
tokio::time::sleep(exponential_back_off(&mut duration)).await;
|
||||
}
|
||||
}
|
||||
|
||||
fn exponential_back_off(duration: &mut Duration) -> Duration {
|
||||
let new = duration.saturating_mul(2);
|
||||
*duration = min(new, Duration::from_secs(1));
|
||||
*duration
|
||||
}
|
||||
264
lan-mouse-ipc/src/lib.rs
Normal file
264
lan-mouse-ipc/src/lib.rs
Normal file
@@ -0,0 +1,264 @@
|
||||
use std::{
|
||||
collections::HashSet,
|
||||
env::VarError,
|
||||
fmt::Display,
|
||||
io,
|
||||
net::{IpAddr, SocketAddr},
|
||||
str::FromStr,
|
||||
};
|
||||
use thiserror::Error;
|
||||
|
||||
#[cfg(unix)]
|
||||
use std::{
|
||||
env,
|
||||
path::{Path, PathBuf},
|
||||
};
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
mod connect;
|
||||
mod connect_async;
|
||||
mod listen;
|
||||
|
||||
pub use connect::{connect, FrontendEventReader, FrontendRequestWriter};
|
||||
pub use connect_async::{connect_async, AsyncFrontendEventReader, AsyncFrontendRequestWriter};
|
||||
pub use listen::AsyncFrontendListener;
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum ConnectionError {
|
||||
#[error(transparent)]
|
||||
SocketPath(#[from] SocketPathError),
|
||||
#[error(transparent)]
|
||||
Io(#[from] io::Error),
|
||||
}
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum ListenerCreationError {
|
||||
#[error("could not determine socket-path: `{0}`")]
|
||||
SocketPath(#[from] SocketPathError),
|
||||
#[error("service already running!")]
|
||||
AlreadyRunning,
|
||||
#[error("failed to bind lan-mouse socket: `{0}`")]
|
||||
Bind(io::Error),
|
||||
}
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum IpcError {
|
||||
#[error("io error occured: `{0}`")]
|
||||
Io(#[from] io::Error),
|
||||
#[error("invalid json: `{0}`")]
|
||||
Json(#[from] serde_json::Error),
|
||||
#[error(transparent)]
|
||||
Connection(#[from] ConnectionError),
|
||||
#[error(transparent)]
|
||||
Listen(#[from] ListenerCreationError),
|
||||
}
|
||||
|
||||
pub const DEFAULT_PORT: u16 = 4242;
|
||||
|
||||
#[derive(Debug, Default, Eq, Hash, PartialEq, Clone, Copy, Serialize, Deserialize)]
|
||||
pub enum Position {
|
||||
#[default]
|
||||
Left,
|
||||
Right,
|
||||
Top,
|
||||
Bottom,
|
||||
}
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
#[error("not a valid position: {pos}")]
|
||||
pub struct PositionParseError {
|
||||
pos: String,
|
||||
}
|
||||
|
||||
impl FromStr for Position {
|
||||
type Err = PositionParseError;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
match s {
|
||||
"left" => Ok(Self::Left),
|
||||
"right" => Ok(Self::Right),
|
||||
"top" => Ok(Self::Top),
|
||||
"bottom" => Ok(Self::Bottom),
|
||||
_ => Err(PositionParseError { pos: s.into() }),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for Position {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(
|
||||
f,
|
||||
"{}",
|
||||
match self {
|
||||
Position::Left => "left",
|
||||
Position::Right => "right",
|
||||
Position::Top => "top",
|
||||
Position::Bottom => "bottom",
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<&str> for Position {
|
||||
type Error = ();
|
||||
|
||||
fn try_from(s: &str) -> Result<Self, Self::Error> {
|
||||
match s {
|
||||
"left" => Ok(Position::Left),
|
||||
"right" => Ok(Position::Right),
|
||||
"top" => Ok(Position::Top),
|
||||
"bottom" => Ok(Position::Bottom),
|
||||
_ => Err(()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Eq, PartialEq, Clone, Serialize, Deserialize)]
|
||||
pub struct ClientConfig {
|
||||
/// hostname of this client
|
||||
pub hostname: Option<String>,
|
||||
/// fix ips, determined by the user
|
||||
pub fix_ips: Vec<IpAddr>,
|
||||
/// both active_addr and addrs can be None / empty so port needs to be stored seperately
|
||||
pub port: u16,
|
||||
/// position of a client on screen
|
||||
pub pos: Position,
|
||||
/// enter hook
|
||||
pub cmd: Option<String>,
|
||||
}
|
||||
|
||||
impl Default for ClientConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
port: DEFAULT_PORT,
|
||||
hostname: Default::default(),
|
||||
fix_ips: Default::default(),
|
||||
pos: Default::default(),
|
||||
cmd: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub type ClientHandle = u64;
|
||||
|
||||
#[derive(Debug, Default, Clone, Serialize, Deserialize)]
|
||||
pub struct ClientState {
|
||||
/// events should be sent to and received from the client
|
||||
pub active: bool,
|
||||
/// `active` address of the client, used to send data to.
|
||||
/// This should generally be the socket address where data
|
||||
/// was last received from.
|
||||
pub active_addr: Option<SocketAddr>,
|
||||
/// tracks whether or not the client is responding to pings
|
||||
pub alive: bool,
|
||||
/// ips from dns
|
||||
pub dns_ips: Vec<IpAddr>,
|
||||
/// all ip addresses associated with a particular client
|
||||
/// e.g. Laptops usually have at least an ethernet and a wifi port
|
||||
/// which have different ip addresses
|
||||
pub ips: HashSet<IpAddr>,
|
||||
/// client has pressed keys
|
||||
pub has_pressed_keys: bool,
|
||||
/// dns resolving in progress
|
||||
pub resolving: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub enum FrontendEvent {
|
||||
/// client state has changed, new state must be requested via [`FrontendRequest::GetState`]
|
||||
Changed(ClientHandle),
|
||||
/// a client was created
|
||||
Created(ClientHandle, ClientConfig, ClientState),
|
||||
/// no such client
|
||||
NoSuchClient(ClientHandle),
|
||||
/// state changed
|
||||
State(ClientHandle, ClientConfig, ClientState),
|
||||
/// the client was deleted
|
||||
Deleted(ClientHandle),
|
||||
/// new port, reason of failure (if failed)
|
||||
PortChanged(u16, Option<String>),
|
||||
/// list of all clients, used for initial state synchronization
|
||||
Enumerate(Vec<(ClientHandle, ClientConfig, ClientState)>),
|
||||
/// an error occured
|
||||
Error(String),
|
||||
/// capture status
|
||||
CaptureStatus(Status),
|
||||
/// emulation status
|
||||
EmulationStatus(Status),
|
||||
}
|
||||
|
||||
#[derive(Debug, Eq, PartialEq, Clone, Serialize, Deserialize)]
|
||||
pub enum FrontendRequest {
|
||||
/// activate/deactivate client
|
||||
Activate(ClientHandle, bool),
|
||||
/// add a new client
|
||||
Create,
|
||||
/// change the listen port (recreate udp listener)
|
||||
ChangePort(u16),
|
||||
/// remove a client
|
||||
Delete(ClientHandle),
|
||||
/// request an enumeration of all clients
|
||||
Enumerate(),
|
||||
/// resolve dns
|
||||
ResolveDns(ClientHandle),
|
||||
/// update hostname
|
||||
UpdateHostname(ClientHandle, Option<String>),
|
||||
/// update port
|
||||
UpdatePort(ClientHandle, u16),
|
||||
/// update position
|
||||
UpdatePosition(ClientHandle, Position),
|
||||
/// update fix-ips
|
||||
UpdateFixIps(ClientHandle, Vec<IpAddr>),
|
||||
/// request the state of the given client
|
||||
GetState(ClientHandle),
|
||||
/// request reenabling input capture
|
||||
EnableCapture,
|
||||
/// request reenabling input emulation
|
||||
EnableEmulation,
|
||||
/// synchronize all state
|
||||
Sync,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Default, Serialize, Deserialize)]
|
||||
pub enum Status {
|
||||
#[default]
|
||||
Disabled,
|
||||
Enabled,
|
||||
}
|
||||
|
||||
impl From<Status> for bool {
|
||||
fn from(status: Status) -> Self {
|
||||
match status {
|
||||
Status::Enabled => true,
|
||||
Status::Disabled => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
const LAN_MOUSE_SOCKET_NAME: &str = "lan-mouse-socket.sock";
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum SocketPathError {
|
||||
#[error("could not determine $XDG_RUNTIME_DIR: `{0}`")]
|
||||
XdgRuntimeDirNotFound(VarError),
|
||||
#[error("could not determine $HOME: `{0}`")]
|
||||
HomeDirNotFound(VarError),
|
||||
}
|
||||
|
||||
#[cfg(all(unix, not(target_os = "macos")))]
|
||||
pub fn default_socket_path() -> Result<PathBuf, SocketPathError> {
|
||||
let xdg_runtime_dir =
|
||||
env::var("XDG_RUNTIME_DIR").map_err(SocketPathError::XdgRuntimeDirNotFound)?;
|
||||
Ok(Path::new(xdg_runtime_dir.as_str()).join(LAN_MOUSE_SOCKET_NAME))
|
||||
}
|
||||
|
||||
#[cfg(all(unix, target_os = "macos"))]
|
||||
pub fn default_socket_path() -> Result<PathBuf, SocketPathError> {
|
||||
let home = env::var("HOME").map_err(SocketPathError::HomeDirNotFound)?;
|
||||
Ok(Path::new(home.as_str())
|
||||
.join("Library")
|
||||
.join("Caches")
|
||||
.join(LAN_MOUSE_SOCKET_NAME))
|
||||
}
|
||||
147
lan-mouse-ipc/src/listen.rs
Normal file
147
lan-mouse-ipc/src/listen.rs
Normal file
@@ -0,0 +1,147 @@
|
||||
use futures::{stream::SelectAll, Stream, StreamExt};
|
||||
#[cfg(unix)]
|
||||
use std::path::PathBuf;
|
||||
use std::{
|
||||
io::ErrorKind,
|
||||
pin::Pin,
|
||||
task::{Context, Poll},
|
||||
};
|
||||
|
||||
use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader, ReadHalf, WriteHalf};
|
||||
use tokio_stream::wrappers::LinesStream;
|
||||
|
||||
#[cfg(unix)]
|
||||
use tokio::net::UnixListener;
|
||||
#[cfg(unix)]
|
||||
use tokio::net::UnixStream;
|
||||
|
||||
#[cfg(windows)]
|
||||
use tokio::net::TcpListener;
|
||||
#[cfg(windows)]
|
||||
use tokio::net::TcpStream;
|
||||
|
||||
use crate::{FrontendEvent, FrontendRequest, IpcError, ListenerCreationError};
|
||||
|
||||
pub struct AsyncFrontendListener {
|
||||
#[cfg(windows)]
|
||||
listener: TcpListener,
|
||||
#[cfg(unix)]
|
||||
listener: UnixListener,
|
||||
#[cfg(unix)]
|
||||
socket_path: PathBuf,
|
||||
#[cfg(unix)]
|
||||
line_streams: SelectAll<LinesStream<BufReader<ReadHalf<UnixStream>>>>,
|
||||
#[cfg(windows)]
|
||||
line_streams: SelectAll<LinesStream<BufReader<ReadHalf<TcpStream>>>>,
|
||||
#[cfg(unix)]
|
||||
tx_streams: Vec<WriteHalf<UnixStream>>,
|
||||
#[cfg(windows)]
|
||||
tx_streams: Vec<WriteHalf<TcpStream>>,
|
||||
}
|
||||
|
||||
impl AsyncFrontendListener {
|
||||
pub async fn new() -> Result<Self, ListenerCreationError> {
|
||||
#[cfg(unix)]
|
||||
let (socket_path, listener) = {
|
||||
let socket_path = crate::default_socket_path()?;
|
||||
|
||||
log::debug!("remove socket: {:?}", socket_path);
|
||||
if socket_path.exists() {
|
||||
// try to connect to see if some other instance
|
||||
// of lan-mouse is already running
|
||||
match UnixStream::connect(&socket_path).await {
|
||||
// connected -> lan-mouse is already running
|
||||
Ok(_) => return Err(ListenerCreationError::AlreadyRunning),
|
||||
// lan-mouse is not running but a socket was left behind
|
||||
Err(e) => {
|
||||
log::debug!("{socket_path:?}: {e} - removing left behind socket");
|
||||
let _ = std::fs::remove_file(&socket_path);
|
||||
}
|
||||
}
|
||||
}
|
||||
let listener = match UnixListener::bind(&socket_path) {
|
||||
Ok(ls) => ls,
|
||||
// some other lan-mouse instance has bound the socket in the meantime
|
||||
Err(e) if e.kind() == ErrorKind::AddrInUse => {
|
||||
return Err(ListenerCreationError::AlreadyRunning)
|
||||
}
|
||||
Err(e) => return Err(ListenerCreationError::Bind(e)),
|
||||
};
|
||||
(socket_path, listener)
|
||||
};
|
||||
|
||||
#[cfg(windows)]
|
||||
let listener = match TcpListener::bind("127.0.0.1:5252").await {
|
||||
Ok(ls) => ls,
|
||||
// some other lan-mouse instance has bound the socket in the meantime
|
||||
Err(e) if e.kind() == ErrorKind::AddrInUse => {
|
||||
return Err(ListenerCreationError::AlreadyRunning)
|
||||
}
|
||||
Err(e) => return Err(ListenerCreationError::Bind(e)),
|
||||
};
|
||||
|
||||
let adapter = Self {
|
||||
listener,
|
||||
#[cfg(unix)]
|
||||
socket_path,
|
||||
line_streams: SelectAll::new(),
|
||||
tx_streams: vec![],
|
||||
};
|
||||
|
||||
Ok(adapter)
|
||||
}
|
||||
|
||||
pub async fn broadcast(&mut self, notify: FrontendEvent) {
|
||||
// encode event
|
||||
let mut json = serde_json::to_string(¬ify).unwrap();
|
||||
json.push('\n');
|
||||
|
||||
let mut keep = vec![];
|
||||
// TODO do simultaneously
|
||||
for tx in self.tx_streams.iter_mut() {
|
||||
// write len + payload
|
||||
if tx.write(json.as_bytes()).await.is_err() {
|
||||
keep.push(false);
|
||||
continue;
|
||||
}
|
||||
keep.push(true);
|
||||
}
|
||||
|
||||
// could not find a better solution because async
|
||||
let mut keep = keep.into_iter();
|
||||
self.tx_streams.retain(|_| keep.next().unwrap());
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
impl Drop for AsyncFrontendListener {
|
||||
fn drop(&mut self) {
|
||||
log::debug!("remove socket: {:?}", self.socket_path);
|
||||
let _ = std::fs::remove_file(&self.socket_path);
|
||||
}
|
||||
}
|
||||
|
||||
impl Stream for AsyncFrontendListener {
|
||||
type Item = Result<FrontendRequest, IpcError>;
|
||||
fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
|
||||
if let Poll::Ready(Some(Ok(l))) = self.line_streams.poll_next_unpin(cx) {
|
||||
let request = serde_json::from_str(l.as_str()).map_err(|e| e.into());
|
||||
return Poll::Ready(Some(request));
|
||||
}
|
||||
let mut sync = false;
|
||||
while let Poll::Ready(Ok((stream, _))) = self.listener.poll_accept(cx) {
|
||||
let (rx, tx) = tokio::io::split(stream);
|
||||
let buf_reader = BufReader::new(rx);
|
||||
let lines = buf_reader.lines();
|
||||
let lines = LinesStream::new(lines);
|
||||
self.line_streams.push(lines);
|
||||
self.tx_streams.push(tx);
|
||||
sync = true;
|
||||
}
|
||||
if sync {
|
||||
Poll::Ready(Some(Ok(FrontendRequest::Sync)))
|
||||
} else {
|
||||
Poll::Pending
|
||||
}
|
||||
}
|
||||
}
|
||||
13
lan-mouse-proto/Cargo.toml
Normal file
13
lan-mouse-proto/Cargo.toml
Normal file
@@ -0,0 +1,13 @@
|
||||
[package]
|
||||
name = "lan-mouse-proto"
|
||||
description = "network protocol for lan-mouse"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
license = "GPL-3.0-or-later"
|
||||
repository = "https://github.com/feschber/lan-mouse"
|
||||
|
||||
[dependencies]
|
||||
num_enum = "0.7.2"
|
||||
thiserror = "1.0.61"
|
||||
input-event = { path = "../input-event", version = "0.2.1" }
|
||||
paste = "1.0"
|
||||
251
lan-mouse-proto/src/lib.rs
Normal file
251
lan-mouse-proto/src/lib.rs
Normal file
@@ -0,0 +1,251 @@
|
||||
use input_event::{Event as InputEvent, KeyboardEvent, PointerEvent};
|
||||
use num_enum::{IntoPrimitive, TryFromPrimitive, TryFromPrimitiveError};
|
||||
use paste::paste;
|
||||
use std::{
|
||||
fmt::{Debug, Display},
|
||||
mem::size_of,
|
||||
};
|
||||
use thiserror::Error;
|
||||
|
||||
/// defines the maximum size an encoded event can take up
|
||||
/// this is currently the pointer motion event
|
||||
/// type: u8, time: u32, dx: f64, dy: f64
|
||||
pub const MAX_EVENT_SIZE: usize = size_of::<u8>() + size_of::<u32>() + 2 * size_of::<f64>();
|
||||
|
||||
/// error type for protocol violations
|
||||
#[derive(Debug, Error)]
|
||||
pub enum ProtocolError {
|
||||
/// event type does not exist
|
||||
#[error("invalid event id: `{0}`")]
|
||||
InvalidEventId(#[from] TryFromPrimitiveError<EventType>),
|
||||
}
|
||||
|
||||
/// main lan-mouse protocol event type
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub enum ProtoEvent {
|
||||
/// notify a client that the cursor entered its region
|
||||
/// [`ProtoEvent::Ack`] with the same serial is used for synchronization between devices
|
||||
Enter(u32),
|
||||
/// notify a client that the cursor left its region
|
||||
/// [`ProtoEvent::Ack`] with the same serial is used for synchronization between devices
|
||||
Leave(u32),
|
||||
/// acknowledge of an [`ProtoEvent::Enter`] or [`ProtoEvent::Leave`] event
|
||||
Ack(u32),
|
||||
/// Input event
|
||||
Input(InputEvent),
|
||||
/// Ping event for tracking unresponsive clients.
|
||||
/// A client has to respond with [`ProtoEvent::Pong`].
|
||||
Ping,
|
||||
/// Response to [`ProtoEvent::Ping`]
|
||||
Pong,
|
||||
}
|
||||
|
||||
impl Display for ProtoEvent {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
ProtoEvent::Enter(s) => write!(f, "Enter({s})"),
|
||||
ProtoEvent::Leave(s) => write!(f, "Leave({s})"),
|
||||
ProtoEvent::Ack(s) => write!(f, "Ack({s})"),
|
||||
ProtoEvent::Input(e) => write!(f, "{e}"),
|
||||
ProtoEvent::Ping => write!(f, "ping"),
|
||||
ProtoEvent::Pong => write!(f, "pong"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(TryFromPrimitive, IntoPrimitive)]
|
||||
#[repr(u8)]
|
||||
pub enum EventType {
|
||||
PointerMotion,
|
||||
PointerButton,
|
||||
PointerAxis,
|
||||
PointerAxisValue120,
|
||||
KeyboardKey,
|
||||
KeyboardModifiers,
|
||||
Ping,
|
||||
Pong,
|
||||
Enter,
|
||||
Leave,
|
||||
Ack,
|
||||
}
|
||||
|
||||
impl ProtoEvent {
|
||||
fn event_type(&self) -> EventType {
|
||||
match self {
|
||||
ProtoEvent::Input(e) => match e {
|
||||
InputEvent::Pointer(p) => match p {
|
||||
PointerEvent::Motion { .. } => EventType::PointerMotion,
|
||||
PointerEvent::Button { .. } => EventType::PointerButton,
|
||||
PointerEvent::Axis { .. } => EventType::PointerAxis,
|
||||
PointerEvent::AxisDiscrete120 { .. } => EventType::PointerAxisValue120,
|
||||
},
|
||||
InputEvent::Keyboard(k) => match k {
|
||||
KeyboardEvent::Key { .. } => EventType::KeyboardKey,
|
||||
KeyboardEvent::Modifiers { .. } => EventType::KeyboardModifiers,
|
||||
},
|
||||
},
|
||||
ProtoEvent::Ping => EventType::Ping,
|
||||
ProtoEvent::Pong => EventType::Pong,
|
||||
ProtoEvent::Enter(_) => EventType::Enter,
|
||||
ProtoEvent::Leave(_) => EventType::Leave,
|
||||
ProtoEvent::Ack(_) => EventType::Ack,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<[u8; MAX_EVENT_SIZE]> for ProtoEvent {
|
||||
type Error = ProtocolError;
|
||||
|
||||
fn try_from(buf: [u8; MAX_EVENT_SIZE]) -> Result<Self, Self::Error> {
|
||||
let mut buf = &buf[..];
|
||||
let event_type = decode_u8(&mut buf)?;
|
||||
match EventType::try_from(event_type)? {
|
||||
EventType::PointerMotion => {
|
||||
Ok(Self::Input(InputEvent::Pointer(PointerEvent::Motion {
|
||||
time: decode_u32(&mut buf)?,
|
||||
dx: decode_f64(&mut buf)?,
|
||||
dy: decode_f64(&mut buf)?,
|
||||
})))
|
||||
}
|
||||
EventType::PointerButton => {
|
||||
Ok(Self::Input(InputEvent::Pointer(PointerEvent::Button {
|
||||
time: decode_u32(&mut buf)?,
|
||||
button: decode_u32(&mut buf)?,
|
||||
state: decode_u32(&mut buf)?,
|
||||
})))
|
||||
}
|
||||
EventType::PointerAxis => Ok(Self::Input(InputEvent::Pointer(PointerEvent::Axis {
|
||||
time: decode_u32(&mut buf)?,
|
||||
axis: decode_u8(&mut buf)?,
|
||||
value: decode_f64(&mut buf)?,
|
||||
}))),
|
||||
EventType::PointerAxisValue120 => Ok(Self::Input(InputEvent::Pointer(
|
||||
PointerEvent::AxisDiscrete120 {
|
||||
axis: decode_u8(&mut buf)?,
|
||||
value: decode_i32(&mut buf)?,
|
||||
},
|
||||
))),
|
||||
EventType::KeyboardKey => Ok(Self::Input(InputEvent::Keyboard(KeyboardEvent::Key {
|
||||
time: decode_u32(&mut buf)?,
|
||||
key: decode_u32(&mut buf)?,
|
||||
state: decode_u8(&mut buf)?,
|
||||
}))),
|
||||
EventType::KeyboardModifiers => Ok(Self::Input(InputEvent::Keyboard(
|
||||
KeyboardEvent::Modifiers {
|
||||
depressed: decode_u32(&mut buf)?,
|
||||
latched: decode_u32(&mut buf)?,
|
||||
locked: decode_u32(&mut buf)?,
|
||||
group: decode_u32(&mut buf)?,
|
||||
},
|
||||
))),
|
||||
EventType::Ping => Ok(Self::Ping),
|
||||
EventType::Pong => Ok(Self::Pong),
|
||||
EventType::Enter => Ok(Self::Enter(decode_u32(&mut buf)?)),
|
||||
EventType::Leave => Ok(Self::Leave(decode_u32(&mut buf)?)),
|
||||
EventType::Ack => Ok(Self::Ack(decode_u32(&mut buf)?)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ProtoEvent> for ([u8; MAX_EVENT_SIZE], usize) {
|
||||
fn from(event: ProtoEvent) -> Self {
|
||||
let mut buf = [0u8; MAX_EVENT_SIZE];
|
||||
let mut len = 0usize;
|
||||
{
|
||||
let mut buf = &mut buf[..];
|
||||
let buf = &mut buf;
|
||||
let len = &mut len;
|
||||
encode_u8(buf, len, event.event_type() as u8);
|
||||
match event {
|
||||
ProtoEvent::Input(event) => match event {
|
||||
InputEvent::Pointer(p) => match p {
|
||||
PointerEvent::Motion { time, dx, dy } => {
|
||||
encode_u32(buf, len, time);
|
||||
encode_f64(buf, len, dx);
|
||||
encode_f64(buf, len, dy);
|
||||
}
|
||||
PointerEvent::Button {
|
||||
time,
|
||||
button,
|
||||
state,
|
||||
} => {
|
||||
encode_u32(buf, len, time);
|
||||
encode_u32(buf, len, button);
|
||||
encode_u32(buf, len, state);
|
||||
}
|
||||
PointerEvent::Axis { time, axis, value } => {
|
||||
encode_u32(buf, len, time);
|
||||
encode_u8(buf, len, axis);
|
||||
encode_f64(buf, len, value);
|
||||
}
|
||||
PointerEvent::AxisDiscrete120 { axis, value } => {
|
||||
encode_u8(buf, len, axis);
|
||||
encode_i32(buf, len, value);
|
||||
}
|
||||
},
|
||||
InputEvent::Keyboard(k) => match k {
|
||||
KeyboardEvent::Key { time, key, state } => {
|
||||
encode_u32(buf, len, time);
|
||||
encode_u32(buf, len, key);
|
||||
encode_u8(buf, len, state);
|
||||
}
|
||||
KeyboardEvent::Modifiers {
|
||||
depressed,
|
||||
latched,
|
||||
locked,
|
||||
group,
|
||||
} => {
|
||||
encode_u32(buf, len, depressed);
|
||||
encode_u32(buf, len, latched);
|
||||
encode_u32(buf, len, locked);
|
||||
encode_u32(buf, len, group);
|
||||
}
|
||||
},
|
||||
},
|
||||
ProtoEvent::Ping => {}
|
||||
ProtoEvent::Pong => {}
|
||||
ProtoEvent::Enter(serial) => encode_u32(buf, len, serial),
|
||||
ProtoEvent::Leave(serial) => encode_u32(buf, len, serial),
|
||||
ProtoEvent::Ack(serial) => encode_u32(buf, len, serial),
|
||||
}
|
||||
}
|
||||
(buf, len)
|
||||
}
|
||||
}
|
||||
|
||||
macro_rules! decode_impl {
|
||||
($t:ty) => {
|
||||
paste! {
|
||||
fn [<decode_ $t>](data: &mut &[u8]) -> Result<$t, ProtocolError> {
|
||||
let (int_bytes, rest) = data.split_at(size_of::<$t>());
|
||||
*data = rest;
|
||||
Ok($t::from_be_bytes(int_bytes.try_into().unwrap()))
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
decode_impl!(u8);
|
||||
decode_impl!(u32);
|
||||
decode_impl!(i32);
|
||||
decode_impl!(f64);
|
||||
|
||||
macro_rules! encode_impl {
|
||||
($t:ty) => {
|
||||
paste! {
|
||||
fn [<encode_ $t>](buf: &mut &mut [u8], amt: &mut usize, n: $t) {
|
||||
let src = n.to_be_bytes();
|
||||
let data = std::mem::take(buf);
|
||||
let (int_bytes, rest) = data.split_at_mut(size_of::<$t>());
|
||||
int_bytes.copy_from_slice(&src);
|
||||
*amt += size_of::<$t>();
|
||||
*buf = rest
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
encode_impl!(u8);
|
||||
encode_impl!(u32);
|
||||
encode_impl!(i32);
|
||||
encode_impl!(f64);
|
||||
43
nix/README.md
Normal file
43
nix/README.md
Normal file
@@ -0,0 +1,43 @@
|
||||
# Nix Flake Usage
|
||||
|
||||
## run
|
||||
|
||||
```bash
|
||||
nix run github:feschber/lan-mouse
|
||||
|
||||
# with params
|
||||
nix run github:feschber/lan-mouse -- --help
|
||||
|
||||
```
|
||||
|
||||
## home-manager module
|
||||
|
||||
add input
|
||||
|
||||
```nix
|
||||
inputs = {
|
||||
lan-mouse.url = "github:feschber/lan-mouse";
|
||||
}
|
||||
```
|
||||
|
||||
enable lan-mouse
|
||||
|
||||
``` nix
|
||||
{
|
||||
inputs,
|
||||
...
|
||||
}: {
|
||||
# add the home manager module
|
||||
imports = [inputs.lan-mouse.homeManagerModules.default];
|
||||
|
||||
programs.lan-mouse = {
|
||||
enable = true;
|
||||
# systemd = false;
|
||||
# package = inputs.lan-mouse.packages.${pkgs.stdenv.hostPlatform.system}.default
|
||||
# Optional configuration in nix syntax, see config.toml for available options
|
||||
# settings = { };
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
```
|
||||
61
nix/default.nix
Normal file
61
nix/default.nix
Normal file
@@ -0,0 +1,61 @@
|
||||
{
|
||||
rustPlatform,
|
||||
lib,
|
||||
pkgs,
|
||||
}: let
|
||||
cargoToml = builtins.fromTOML (builtins.readFile ../Cargo.toml);
|
||||
pname = cargoToml.package.name;
|
||||
version = cargoToml.package.version;
|
||||
in
|
||||
rustPlatform.buildRustPackage {
|
||||
pname = pname;
|
||||
version = version;
|
||||
|
||||
nativeBuildInputs = with pkgs; [
|
||||
git
|
||||
pkg-config
|
||||
cmake
|
||||
makeWrapper
|
||||
buildPackages.gtk4
|
||||
];
|
||||
|
||||
buildInputs = with pkgs; [
|
||||
xorg.libX11
|
||||
gtk4
|
||||
libadwaita
|
||||
xorg.libXtst
|
||||
] ++ lib.optionals stdenv.isDarwin
|
||||
(with darwin.apple_sdk_11_0.frameworks; [
|
||||
CoreGraphics
|
||||
ApplicationServices
|
||||
]);
|
||||
|
||||
src = builtins.path {
|
||||
name = pname;
|
||||
path = lib.cleanSource ../.;
|
||||
};
|
||||
|
||||
cargoLock.lockFile = ../Cargo.lock;
|
||||
|
||||
# Set Environment Variables
|
||||
RUST_BACKTRACE = "full";
|
||||
|
||||
# Needed to enable support for SVG icons in GTK
|
||||
postInstall = ''
|
||||
wrapProgram "$out/bin/lan-mouse" \
|
||||
--set GDK_PIXBUF_MODULE_FILE ${pkgs.librsvg.out}/lib/gdk-pixbuf-2.0/2.10.0/loaders.cache
|
||||
|
||||
install -Dm444 *.desktop -t $out/share/applications
|
||||
install -Dm444 lan-mouse-gtk/resources/*.svg -t $out/share/icons/hicolor/scalable/apps
|
||||
'';
|
||||
|
||||
meta = with lib; {
|
||||
description = "Lan Mouse is a mouse and keyboard sharing software";
|
||||
longDescription = ''
|
||||
Lan Mouse is a mouse and keyboard sharing software similar to universal-control on Apple devices. It allows for using multiple pcs with a single set of mouse and keyboard. This is also known as a Software KVM switch.
|
||||
The primary target is Wayland on Linux but Windows and MacOS and Linux on Xorg have partial support as well (see below for more details).
|
||||
'';
|
||||
mainProgram = pname;
|
||||
platforms = platforms.all;
|
||||
};
|
||||
}
|
||||
82
nix/hm-module.nix
Normal file
82
nix/hm-module.nix
Normal file
@@ -0,0 +1,82 @@
|
||||
self: {
|
||||
config,
|
||||
pkgs,
|
||||
lib,
|
||||
...
|
||||
}:
|
||||
with lib; let
|
||||
cfg = config.programs.lan-mouse;
|
||||
defaultPackage = self.packages.${pkgs.stdenv.hostPlatform.system}.default;
|
||||
tomlFormat = pkgs.formats.toml {};
|
||||
in {
|
||||
options.programs.lan-mouse = with types; {
|
||||
enable = mkEnableOption "Whether or not to enable lan-mouse.";
|
||||
package = mkOption {
|
||||
type = with types; nullOr package;
|
||||
default = defaultPackage;
|
||||
defaultText = literalExpression "inputs.lan-mouse.packages.${pkgs.stdenv.hostPlatform.system}.default";
|
||||
description = ''
|
||||
The lan-mouse package to use.
|
||||
|
||||
By default, this option will use the `packages.default` as exposed by this flake.
|
||||
'';
|
||||
};
|
||||
systemd = mkOption {
|
||||
type = types.bool;
|
||||
default = pkgs.stdenv.isLinux;
|
||||
description = "Whether to enable to systemd service for lan-mouse on linux.";
|
||||
};
|
||||
launchd = mkOption {
|
||||
type = types.bool;
|
||||
default = pkgs.stdenv.isDarwin;
|
||||
description = "Whether to enable to launchd service for lan-mouse on macOS.";
|
||||
};
|
||||
settings = lib.mkOption {
|
||||
inherit (tomlFormat) type;
|
||||
default = {};
|
||||
example = builtins.fromTOML (builtins.readFile (self + /config.toml));
|
||||
description = ''
|
||||
Optional configuration written to {file}`$XDG_CONFIG_HOME/lan-mouse/config.toml`.
|
||||
|
||||
See <https://github.com/feschber/lan-mouse/> for
|
||||
available options and documentation.
|
||||
'';
|
||||
};
|
||||
};
|
||||
|
||||
config = mkIf cfg.enable {
|
||||
systemd.user.services.lan-mouse = lib.mkIf cfg.systemd {
|
||||
Unit = {
|
||||
Description = "Systemd service for Lan Mouse";
|
||||
Requires = ["graphical-session.target"];
|
||||
};
|
||||
Service = {
|
||||
Type = "simple";
|
||||
ExecStart = "${cfg.package}/bin/lan-mouse --daemon";
|
||||
};
|
||||
Install.WantedBy = [
|
||||
(lib.mkIf config.wayland.windowManager.hyprland.systemd.enable "hyprland-session.target")
|
||||
(lib.mkIf config.wayland.windowManager.sway.systemd.enable "sway-session.target")
|
||||
];
|
||||
};
|
||||
|
||||
launchd.agents.lan-mouse = lib.mkIf cfg.launchd {
|
||||
enable = true;
|
||||
config = {
|
||||
ProgramArguments = [
|
||||
"${cfg.package}/bin/lan-mouse"
|
||||
"--daemon"
|
||||
];
|
||||
KeepAlive = true;
|
||||
};
|
||||
};
|
||||
|
||||
home.packages = [
|
||||
cfg.package
|
||||
];
|
||||
|
||||
xdg.configFile."lan-mouse/config.toml" = lib.mkIf (cfg.settings != {}) {
|
||||
source = tomlFormat.generate "config.toml" cfg.settings;
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
#delete-button {
|
||||
color: @red_1;
|
||||
}
|
||||
|
||||
#port-edit-cancel {
|
||||
color: @red_1;
|
||||
}
|
||||
|
||||
#port-edit-apply {
|
||||
color: @green_1;
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
#delete-button {
|
||||
color: @red_3;
|
||||
}
|
||||
|
||||
#port-edit-cancel {
|
||||
color: @red_3;
|
||||
}
|
||||
|
||||
#port-edit-apply {
|
||||
color: @green_3;
|
||||
}
|
||||
@@ -1,146 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<interface>
|
||||
<requires lib="gtk" version="4.0"/>
|
||||
<requires lib="libadwaita" version="1.0"/>
|
||||
<menu id="main-menu">
|
||||
<item>
|
||||
<attribute name="label" translatable="yes">_Close window</attribute>
|
||||
<attribute name="action">window.close</attribute>
|
||||
</item>
|
||||
</menu>
|
||||
<template class="LanMouseWindow" parent="AdwApplicationWindow">
|
||||
<property name="width-request">600</property>
|
||||
<property name="height-request">700</property>
|
||||
<property name="title" translatable="yes">Lan Mouse</property>
|
||||
<property name="show-menubar">True</property>
|
||||
<property name="content">
|
||||
<object class="GtkBox">
|
||||
<property name="orientation">vertical</property>
|
||||
<child type="top">
|
||||
<object class="AdwHeaderBar">
|
||||
<child type ="end">
|
||||
<object class="GtkMenuButton">
|
||||
<property name="icon-name">open-menu-symbolic</property>
|
||||
<property name="menu-model">main-menu</property>
|
||||
</object>
|
||||
</child>
|
||||
<style>
|
||||
<class name="flat"/>
|
||||
</style>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="AdwToastOverlay" id="toast_overlay">
|
||||
<child>
|
||||
<object class="AdwStatusPage">
|
||||
<property name="title" translatable="yes">Lan Mouse</property>
|
||||
<property name="description" translatable="yes">easily use your mouse and keyboard on multiple computers</property>
|
||||
<property name="icon-name">mouse-icon</property>
|
||||
<property name="child">
|
||||
<object class="AdwClamp">
|
||||
<property name="maximum-size">600</property>
|
||||
<property name="tightening-threshold">0</property>
|
||||
<property name="child">
|
||||
<object class="GtkBox">
|
||||
<property name="orientation">vertical</property>
|
||||
<property name="spacing">12</property>
|
||||
<child>
|
||||
<object class="AdwPreferencesGroup">
|
||||
<property name="title" translatable="yes">General</property>
|
||||
<!--
|
||||
<child>
|
||||
<object class="AdwActionRow">
|
||||
<property name="title" translatable="yes">enable</property>
|
||||
<child type="suffix">
|
||||
<object class="GtkSwitch">
|
||||
<property name="valign">center</property>
|
||||
<property name="tooltip-text" translatable="yes">enable</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
-->
|
||||
<child>
|
||||
<object class="AdwActionRow">
|
||||
<property name="title">port</property>
|
||||
<child>
|
||||
<object class="GtkEntry" id="port_entry">
|
||||
<signal name="activate" handler="handle_port_edit_apply" swapped="true"/>
|
||||
<signal name="changed" handler="handle_port_changed" swapped="true"/>
|
||||
<!-- <signal name="delete-text" handler="handle_port_changed" swapped="true"/> -->
|
||||
<!-- <property name="title" translatable="yes">port</property> -->
|
||||
<property name="placeholder-text">4242</property>
|
||||
<property name="width-chars">5</property>
|
||||
<property name="xalign">0.5</property>
|
||||
<property name="valign">center</property>
|
||||
<!-- <property name="show-apply-button">True</property> -->
|
||||
<property name="input-purpose">GTK_INPUT_PURPOSE_DIGITS</property>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkButton" id="port_edit_apply">
|
||||
<signal name="clicked" handler="handle_port_edit_apply" swapped="true"/>
|
||||
<property name="icon-name">object-select-symbolic</property>
|
||||
<property name="valign">center</property>
|
||||
<property name="visible">false</property>
|
||||
<property name="name">port-edit-apply</property>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkButton" id="port_edit_cancel">
|
||||
<signal name="clicked" handler="handle_port_edit_cancel" swapped="true"/>
|
||||
<property name="icon-name">process-stop-symbolic</property>
|
||||
<property name="valign">center</property>
|
||||
<property name="visible">false</property>
|
||||
<property name="name">port-edit-cancel</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="AdwPreferencesGroup">
|
||||
<property name="title" translatable="yes">Connections</property>
|
||||
<property name="header-suffix">
|
||||
<object class="GtkButton">
|
||||
<signal name="clicked" handler="handle_add_client_pressed" swapped="true"/>
|
||||
<property name="child">
|
||||
<object class="AdwButtonContent">
|
||||
<property name="icon-name">list-add-symbolic</property>
|
||||
<property name="label" translatable="yes">Add</property>
|
||||
</object>
|
||||
</property>
|
||||
<style>
|
||||
<class name="flat"/>
|
||||
</style>
|
||||
</object>
|
||||
</property>
|
||||
<child>
|
||||
<object class="GtkListBox" id="client_list">
|
||||
<property name="selection-mode">none</property>
|
||||
<child type="placeholder">
|
||||
<object class="AdwActionRow" id="client_placeholder">
|
||||
<property name="title">No connections!</property>
|
||||
<property name="subtitle">add a new client via the + button</property>
|
||||
</object>
|
||||
</child>
|
||||
<style>
|
||||
<class name="boxed-list" />
|
||||
</style>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</property>
|
||||
</object>
|
||||
</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</property>
|
||||
</template>
|
||||
</interface>
|
||||
13
service/lan-mouse.service
Normal file
13
service/lan-mouse.service
Normal file
@@ -0,0 +1,13 @@
|
||||
[Unit]
|
||||
Description=Lan Mouse
|
||||
# lan mouse needs an active graphical session
|
||||
After=graphical-session.target
|
||||
# make sure the service terminates with the graphical session
|
||||
BindsTo=graphical-session.target
|
||||
|
||||
[Service]
|
||||
ExecStart=/usr/bin/lan-mouse --daemon
|
||||
Restart=on-failure
|
||||
|
||||
[Install]
|
||||
WantedBy=graphical-session.target
|
||||
1
shell.nix
Normal file
1
shell.nix
Normal file
@@ -0,0 +1 @@
|
||||
(builtins.getFlake ("git+file://" + toString ./.)).devShells.${builtins.currentSystem}.default
|
||||
@@ -1,2 +0,0 @@
|
||||
pub mod consumer;
|
||||
pub mod producer;
|
||||
@@ -1,20 +0,0 @@
|
||||
#[cfg(windows)]
|
||||
pub mod windows;
|
||||
|
||||
#[cfg(all(unix, feature = "x11", not(target_os = "macos")))]
|
||||
pub mod x11;
|
||||
|
||||
#[cfg(all(unix, feature = "wayland", not(target_os = "macos")))]
|
||||
pub mod wlroots;
|
||||
|
||||
#[cfg(all(unix, feature = "xdg_desktop_portal", not(target_os = "macos")))]
|
||||
pub mod xdg_desktop_portal;
|
||||
|
||||
#[cfg(all(unix, feature = "libei", not(target_os = "macos")))]
|
||||
pub mod libei;
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
pub mod macos;
|
||||
|
||||
/// fallback consumer
|
||||
pub mod dummy;
|
||||
@@ -1,26 +0,0 @@
|
||||
use crate::{
|
||||
client::{ClientEvent, ClientHandle},
|
||||
consumer::EventConsumer,
|
||||
event::Event,
|
||||
};
|
||||
use async_trait::async_trait;
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct DummyConsumer;
|
||||
|
||||
impl DummyConsumer {
|
||||
pub fn new() -> Self {
|
||||
Self {}
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl EventConsumer for DummyConsumer {
|
||||
async fn consume(&mut self, event: Event, client_handle: ClientHandle) {
|
||||
log::info!("received event: ({client_handle}) {event}");
|
||||
}
|
||||
async fn notify(&mut self, client_event: ClientEvent) {
|
||||
log::info!("{client_event:?}");
|
||||
}
|
||||
async fn destroy(&mut self) {}
|
||||
}
|
||||
@@ -1,381 +0,0 @@
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
io,
|
||||
os::{
|
||||
fd::{FromRawFd, RawFd},
|
||||
unix::net::UnixStream,
|
||||
},
|
||||
time::{SystemTime, UNIX_EPOCH},
|
||||
};
|
||||
|
||||
use anyhow::{anyhow, Result};
|
||||
use ashpd::desktop::remote_desktop::{DeviceType, RemoteDesktop};
|
||||
use async_trait::async_trait;
|
||||
use futures::StreamExt;
|
||||
|
||||
use reis::{
|
||||
ei::{self, button::ButtonState, handshake::ContextType, keyboard::KeyState},
|
||||
tokio::EiEventStream,
|
||||
PendingRequestResult,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
client::{ClientEvent, ClientHandle},
|
||||
consumer::EventConsumer,
|
||||
event::Event,
|
||||
};
|
||||
|
||||
pub struct LibeiConsumer {
|
||||
handshake: bool,
|
||||
context: ei::Context,
|
||||
events: EiEventStream,
|
||||
pointer: Option<(ei::Device, ei::Pointer)>,
|
||||
has_pointer: bool,
|
||||
scroll: Option<(ei::Device, ei::Scroll)>,
|
||||
has_scroll: bool,
|
||||
button: Option<(ei::Device, ei::Button)>,
|
||||
has_button: bool,
|
||||
keyboard: Option<(ei::Device, ei::Keyboard)>,
|
||||
has_keyboard: bool,
|
||||
capabilities: HashMap<String, u64>,
|
||||
capability_mask: u64,
|
||||
sequence: u32,
|
||||
serial: u32,
|
||||
}
|
||||
|
||||
async fn get_ei_fd() -> Result<RawFd, ashpd::Error> {
|
||||
let proxy = RemoteDesktop::new().await?;
|
||||
let session = proxy.create_session().await?;
|
||||
|
||||
// I HATE EVERYTHING, THIS TOOK 8 HOURS OF DEBUGGING
|
||||
proxy
|
||||
.select_devices(
|
||||
&session,
|
||||
DeviceType::Pointer | DeviceType::Keyboard | DeviceType::Touchscreen,
|
||||
)
|
||||
.await?;
|
||||
|
||||
proxy
|
||||
.start(&session, &ashpd::WindowIdentifier::default())
|
||||
.await?
|
||||
.response()?;
|
||||
proxy.connect_to_eis(&session).await
|
||||
}
|
||||
|
||||
impl LibeiConsumer {
|
||||
pub async fn new() -> Result<Self> {
|
||||
// fd is owned by the message, so we need to dup it
|
||||
let eifd = get_ei_fd().await?;
|
||||
let eifd = unsafe {
|
||||
let ret = libc::dup(eifd);
|
||||
if ret < 0 {
|
||||
Err(io::Error::last_os_error())
|
||||
} else {
|
||||
Ok(ret)
|
||||
}
|
||||
}?;
|
||||
let stream = unsafe { UnixStream::from_raw_fd(eifd) };
|
||||
// let stream = UnixStream::connect("/run/user/1000/eis-0")?;
|
||||
stream.set_nonblocking(true)?;
|
||||
let context = ei::Context::new(stream)?;
|
||||
context.flush()?;
|
||||
let events = EiEventStream::new(context.clone())?;
|
||||
Ok(Self {
|
||||
handshake: false,
|
||||
context,
|
||||
events,
|
||||
pointer: None,
|
||||
button: None,
|
||||
scroll: None,
|
||||
keyboard: None,
|
||||
has_pointer: false,
|
||||
has_button: false,
|
||||
has_scroll: false,
|
||||
has_keyboard: false,
|
||||
capabilities: HashMap::new(),
|
||||
capability_mask: 0,
|
||||
sequence: 0,
|
||||
serial: 0,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl EventConsumer for LibeiConsumer {
|
||||
async fn consume(&mut self, event: Event, _client_handle: ClientHandle) {
|
||||
let now = SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.unwrap()
|
||||
.as_micros() as u64;
|
||||
match event {
|
||||
Event::Pointer(p) => match p {
|
||||
crate::event::PointerEvent::Motion {
|
||||
time: _,
|
||||
relative_x,
|
||||
relative_y,
|
||||
} => {
|
||||
if !self.has_pointer {
|
||||
return;
|
||||
}
|
||||
if let Some((d, p)) = self.pointer.as_mut() {
|
||||
p.motion_relative(relative_x as f32, relative_y as f32);
|
||||
d.frame(self.serial, now);
|
||||
}
|
||||
}
|
||||
crate::event::PointerEvent::Button {
|
||||
time: _,
|
||||
button,
|
||||
state,
|
||||
} => {
|
||||
if !self.has_button {
|
||||
return;
|
||||
}
|
||||
if let Some((d, b)) = self.button.as_mut() {
|
||||
b.button(
|
||||
button,
|
||||
match state {
|
||||
0 => ButtonState::Released,
|
||||
_ => ButtonState::Press,
|
||||
},
|
||||
);
|
||||
d.frame(self.serial, now);
|
||||
}
|
||||
}
|
||||
crate::event::PointerEvent::Axis {
|
||||
time: _,
|
||||
axis,
|
||||
value,
|
||||
} => {
|
||||
if !self.has_scroll {
|
||||
return;
|
||||
}
|
||||
if let Some((d, s)) = self.scroll.as_mut() {
|
||||
match axis {
|
||||
0 => s.scroll(0., value as f32),
|
||||
_ => s.scroll(value as f32, 0.),
|
||||
}
|
||||
d.frame(self.serial, now);
|
||||
}
|
||||
}
|
||||
crate::event::PointerEvent::Frame {} => {}
|
||||
},
|
||||
Event::Keyboard(k) => match k {
|
||||
crate::event::KeyboardEvent::Key {
|
||||
time: _,
|
||||
key,
|
||||
state,
|
||||
} => {
|
||||
if !self.has_keyboard {
|
||||
return;
|
||||
}
|
||||
if let Some((d, k)) = &mut self.keyboard {
|
||||
k.key(
|
||||
key,
|
||||
match state {
|
||||
0 => KeyState::Released,
|
||||
_ => KeyState::Press,
|
||||
},
|
||||
);
|
||||
d.frame(self.serial, now);
|
||||
}
|
||||
}
|
||||
crate::event::KeyboardEvent::Modifiers { .. } => {}
|
||||
},
|
||||
_ => {}
|
||||
}
|
||||
self.context.flush().unwrap();
|
||||
}
|
||||
|
||||
async fn dispatch(&mut self) -> Result<()> {
|
||||
let event = match self.events.next().await {
|
||||
Some(e) => e?,
|
||||
None => return Err(anyhow!("libei connection lost")),
|
||||
};
|
||||
let event = match event {
|
||||
PendingRequestResult::Request(result) => result,
|
||||
PendingRequestResult::ParseError(e) => {
|
||||
return Err(anyhow!("libei protocol violation: {e}"))
|
||||
}
|
||||
PendingRequestResult::InvalidObject(e) => return Err(anyhow!("invalid object {e}")),
|
||||
};
|
||||
match event {
|
||||
ei::Event::Handshake(handshake, request) => match request {
|
||||
ei::handshake::Event::HandshakeVersion { version } => {
|
||||
if self.handshake {
|
||||
return Ok(());
|
||||
}
|
||||
log::info!("libei version {}", version);
|
||||
// sender means we are sending events _to_ the eis server
|
||||
handshake.handshake_version(version); // FIXME
|
||||
handshake.context_type(ContextType::Sender);
|
||||
handshake.name("ei-demo-client");
|
||||
handshake.interface_version("ei_connection", 1);
|
||||
handshake.interface_version("ei_callback", 1);
|
||||
handshake.interface_version("ei_pingpong", 1);
|
||||
handshake.interface_version("ei_seat", 1);
|
||||
handshake.interface_version("ei_device", 2);
|
||||
handshake.interface_version("ei_pointer", 1);
|
||||
handshake.interface_version("ei_pointer_absolute", 1);
|
||||
handshake.interface_version("ei_scroll", 1);
|
||||
handshake.interface_version("ei_button", 1);
|
||||
handshake.interface_version("ei_keyboard", 1);
|
||||
handshake.interface_version("ei_touchscreen", 1);
|
||||
handshake.finish();
|
||||
self.handshake = true;
|
||||
}
|
||||
ei::handshake::Event::InterfaceVersion { name, version } => {
|
||||
log::debug!("handshake: Interface {name} @ {version}");
|
||||
}
|
||||
ei::handshake::Event::Connection { serial, connection } => {
|
||||
connection.sync(1);
|
||||
self.serial = serial;
|
||||
}
|
||||
_ => unreachable!(),
|
||||
},
|
||||
ei::Event::Connection(_connection, request) => match request {
|
||||
ei::connection::Event::Seat { seat } => {
|
||||
log::debug!("connected to seat: {seat:?}");
|
||||
}
|
||||
ei::connection::Event::Ping { ping } => {
|
||||
ping.done(0);
|
||||
}
|
||||
ei::connection::Event::Disconnected {
|
||||
last_serial: _,
|
||||
reason,
|
||||
explanation,
|
||||
} => {
|
||||
log::debug!("ei - disconnected: reason: {reason:?}: {explanation}")
|
||||
}
|
||||
ei::connection::Event::InvalidObject {
|
||||
last_serial,
|
||||
invalid_id,
|
||||
} => {
|
||||
return Err(anyhow!(
|
||||
"invalid object: id: {invalid_id}, serial: {last_serial}"
|
||||
));
|
||||
}
|
||||
_ => unreachable!(),
|
||||
},
|
||||
ei::Event::Device(device, request) => match request {
|
||||
ei::device::Event::Destroyed { serial } => {
|
||||
log::debug!("device destroyed: {device:?} - serial: {serial}")
|
||||
}
|
||||
ei::device::Event::Name { name } => {
|
||||
log::debug!("device name: {name}")
|
||||
}
|
||||
ei::device::Event::DeviceType { device_type } => {
|
||||
log::debug!("device type: {device_type:?}")
|
||||
}
|
||||
ei::device::Event::Dimensions { width, height } => {
|
||||
log::debug!("device dimensions: {width}x{height}")
|
||||
}
|
||||
ei::device::Event::Region {
|
||||
offset_x,
|
||||
offset_y,
|
||||
width,
|
||||
hight,
|
||||
scale,
|
||||
} => log::debug!(
|
||||
"device region: {width}x{hight} @ ({offset_x},{offset_y}), scale: {scale}"
|
||||
),
|
||||
ei::device::Event::Interface { object } => {
|
||||
log::debug!("device interface: {object:?}");
|
||||
if object.interface().eq("ei_pointer") {
|
||||
log::debug!("GOT POINTER DEVICE");
|
||||
self.pointer.replace((device, object.downcast().unwrap()));
|
||||
} else if object.interface().eq("ei_button") {
|
||||
log::debug!("GOT BUTTON DEVICE");
|
||||
self.button.replace((device, object.downcast().unwrap()));
|
||||
} else if object.interface().eq("ei_scroll") {
|
||||
log::debug!("GOT SCROLL DEVICE");
|
||||
self.scroll.replace((device, object.downcast().unwrap()));
|
||||
} else if object.interface().eq("ei_keyboard") {
|
||||
log::debug!("GOT KEYBOARD DEVICE");
|
||||
self.keyboard.replace((device, object.downcast().unwrap()));
|
||||
}
|
||||
}
|
||||
ei::device::Event::Done => {
|
||||
log::debug!("device: done {device:?}");
|
||||
}
|
||||
ei::device::Event::Resumed { serial } => {
|
||||
self.serial = serial;
|
||||
device.start_emulating(serial, self.sequence);
|
||||
self.sequence += 1;
|
||||
log::debug!("resumed: {device:?}");
|
||||
if let Some((d, _)) = &mut self.pointer {
|
||||
if d == &device {
|
||||
log::debug!("pointer resumed {serial}");
|
||||
self.has_pointer = true;
|
||||
}
|
||||
}
|
||||
if let Some((d, _)) = &mut self.button {
|
||||
if d == &device {
|
||||
log::debug!("button resumed {serial}");
|
||||
self.has_button = true;
|
||||
}
|
||||
}
|
||||
if let Some((d, _)) = &mut self.scroll {
|
||||
if d == &device {
|
||||
log::debug!("scroll resumed {serial}");
|
||||
self.has_scroll = true;
|
||||
}
|
||||
}
|
||||
if let Some((d, _)) = &mut self.keyboard {
|
||||
if d == &device {
|
||||
log::debug!("keyboard resumed {serial}");
|
||||
self.has_keyboard = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
ei::device::Event::Paused { serial } => {
|
||||
self.has_pointer = false;
|
||||
self.has_button = false;
|
||||
self.serial = serial;
|
||||
}
|
||||
ei::device::Event::StartEmulating { serial, sequence } => {
|
||||
log::debug!("start emulating {serial}, {sequence}")
|
||||
}
|
||||
ei::device::Event::StopEmulating { serial } => {
|
||||
log::debug!("stop emulating {serial}")
|
||||
}
|
||||
ei::device::Event::Frame { serial, timestamp } => {
|
||||
log::debug!("frame: {serial}, {timestamp}");
|
||||
}
|
||||
ei::device::Event::RegionMappingId { mapping_id } => {
|
||||
log::debug!("RegionMappingId {mapping_id}")
|
||||
}
|
||||
e => log::debug!("invalid event: {e:?}"),
|
||||
},
|
||||
ei::Event::Seat(seat, request) => match request {
|
||||
ei::seat::Event::Destroyed { serial } => {
|
||||
self.serial = serial;
|
||||
log::debug!("seat destroyed: {seat:?}");
|
||||
}
|
||||
ei::seat::Event::Name { name } => {
|
||||
log::debug!("seat name: {name}");
|
||||
}
|
||||
ei::seat::Event::Capability { mask, interface } => {
|
||||
log::debug!("seat capabilities: {mask}, interface: {interface:?}");
|
||||
self.capabilities.insert(interface, mask);
|
||||
self.capability_mask |= mask;
|
||||
}
|
||||
ei::seat::Event::Done => {
|
||||
log::debug!("seat done");
|
||||
log::debug!("binding capabilities: {}", self.capability_mask);
|
||||
seat.bind(self.capability_mask);
|
||||
}
|
||||
ei::seat::Event::Device { device } => {
|
||||
log::debug!("seat: new device - {device:?}");
|
||||
}
|
||||
_ => todo!(),
|
||||
},
|
||||
e => log::debug!("unhandled event: {e:?}"),
|
||||
}
|
||||
self.context.flush()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn notify(&mut self, _client_event: ClientEvent) {}
|
||||
|
||||
async fn destroy(&mut self) {}
|
||||
}
|
||||
@@ -1,238 +0,0 @@
|
||||
use crate::client::{ClientEvent, ClientHandle};
|
||||
use crate::consumer::EventConsumer;
|
||||
use crate::event::{Event, KeyboardEvent, PointerEvent};
|
||||
use anyhow::{anyhow, Result};
|
||||
use async_trait::async_trait;
|
||||
use core_graphics::display::{CGDisplayBounds, CGMainDisplayID, CGPoint};
|
||||
use core_graphics::event::{
|
||||
CGEvent, CGEventTapLocation, CGEventType, CGMouseButton, EventField, ScrollEventUnit,
|
||||
};
|
||||
use core_graphics::event_source::{CGEventSource, CGEventSourceStateID};
|
||||
use std::ops::{Index, IndexMut};
|
||||
|
||||
pub struct MacOSConsumer {
|
||||
pub event_source: CGEventSource,
|
||||
button_state: ButtonState,
|
||||
}
|
||||
|
||||
struct ButtonState {
|
||||
left: bool,
|
||||
right: bool,
|
||||
center: bool,
|
||||
}
|
||||
|
||||
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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
unsafe impl Send for MacOSConsumer {}
|
||||
|
||||
impl MacOSConsumer {
|
||||
pub fn new() -> Result<Self> {
|
||||
let event_source = match CGEventSource::new(CGEventSourceStateID::CombinedSessionState) {
|
||||
Ok(e) => e,
|
||||
Err(_) => return Err(anyhow!("event source creation failed!")),
|
||||
};
|
||||
let button_state = ButtonState {
|
||||
left: false,
|
||||
right: false,
|
||||
center: false,
|
||||
};
|
||||
Ok(Self {
|
||||
event_source,
|
||||
button_state,
|
||||
})
|
||||
}
|
||||
|
||||
fn get_mouse_location(&self) -> Option<CGPoint> {
|
||||
let event: CGEvent = CGEvent::new(self.event_source.clone()).ok()?;
|
||||
Some(event.location())
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl EventConsumer for MacOSConsumer {
|
||||
async fn consume(&mut self, event: Event, _client_handle: ClientHandle) {
|
||||
match event {
|
||||
Event::Pointer(pointer_event) => match pointer_event {
|
||||
PointerEvent::Motion {
|
||||
time: _,
|
||||
relative_x,
|
||||
relative_y,
|
||||
} => {
|
||||
// FIXME secondary displays?
|
||||
let (min_x, min_y, max_x, max_y) = unsafe {
|
||||
let display = CGMainDisplayID();
|
||||
let bounds = CGDisplayBounds(display);
|
||||
let min_x = bounds.origin.x;
|
||||
let max_x = bounds.origin.x + bounds.size.width;
|
||||
let min_y = bounds.origin.y;
|
||||
let max_y = bounds.origin.y + bounds.size.height;
|
||||
(min_x as f64, min_y as f64, max_x as f64, max_y as f64)
|
||||
};
|
||||
let mut mouse_location = match self.get_mouse_location() {
|
||||
Some(l) => l,
|
||||
None => {
|
||||
log::warn!("could not get mouse location!");
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
mouse_location.x = (mouse_location.x + relative_x).clamp(min_x, max_x - 1.);
|
||||
mouse_location.y = (mouse_location.y + relative_y).clamp(min_y, max_y - 1.);
|
||||
|
||||
let mut event_type = CGEventType::MouseMoved;
|
||||
if self.button_state.left {
|
||||
event_type = CGEventType::LeftMouseDragged
|
||||
} else if self.button_state.right {
|
||||
event_type = CGEventType::RightMouseDragged
|
||||
} else if self.button_state.center {
|
||||
event_type = CGEventType::OtherMouseDragged
|
||||
};
|
||||
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;
|
||||
}
|
||||
};
|
||||
event.set_integer_value_field(
|
||||
EventField::MOUSE_EVENT_DELTA_X,
|
||||
relative_x as i64,
|
||||
);
|
||||
event.set_integer_value_field(
|
||||
EventField::MOUSE_EVENT_DELTA_Y,
|
||||
relative_y as i64,
|
||||
);
|
||||
event.post(CGEventTapLocation::HID);
|
||||
}
|
||||
PointerEvent::Button {
|
||||
time: _,
|
||||
button,
|
||||
state,
|
||||
} => {
|
||||
let (event_type, mouse_button) = match (button, state) {
|
||||
(b, 1) if b == crate::event::BTN_LEFT => {
|
||||
(CGEventType::LeftMouseDown, CGMouseButton::Left)
|
||||
}
|
||||
(b, 0) if b == crate::event::BTN_LEFT => {
|
||||
(CGEventType::LeftMouseUp, CGMouseButton::Left)
|
||||
}
|
||||
(b, 1) if b == crate::event::BTN_RIGHT => {
|
||||
(CGEventType::RightMouseDown, CGMouseButton::Right)
|
||||
}
|
||||
(b, 0) if b == crate::event::BTN_RIGHT => {
|
||||
(CGEventType::RightMouseUp, CGMouseButton::Right)
|
||||
}
|
||||
(b, 1) if b == crate::event::BTN_MIDDLE => {
|
||||
(CGEventType::OtherMouseDown, CGMouseButton::Center)
|
||||
}
|
||||
(b, 0) if b == crate::event::BTN_MIDDLE => {
|
||||
(CGEventType::OtherMouseUp, CGMouseButton::Center)
|
||||
}
|
||||
_ => {
|
||||
log::warn!("invalid button event: {button},{state}");
|
||||
return;
|
||||
}
|
||||
};
|
||||
// store button state
|
||||
self.button_state[mouse_button] = state == 1;
|
||||
|
||||
let location = self.get_mouse_location().unwrap();
|
||||
let event = match CGEvent::new_mouse_event(
|
||||
self.event_source.clone(),
|
||||
event_type,
|
||||
location,
|
||||
mouse_button,
|
||||
) {
|
||||
Ok(e) => e,
|
||||
Err(()) => {
|
||||
log::warn!("mouse event creation failed!");
|
||||
return;
|
||||
}
|
||||
};
|
||||
event.post(CGEventTapLocation::HID);
|
||||
}
|
||||
PointerEvent::Axis {
|
||||
time: _,
|
||||
axis,
|
||||
value,
|
||||
} => {
|
||||
let value = value as i32 / 10; // FIXME: high precision scroll events
|
||||
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;
|
||||
}
|
||||
};
|
||||
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;
|
||||
}
|
||||
};
|
||||
event.post(CGEventTapLocation::HID);
|
||||
}
|
||||
PointerEvent::Frame { .. } => {}
|
||||
},
|
||||
Event::Keyboard(keyboard_event) => match keyboard_event {
|
||||
KeyboardEvent::Key { .. } => {
|
||||
/*
|
||||
let code = CGKeyCode::from_le(key as u16);
|
||||
let event = match CGEvent::new_keyboard_event(
|
||||
self.event_source.clone(),
|
||||
code,
|
||||
match state { 1 => true, _ => false }
|
||||
) {
|
||||
Ok(e) => e,
|
||||
Err(_) => {
|
||||
log::warn!("unable to create key event");
|
||||
return
|
||||
}
|
||||
};
|
||||
event.post(CGEventTapLocation::HID);
|
||||
*/
|
||||
}
|
||||
KeyboardEvent::Modifiers { .. } => {}
|
||||
},
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
|
||||
async fn notify(&mut self, _client_event: ClientEvent) {}
|
||||
|
||||
async fn destroy(&mut self) {}
|
||||
}
|
||||
@@ -1,139 +0,0 @@
|
||||
use anyhow::Result;
|
||||
use ashpd::{
|
||||
desktop::{
|
||||
remote_desktop::{Axis, DeviceType, KeyState, RemoteDesktop},
|
||||
Session,
|
||||
},
|
||||
WindowIdentifier,
|
||||
};
|
||||
use async_trait::async_trait;
|
||||
|
||||
use crate::{
|
||||
client::ClientEvent,
|
||||
consumer::EventConsumer,
|
||||
event::{
|
||||
Event::{Keyboard, Pointer},
|
||||
KeyboardEvent, PointerEvent,
|
||||
},
|
||||
};
|
||||
|
||||
pub struct DesktopPortalConsumer<'a> {
|
||||
proxy: RemoteDesktop<'a>,
|
||||
session: Session<'a>,
|
||||
}
|
||||
|
||||
impl<'a> DesktopPortalConsumer<'a> {
|
||||
pub async fn new() -> Result<DesktopPortalConsumer<'a>> {
|
||||
log::debug!("connecting to org.freedesktop.portal.RemoteDesktop portal ...");
|
||||
let proxy = RemoteDesktop::new().await?;
|
||||
log::debug!("creating session ...");
|
||||
let session = proxy.create_session().await?;
|
||||
log::debug!("selecting devices ...");
|
||||
proxy
|
||||
.select_devices(&session, DeviceType::Keyboard | DeviceType::Pointer)
|
||||
.await?;
|
||||
|
||||
let _ = proxy
|
||||
.start(&session, &WindowIdentifier::default())
|
||||
.await?
|
||||
.response()?;
|
||||
log::debug!("started session");
|
||||
|
||||
Ok(Self { proxy, session })
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl<'a> EventConsumer for DesktopPortalConsumer<'a> {
|
||||
async fn consume(&mut self, event: crate::event::Event, _client: crate::client::ClientHandle) {
|
||||
match event {
|
||||
Pointer(p) => {
|
||||
match p {
|
||||
PointerEvent::Motion {
|
||||
time: _,
|
||||
relative_x,
|
||||
relative_y,
|
||||
} => {
|
||||
if let Err(e) = self
|
||||
.proxy
|
||||
.notify_pointer_motion(&self.session, relative_x, relative_y)
|
||||
.await
|
||||
{
|
||||
log::warn!("{e}");
|
||||
}
|
||||
}
|
||||
PointerEvent::Button {
|
||||
time: _,
|
||||
button,
|
||||
state,
|
||||
} => {
|
||||
let state = match state {
|
||||
0 => KeyState::Released,
|
||||
_ => KeyState::Pressed,
|
||||
};
|
||||
if let Err(e) = self
|
||||
.proxy
|
||||
.notify_pointer_button(&self.session, button as i32, state)
|
||||
.await
|
||||
{
|
||||
log::warn!("{e}");
|
||||
}
|
||||
}
|
||||
PointerEvent::Axis {
|
||||
time: _,
|
||||
axis,
|
||||
value,
|
||||
} => {
|
||||
let axis = match axis {
|
||||
0 => Axis::Vertical,
|
||||
_ => Axis::Horizontal,
|
||||
};
|
||||
// TODO smooth scrolling
|
||||
if let Err(e) = self
|
||||
.proxy
|
||||
.notify_pointer_axis_discrete(&self.session, axis, value as i32)
|
||||
.await
|
||||
{
|
||||
log::warn!("{e}");
|
||||
}
|
||||
}
|
||||
PointerEvent::Frame {} => {}
|
||||
}
|
||||
}
|
||||
Keyboard(k) => {
|
||||
match k {
|
||||
KeyboardEvent::Key {
|
||||
time: _,
|
||||
key,
|
||||
state,
|
||||
} => {
|
||||
let state = match state {
|
||||
0 => KeyState::Released,
|
||||
_ => KeyState::Pressed,
|
||||
};
|
||||
if let Err(e) = self
|
||||
.proxy
|
||||
.notify_keyboard_keycode(&self.session, key as i32, state)
|
||||
.await
|
||||
{
|
||||
log::warn!("{e}");
|
||||
}
|
||||
}
|
||||
KeyboardEvent::Modifiers { .. } => {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
async fn notify(&mut self, _client: ClientEvent) {}
|
||||
|
||||
async fn destroy(&mut self) {
|
||||
log::debug!("closing remote desktop session");
|
||||
if let Err(e) = self.session.close().await {
|
||||
log::error!("failed to close remote desktop session: {e}");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
#[cfg(all(unix, feature = "libei", not(target_os = "macos")))]
|
||||
pub mod libei;
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
pub mod macos;
|
||||
|
||||
#[cfg(all(unix, feature = "wayland", not(target_os = "macos")))]
|
||||
pub mod wayland;
|
||||
|
||||
#[cfg(windows)]
|
||||
pub mod windows;
|
||||
|
||||
#[cfg(all(unix, feature = "x11", not(target_os = "macos")))]
|
||||
pub mod x11;
|
||||
|
||||
/// fallback event producer
|
||||
pub mod dummy;
|
||||
@@ -1,42 +0,0 @@
|
||||
use std::io;
|
||||
use std::pin::Pin;
|
||||
use std::task::{Context, Poll};
|
||||
|
||||
use futures_core::Stream;
|
||||
|
||||
use crate::event::Event;
|
||||
use crate::producer::EventProducer;
|
||||
|
||||
use crate::client::{ClientEvent, ClientHandle};
|
||||
|
||||
pub struct DummyProducer {}
|
||||
|
||||
impl DummyProducer {
|
||||
pub fn new() -> Self {
|
||||
Self {}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for DummyProducer {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl EventProducer for DummyProducer {
|
||||
fn notify(&mut self, _event: ClientEvent) -> io::Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn release(&mut self) -> io::Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl Stream for DummyProducer {
|
||||
type Item = io::Result<(ClientHandle, Event)>;
|
||||
|
||||
fn poll_next(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
|
||||
Poll::Pending
|
||||
}
|
||||
}
|
||||
@@ -1,39 +0,0 @@
|
||||
use anyhow::{anyhow, Result};
|
||||
use std::{io, task::Poll};
|
||||
|
||||
use futures_core::Stream;
|
||||
|
||||
use crate::{
|
||||
client::{ClientEvent, ClientHandle},
|
||||
event::Event,
|
||||
producer::EventProducer,
|
||||
};
|
||||
|
||||
pub struct LibeiProducer {}
|
||||
|
||||
impl LibeiProducer {
|
||||
pub fn new() -> Result<Self> {
|
||||
Err(anyhow!("not implemented"))
|
||||
}
|
||||
}
|
||||
|
||||
impl EventProducer for LibeiProducer {
|
||||
fn notify(&mut self, _event: ClientEvent) -> io::Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn release(&mut self) -> io::Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl Stream for LibeiProducer {
|
||||
type Item = io::Result<(ClientHandle, Event)>;
|
||||
|
||||
fn poll_next(
|
||||
self: std::pin::Pin<&mut Self>,
|
||||
_cx: &mut std::task::Context<'_>,
|
||||
) -> std::task::Poll<Option<Self::Item>> {
|
||||
Poll::Pending
|
||||
}
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
use crate::client::{ClientEvent, ClientHandle};
|
||||
use crate::event::Event;
|
||||
use crate::producer::EventProducer;
|
||||
use anyhow::{anyhow, Result};
|
||||
use futures_core::Stream;
|
||||
use std::task::{Context, Poll};
|
||||
use std::{io, pin::Pin};
|
||||
|
||||
pub struct MacOSProducer;
|
||||
|
||||
impl MacOSProducer {
|
||||
pub fn new() -> Result<Self> {
|
||||
Err(anyhow!("not yet implemented"))
|
||||
}
|
||||
}
|
||||
|
||||
impl Stream for MacOSProducer {
|
||||
type Item = io::Result<(ClientHandle, Event)>;
|
||||
|
||||
fn poll_next(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
|
||||
Poll::Pending
|
||||
}
|
||||
}
|
||||
|
||||
impl EventProducer for MacOSProducer {
|
||||
fn notify(&mut self, _event: ClientEvent) -> io::Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn release(&mut self) -> io::Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -1,35 +0,0 @@
|
||||
use anyhow::{anyhow, Result};
|
||||
use core::task::{Context, Poll};
|
||||
use futures::Stream;
|
||||
use std::{io, pin::Pin};
|
||||
|
||||
use crate::{
|
||||
client::{ClientEvent, ClientHandle},
|
||||
event::Event,
|
||||
producer::EventProducer,
|
||||
};
|
||||
|
||||
pub struct WindowsProducer {}
|
||||
|
||||
impl EventProducer for WindowsProducer {
|
||||
fn notify(&mut self, _event: ClientEvent) -> io::Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn release(&mut self) -> io::Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl WindowsProducer {
|
||||
pub(crate) fn new() -> Result<Self> {
|
||||
Err(anyhow!("not implemented"))
|
||||
}
|
||||
}
|
||||
|
||||
impl Stream for WindowsProducer {
|
||||
type Item = io::Result<(ClientHandle, Event)>;
|
||||
fn poll_next(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
|
||||
Poll::Pending
|
||||
}
|
||||
}
|
||||
@@ -1,39 +0,0 @@
|
||||
use anyhow::{anyhow, Result};
|
||||
use std::io;
|
||||
use std::task::Poll;
|
||||
|
||||
use futures_core::Stream;
|
||||
|
||||
use crate::event::Event;
|
||||
use crate::producer::EventProducer;
|
||||
|
||||
use crate::client::{ClientEvent, ClientHandle};
|
||||
|
||||
pub struct X11Producer {}
|
||||
|
||||
impl X11Producer {
|
||||
pub fn new() -> Result<Self> {
|
||||
Err(anyhow!("not implemented"))
|
||||
}
|
||||
}
|
||||
|
||||
impl EventProducer for X11Producer {
|
||||
fn notify(&mut self, _event: ClientEvent) -> io::Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn release(&mut self) -> io::Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl Stream for X11Producer {
|
||||
type Item = io::Result<(ClientHandle, Event)>;
|
||||
|
||||
fn poll_next(
|
||||
self: std::pin::Pin<&mut Self>,
|
||||
_cx: &mut std::task::Context<'_>,
|
||||
) -> std::task::Poll<Option<Self::Item>> {
|
||||
Poll::Pending
|
||||
}
|
||||
}
|
||||
44
src/capture_test.rs
Normal file
44
src/capture_test.rs
Normal file
@@ -0,0 +1,44 @@
|
||||
use crate::config::Config;
|
||||
use futures::StreamExt;
|
||||
use input_capture::{self, CaptureError, CaptureEvent, InputCapture, InputCaptureError, Position};
|
||||
use input_event::{Event, KeyboardEvent};
|
||||
|
||||
pub async fn run(config: Config) -> Result<(), InputCaptureError> {
|
||||
log::info!("running input capture test");
|
||||
log::info!("creating input capture");
|
||||
let backend = config.capture_backend.map(|b| b.into());
|
||||
loop {
|
||||
let mut input_capture = InputCapture::new(backend).await?;
|
||||
log::info!("creating clients");
|
||||
input_capture.create(0, Position::Left).await?;
|
||||
input_capture.create(4, Position::Left).await?;
|
||||
input_capture.create(1, Position::Right).await?;
|
||||
input_capture.create(2, Position::Top).await?;
|
||||
input_capture.create(3, Position::Bottom).await?;
|
||||
if let Err(e) = do_capture(&mut input_capture).await {
|
||||
log::warn!("{e} - recreating capture");
|
||||
}
|
||||
let _ = input_capture.terminate().await;
|
||||
}
|
||||
}
|
||||
|
||||
async fn do_capture(input_capture: &mut InputCapture) -> Result<(), CaptureError> {
|
||||
loop {
|
||||
let (client, event) = input_capture
|
||||
.next()
|
||||
.await
|
||||
.ok_or(CaptureError::EndOfStream)??;
|
||||
let pos = match client {
|
||||
0 | 4 => Position::Left,
|
||||
1 => Position::Right,
|
||||
2 => Position::Top,
|
||||
3 => Position::Bottom,
|
||||
_ => panic!(),
|
||||
};
|
||||
log::info!("position: {client} ({pos}), event: {event}");
|
||||
if let CaptureEvent::Input(Event::Keyboard(KeyboardEvent::Key { key: 1, .. })) = event {
|
||||
input_capture.release().await?;
|
||||
break Ok(());
|
||||
}
|
||||
}
|
||||
}
|
||||
206
src/client.rs
206
src/client.rs
@@ -1,153 +1,18 @@
|
||||
use std::{
|
||||
collections::HashSet,
|
||||
fmt::Display,
|
||||
net::{IpAddr, SocketAddr},
|
||||
};
|
||||
use std::net::SocketAddr;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use slab::Slab;
|
||||
|
||||
#[derive(Debug, Eq, Hash, PartialEq, Clone, Copy, Serialize, Deserialize)]
|
||||
pub enum Position {
|
||||
Left,
|
||||
Right,
|
||||
Top,
|
||||
Bottom,
|
||||
}
|
||||
|
||||
impl Default for Position {
|
||||
fn default() -> Self {
|
||||
Self::Left
|
||||
}
|
||||
}
|
||||
|
||||
impl Position {
|
||||
pub fn opposite(&self) -> Self {
|
||||
match self {
|
||||
Position::Left => Self::Right,
|
||||
Position::Right => Self::Left,
|
||||
Position::Top => Self::Bottom,
|
||||
Position::Bottom => Self::Top,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for Position {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(
|
||||
f,
|
||||
"{}",
|
||||
match self {
|
||||
Position::Left => "left",
|
||||
Position::Right => "right",
|
||||
Position::Top => "top",
|
||||
Position::Bottom => "bottom",
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Eq, PartialEq, Clone, Serialize, Deserialize)]
|
||||
pub struct Client {
|
||||
/// hostname of this client
|
||||
pub hostname: Option<String>,
|
||||
/// fix ips, determined by the user
|
||||
pub fix_ips: Vec<IpAddr>,
|
||||
/// unique handle to refer to the client.
|
||||
/// This way any event consumer / producer backend does not
|
||||
/// need to know anything about a client other than its handle.
|
||||
pub handle: ClientHandle,
|
||||
/// all socket addresses associated with a particular client
|
||||
/// e.g. Laptops usually have at least an ethernet and a wifi port
|
||||
/// which have different ip addresses
|
||||
pub addrs: HashSet<SocketAddr>,
|
||||
/// both active_addr and addrs can be None / empty so port needs to be stored seperately
|
||||
pub port: u16,
|
||||
/// position of a client on screen
|
||||
pub pos: Position,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub enum ClientEvent {
|
||||
Create(ClientHandle, Position),
|
||||
Destroy(ClientHandle),
|
||||
}
|
||||
|
||||
pub type ClientHandle = u32;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ClientState {
|
||||
/// information about the client
|
||||
pub client: Client,
|
||||
/// events should be sent to and received from the client
|
||||
pub active: bool,
|
||||
/// `active` address of the client, used to send data to.
|
||||
/// This should generally be the socket address where data
|
||||
/// was last received from.
|
||||
pub active_addr: Option<SocketAddr>,
|
||||
/// tracks whether or not the client is responding to pings
|
||||
pub alive: bool,
|
||||
/// keys currently pressed by this client
|
||||
pub pressed_keys: HashSet<u32>,
|
||||
}
|
||||
use lan_mouse_ipc::{ClientConfig, ClientHandle, ClientState, Position};
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct ClientManager {
|
||||
clients: Vec<Option<ClientState>>, // HashMap likely not beneficial
|
||||
}
|
||||
|
||||
impl Default for ClientManager {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
clients: Slab<(ClientConfig, ClientState)>,
|
||||
}
|
||||
|
||||
impl ClientManager {
|
||||
pub fn new() -> Self {
|
||||
Self { clients: vec![] }
|
||||
}
|
||||
|
||||
/// add a new client to this manager
|
||||
pub fn add_client(
|
||||
&mut self,
|
||||
hostname: Option<String>,
|
||||
ips: HashSet<IpAddr>,
|
||||
port: u16,
|
||||
pos: Position,
|
||||
) -> ClientHandle {
|
||||
// get a new client_handle
|
||||
let handle = self.free_id();
|
||||
|
||||
// store fix ip addresses
|
||||
let fix_ips = ips.iter().cloned().collect();
|
||||
|
||||
// map ip addresses to socket addresses
|
||||
let addrs = HashSet::from_iter(ips.into_iter().map(|ip| SocketAddr::new(ip, port)));
|
||||
|
||||
// store the client
|
||||
let client = Client {
|
||||
hostname,
|
||||
fix_ips,
|
||||
handle,
|
||||
addrs,
|
||||
port,
|
||||
pos,
|
||||
};
|
||||
|
||||
// client was never seen, nor pinged
|
||||
let client_state = ClientState {
|
||||
client,
|
||||
active: false,
|
||||
active_addr: None,
|
||||
alive: false,
|
||||
pressed_keys: HashSet::new(),
|
||||
};
|
||||
|
||||
if handle as usize >= self.clients.len() {
|
||||
assert_eq!(handle as usize, self.clients.len());
|
||||
self.clients.push(Some(client_state));
|
||||
} else {
|
||||
self.clients[handle as usize] = Some(client_state);
|
||||
}
|
||||
handle
|
||||
pub fn add_client(&mut self) -> ClientHandle {
|
||||
self.clients.insert(Default::default()) as ClientHandle
|
||||
}
|
||||
|
||||
/// find a client by its address
|
||||
@@ -156,49 +21,54 @@ impl ClientManager {
|
||||
// time this is likely faster than using a HashMap
|
||||
self.clients
|
||||
.iter()
|
||||
.position(|c| {
|
||||
if let Some(c) = c {
|
||||
c.active && c.client.addrs.contains(&addr)
|
||||
.find_map(|(k, (_, s))| {
|
||||
if s.active && s.ips.contains(&addr.ip()) {
|
||||
Some(k)
|
||||
} else {
|
||||
false
|
||||
None
|
||||
}
|
||||
})
|
||||
.map(|p| p as ClientHandle)
|
||||
}
|
||||
|
||||
pub fn find_client(&self, pos: Position) -> Option<ClientHandle> {
|
||||
self.clients
|
||||
.iter()
|
||||
.find_map(|(k, (c, s))| {
|
||||
if s.active && c.pos == pos {
|
||||
Some(k)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.map(|p| p as ClientHandle)
|
||||
}
|
||||
|
||||
/// remove a client from the list
|
||||
pub fn remove_client(&mut self, client: ClientHandle) -> Option<ClientState> {
|
||||
pub fn remove_client(&mut self, client: ClientHandle) -> Option<(ClientConfig, ClientState)> {
|
||||
// remove id from occupied ids
|
||||
self.clients.get_mut(client as usize)?.take()
|
||||
}
|
||||
|
||||
/// get a free slot in the client list
|
||||
fn free_id(&mut self) -> ClientHandle {
|
||||
for i in 0..u32::MAX {
|
||||
if self.clients.get(i as usize).is_none()
|
||||
|| self.clients.get(i as usize).unwrap().is_none()
|
||||
{
|
||||
return i;
|
||||
}
|
||||
}
|
||||
panic!("Out of client ids");
|
||||
self.clients.try_remove(client as usize)
|
||||
}
|
||||
|
||||
// returns an immutable reference to the client state corresponding to `client`
|
||||
pub fn get(&self, client: ClientHandle) -> Option<&ClientState> {
|
||||
self.clients.get(client as usize)?.as_ref()
|
||||
pub fn get(&self, handle: ClientHandle) -> Option<&(ClientConfig, ClientState)> {
|
||||
self.clients.get(handle as usize)
|
||||
}
|
||||
|
||||
/// returns a mutable reference to the client state corresponding to `client`
|
||||
pub fn get_mut(&mut self, client: ClientHandle) -> Option<&mut ClientState> {
|
||||
self.clients.get_mut(client as usize)?.as_mut()
|
||||
pub fn get_mut(&mut self, handle: ClientHandle) -> Option<&mut (ClientConfig, ClientState)> {
|
||||
self.clients.get_mut(handle as usize)
|
||||
}
|
||||
|
||||
pub fn get_client_states(&self) -> impl Iterator<Item = &ClientState> {
|
||||
self.clients.iter().filter_map(|x| x.as_ref())
|
||||
pub fn get_client_states(
|
||||
&self,
|
||||
) -> impl Iterator<Item = (ClientHandle, &(ClientConfig, ClientState))> {
|
||||
self.clients.iter().map(|(k, v)| (k as ClientHandle, v))
|
||||
}
|
||||
|
||||
pub fn get_client_states_mut(&mut self) -> impl Iterator<Item = &mut ClientState> {
|
||||
self.clients.iter_mut().filter_map(|x| x.as_mut())
|
||||
pub fn get_client_states_mut(
|
||||
&mut self,
|
||||
) -> impl Iterator<Item = (ClientHandle, &mut (ClientConfig, ClientState))> {
|
||||
self.clients.iter_mut().map(|(k, v)| (k as ClientHandle, v))
|
||||
}
|
||||
}
|
||||
|
||||
296
src/config.rs
296
src/config.rs
@@ -1,35 +1,46 @@
|
||||
use anyhow::Result;
|
||||
use clap::Parser;
|
||||
use clap::{Parser, ValueEnum};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashSet;
|
||||
use std::env;
|
||||
use std::env::{self, VarError};
|
||||
use std::fmt::Display;
|
||||
use std::fs;
|
||||
use std::net::IpAddr;
|
||||
use std::{error::Error, fs};
|
||||
use std::{collections::HashSet, io};
|
||||
use thiserror::Error;
|
||||
use toml;
|
||||
|
||||
use crate::client::Position;
|
||||
use lan_mouse_ipc::{Position, DEFAULT_PORT};
|
||||
|
||||
pub const DEFAULT_PORT: u16 = 4242;
|
||||
use input_event::scancode::{
|
||||
self,
|
||||
Linux::{KeyLeftAlt, KeyLeftCtrl, KeyLeftMeta, KeyLeftShift},
|
||||
};
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub struct ConfigToml {
|
||||
pub capture_backend: Option<CaptureBackend>,
|
||||
pub emulation_backend: Option<EmulationBackend>,
|
||||
pub port: Option<u16>,
|
||||
pub frontend: Option<String>,
|
||||
pub left: Option<Client>,
|
||||
pub right: Option<Client>,
|
||||
pub top: Option<Client>,
|
||||
pub bottom: Option<Client>,
|
||||
pub frontend: Option<Frontend>,
|
||||
pub release_bind: Option<Vec<scancode::Linux>>,
|
||||
pub left: Option<TomlClient>,
|
||||
pub right: Option<TomlClient>,
|
||||
pub top: Option<TomlClient>,
|
||||
pub bottom: Option<TomlClient>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Eq, PartialEq)]
|
||||
pub struct Client {
|
||||
pub struct TomlClient {
|
||||
pub capture_backend: Option<CaptureBackend>,
|
||||
pub hostname: Option<String>,
|
||||
pub host_name: Option<String>,
|
||||
pub ips: Option<Vec<IpAddr>>,
|
||||
pub port: Option<u16>,
|
||||
pub activate_on_startup: Option<bool>,
|
||||
pub enter_hook: Option<String>,
|
||||
}
|
||||
|
||||
impl ConfigToml {
|
||||
pub fn new(path: &str) -> Result<ConfigToml, Box<dyn Error>> {
|
||||
pub fn new(path: &str) -> Result<ConfigToml, ConfigError> {
|
||||
let config = fs::read_to_string(path)?;
|
||||
log::info!("using config: \"{path}\"");
|
||||
Ok(toml::from_str::<_>(&config)?)
|
||||
@@ -37,7 +48,7 @@ impl ConfigToml {
|
||||
}
|
||||
|
||||
#[derive(Parser, Debug)]
|
||||
#[command(author, version, about, long_about = None)]
|
||||
#[command(author, version=env!("GIT_DESCRIBE"), about, long_about = None)]
|
||||
struct CliArgs {
|
||||
/// the listen port for lan-mouse
|
||||
#[arg(short, long)]
|
||||
@@ -45,7 +56,7 @@ struct CliArgs {
|
||||
|
||||
/// the frontend to use [cli | gtk]
|
||||
#[arg(short, long)]
|
||||
frontend: Option<String>,
|
||||
frontend: Option<Frontend>,
|
||||
|
||||
/// non-default config file location
|
||||
#[arg(short, long)]
|
||||
@@ -54,24 +65,185 @@ struct CliArgs {
|
||||
/// run only the service as a daemon without the frontend
|
||||
#[arg(short, long)]
|
||||
daemon: bool,
|
||||
|
||||
/// test input capture
|
||||
#[arg(long)]
|
||||
test_capture: bool,
|
||||
|
||||
/// test input emulation
|
||||
#[arg(long)]
|
||||
test_emulation: bool,
|
||||
|
||||
/// capture backend override
|
||||
#[arg(long)]
|
||||
capture_backend: Option<CaptureBackend>,
|
||||
|
||||
/// emulation backend override
|
||||
#[arg(long)]
|
||||
emulation_backend: Option<EmulationBackend>,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize, ValueEnum)]
|
||||
pub enum CaptureBackend {
|
||||
#[cfg(all(unix, feature = "libei", not(target_os = "macos")))]
|
||||
InputCapturePortal,
|
||||
#[cfg(all(unix, feature = "wayland", not(target_os = "macos")))]
|
||||
LayerShell,
|
||||
#[cfg(all(unix, feature = "x11", not(target_os = "macos")))]
|
||||
X11,
|
||||
#[cfg(windows)]
|
||||
Windows,
|
||||
#[cfg(target_os = "macos")]
|
||||
MacOs,
|
||||
Dummy,
|
||||
}
|
||||
|
||||
impl Display for CaptureBackend {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
#[cfg(all(unix, feature = "libei", not(target_os = "macos")))]
|
||||
CaptureBackend::InputCapturePortal => write!(f, "input-capture-portal"),
|
||||
#[cfg(all(unix, feature = "wayland", not(target_os = "macos")))]
|
||||
CaptureBackend::LayerShell => write!(f, "layer-shell"),
|
||||
#[cfg(all(unix, feature = "x11", not(target_os = "macos")))]
|
||||
CaptureBackend::X11 => write!(f, "X11"),
|
||||
#[cfg(windows)]
|
||||
CaptureBackend::Windows => write!(f, "windows"),
|
||||
#[cfg(target_os = "macos")]
|
||||
CaptureBackend::MacOs => write!(f, "MacOS"),
|
||||
CaptureBackend::Dummy => write!(f, "dummy"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<CaptureBackend> for input_capture::Backend {
|
||||
fn from(backend: CaptureBackend) -> Self {
|
||||
match backend {
|
||||
#[cfg(all(unix, feature = "libei", not(target_os = "macos")))]
|
||||
CaptureBackend::InputCapturePortal => Self::InputCapturePortal,
|
||||
#[cfg(all(unix, feature = "wayland", not(target_os = "macos")))]
|
||||
CaptureBackend::LayerShell => Self::LayerShell,
|
||||
#[cfg(all(unix, feature = "x11", not(target_os = "macos")))]
|
||||
CaptureBackend::X11 => Self::X11,
|
||||
#[cfg(windows)]
|
||||
CaptureBackend::Windows => Self::Windows,
|
||||
#[cfg(target_os = "macos")]
|
||||
CaptureBackend::MacOs => Self::MacOs,
|
||||
CaptureBackend::Dummy => Self::Dummy,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize, ValueEnum)]
|
||||
pub enum EmulationBackend {
|
||||
#[cfg(all(unix, feature = "wayland", not(target_os = "macos")))]
|
||||
Wlroots,
|
||||
#[cfg(all(unix, feature = "libei", not(target_os = "macos")))]
|
||||
Libei,
|
||||
#[cfg(all(unix, feature = "xdg_desktop_portal", not(target_os = "macos")))]
|
||||
Xdp,
|
||||
#[cfg(all(unix, feature = "x11", not(target_os = "macos")))]
|
||||
X11,
|
||||
#[cfg(windows)]
|
||||
Windows,
|
||||
#[cfg(target_os = "macos")]
|
||||
MacOs,
|
||||
Dummy,
|
||||
}
|
||||
|
||||
impl From<EmulationBackend> for input_emulation::Backend {
|
||||
fn from(backend: EmulationBackend) -> Self {
|
||||
match backend {
|
||||
#[cfg(all(unix, feature = "wayland", not(target_os = "macos")))]
|
||||
EmulationBackend::Wlroots => Self::Wlroots,
|
||||
#[cfg(all(unix, feature = "libei", not(target_os = "macos")))]
|
||||
EmulationBackend::Libei => Self::Libei,
|
||||
#[cfg(all(unix, feature = "xdg_desktop_portal", not(target_os = "macos")))]
|
||||
EmulationBackend::Xdp => Self::Xdp,
|
||||
#[cfg(all(unix, feature = "x11", not(target_os = "macos")))]
|
||||
EmulationBackend::X11 => Self::X11,
|
||||
#[cfg(windows)]
|
||||
EmulationBackend::Windows => Self::Windows,
|
||||
#[cfg(target_os = "macos")]
|
||||
EmulationBackend::MacOs => Self::MacOs,
|
||||
EmulationBackend::Dummy => Self::Dummy,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for EmulationBackend {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
#[cfg(all(unix, feature = "wayland", not(target_os = "macos")))]
|
||||
EmulationBackend::Wlroots => write!(f, "wlroots"),
|
||||
#[cfg(all(unix, feature = "libei", not(target_os = "macos")))]
|
||||
EmulationBackend::Libei => write!(f, "libei"),
|
||||
#[cfg(all(unix, feature = "xdg_desktop_portal", not(target_os = "macos")))]
|
||||
EmulationBackend::Xdp => write!(f, "xdg-desktop-portal"),
|
||||
#[cfg(all(unix, feature = "x11", not(target_os = "macos")))]
|
||||
EmulationBackend::X11 => write!(f, "X11"),
|
||||
#[cfg(windows)]
|
||||
EmulationBackend::Windows => write!(f, "windows"),
|
||||
#[cfg(target_os = "macos")]
|
||||
EmulationBackend::MacOs => write!(f, "macos"),
|
||||
EmulationBackend::Dummy => write!(f, "dummy"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Deserialize, PartialEq, Eq, Serialize, ValueEnum)]
|
||||
pub enum Frontend {
|
||||
Gtk,
|
||||
Cli,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Config {
|
||||
pub frontend: Frontend,
|
||||
pub port: u16,
|
||||
pub clients: Vec<(Client, Position)>,
|
||||
pub daemon: bool,
|
||||
impl Default for Frontend {
|
||||
fn default() -> Self {
|
||||
if cfg!(feature = "gtk") {
|
||||
Self::Gtk
|
||||
} else {
|
||||
Self::Cli
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Config {
|
||||
pub capture_backend: Option<CaptureBackend>,
|
||||
pub emulation_backend: Option<EmulationBackend>,
|
||||
pub frontend: Frontend,
|
||||
pub port: u16,
|
||||
pub clients: Vec<(TomlClient, Position)>,
|
||||
pub daemon: bool,
|
||||
pub release_bind: Vec<scancode::Linux>,
|
||||
pub test_capture: bool,
|
||||
pub test_emulation: bool,
|
||||
}
|
||||
|
||||
pub struct ConfigClient {
|
||||
pub ips: HashSet<IpAddr>,
|
||||
pub hostname: Option<String>,
|
||||
pub port: u16,
|
||||
pub pos: Position,
|
||||
pub active: bool,
|
||||
pub enter_hook: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum ConfigError {
|
||||
#[error(transparent)]
|
||||
Toml(#[from] toml::de::Error),
|
||||
#[error(transparent)]
|
||||
Io(#[from] io::Error),
|
||||
#[error(transparent)]
|
||||
Var(#[from] VarError),
|
||||
}
|
||||
|
||||
const DEFAULT_RELEASE_KEYS: [scancode::Linux; 4] =
|
||||
[KeyLeftCtrl, KeyLeftShift, KeyLeftMeta, KeyLeftAlt];
|
||||
|
||||
impl Config {
|
||||
pub fn new() -> Result<Self> {
|
||||
pub fn new() -> Result<Self, ConfigError> {
|
||||
let args = CliArgs::parse();
|
||||
let config_file = "config.toml";
|
||||
#[cfg(unix)]
|
||||
@@ -93,42 +265,37 @@ impl Config {
|
||||
|
||||
let config_toml = match ConfigToml::new(config_path.as_str()) {
|
||||
Err(e) => {
|
||||
log::error!("{config_path}: {e}");
|
||||
log::warn!("{config_path}: {e}");
|
||||
log::warn!("Continuing without config file ...");
|
||||
None
|
||||
}
|
||||
Ok(c) => Some(c),
|
||||
};
|
||||
|
||||
let frontend = match args.frontend {
|
||||
None => match &config_toml {
|
||||
Some(c) => c.frontend.clone(),
|
||||
None => None,
|
||||
},
|
||||
frontend => frontend,
|
||||
};
|
||||
let frontend_arg = args.frontend;
|
||||
let frontend_cfg = config_toml.as_ref().and_then(|c| c.frontend);
|
||||
let frontend = frontend_arg.or(frontend_cfg).unwrap_or_default();
|
||||
|
||||
let frontend = match frontend {
|
||||
#[cfg(feature = "gtk")]
|
||||
None => Frontend::Gtk,
|
||||
#[cfg(not(feature = "gtk"))]
|
||||
None => Frontend::Cli,
|
||||
Some(s) => match s.as_str() {
|
||||
"cli" => Frontend::Cli,
|
||||
"gtk" => Frontend::Gtk,
|
||||
_ => Frontend::Cli,
|
||||
},
|
||||
};
|
||||
let port = args
|
||||
.port
|
||||
.or(config_toml.as_ref().and_then(|c| c.port))
|
||||
.unwrap_or(DEFAULT_PORT);
|
||||
|
||||
let port = match args.port {
|
||||
Some(port) => port,
|
||||
None => match &config_toml {
|
||||
Some(c) => c.port.unwrap_or(DEFAULT_PORT),
|
||||
None => DEFAULT_PORT,
|
||||
},
|
||||
};
|
||||
log::debug!("{config_toml:?}");
|
||||
let release_bind = config_toml
|
||||
.as_ref()
|
||||
.and_then(|c| c.release_bind.clone())
|
||||
.unwrap_or(Vec::from_iter(DEFAULT_RELEASE_KEYS.iter().cloned()));
|
||||
|
||||
let mut clients: Vec<(Client, Position)> = vec![];
|
||||
let capture_backend = args
|
||||
.capture_backend
|
||||
.or(config_toml.as_ref().and_then(|c| c.capture_backend));
|
||||
|
||||
let emulation_backend = args
|
||||
.emulation_backend
|
||||
.or(config_toml.as_ref().and_then(|c| c.emulation_backend));
|
||||
|
||||
let mut clients: Vec<(TomlClient, Position)> = vec![];
|
||||
|
||||
if let Some(config_toml) = config_toml {
|
||||
if let Some(c) = config_toml.right {
|
||||
@@ -146,27 +313,46 @@ impl Config {
|
||||
}
|
||||
|
||||
let daemon = args.daemon;
|
||||
let test_capture = args.test_capture;
|
||||
let test_emulation = args.test_emulation;
|
||||
|
||||
Ok(Config {
|
||||
capture_backend,
|
||||
emulation_backend,
|
||||
daemon,
|
||||
frontend,
|
||||
clients,
|
||||
port,
|
||||
release_bind,
|
||||
test_capture,
|
||||
test_emulation,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn get_clients(&self) -> Vec<(HashSet<IpAddr>, Option<String>, u16, Position)> {
|
||||
pub fn get_clients(&self) -> Vec<ConfigClient> {
|
||||
self.clients
|
||||
.iter()
|
||||
.map(|(c, p)| {
|
||||
.map(|(c, pos)| {
|
||||
let port = c.port.unwrap_or(DEFAULT_PORT);
|
||||
let ips: HashSet<IpAddr> = if let Some(ips) = c.ips.as_ref() {
|
||||
HashSet::from_iter(ips.iter().cloned())
|
||||
} else {
|
||||
HashSet::new()
|
||||
};
|
||||
let host_name = c.host_name.clone();
|
||||
(ips, host_name, port, *p)
|
||||
let hostname = match &c.hostname {
|
||||
Some(h) => Some(h.clone()),
|
||||
None => c.host_name.clone(),
|
||||
};
|
||||
let active = c.activate_on_startup.unwrap_or(false);
|
||||
let enter_hook = c.enter_hook.clone();
|
||||
ConfigClient {
|
||||
ips,
|
||||
hostname,
|
||||
port,
|
||||
pos: *pos,
|
||||
active,
|
||||
enter_hook,
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
@@ -1,78 +0,0 @@
|
||||
use async_trait::async_trait;
|
||||
use std::future;
|
||||
|
||||
use crate::{
|
||||
backend::consumer,
|
||||
client::{ClientEvent, ClientHandle},
|
||||
event::Event,
|
||||
};
|
||||
use anyhow::Result;
|
||||
|
||||
#[async_trait]
|
||||
pub trait EventConsumer: Send {
|
||||
async fn consume(&mut self, event: Event, client_handle: ClientHandle);
|
||||
async fn notify(&mut self, client_event: ClientEvent);
|
||||
/// this function is waited on continuously and can be used to handle events
|
||||
async fn dispatch(&mut self) -> Result<()> {
|
||||
let _: () = future::pending().await;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn destroy(&mut self);
|
||||
}
|
||||
|
||||
pub async fn create() -> Box<dyn EventConsumer> {
|
||||
#[cfg(windows)]
|
||||
match consumer::windows::WindowsConsumer::new() {
|
||||
Ok(c) => return Box::new(c),
|
||||
Err(e) => log::warn!("windows event consumer unavailable: {e}"),
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
match consumer::macos::MacOSConsumer::new() {
|
||||
Ok(c) => {
|
||||
log::info!("using macos event consumer");
|
||||
return Box::new(c);
|
||||
}
|
||||
Err(e) => log::error!("macos consumer not available: {e}"),
|
||||
}
|
||||
|
||||
#[cfg(all(unix, feature = "wayland", not(target_os = "macos")))]
|
||||
match consumer::wlroots::WlrootsConsumer::new() {
|
||||
Ok(c) => {
|
||||
log::info!("using wlroots event consumer");
|
||||
return Box::new(c);
|
||||
}
|
||||
Err(e) => log::info!("wayland backend not available: {e}"),
|
||||
}
|
||||
|
||||
#[cfg(all(unix, feature = "libei", not(target_os = "macos")))]
|
||||
match consumer::libei::LibeiConsumer::new().await {
|
||||
Ok(c) => {
|
||||
log::info!("using libei event consumer");
|
||||
return Box::new(c);
|
||||
}
|
||||
Err(e) => log::info!("libei not available: {e}"),
|
||||
}
|
||||
|
||||
#[cfg(all(unix, feature = "xdg_desktop_portal", not(target_os = "macos")))]
|
||||
match consumer::xdg_desktop_portal::DesktopPortalConsumer::new().await {
|
||||
Ok(c) => {
|
||||
log::info!("using xdg-remote-desktop-portal event consumer");
|
||||
return Box::new(c);
|
||||
}
|
||||
Err(e) => log::info!("remote desktop portal not available: {e}"),
|
||||
}
|
||||
|
||||
#[cfg(all(unix, feature = "x11", not(target_os = "macos")))]
|
||||
match consumer::x11::X11Consumer::new() {
|
||||
Ok(c) => {
|
||||
log::info!("using x11 event consumer");
|
||||
return Box::new(c);
|
||||
}
|
||||
Err(e) => log::info!("x11 consumer not available: {e}"),
|
||||
}
|
||||
|
||||
log::error!("falling back to dummy event consumer");
|
||||
Box::new(consumer::dummy::DummyConsumer::new())
|
||||
}
|
||||
56
src/dns.rs
56
src/dns.rs
@@ -1,23 +1,63 @@
|
||||
use anyhow::Result;
|
||||
use std::{error::Error, net::IpAddr};
|
||||
use local_channel::mpsc::Receiver;
|
||||
use std::net::IpAddr;
|
||||
|
||||
use trust_dns_resolver::TokioAsyncResolver;
|
||||
use hickory_resolver::{error::ResolveError, TokioAsyncResolver};
|
||||
|
||||
pub struct DnsResolver {
|
||||
use crate::server::Server;
|
||||
use lan_mouse_ipc::ClientHandle;
|
||||
|
||||
pub(crate) struct DnsResolver {
|
||||
resolver: TokioAsyncResolver,
|
||||
dns_request: Receiver<ClientHandle>,
|
||||
}
|
||||
|
||||
impl DnsResolver {
|
||||
pub(crate) async fn new() -> Result<Self> {
|
||||
pub(crate) fn new(dns_request: Receiver<ClientHandle>) -> Result<Self, ResolveError> {
|
||||
let resolver = TokioAsyncResolver::tokio_from_system_conf()?;
|
||||
Ok(Self { resolver })
|
||||
Ok(Self {
|
||||
resolver,
|
||||
dns_request,
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) async fn resolve(&self, host: &str) -> Result<Vec<IpAddr>, Box<dyn Error>> {
|
||||
log::info!("resolving {host} ...");
|
||||
async fn resolve(&self, host: &str) -> Result<Vec<IpAddr>, ResolveError> {
|
||||
let response = self.resolver.lookup_ip(host).await?;
|
||||
for ip in response.iter() {
|
||||
log::info!("{host}: adding ip {ip}");
|
||||
}
|
||||
Ok(response.iter().collect())
|
||||
}
|
||||
|
||||
pub(crate) async fn run(mut self, server: Server) {
|
||||
tokio::select! {
|
||||
_ = server.cancelled() => {},
|
||||
_ = self.do_dns(&server) => {},
|
||||
}
|
||||
}
|
||||
|
||||
async fn do_dns(&mut self, server: &Server) {
|
||||
loop {
|
||||
let handle = self.dns_request.recv().await.expect("channel closed");
|
||||
|
||||
/* update resolving status */
|
||||
let hostname = match server.get_hostname(handle) {
|
||||
Some(hostname) => hostname,
|
||||
None => continue,
|
||||
};
|
||||
|
||||
log::info!("resolving ({handle}) `{hostname}` ...");
|
||||
server.set_resolving(handle, true);
|
||||
|
||||
let ips = match self.resolve(&hostname).await {
|
||||
Ok(ips) => ips,
|
||||
Err(e) => {
|
||||
log::warn!("could not resolve host '{hostname}': {e}");
|
||||
vec![]
|
||||
}
|
||||
};
|
||||
|
||||
server.update_dns_ips(handle, ips);
|
||||
server.set_resolving(handle, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
35
src/emulation_test.rs
Normal file
35
src/emulation_test.rs
Normal file
@@ -0,0 +1,35 @@
|
||||
use crate::config::Config;
|
||||
use input_emulation::{InputEmulation, InputEmulationError};
|
||||
use input_event::{Event, PointerEvent};
|
||||
use std::f64::consts::PI;
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
const FREQUENCY_HZ: f64 = 1.0;
|
||||
const RADIUS: f64 = 100.0;
|
||||
|
||||
pub async fn run(config: Config) -> Result<(), InputEmulationError> {
|
||||
log::info!("running input emulation test");
|
||||
|
||||
let backend = config.emulation_backend.map(|b| b.into());
|
||||
let mut emulation = InputEmulation::new(backend).await?;
|
||||
emulation.create(0).await;
|
||||
|
||||
let start = Instant::now();
|
||||
let mut offset = (0, 0);
|
||||
loop {
|
||||
tokio::time::sleep(Duration::from_millis(1)).await;
|
||||
let elapsed = start.elapsed();
|
||||
let elapsed_sec_f64 = elapsed.as_secs_f64();
|
||||
let second_fraction = elapsed_sec_f64 - elapsed_sec_f64 as u64 as f64;
|
||||
let radians = second_fraction * 2. * PI * FREQUENCY_HZ;
|
||||
let new_offset_f = (radians.cos() * RADIUS * 2., (radians * 2.).sin() * RADIUS);
|
||||
let new_offset = (new_offset_f.0 as i32, new_offset_f.1 as i32);
|
||||
if new_offset != offset {
|
||||
let relative_motion = (new_offset.0 - offset.0, new_offset.1 - offset.1);
|
||||
offset = new_offset;
|
||||
let (dx, dy) = (relative_motion.0 as f64, relative_motion.1 as f64);
|
||||
let event = Event::Pointer(PointerEvent::Motion { time: 0, dx, dy });
|
||||
emulation.consume(event, 0).await?;
|
||||
}
|
||||
}
|
||||
}
|
||||
530
src/event.rs
530
src/event.rs
@@ -1,530 +0,0 @@
|
||||
use anyhow::{anyhow, Result};
|
||||
use std::{
|
||||
error::Error,
|
||||
fmt::{self, Display},
|
||||
};
|
||||
|
||||
// FIXME
|
||||
pub const BTN_LEFT: u32 = 0x110;
|
||||
pub const BTN_RIGHT: u32 = 0x111;
|
||||
pub const BTN_MIDDLE: u32 = 0x112;
|
||||
pub const BTN_BACK: u32 = 0x113;
|
||||
pub const BTN_FORWARD: u32 = 0x114;
|
||||
|
||||
#[derive(Debug, PartialEq, Clone, Copy)]
|
||||
pub enum PointerEvent {
|
||||
Motion {
|
||||
time: u32,
|
||||
relative_x: f64,
|
||||
relative_y: f64,
|
||||
},
|
||||
Button {
|
||||
time: u32,
|
||||
button: u32,
|
||||
state: u32,
|
||||
},
|
||||
Axis {
|
||||
time: u32,
|
||||
axis: u8,
|
||||
value: f64,
|
||||
},
|
||||
Frame {},
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Clone, Copy)]
|
||||
pub enum KeyboardEvent {
|
||||
Key {
|
||||
time: u32,
|
||||
key: u32,
|
||||
state: u8,
|
||||
},
|
||||
Modifiers {
|
||||
mods_depressed: u32,
|
||||
mods_latched: u32,
|
||||
mods_locked: u32,
|
||||
group: u32,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Debug, Clone, Copy)]
|
||||
pub enum Event {
|
||||
/// pointer event (motion / button / axis)
|
||||
Pointer(PointerEvent),
|
||||
/// keyboard events (key / modifiers)
|
||||
Keyboard(KeyboardEvent),
|
||||
/// enter event: request to enter a client.
|
||||
/// The client must release the pointer if it is grabbed
|
||||
/// and reply with a leave event, as soon as its ready to
|
||||
/// receive events
|
||||
Enter(),
|
||||
/// leave event: this client is now ready to receive events and will
|
||||
/// not send any events after until it sends an enter event
|
||||
Leave(),
|
||||
/// ping a client, to see if it is still alive. A client that does
|
||||
/// not respond with a pong event will be assumed to be offline.
|
||||
Ping(),
|
||||
/// response to a ping event: this event signals that a client
|
||||
/// is still alive but must otherwise be ignored
|
||||
Pong(),
|
||||
/// explicit disconnect request. The client will no longer
|
||||
/// send events until the next Enter event. All of its keys should be released.
|
||||
Disconnect(),
|
||||
}
|
||||
|
||||
impl Display for PointerEvent {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
PointerEvent::Motion {
|
||||
time: _,
|
||||
relative_x,
|
||||
relative_y,
|
||||
} => write!(f, "motion({relative_x},{relative_y})"),
|
||||
PointerEvent::Button {
|
||||
time: _,
|
||||
button,
|
||||
state,
|
||||
} => write!(f, "button({button}, {state})"),
|
||||
PointerEvent::Axis {
|
||||
time: _,
|
||||
axis,
|
||||
value,
|
||||
} => write!(f, "scroll({axis}, {value})"),
|
||||
PointerEvent::Frame {} => write!(f, "frame()"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for KeyboardEvent {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
KeyboardEvent::Key {
|
||||
time: _,
|
||||
key,
|
||||
state,
|
||||
} => write!(f, "key({key}, {state})"),
|
||||
KeyboardEvent::Modifiers {
|
||||
mods_depressed,
|
||||
mods_latched,
|
||||
mods_locked,
|
||||
group,
|
||||
} => write!(
|
||||
f,
|
||||
"modifiers({mods_depressed},{mods_latched},{mods_locked},{group})"
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for Event {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
Event::Pointer(p) => write!(f, "{}", p),
|
||||
Event::Keyboard(k) => write!(f, "{}", k),
|
||||
Event::Enter() => write!(f, "enter"),
|
||||
Event::Leave() => write!(f, "leave"),
|
||||
Event::Ping() => write!(f, "ping"),
|
||||
Event::Pong() => write!(f, "pong"),
|
||||
Event::Disconnect() => write!(f, "disconnect"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Event {
|
||||
fn event_type(&self) -> EventType {
|
||||
match self {
|
||||
Self::Pointer(_) => EventType::Pointer,
|
||||
Self::Keyboard(_) => EventType::Keyboard,
|
||||
Self::Enter() => EventType::Enter,
|
||||
Self::Leave() => EventType::Leave,
|
||||
Self::Ping() => EventType::Ping,
|
||||
Self::Pong() => EventType::Pong,
|
||||
Self::Disconnect() => EventType::Disconnect,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl PointerEvent {
|
||||
fn event_type(&self) -> PointerEventType {
|
||||
match self {
|
||||
Self::Motion { .. } => PointerEventType::Motion,
|
||||
Self::Button { .. } => PointerEventType::Button,
|
||||
Self::Axis { .. } => PointerEventType::Axis,
|
||||
Self::Frame { .. } => PointerEventType::Frame,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl KeyboardEvent {
|
||||
fn event_type(&self) -> KeyboardEventType {
|
||||
match self {
|
||||
KeyboardEvent::Key { .. } => KeyboardEventType::Key,
|
||||
KeyboardEvent::Modifiers { .. } => KeyboardEventType::Modifiers,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum PointerEventType {
|
||||
Motion,
|
||||
Button,
|
||||
Axis,
|
||||
Frame,
|
||||
}
|
||||
enum KeyboardEventType {
|
||||
Key,
|
||||
Modifiers,
|
||||
}
|
||||
enum EventType {
|
||||
Pointer,
|
||||
Keyboard,
|
||||
Enter,
|
||||
Leave,
|
||||
Ping,
|
||||
Pong,
|
||||
Disconnect,
|
||||
}
|
||||
|
||||
impl TryFrom<u8> for PointerEventType {
|
||||
type Error = anyhow::Error;
|
||||
|
||||
fn try_from(value: u8) -> Result<Self> {
|
||||
match value {
|
||||
x if x == Self::Motion as u8 => Ok(Self::Motion),
|
||||
x if x == Self::Button as u8 => Ok(Self::Button),
|
||||
x if x == Self::Axis as u8 => Ok(Self::Axis),
|
||||
x if x == Self::Frame as u8 => Ok(Self::Frame),
|
||||
_ => Err(anyhow!(ProtocolError {
|
||||
msg: format!("invalid pointer event type {}", value),
|
||||
})),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<u8> for KeyboardEventType {
|
||||
type Error = anyhow::Error;
|
||||
|
||||
fn try_from(value: u8) -> Result<Self> {
|
||||
match value {
|
||||
x if x == Self::Key as u8 => Ok(Self::Key),
|
||||
x if x == Self::Modifiers as u8 => Ok(Self::Modifiers),
|
||||
_ => Err(anyhow!(ProtocolError {
|
||||
msg: format!("invalid keyboard event type {}", value),
|
||||
})),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&Event> for Vec<u8> {
|
||||
fn from(event: &Event) -> Self {
|
||||
let event_id = vec![event.event_type() as u8];
|
||||
let event_data = match event {
|
||||
Event::Pointer(p) => p.into(),
|
||||
Event::Keyboard(k) => k.into(),
|
||||
Event::Enter() => vec![],
|
||||
Event::Leave() => vec![],
|
||||
Event::Ping() => vec![],
|
||||
Event::Pong() => vec![],
|
||||
Event::Disconnect() => vec![],
|
||||
};
|
||||
[event_id, event_data].concat()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct ProtocolError {
|
||||
msg: String,
|
||||
}
|
||||
|
||||
impl fmt::Display for ProtocolError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "Protocol violation: {}", self.msg)
|
||||
}
|
||||
}
|
||||
impl Error for ProtocolError {}
|
||||
|
||||
impl TryFrom<Vec<u8>> for Event {
|
||||
type Error = anyhow::Error;
|
||||
|
||||
fn try_from(value: Vec<u8>) -> Result<Self> {
|
||||
let event_id = u8::from_be_bytes(value[..1].try_into()?);
|
||||
match event_id {
|
||||
i if i == (EventType::Pointer as u8) => Ok(Event::Pointer(value.try_into()?)),
|
||||
i if i == (EventType::Keyboard as u8) => Ok(Event::Keyboard(value.try_into()?)),
|
||||
i if i == (EventType::Enter as u8) => Ok(Event::Enter()),
|
||||
i if i == (EventType::Leave as u8) => Ok(Event::Leave()),
|
||||
i if i == (EventType::Ping as u8) => Ok(Event::Ping()),
|
||||
i if i == (EventType::Pong as u8) => Ok(Event::Pong()),
|
||||
i if i == (EventType::Disconnect as u8) => Ok(Event::Disconnect()),
|
||||
_ => Err(anyhow!(ProtocolError {
|
||||
msg: format!("invalid event_id {}", event_id),
|
||||
})),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&PointerEvent> for Vec<u8> {
|
||||
fn from(event: &PointerEvent) -> Self {
|
||||
let id = vec![event.event_type() as u8];
|
||||
let data = match event {
|
||||
PointerEvent::Motion {
|
||||
time,
|
||||
relative_x,
|
||||
relative_y,
|
||||
} => {
|
||||
let time = time.to_be_bytes();
|
||||
let relative_x = relative_x.to_be_bytes();
|
||||
let relative_y = relative_y.to_be_bytes();
|
||||
[&time[..], &relative_x[..], &relative_y[..]].concat()
|
||||
}
|
||||
PointerEvent::Button {
|
||||
time,
|
||||
button,
|
||||
state,
|
||||
} => {
|
||||
let time = time.to_be_bytes();
|
||||
let button = button.to_be_bytes();
|
||||
let state = state.to_be_bytes();
|
||||
[&time[..], &button[..], &state[..]].concat()
|
||||
}
|
||||
PointerEvent::Axis { time, axis, value } => {
|
||||
let time = time.to_be_bytes();
|
||||
let axis = axis.to_be_bytes();
|
||||
let value = value.to_be_bytes();
|
||||
[&time[..], &axis[..], &value[..]].concat()
|
||||
}
|
||||
PointerEvent::Frame {} => {
|
||||
vec![]
|
||||
}
|
||||
};
|
||||
[id, data].concat()
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<Vec<u8>> for PointerEvent {
|
||||
type Error = anyhow::Error;
|
||||
|
||||
fn try_from(data: Vec<u8>) -> Result<Self> {
|
||||
match data.get(1) {
|
||||
Some(id) => {
|
||||
let event_type = match id.to_owned().try_into() {
|
||||
Ok(event_type) => event_type,
|
||||
Err(e) => return Err(e),
|
||||
};
|
||||
match event_type {
|
||||
PointerEventType::Motion => {
|
||||
let time = match data.get(2..6) {
|
||||
Some(d) => u32::from_be_bytes(d.try_into()?),
|
||||
None => {
|
||||
return Err(anyhow!(ProtocolError {
|
||||
msg: "Expected 4 Bytes at index 2".into(),
|
||||
}))
|
||||
}
|
||||
};
|
||||
let relative_x = match data.get(6..14) {
|
||||
Some(d) => f64::from_be_bytes(d.try_into()?),
|
||||
None => {
|
||||
return Err(anyhow!(ProtocolError {
|
||||
msg: "Expected 8 Bytes at index 6".into(),
|
||||
}))
|
||||
}
|
||||
};
|
||||
let relative_y = match data.get(14..22) {
|
||||
Some(d) => f64::from_be_bytes(d.try_into()?),
|
||||
None => {
|
||||
return Err(anyhow!(ProtocolError {
|
||||
msg: "Expected 8 Bytes at index 14".into(),
|
||||
}))
|
||||
}
|
||||
};
|
||||
Ok(Self::Motion {
|
||||
time,
|
||||
relative_x,
|
||||
relative_y,
|
||||
})
|
||||
}
|
||||
PointerEventType::Button => {
|
||||
let time = match data.get(2..6) {
|
||||
Some(d) => u32::from_be_bytes(d.try_into()?),
|
||||
None => {
|
||||
return Err(anyhow!(ProtocolError {
|
||||
msg: "Expected 4 Bytes at index 2".into(),
|
||||
}))
|
||||
}
|
||||
};
|
||||
let button = match data.get(6..10) {
|
||||
Some(d) => u32::from_be_bytes(d.try_into()?),
|
||||
None => {
|
||||
return Err(anyhow!(ProtocolError {
|
||||
msg: "Expected 4 Bytes at index 10".into(),
|
||||
}))
|
||||
}
|
||||
};
|
||||
let state = match data.get(10..14) {
|
||||
Some(d) => u32::from_be_bytes(d.try_into()?),
|
||||
None => {
|
||||
return Err(anyhow!(ProtocolError {
|
||||
msg: "Expected 4 Bytes at index 14".into(),
|
||||
}))
|
||||
}
|
||||
};
|
||||
Ok(Self::Button {
|
||||
time,
|
||||
button,
|
||||
state,
|
||||
})
|
||||
}
|
||||
PointerEventType::Axis => {
|
||||
let time = match data.get(2..6) {
|
||||
Some(d) => u32::from_be_bytes(d.try_into()?),
|
||||
None => {
|
||||
return Err(anyhow!(ProtocolError {
|
||||
msg: "Expected 4 Bytes at index 2".into(),
|
||||
}))
|
||||
}
|
||||
};
|
||||
let axis = match data.get(6) {
|
||||
Some(d) => *d,
|
||||
None => {
|
||||
return Err(anyhow!(ProtocolError {
|
||||
msg: "Expected 1 Byte at index 6".into(),
|
||||
}));
|
||||
}
|
||||
};
|
||||
let value = match data.get(7..15) {
|
||||
Some(d) => f64::from_be_bytes(d.try_into()?),
|
||||
None => {
|
||||
return Err(anyhow!(ProtocolError {
|
||||
msg: "Expected 8 Bytes at index 7".into(),
|
||||
}));
|
||||
}
|
||||
};
|
||||
Ok(Self::Axis { time, axis, value })
|
||||
}
|
||||
PointerEventType::Frame => Ok(Self::Frame {}),
|
||||
}
|
||||
}
|
||||
None => Err(anyhow!(ProtocolError {
|
||||
msg: "Expected an element at index 0".into(),
|
||||
})),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&KeyboardEvent> for Vec<u8> {
|
||||
fn from(event: &KeyboardEvent) -> Self {
|
||||
let id = vec![event.event_type() as u8];
|
||||
let data = match event {
|
||||
KeyboardEvent::Key { time, key, state } => {
|
||||
let time = time.to_be_bytes();
|
||||
let key = key.to_be_bytes();
|
||||
let state = state.to_be_bytes();
|
||||
[&time[..], &key[..], &state[..]].concat()
|
||||
}
|
||||
KeyboardEvent::Modifiers {
|
||||
mods_depressed,
|
||||
mods_latched,
|
||||
mods_locked,
|
||||
group,
|
||||
} => {
|
||||
let mods_depressed = mods_depressed.to_be_bytes();
|
||||
let mods_latched = mods_latched.to_be_bytes();
|
||||
let mods_locked = mods_locked.to_be_bytes();
|
||||
let group = group.to_be_bytes();
|
||||
[
|
||||
&mods_depressed[..],
|
||||
&mods_latched[..],
|
||||
&mods_locked[..],
|
||||
&group[..],
|
||||
]
|
||||
.concat()
|
||||
}
|
||||
};
|
||||
[id, data].concat()
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<Vec<u8>> for KeyboardEvent {
|
||||
type Error = anyhow::Error;
|
||||
|
||||
fn try_from(data: Vec<u8>) -> Result<Self> {
|
||||
match data.get(1) {
|
||||
Some(id) => {
|
||||
let event_type = match id.to_owned().try_into() {
|
||||
Ok(event_type) => event_type,
|
||||
Err(e) => return Err(e),
|
||||
};
|
||||
match event_type {
|
||||
KeyboardEventType::Key => {
|
||||
let time = match data.get(2..6) {
|
||||
Some(d) => u32::from_be_bytes(d.try_into()?),
|
||||
None => {
|
||||
return Err(anyhow!(ProtocolError {
|
||||
msg: "Expected 4 Bytes at index 6".into(),
|
||||
}))
|
||||
}
|
||||
};
|
||||
let key = match data.get(6..10) {
|
||||
Some(d) => u32::from_be_bytes(d.try_into()?),
|
||||
None => {
|
||||
return Err(anyhow!(ProtocolError {
|
||||
msg: "Expected 4 Bytes at index 10".into(),
|
||||
}))
|
||||
}
|
||||
};
|
||||
let state = match data.get(10) {
|
||||
Some(d) => *d,
|
||||
None => {
|
||||
return Err(anyhow!(ProtocolError {
|
||||
msg: "Expected 1 Bytes at index 14".into(),
|
||||
}))
|
||||
}
|
||||
};
|
||||
Ok(KeyboardEvent::Key { time, key, state })
|
||||
}
|
||||
KeyboardEventType::Modifiers => {
|
||||
let mods_depressed = match data.get(2..6) {
|
||||
Some(d) => u32::from_be_bytes(d.try_into()?),
|
||||
None => {
|
||||
return Err(anyhow!(ProtocolError {
|
||||
msg: "Expected 4 Bytes at index 6".into(),
|
||||
}))
|
||||
}
|
||||
};
|
||||
let mods_latched = match data.get(6..10) {
|
||||
Some(d) => u32::from_be_bytes(d.try_into()?),
|
||||
None => {
|
||||
return Err(anyhow!(ProtocolError {
|
||||
msg: "Expected 4 Bytes at index 10".into(),
|
||||
}))
|
||||
}
|
||||
};
|
||||
let mods_locked = match data.get(10..14) {
|
||||
Some(d) => u32::from_be_bytes(d.try_into()?),
|
||||
None => {
|
||||
return Err(anyhow!(ProtocolError {
|
||||
msg: "Expected 4 Bytes at index 14".into(),
|
||||
}))
|
||||
}
|
||||
};
|
||||
let group = match data.get(14..18) {
|
||||
Some(d) => u32::from_be_bytes(d.try_into()?),
|
||||
None => {
|
||||
return Err(anyhow!(ProtocolError {
|
||||
msg: "Expected 4 Bytes at index 18".into(),
|
||||
}))
|
||||
}
|
||||
};
|
||||
Ok(KeyboardEvent::Modifiers {
|
||||
mods_depressed,
|
||||
mods_latched,
|
||||
mods_locked,
|
||||
group,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
None => Err(anyhow!(ProtocolError {
|
||||
msg: "Expected an element at index 0".into(),
|
||||
})),
|
||||
}
|
||||
}
|
||||
}
|
||||
272
src/frontend.rs
272
src/frontend.rs
@@ -1,272 +0,0 @@
|
||||
use anyhow::{anyhow, Result};
|
||||
use std::{cmp::min, io::ErrorKind, str, time::Duration};
|
||||
|
||||
#[cfg(unix)]
|
||||
use std::{
|
||||
env,
|
||||
path::{Path, PathBuf},
|
||||
};
|
||||
|
||||
use tokio::io::ReadHalf;
|
||||
use tokio::io::{AsyncReadExt, AsyncWriteExt, WriteHalf};
|
||||
|
||||
#[cfg(unix)]
|
||||
use tokio::net::UnixListener;
|
||||
#[cfg(unix)]
|
||||
use tokio::net::UnixStream;
|
||||
|
||||
#[cfg(windows)]
|
||||
use tokio::net::TcpListener;
|
||||
#[cfg(windows)]
|
||||
use tokio::net::TcpStream;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::{
|
||||
client::{Client, ClientHandle, Position},
|
||||
config::{Config, Frontend},
|
||||
};
|
||||
|
||||
/// cli frontend
|
||||
pub mod cli;
|
||||
|
||||
/// gtk frontend
|
||||
#[cfg(feature = "gtk")]
|
||||
pub mod gtk;
|
||||
|
||||
pub fn run_frontend(config: &Config) -> Result<()> {
|
||||
match config.frontend {
|
||||
#[cfg(feature = "gtk")]
|
||||
Frontend::Gtk => {
|
||||
gtk::run();
|
||||
}
|
||||
#[cfg(not(feature = "gtk"))]
|
||||
Frontend::Gtk => panic!("gtk frontend requested but feature not enabled!"),
|
||||
Frontend::Cli => {
|
||||
cli::run()?;
|
||||
}
|
||||
};
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn exponential_back_off(duration: &mut Duration) -> &Duration {
|
||||
let new = duration.saturating_mul(2);
|
||||
*duration = min(new, Duration::from_secs(1));
|
||||
duration
|
||||
}
|
||||
|
||||
/// wait for the lan-mouse socket to come online
|
||||
#[cfg(unix)]
|
||||
pub fn wait_for_service() -> Result<std::os::unix::net::UnixStream> {
|
||||
let socket_path = FrontendListener::socket_path()?;
|
||||
let mut duration = Duration::from_millis(1);
|
||||
loop {
|
||||
use std::os::unix::net::UnixStream;
|
||||
if let Ok(stream) = UnixStream::connect(&socket_path) {
|
||||
break Ok(stream);
|
||||
}
|
||||
// a signaling mechanism or inotify could be used to
|
||||
// improve this
|
||||
std::thread::sleep(*exponential_back_off(&mut duration));
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
pub fn wait_for_service() -> Result<std::net::TcpStream> {
|
||||
let mut duration = Duration::from_millis(1);
|
||||
loop {
|
||||
use std::net::TcpStream;
|
||||
if let Ok(stream) = TcpStream::connect("127.0.0.1:5252") {
|
||||
break Ok(stream);
|
||||
}
|
||||
std::thread::sleep(*exponential_back_off(&mut duration));
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Eq, PartialEq, Clone, Serialize, Deserialize)]
|
||||
pub enum FrontendEvent {
|
||||
/// add a new client
|
||||
AddClient(Option<String>, u16, Position),
|
||||
/// activate/deactivate client
|
||||
ActivateClient(ClientHandle, bool),
|
||||
/// change the listen port (recreate udp listener)
|
||||
ChangePort(u16),
|
||||
/// remove a client
|
||||
DelClient(ClientHandle),
|
||||
/// request an enumertaion of all clients
|
||||
Enumerate(),
|
||||
/// service shutdown
|
||||
Shutdown(),
|
||||
/// update a client (hostname, port, position)
|
||||
UpdateClient(ClientHandle, Option<String>, u16, Position),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub enum FrontendNotify {
|
||||
NotifyClientCreate(ClientHandle, Option<String>, u16, Position),
|
||||
NotifyClientUpdate(ClientHandle, Option<String>, u16, Position),
|
||||
NotifyClientDelete(ClientHandle),
|
||||
/// new port, reason of failure (if failed)
|
||||
NotifyPortChange(u16, Option<String>),
|
||||
/// Client State, active
|
||||
Enumerate(Vec<(Client, bool)>),
|
||||
NotifyError(String),
|
||||
}
|
||||
|
||||
pub struct FrontendListener {
|
||||
#[cfg(windows)]
|
||||
listener: TcpListener,
|
||||
#[cfg(unix)]
|
||||
listener: UnixListener,
|
||||
#[cfg(unix)]
|
||||
socket_path: PathBuf,
|
||||
#[cfg(unix)]
|
||||
tx_streams: Vec<WriteHalf<UnixStream>>,
|
||||
#[cfg(windows)]
|
||||
tx_streams: Vec<WriteHalf<TcpStream>>,
|
||||
}
|
||||
|
||||
impl FrontendListener {
|
||||
#[cfg(all(unix, not(target_os = "macos")))]
|
||||
pub fn socket_path() -> Result<PathBuf> {
|
||||
let xdg_runtime_dir = match env::var("XDG_RUNTIME_DIR") {
|
||||
Ok(d) => d,
|
||||
Err(e) => return Err(anyhow!("could not find XDG_RUNTIME_DIR: {e}")),
|
||||
};
|
||||
let xdg_runtime_dir = Path::new(xdg_runtime_dir.as_str());
|
||||
Ok(xdg_runtime_dir.join("lan-mouse-socket.sock"))
|
||||
}
|
||||
|
||||
#[cfg(all(unix, target_os = "macos"))]
|
||||
pub fn socket_path() -> Result<PathBuf> {
|
||||
let home = match env::var("HOME") {
|
||||
Ok(d) => d,
|
||||
Err(e) => return Err(anyhow!("could not find HOME: {e}")),
|
||||
};
|
||||
let home = Path::new(home.as_str());
|
||||
let path = home
|
||||
.join("Library")
|
||||
.join("Caches")
|
||||
.join("lan-mouse-socket.sock");
|
||||
Ok(path)
|
||||
}
|
||||
|
||||
pub async fn new() -> Option<Result<Self>> {
|
||||
#[cfg(unix)]
|
||||
let (socket_path, listener) = {
|
||||
let socket_path = match Self::socket_path() {
|
||||
Ok(path) => path,
|
||||
Err(e) => return Some(Err(e)),
|
||||
};
|
||||
|
||||
log::debug!("remove socket: {:?}", socket_path);
|
||||
if socket_path.exists() {
|
||||
// try to connect to see if some other instance
|
||||
// of lan-mouse is already running
|
||||
match UnixStream::connect(&socket_path).await {
|
||||
// connected -> lan-mouse is already running
|
||||
Ok(_) => return None,
|
||||
// lan-mouse is not running but a socket was left behind
|
||||
Err(e) => {
|
||||
log::debug!("{socket_path:?}: {e} - removing left behind socket");
|
||||
let _ = std::fs::remove_file(&socket_path);
|
||||
}
|
||||
}
|
||||
}
|
||||
let listener = match UnixListener::bind(&socket_path) {
|
||||
Ok(ls) => ls,
|
||||
// some other lan-mouse instance has bound the socket in the meantime
|
||||
Err(e) if e.kind() == ErrorKind::AddrInUse => return None,
|
||||
Err(e) => return Some(Err(anyhow!("failed to bind lan-mouse-socket: {e}"))),
|
||||
};
|
||||
(socket_path, listener)
|
||||
};
|
||||
|
||||
#[cfg(windows)]
|
||||
let listener = match TcpListener::bind("127.0.0.1:5252").await {
|
||||
Ok(ls) => ls,
|
||||
// some other lan-mouse instance has bound the socket in the meantime
|
||||
Err(e) if e.kind() == ErrorKind::AddrInUse => return None,
|
||||
Err(e) => return Some(Err(anyhow!("failed to bind lan-mouse-socket: {e}"))),
|
||||
};
|
||||
|
||||
let adapter = Self {
|
||||
listener,
|
||||
#[cfg(unix)]
|
||||
socket_path,
|
||||
tx_streams: vec![],
|
||||
};
|
||||
|
||||
Some(Ok(adapter))
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
pub async fn accept(&mut self) -> Result<ReadHalf<UnixStream>> {
|
||||
let stream = self.listener.accept().await?.0;
|
||||
let (rx, tx) = tokio::io::split(stream);
|
||||
self.tx_streams.push(tx);
|
||||
Ok(rx)
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
pub async fn accept(&mut self) -> Result<ReadHalf<TcpStream>> {
|
||||
let stream = self.listener.accept().await?.0;
|
||||
let (rx, tx) = tokio::io::split(stream);
|
||||
self.tx_streams.push(tx);
|
||||
Ok(rx)
|
||||
}
|
||||
|
||||
pub(crate) async fn notify_all(&mut self, notify: FrontendNotify) -> Result<()> {
|
||||
// encode event
|
||||
let json = serde_json::to_string(¬ify).unwrap();
|
||||
let payload = json.as_bytes();
|
||||
let len = payload.len().to_be_bytes();
|
||||
log::debug!("json: {json}, len: {}", payload.len());
|
||||
|
||||
let mut keep = vec![];
|
||||
|
||||
// TODO do simultaneously
|
||||
for tx in self.tx_streams.iter_mut() {
|
||||
// write len + payload
|
||||
if tx.write(&len).await.is_err() {
|
||||
keep.push(false);
|
||||
continue;
|
||||
}
|
||||
if tx.write(payload).await.is_err() {
|
||||
keep.push(false);
|
||||
continue;
|
||||
}
|
||||
keep.push(true);
|
||||
}
|
||||
|
||||
// could not find a better solution because async
|
||||
let mut keep = keep.into_iter();
|
||||
self.tx_streams.retain(|_| keep.next().unwrap());
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
impl Drop for FrontendListener {
|
||||
fn drop(&mut self) {
|
||||
log::debug!("remove socket: {:?}", self.socket_path);
|
||||
let _ = std::fs::remove_file(&self.socket_path);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
pub async fn read_event(stream: &mut ReadHalf<UnixStream>) -> Result<FrontendEvent> {
|
||||
let len = stream.read_u64().await?;
|
||||
assert!(len <= 256);
|
||||
let mut buf = [0u8; 256];
|
||||
stream.read_exact(&mut buf[..len as usize]).await?;
|
||||
Ok(serde_json::from_slice(&buf[..len as usize])?)
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
pub async fn read_event(stream: &mut ReadHalf<TcpStream>) -> Result<FrontendEvent> {
|
||||
let len = stream.read_u64().await?;
|
||||
let mut buf = [0u8; 256];
|
||||
stream.read_exact(&mut buf[..len as usize]).await?;
|
||||
Ok(serde_json::from_slice(&buf[..len as usize])?)
|
||||
}
|
||||
@@ -1,233 +0,0 @@
|
||||
use anyhow::{anyhow, Context, Result};
|
||||
|
||||
use std::{
|
||||
io::{ErrorKind, Read, Write},
|
||||
str::SplitWhitespace,
|
||||
thread,
|
||||
};
|
||||
|
||||
use crate::{client::Position, config::DEFAULT_PORT};
|
||||
|
||||
use super::{FrontendEvent, FrontendNotify};
|
||||
|
||||
pub fn run() -> Result<()> {
|
||||
let Ok(mut tx) = super::wait_for_service() else {
|
||||
return Err(anyhow!("Could not connect to lan-mouse-socket"));
|
||||
};
|
||||
|
||||
let mut rx = tx.try_clone()?;
|
||||
|
||||
let reader = thread::Builder::new()
|
||||
.name("cli-frontend".to_string())
|
||||
.spawn(move || {
|
||||
// all further prompts
|
||||
prompt();
|
||||
loop {
|
||||
let mut buf = String::new();
|
||||
match std::io::stdin().read_line(&mut buf) {
|
||||
Ok(0) => return,
|
||||
Ok(len) => {
|
||||
if let Some(events) = parse_cmd(buf, len) {
|
||||
for event in events.iter() {
|
||||
let json = serde_json::to_string(&event).unwrap();
|
||||
let bytes = json.as_bytes();
|
||||
let len = bytes.len().to_be_bytes();
|
||||
if let Err(e) = tx.write(&len) {
|
||||
log::error!("error sending message: {e}");
|
||||
};
|
||||
if let Err(e) = tx.write(bytes) {
|
||||
log::error!("error sending message: {e}");
|
||||
};
|
||||
if *event == FrontendEvent::Shutdown() {
|
||||
return;
|
||||
}
|
||||
}
|
||||
// prompt is printed after the server response is received
|
||||
} else {
|
||||
prompt();
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
if e.kind() != ErrorKind::UnexpectedEof {
|
||||
log::error!("error reading from stdin: {e}");
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
})?;
|
||||
|
||||
let _ = thread::Builder::new()
|
||||
.name("cli-frontend-notify".to_string())
|
||||
.spawn(move || {
|
||||
loop {
|
||||
// read len
|
||||
let mut len = [0u8; 8];
|
||||
match rx.read_exact(&mut len) {
|
||||
Ok(()) => (),
|
||||
Err(e) if e.kind() == ErrorKind::UnexpectedEof => break,
|
||||
Err(e) => break log::error!("{e}"),
|
||||
};
|
||||
let len = usize::from_be_bytes(len);
|
||||
|
||||
// read payload
|
||||
let mut buf: Vec<u8> = vec![0u8; len];
|
||||
match rx.read_exact(&mut buf[..len]) {
|
||||
Ok(()) => (),
|
||||
Err(e) if e.kind() == ErrorKind::UnexpectedEof => break,
|
||||
Err(e) => break log::error!("{e}"),
|
||||
};
|
||||
|
||||
let notify: FrontendNotify = match serde_json::from_slice(&buf) {
|
||||
Ok(n) => n,
|
||||
Err(e) => break log::error!("{e}"),
|
||||
};
|
||||
match notify {
|
||||
FrontendNotify::NotifyClientCreate(client, host, port, pos) => {
|
||||
log::info!(
|
||||
"new client ({client}): {}:{port} - {pos}",
|
||||
host.as_deref().unwrap_or("")
|
||||
);
|
||||
}
|
||||
FrontendNotify::NotifyClientUpdate(client, host, port, pos) => {
|
||||
log::info!(
|
||||
"client ({client}) updated: {}:{port} - {pos}",
|
||||
host.as_deref().unwrap_or("")
|
||||
);
|
||||
}
|
||||
FrontendNotify::NotifyClientDelete(client) => {
|
||||
log::info!("client ({client}) deleted.");
|
||||
}
|
||||
FrontendNotify::NotifyError(e) => {
|
||||
log::warn!("{e}");
|
||||
}
|
||||
FrontendNotify::Enumerate(clients) => {
|
||||
for (client, active) in clients.into_iter() {
|
||||
log::info!(
|
||||
"client ({}) [{}]: active: {}, associated addresses: [{}]",
|
||||
client.handle,
|
||||
client.hostname.as_deref().unwrap_or(""),
|
||||
if active { "yes" } else { "no" },
|
||||
client
|
||||
.addrs
|
||||
.into_iter()
|
||||
.map(|a| a.to_string())
|
||||
.collect::<Vec<String>>()
|
||||
.join(", ")
|
||||
);
|
||||
}
|
||||
}
|
||||
FrontendNotify::NotifyPortChange(port, msg) => match msg {
|
||||
Some(msg) => log::info!("could not change port: {msg}"),
|
||||
None => log::info!("port changed: {port}"),
|
||||
},
|
||||
}
|
||||
prompt();
|
||||
}
|
||||
})?;
|
||||
match reader.join() {
|
||||
Ok(_) => {}
|
||||
Err(e) => {
|
||||
let msg = match (e.downcast_ref::<&str>(), e.downcast_ref::<String>()) {
|
||||
(Some(&s), _) => s,
|
||||
(_, Some(s)) => s,
|
||||
_ => "no panic info",
|
||||
};
|
||||
log::error!("reader thread paniced: {msg}");
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn prompt() {
|
||||
eprint!("lan-mouse > ");
|
||||
std::io::stderr().flush().unwrap();
|
||||
}
|
||||
|
||||
fn parse_cmd(s: String, len: usize) -> Option<Vec<FrontendEvent>> {
|
||||
if len == 0 {
|
||||
return Some(vec![FrontendEvent::Shutdown()]);
|
||||
}
|
||||
let mut l = s.split_whitespace();
|
||||
let cmd = l.next()?;
|
||||
let res = match cmd {
|
||||
"help" => {
|
||||
log::info!("list list clients");
|
||||
log::info!("connect <host> left|right|top|bottom [port] add a new client");
|
||||
log::info!("disconnect <client> remove a client");
|
||||
log::info!("activate <client> activate a client");
|
||||
log::info!("deactivate <client> deactivate a client");
|
||||
log::info!("exit exit lan-mouse");
|
||||
log::info!("setport <port> change port");
|
||||
None
|
||||
}
|
||||
"exit" => return Some(vec![FrontendEvent::Shutdown()]),
|
||||
"list" => return Some(vec![FrontendEvent::Enumerate()]),
|
||||
"connect" => Some(parse_connect(l)),
|
||||
"disconnect" => Some(parse_disconnect(l)),
|
||||
"activate" => Some(parse_activate(l)),
|
||||
"deactivate" => Some(parse_deactivate(l)),
|
||||
"setport" => Some(parse_port(l)),
|
||||
_ => {
|
||||
log::error!("unknown command: {s}");
|
||||
None
|
||||
}
|
||||
};
|
||||
match res {
|
||||
Some(Ok(e)) => Some(e),
|
||||
Some(Err(e)) => {
|
||||
log::warn!("{e}");
|
||||
None
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_connect(mut l: SplitWhitespace) -> Result<Vec<FrontendEvent>> {
|
||||
let usage = "usage: connect <host> left|right|top|bottom [port]";
|
||||
let host = l.next().context(usage)?.to_owned();
|
||||
let pos = match l.next().context(usage)? {
|
||||
"right" => Position::Right,
|
||||
"top" => Position::Top,
|
||||
"bottom" => Position::Bottom,
|
||||
_ => Position::Left,
|
||||
};
|
||||
let port = if let Some(p) = l.next() {
|
||||
p.parse()?
|
||||
} else {
|
||||
DEFAULT_PORT
|
||||
};
|
||||
Ok(vec![
|
||||
FrontendEvent::AddClient(Some(host), port, pos),
|
||||
FrontendEvent::Enumerate(),
|
||||
])
|
||||
}
|
||||
|
||||
fn parse_disconnect(mut l: SplitWhitespace) -> Result<Vec<FrontendEvent>> {
|
||||
let client = l.next().context("usage: disconnect <client_id>")?.parse()?;
|
||||
Ok(vec![
|
||||
FrontendEvent::DelClient(client),
|
||||
FrontendEvent::Enumerate(),
|
||||
])
|
||||
}
|
||||
|
||||
fn parse_activate(mut l: SplitWhitespace) -> Result<Vec<FrontendEvent>> {
|
||||
let client = l.next().context("usage: activate <client_id>")?.parse()?;
|
||||
Ok(vec![
|
||||
FrontendEvent::ActivateClient(client, true),
|
||||
FrontendEvent::Enumerate(),
|
||||
])
|
||||
}
|
||||
|
||||
fn parse_deactivate(mut l: SplitWhitespace) -> Result<Vec<FrontendEvent>> {
|
||||
let client = l.next().context("usage: deactivate <client_id>")?.parse()?;
|
||||
Ok(vec![
|
||||
FrontendEvent::ActivateClient(client, false),
|
||||
FrontendEvent::Enumerate(),
|
||||
])
|
||||
}
|
||||
|
||||
fn parse_port(mut l: SplitWhitespace) -> Result<Vec<FrontendEvent>> {
|
||||
let port = l.next().context("usage: setport <port>")?.parse()?;
|
||||
Ok(vec![FrontendEvent::ChangePort(port)])
|
||||
}
|
||||
@@ -1,208 +0,0 @@
|
||||
mod client_object;
|
||||
mod client_row;
|
||||
mod window;
|
||||
|
||||
use std::{
|
||||
env,
|
||||
io::{ErrorKind, Read},
|
||||
process, str,
|
||||
};
|
||||
|
||||
use crate::{config::DEFAULT_PORT, frontend::gtk::window::Window};
|
||||
|
||||
use adw::Application;
|
||||
use gtk::{
|
||||
gdk::Display,
|
||||
gio::{SimpleAction, SimpleActionGroup},
|
||||
glib::clone,
|
||||
prelude::*,
|
||||
subclass::prelude::ObjectSubclassIsExt,
|
||||
CssProvider, IconTheme,
|
||||
};
|
||||
use gtk::{gio, glib, prelude::ApplicationExt};
|
||||
|
||||
use self::client_object::ClientObject;
|
||||
|
||||
use super::FrontendNotify;
|
||||
|
||||
pub fn run() -> glib::ExitCode {
|
||||
log::debug!("running gtk frontend");
|
||||
#[cfg(windows)]
|
||||
let ret = std::thread::Builder::new()
|
||||
.stack_size(8 * 1024 * 1024) // https://gitlab.gnome.org/GNOME/gtk/-/commit/52dbb3f372b2c3ea339e879689c1de535ba2c2c3 -> caused crash on windows
|
||||
.name("gtk".into())
|
||||
.spawn(gtk_main)
|
||||
.unwrap()
|
||||
.join()
|
||||
.unwrap();
|
||||
#[cfg(not(windows))]
|
||||
let ret = gtk_main();
|
||||
|
||||
log::debug!("frontend exited");
|
||||
ret
|
||||
}
|
||||
|
||||
fn gtk_main() -> glib::ExitCode {
|
||||
gio::resources_register_include!("lan-mouse.gresource").expect("Failed to register resources.");
|
||||
|
||||
let app = Application::builder()
|
||||
.application_id("de.feschber.lan-mouse")
|
||||
.build();
|
||||
|
||||
app.connect_startup(|_| load_icons());
|
||||
app.connect_startup(|_| load_css());
|
||||
app.connect_activate(build_ui);
|
||||
|
||||
let args: Vec<&'static str> = vec![];
|
||||
app.run_with_args(&args)
|
||||
}
|
||||
|
||||
fn load_css() {
|
||||
let provider = CssProvider::new();
|
||||
provider.load_from_resource("de/feschber/LanMouse/style.css");
|
||||
gtk::style_context_add_provider_for_display(
|
||||
&Display::default().expect("Could not connect to a display."),
|
||||
&provider,
|
||||
gtk::STYLE_PROVIDER_PRIORITY_APPLICATION,
|
||||
);
|
||||
}
|
||||
|
||||
fn load_icons() {
|
||||
let icon_theme =
|
||||
IconTheme::for_display(&Display::default().expect("Could not connect to a display."));
|
||||
icon_theme.add_resource_path("/de/feschber/LanMouse/icons");
|
||||
}
|
||||
|
||||
fn build_ui(app: &Application) {
|
||||
log::debug!("connecting to lan-mouse-socket");
|
||||
let mut rx = match super::wait_for_service() {
|
||||
Ok(stream) => stream,
|
||||
Err(e) => {
|
||||
log::error!("could not connect to lan-mouse-socket: {e}");
|
||||
process::exit(1);
|
||||
}
|
||||
};
|
||||
let tx = match rx.try_clone() {
|
||||
Ok(sock) => sock,
|
||||
Err(e) => {
|
||||
log::error!("{e}");
|
||||
process::exit(1);
|
||||
}
|
||||
};
|
||||
log::debug!("connected to lan-mouse-socket");
|
||||
|
||||
let (sender, receiver) = async_channel::bounded(10);
|
||||
|
||||
gio::spawn_blocking(move || {
|
||||
match loop {
|
||||
// read length
|
||||
let mut len = [0u8; 8];
|
||||
match rx.read_exact(&mut len) {
|
||||
Ok(_) => (),
|
||||
Err(e) if e.kind() == ErrorKind::UnexpectedEof => break Ok(()),
|
||||
Err(e) => break Err(e),
|
||||
};
|
||||
let len = usize::from_be_bytes(len);
|
||||
|
||||
// read payload
|
||||
let mut buf = vec![0u8; len];
|
||||
match rx.read_exact(&mut buf) {
|
||||
Ok(_) => (),
|
||||
Err(e) if e.kind() == ErrorKind::UnexpectedEof => break Ok(()),
|
||||
Err(e) => break Err(e),
|
||||
};
|
||||
|
||||
// parse json
|
||||
let json = str::from_utf8(&buf).unwrap();
|
||||
match serde_json::from_str(json) {
|
||||
Ok(notify) => sender.send_blocking(notify).unwrap(),
|
||||
Err(e) => log::error!("{e}"),
|
||||
}
|
||||
} {
|
||||
Ok(()) => {}
|
||||
Err(e) => log::error!("{e}"),
|
||||
}
|
||||
});
|
||||
|
||||
let window = Window::new(app);
|
||||
window.imp().stream.borrow_mut().replace(tx);
|
||||
glib::spawn_future_local(clone!(@weak window => async move {
|
||||
loop {
|
||||
let notify = receiver.recv().await.unwrap();
|
||||
match notify {
|
||||
FrontendNotify::NotifyClientCreate(client, hostname, port, position) => {
|
||||
window.new_client(client, hostname, port, position, false);
|
||||
},
|
||||
FrontendNotify::NotifyClientUpdate(client, hostname, port, position) => {
|
||||
log::info!("client updated: {client}, {}:{port}, {position}", hostname.unwrap_or("".to_string()));
|
||||
}
|
||||
FrontendNotify::NotifyError(e) => {
|
||||
// TODO
|
||||
log::error!("{e}");
|
||||
},
|
||||
FrontendNotify::NotifyClientDelete(client) => {
|
||||
window.delete_client(client);
|
||||
}
|
||||
FrontendNotify::Enumerate(clients) => {
|
||||
for (client, active) in clients {
|
||||
if window.client_idx(client.handle).is_some() {
|
||||
continue
|
||||
}
|
||||
window.new_client(
|
||||
client.handle,
|
||||
client.hostname,
|
||||
client.addrs
|
||||
.iter()
|
||||
.next()
|
||||
.map(|s| s.port())
|
||||
.unwrap_or(DEFAULT_PORT),
|
||||
client.pos,
|
||||
active,
|
||||
);
|
||||
}
|
||||
},
|
||||
FrontendNotify::NotifyPortChange(port, msg) => {
|
||||
match msg {
|
||||
None => window.show_toast(format!("port changed: {port}").as_str()),
|
||||
Some(msg) => window.show_toast(msg.as_str()),
|
||||
}
|
||||
window.imp().set_port(port);
|
||||
}
|
||||
}
|
||||
}
|
||||
}));
|
||||
|
||||
let action_request_client_update =
|
||||
SimpleAction::new("request-client-update", Some(&u32::static_variant_type()));
|
||||
|
||||
// remove client
|
||||
let action_client_delete =
|
||||
SimpleAction::new("request-client-delete", Some(&u32::static_variant_type()));
|
||||
|
||||
// update client state
|
||||
action_request_client_update.connect_activate(clone!(@weak window => move |_action, param| {
|
||||
log::debug!("request-client-update");
|
||||
let index = param.unwrap()
|
||||
.get::<u32>()
|
||||
.unwrap();
|
||||
let Some(client) = window.clients().item(index) else {
|
||||
return;
|
||||
};
|
||||
let client = client.downcast_ref::<ClientObject>().unwrap();
|
||||
window.request_client_update(client);
|
||||
}));
|
||||
|
||||
action_client_delete.connect_activate(clone!(@weak window => move |_action, param| {
|
||||
log::debug!("delete-client");
|
||||
let idx = param.unwrap()
|
||||
.get::<u32>()
|
||||
.unwrap();
|
||||
window.request_client_delete(idx);
|
||||
}));
|
||||
|
||||
let actions = SimpleActionGroup::new();
|
||||
window.insert_action_group("win", Some(&actions));
|
||||
actions.add_action(&action_request_client_update);
|
||||
actions.add_action(&action_client_delete);
|
||||
window.present();
|
||||
}
|
||||
@@ -1,41 +0,0 @@
|
||||
mod imp;
|
||||
|
||||
use adw::subclass::prelude::*;
|
||||
use gtk::glib::{self, Object};
|
||||
|
||||
use crate::client::ClientHandle;
|
||||
|
||||
glib::wrapper! {
|
||||
pub struct ClientObject(ObjectSubclass<imp::ClientObject>);
|
||||
}
|
||||
|
||||
impl ClientObject {
|
||||
pub fn new(
|
||||
handle: ClientHandle,
|
||||
hostname: Option<String>,
|
||||
port: u32,
|
||||
position: String,
|
||||
active: bool,
|
||||
) -> Self {
|
||||
Object::builder()
|
||||
.property("handle", handle)
|
||||
.property("hostname", hostname)
|
||||
.property("port", port)
|
||||
.property("active", active)
|
||||
.property("position", position)
|
||||
.build()
|
||||
}
|
||||
|
||||
pub fn get_data(&self) -> ClientData {
|
||||
self.imp().data.borrow().clone()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default, Clone)]
|
||||
pub struct ClientData {
|
||||
pub handle: ClientHandle,
|
||||
pub hostname: Option<String>,
|
||||
pub port: u32,
|
||||
pub active: bool,
|
||||
pub position: String,
|
||||
}
|
||||
@@ -1,176 +0,0 @@
|
||||
mod imp;
|
||||
|
||||
use std::io::Write;
|
||||
|
||||
use adw::prelude::*;
|
||||
use adw::subclass::prelude::*;
|
||||
use glib::{clone, Object};
|
||||
use gtk::{gio, glib, NoSelection};
|
||||
|
||||
use crate::{
|
||||
client::{ClientHandle, Position},
|
||||
config::DEFAULT_PORT,
|
||||
frontend::{gtk::client_object::ClientObject, FrontendEvent},
|
||||
};
|
||||
|
||||
use super::client_row::ClientRow;
|
||||
|
||||
glib::wrapper! {
|
||||
pub struct Window(ObjectSubclass<imp::Window>)
|
||||
@extends adw::ApplicationWindow, gtk::Window, gtk::Widget,
|
||||
@implements gio::ActionGroup, gio::ActionMap, gtk::Accessible, gtk::Buildable,
|
||||
gtk::ConstraintTarget, gtk::Native, gtk::Root, gtk::ShortcutManager;
|
||||
}
|
||||
|
||||
impl Window {
|
||||
pub(crate) fn new(app: &adw::Application) -> Self {
|
||||
Object::builder().property("application", app).build()
|
||||
}
|
||||
|
||||
pub fn clients(&self) -> gio::ListStore {
|
||||
self.imp()
|
||||
.clients
|
||||
.borrow()
|
||||
.clone()
|
||||
.expect("Could not get clients")
|
||||
}
|
||||
|
||||
fn setup_clients(&self) {
|
||||
let model = gio::ListStore::new::<ClientObject>();
|
||||
self.imp().clients.replace(Some(model));
|
||||
|
||||
let selection_model = NoSelection::new(Some(self.clients()));
|
||||
self.imp().client_list.bind_model(
|
||||
Some(&selection_model),
|
||||
clone!(@weak self as window => @default-panic, move |obj| {
|
||||
let client_object = obj.downcast_ref().expect("Expected object of type `ClientObject`.");
|
||||
let row = window.create_client_row(client_object);
|
||||
row.upcast()
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
/// workaround for a bug in libadwaita that shows an ugly line beneath
|
||||
/// the last element if a placeholder is set.
|
||||
/// https://gitlab.gnome.org/GNOME/gtk/-/merge_requests/6308
|
||||
pub fn set_placeholder_visible(&self, visible: bool) {
|
||||
let placeholder = self.imp().client_placeholder.get();
|
||||
self.imp().client_list.set_placeholder(match visible {
|
||||
true => Some(&placeholder),
|
||||
false => None,
|
||||
});
|
||||
}
|
||||
|
||||
fn setup_icon(&self) {
|
||||
self.set_icon_name(Some("mouse-icon"));
|
||||
}
|
||||
|
||||
fn create_client_row(&self, client_object: &ClientObject) -> ClientRow {
|
||||
let row = ClientRow::new(client_object);
|
||||
row.bind(client_object);
|
||||
row
|
||||
}
|
||||
|
||||
pub fn new_client(
|
||||
&self,
|
||||
handle: ClientHandle,
|
||||
hostname: Option<String>,
|
||||
port: u16,
|
||||
position: Position,
|
||||
active: bool,
|
||||
) {
|
||||
let client = ClientObject::new(handle, hostname, port as u32, position.to_string(), active);
|
||||
self.clients().append(&client);
|
||||
self.set_placeholder_visible(false);
|
||||
}
|
||||
|
||||
pub fn client_idx(&self, handle: ClientHandle) -> Option<usize> {
|
||||
self.clients().iter::<ClientObject>().position(|c| {
|
||||
if let Ok(c) = c {
|
||||
c.handle() == handle
|
||||
} else {
|
||||
false
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
pub fn delete_client(&self, handle: ClientHandle) {
|
||||
let Some(idx) = self.client_idx(handle) else {
|
||||
log::warn!("could not find client with handle {handle}");
|
||||
return;
|
||||
};
|
||||
|
||||
self.clients().remove(idx as u32);
|
||||
if self.clients().n_items() == 0 {
|
||||
self.set_placeholder_visible(true);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn request_client_create(&self) {
|
||||
let event = FrontendEvent::AddClient(None, DEFAULT_PORT, Position::default());
|
||||
self.imp().set_port(DEFAULT_PORT);
|
||||
self.request(event);
|
||||
}
|
||||
|
||||
pub fn request_port_change(&self) {
|
||||
let port = self.imp().port_entry.get().text().to_string();
|
||||
if let Ok(port) = port.as_str().parse::<u16>() {
|
||||
self.request(FrontendEvent::ChangePort(port));
|
||||
} else {
|
||||
self.request(FrontendEvent::ChangePort(DEFAULT_PORT));
|
||||
}
|
||||
}
|
||||
|
||||
pub fn request_client_update(&self, client: &ClientObject) {
|
||||
let data = client.get_data();
|
||||
let position = match data.position.as_str() {
|
||||
"left" => Position::Left,
|
||||
"right" => Position::Right,
|
||||
"top" => Position::Top,
|
||||
"bottom" => Position::Bottom,
|
||||
_ => {
|
||||
log::error!("invalid position: {}", data.position);
|
||||
return;
|
||||
}
|
||||
};
|
||||
let hostname = data.hostname;
|
||||
let port = data.port as u16;
|
||||
let event = FrontendEvent::UpdateClient(client.handle(), hostname, port, position);
|
||||
self.request(event);
|
||||
|
||||
let event = FrontendEvent::ActivateClient(client.handle(), !client.active());
|
||||
self.request(event);
|
||||
}
|
||||
|
||||
pub fn request_client_delete(&self, idx: u32) {
|
||||
if let Some(obj) = self.clients().item(idx) {
|
||||
let client_object: &ClientObject = obj
|
||||
.downcast_ref()
|
||||
.expect("Expected object of type `ClientObject`.");
|
||||
let handle = client_object.handle();
|
||||
let event = FrontendEvent::DelClient(handle);
|
||||
self.request(event);
|
||||
}
|
||||
}
|
||||
|
||||
fn request(&self, event: FrontendEvent) {
|
||||
let json = serde_json::to_string(&event).unwrap();
|
||||
log::debug!("requesting {json}");
|
||||
let mut stream = self.imp().stream.borrow_mut();
|
||||
let stream = stream.as_mut().unwrap();
|
||||
let bytes = json.as_bytes();
|
||||
let len = bytes.len().to_be_bytes();
|
||||
if let Err(e) = stream.write(&len) {
|
||||
log::error!("error sending message: {e}");
|
||||
};
|
||||
if let Err(e) = stream.write(bytes) {
|
||||
log::error!("error sending message: {e}");
|
||||
};
|
||||
}
|
||||
|
||||
pub fn show_toast(&self, msg: &str) {
|
||||
let toast = adw::Toast::new(msg);
|
||||
let toast_overlay = &self.imp().toast_overlay;
|
||||
toast_overlay.add_toast(toast);
|
||||
}
|
||||
}
|
||||
@@ -1,12 +1,7 @@
|
||||
pub mod client;
|
||||
pub mod config;
|
||||
pub mod dns;
|
||||
pub mod event;
|
||||
pub mod server;
|
||||
|
||||
pub mod consumer;
|
||||
pub mod producer;
|
||||
|
||||
pub mod backend;
|
||||
pub mod frontend;
|
||||
pub mod scancode;
|
||||
pub mod capture_test;
|
||||
pub mod emulation_test;
|
||||
|
||||
102
src/main.rs
102
src/main.rs
@@ -1,11 +1,37 @@
|
||||
use anyhow::Result;
|
||||
use std::process::{self, Child, Command};
|
||||
|
||||
use env_logger::Env;
|
||||
use lan_mouse::{config::Config, frontend, server::Server};
|
||||
|
||||
use input_capture::InputCaptureError;
|
||||
use input_emulation::InputEmulationError;
|
||||
use lan_mouse::{
|
||||
capture_test,
|
||||
config::{Config, ConfigError, Frontend},
|
||||
emulation_test,
|
||||
server::{Server, ServiceError},
|
||||
};
|
||||
use lan_mouse_ipc::IpcError;
|
||||
use std::{
|
||||
future::Future,
|
||||
io,
|
||||
process::{self, Child, Command},
|
||||
};
|
||||
use thiserror::Error;
|
||||
use tokio::task::LocalSet;
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
enum LanMouseError {
|
||||
#[error(transparent)]
|
||||
Service(#[from] ServiceError),
|
||||
#[error(transparent)]
|
||||
IpcError(#[from] IpcError),
|
||||
#[error(transparent)]
|
||||
Config(#[from] ConfigError),
|
||||
#[error(transparent)]
|
||||
Io(#[from] io::Error),
|
||||
#[error(transparent)]
|
||||
Capture(#[from] InputCaptureError),
|
||||
#[error(transparent)]
|
||||
Emulation(#[from] InputEmulationError),
|
||||
}
|
||||
|
||||
pub fn main() {
|
||||
// init logging
|
||||
let env = Env::default().filter_or("LAN_MOUSE_LOG_LEVEL", "info");
|
||||
@@ -17,28 +43,24 @@ pub fn main() {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn start_service() -> Result<Child> {
|
||||
let child = Command::new(std::env::current_exe()?)
|
||||
.args(std::env::args().skip(1))
|
||||
.arg("--daemon")
|
||||
.spawn()?;
|
||||
Ok(child)
|
||||
}
|
||||
|
||||
pub fn run() -> Result<()> {
|
||||
fn run() -> Result<(), LanMouseError> {
|
||||
// parse config file + cli args
|
||||
let config = Config::new()?;
|
||||
log::debug!("{config:?}");
|
||||
log::info!("release bind: {:?}", config.release_bind);
|
||||
|
||||
if config.daemon {
|
||||
if config.test_capture {
|
||||
run_async(capture_test::run(config))?;
|
||||
} else if config.test_emulation {
|
||||
run_async(emulation_test::run(config))?;
|
||||
} else if config.daemon {
|
||||
// if daemon is specified we run the service
|
||||
run_service(&config)?;
|
||||
run_async(run_service(config))?;
|
||||
} else {
|
||||
// otherwise start the service as a child process and
|
||||
// run a frontend
|
||||
let mut service = start_service()?;
|
||||
frontend::run_frontend(&config)?;
|
||||
log::info!("terminating service");
|
||||
run_frontend(&config)?;
|
||||
#[cfg(unix)]
|
||||
{
|
||||
// on unix we give the service a chance to terminate gracefully
|
||||
@@ -51,10 +73,14 @@ pub fn run() -> Result<()> {
|
||||
service.kill()?;
|
||||
}
|
||||
|
||||
anyhow::Ok(())
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn run_service(config: &Config) -> Result<()> {
|
||||
fn run_async<F, E>(f: F) -> Result<(), LanMouseError>
|
||||
where
|
||||
F: Future<Output = Result<(), E>>,
|
||||
LanMouseError: From<E>,
|
||||
{
|
||||
// create single threaded tokio runtime
|
||||
let runtime = tokio::runtime::Builder::new_current_thread()
|
||||
.enable_io()
|
||||
@@ -62,15 +88,35 @@ fn run_service(config: &Config) -> Result<()> {
|
||||
.build()?;
|
||||
|
||||
// run async event loop
|
||||
runtime.block_on(LocalSet::new().run_until(async {
|
||||
// run main loop
|
||||
log::info!("Press Ctrl+Alt+Shift+Super to release the mouse");
|
||||
Ok(runtime.block_on(LocalSet::new().run_until(f))?)
|
||||
}
|
||||
|
||||
let server = Server::new(config);
|
||||
server.run().await?;
|
||||
fn start_service() -> Result<Child, io::Error> {
|
||||
let child = Command::new(std::env::current_exe()?)
|
||||
.args(std::env::args().skip(1))
|
||||
.arg("--daemon")
|
||||
.spawn()?;
|
||||
Ok(child)
|
||||
}
|
||||
|
||||
log::debug!("service exiting");
|
||||
anyhow::Ok(())
|
||||
}))?;
|
||||
async fn run_service(config: Config) -> Result<(), ServiceError> {
|
||||
log::info!("Press {:?} to release the mouse", config.release_bind);
|
||||
Server::new(config).run().await?;
|
||||
log::info!("service exited!");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn run_frontend(config: &Config) -> Result<(), IpcError> {
|
||||
match config.frontend {
|
||||
#[cfg(feature = "gtk")]
|
||||
Frontend::Gtk => {
|
||||
lan_mouse_gtk::run();
|
||||
}
|
||||
#[cfg(not(feature = "gtk"))]
|
||||
Frontend::Gtk => panic!("gtk frontend requested but feature not enabled!"),
|
||||
Frontend::Cli => {
|
||||
lan_mouse_cli::run()?;
|
||||
}
|
||||
};
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -1,61 +0,0 @@
|
||||
use std::io;
|
||||
|
||||
use futures_core::Stream;
|
||||
|
||||
use crate::backend::producer;
|
||||
use crate::{
|
||||
client::{ClientEvent, ClientHandle},
|
||||
event::Event,
|
||||
};
|
||||
|
||||
pub async fn create() -> Box<dyn EventProducer> {
|
||||
#[cfg(target_os = "macos")]
|
||||
match producer::macos::MacOSProducer::new() {
|
||||
Ok(p) => return Box::new(p),
|
||||
Err(e) => log::info!("macos event producer not available: {e}"),
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
match producer::windows::WindowsProducer::new() {
|
||||
Ok(p) => return Box::new(p),
|
||||
Err(e) => log::info!("windows event producer not available: {e}"),
|
||||
}
|
||||
|
||||
#[cfg(all(unix, feature = "libei", not(target_os = "macos")))]
|
||||
match producer::libei::LibeiProducer::new() {
|
||||
Ok(p) => {
|
||||
log::info!("using libei event producer");
|
||||
return Box::new(p);
|
||||
}
|
||||
Err(e) => log::info!("libei event producer not available: {e}"),
|
||||
}
|
||||
|
||||
#[cfg(all(unix, feature = "wayland", not(target_os = "macos")))]
|
||||
match producer::wayland::WaylandEventProducer::new() {
|
||||
Ok(p) => {
|
||||
log::info!("using layer-shell event producer");
|
||||
return Box::new(p);
|
||||
}
|
||||
Err(e) => log::info!("layer_shell event producer not available: {e}"),
|
||||
}
|
||||
|
||||
#[cfg(all(unix, feature = "x11", not(target_os = "macos")))]
|
||||
match producer::x11::X11Producer::new() {
|
||||
Ok(p) => {
|
||||
log::info!("using x11 event producer");
|
||||
return Box::new(p);
|
||||
}
|
||||
Err(e) => log::info!("x11 event producer not available: {e}"),
|
||||
}
|
||||
|
||||
log::error!("falling back to dummy event producer");
|
||||
Box::new(producer::dummy::DummyProducer::new())
|
||||
}
|
||||
|
||||
pub trait EventProducer: Stream<Item = io::Result<(ClientHandle, Event)>> + Unpin {
|
||||
/// notify event producer of configuration changes
|
||||
fn notify(&mut self, event: ClientEvent) -> io::Result<()>;
|
||||
|
||||
/// release mouse
|
||||
fn release(&mut self) -> io::Result<()>;
|
||||
}
|
||||
1359
src/server.rs
1359
src/server.rs
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user