Compare commits

..

1 Commits

Author SHA1 Message Date
Ferdinand Schober
3ee49753c0 simplify configuration 2025-03-15 18:22:31 +01:00
69 changed files with 2059 additions and 3332 deletions

View File

@@ -1,24 +0,0 @@
root = true
[*]
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true
charset = utf-8
[*.rs]
indent_style = space
indent_size = 4
max_line_length = 100
[*.{yml,yaml}]
indent_style = space
indent_size = 2
[*.toml]
indent_style = space
indent_size = 4
[*.nix]
indent_style = space
indent_size = 2

View File

@@ -1,34 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
# Ensure we're at the repo root
cd "$(git rev-parse --show-toplevel)" || exit 1
echo "Running cargo fmt (auto-format)..."
# Run formatter to apply fixes but do not stage them. If formatting changed,
# fail the commit so the user can review and stage the changes manually.
cargo fmt --all
if ! git diff --quiet --exit-code; then
echo "" >&2
echo "ERROR: cargo fmt modified files. Review changes, stage them, and commit again." >&2
git --no-pager diff --name-only
exit 1
fi
echo "Running cargo clippy..."
# Matches CI: deny warnings to keep code health strict
if ! cargo clippy --workspace --all-targets --all-features -- -D warnings; then
echo "" >&2
echo "ERROR: clippy found warnings/errors. Fix them before committing." >&2
exit 1
fi
echo "Running cargo test..."
if ! cargo test --workspace --all-features; then
echo "" >&2
echo "ERROR: Some tests failed. Fix tests before committing." >&2
exit 1
fi
echo "All pre-commit checks passed."
exit 0

View File

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

View File

@@ -1,59 +1,31 @@
name: "Release" name: "pre-release"
run-name: "Release - ${{ startsWith(github.ref, 'refs/tags/') && github.ref_name || github.event.inputs.name || github.ref_name }}"
on: on:
push: push:
branches: [ "main" ] branches: [ "main" ]
tags:
- v**
workflow_dispatch:
inputs:
name:
description: 'Development release name'
required: false
default: ''
env: env:
CARGO_TERM_COLOR: always CARGO_TERM_COLOR: always
jobs: jobs:
linux-release-build: linux-release-build:
runs-on: ubuntu-22.04 runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v6 - uses: actions/checkout@v4
- name: install dependencies - name: install dependencies
run: | run: |
sudo apt-get update sudo apt-get update
sudo apt-get install libx11-dev libxtst-dev sudo apt-get install libx11-dev libxtst-dev
sudo apt-get install libadwaita-1-dev libgtk-4-dev sudo apt-get install libadwaita-1-dev libgtk-4-dev
- name: Release Build - name: Release Build
run: | run: cargo build --release
cargo build --release
cp target/release/lan-mouse lan-mouse-linux-x86_64
- name: Upload build artifact - name: Upload build artifact
uses: actions/upload-artifact@v6 uses: actions/upload-artifact@v4
with: with:
name: lan-mouse-linux-x86_64 name: lan-mouse-linux
path: lan-mouse-linux-x86_64 path: target/release/lan-mouse
linux-arm64-release-build:
runs-on: ubuntu-22.04-arm
steps:
- uses: actions/checkout@v6
- name: install dependencies
run: |
sudo apt-get update
sudo apt-get install libx11-dev libxtst-dev
sudo apt-get install libadwaita-1-dev libgtk-4-dev
- name: Release Build
run: |
cargo build --release
cp target/release/lan-mouse lan-mouse-linux-arm64
- name: Upload build artifact
uses: actions/upload-artifact@v6
with:
name: lan-mouse-linux-arm64
path: lan-mouse-linux-arm64
windows-release-build: windows-release-build:
runs-on: windows-latest runs-on: windows-latest
@@ -92,7 +64,7 @@ jobs:
gvsbuild build --msys-dir=C:\msys64 gtk4 libadwaita librsvg 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\usr\notbin" "C:\Program Files\Git\usr\bin"
Move-Item "C:\Program Files\Git\notbin" "C:\Program Files\Git\bin" Move-Item "C:\Program Files\Git\notbin" "C:\Program Files\Git\bin"
- uses: actions/checkout@v6 - uses: actions/checkout@v4
- name: Release Build - name: Release Build
run: cargo build --release run: cargo build --release
- name: Create Archive - name: Create Archive
@@ -100,21 +72,19 @@ jobs:
mkdir "lan-mouse-windows" mkdir "lan-mouse-windows"
Get-Childitem -Path "C:\\gtk-build\\gtk\\x64\\release\\bin\\*.dll" -File -Recurse | Copy-Item -Destination "lan-mouse-windows" Get-Childitem -Path "C:\\gtk-build\\gtk\\x64\\release\\bin\\*.dll" -File -Recurse | Copy-Item -Destination "lan-mouse-windows"
Copy-Item -Path "target\release\lan-mouse.exe" -Destination "lan-mouse-windows" Copy-Item -Path "target\release\lan-mouse.exe" -Destination "lan-mouse-windows"
Compress-Archive -Path "lan-mouse-windows\*" -DestinationPath lan-mouse-windows-x86_64.zip Compress-Archive -Path "lan-mouse-windows\*" -DestinationPath lan-mouse-windows.zip
- name: Upload build artifact - name: Upload build artifact
uses: actions/upload-artifact@v6 uses: actions/upload-artifact@v4
with: with:
name: lan-mouse-windows-x86_64 name: lan-mouse-windows
path: lan-mouse-windows-x86_64.zip path: lan-mouse-windows.zip
macos-release-build: macos-release-build:
runs-on: macos-15-intel runs-on: macos-13
steps: steps:
- uses: actions/checkout@v6 - uses: actions/checkout@v4
- name: install dependencies - name: install dependencies
run: | run: brew install gtk4 libadwaita imagemagick
brew install --cask inkscape
brew install gtk4 libadwaita imagemagick librsvg
- name: Release Build - name: Release Build
run: | run: |
cargo build --release cargo build --release
@@ -124,81 +94,59 @@ jobs:
- name: Install cargo bundle - name: Install cargo bundle
run: cargo install cargo-bundle run: cargo install cargo-bundle
- name: Bundle - name: Bundle
run: | run: cargo bundle --release
cargo bundle --release
scripts/copy-macos-dylib.sh "target/release/bundle/osx/Lan Mouse.app/Contents/MacOS/lan-mouse"
- name: Zip bundle - name: Zip bundle
run: | run: |
cd target/release/bundle/osx cd target/release/bundle/osx
zip -r "lan-mouse-macos-intel.zip" "Lan Mouse.app" zip -r "lan-mouse-macos-intel.zip" "Lan Mouse.app"
- name: Upload build artifact - name: Upload build artifact
uses: actions/upload-artifact@v6 uses: actions/upload-artifact@v4
with: with:
name: lan-mouse-macos-intel name: lan-mouse-macos-intel
path: target/release/bundle/osx/lan-mouse-macos-intel.zip path: target/release/bundle/osx/lan-mouse-macos-intel.zip
macos-arm64-release-build: macos-aarch64-release-build:
runs-on: macos-15 runs-on: macos-14
steps: steps:
- uses: actions/checkout@v6 - uses: actions/checkout@v4
- name: install dependencies - name: install dependencies
run: | run: brew install gtk4 libadwaita imagemagick
brew install --cask inkscape
brew install gtk4 libadwaita imagemagick librsvg
- name: Release Build - name: Release Build
run: | run: |
cargo build --release cargo build --release
cp target/release/lan-mouse lan-mouse-macos-arm64 cp target/release/lan-mouse lan-mouse-macos-aarch64
- name: Make icns - name: Make icns
run: scripts/makeicns.sh run: scripts/makeicns.sh
- name: Install cargo bundle - name: Install cargo bundle
run: cargo install cargo-bundle run: cargo install cargo-bundle
- name: Bundle - name: Bundle
run: | run: cargo bundle --release
cargo bundle --release
scripts/copy-macos-dylib.sh "target/release/bundle/osx/Lan Mouse.app/Contents/MacOS/lan-mouse"
- name: Zip bundle - name: Zip bundle
run: | run: |
cd target/release/bundle/osx cd target/release/bundle/osx
zip -r "lan-mouse-macos-arm64.zip" "Lan Mouse.app" zip -r "lan-mouse-macos-aarch64.zip" "Lan Mouse.app"
- name: Upload build artifact - name: Upload build artifact
uses: actions/upload-artifact@v6 uses: actions/upload-artifact@v4
with: with:
name: lan-mouse-macos-arm64 name: lan-mouse-macos-aarch64
path: target/release/bundle/osx/lan-mouse-macos-arm64.zip path: target/release/bundle/osx/lan-mouse-macos-aarch64.zip
release: pre-release:
name: "Release" name: "Pre Release"
needs: [windows-release-build, linux-release-build, linux-arm64-release-build, macos-release-build, macos-arm64-release-build] needs: [windows-release-build, linux-release-build, macos-release-build, macos-aarch64-release-build]
runs-on: "ubuntu-latest" runs-on: "ubuntu-latest"
steps: steps:
- name: Download build artifacts - name: Download build artifacts
uses: actions/download-artifact@v4 uses: actions/download-artifact@v4
- name: Create Pre-Release - name: Create Release
if: ${{ !startsWith(github.ref, 'refs/tags/') }} uses: "marvinpinto/action-automatic-releases@latest"
uses: softprops/action-gh-release@v2
with: with:
token: ${{ secrets.GITHUB_TOKEN }} repo_token: "${{ secrets.GITHUB_TOKEN }}"
tag_name: ${{ github.event.inputs.name || github.ref_name }} automatic_release_tag: "latest"
name: ${{ github.event.inputs.name || github.ref_name }}
prerelease: true prerelease: true
generate_release_notes: true title: "Development Build"
files: | files: |
lan-mouse-linux-x86_64/lan-mouse-linux-x86_64 lan-mouse-linux/lan-mouse
lan-mouse-linux-arm64/lan-mouse-linux-arm64
lan-mouse-macos-intel/lan-mouse-macos-intel.zip lan-mouse-macos-intel/lan-mouse-macos-intel.zip
lan-mouse-macos-arm64/lan-mouse-macos-arm64.zip lan-mouse-macos-aarch64/lan-mouse-macos-aarch64.zip
lan-mouse-windows-x86_64/lan-mouse-windows-x86_64.zip lan-mouse-windows/lan-mouse-windows.zip
- name: Create Tagged Release
if: startsWith(github.ref, 'refs/tags/')
uses: softprops/action-gh-release@v2
with:
token: ${{ secrets.GITHUB_TOKEN }}
name: ${{ github.ref_name }}
generate_release_notes: true
files: |
lan-mouse-linux-x86_64/lan-mouse-linux-x86_64
lan-mouse-linux-arm64/lan-mouse-linux-arm64
lan-mouse-macos-intel/lan-mouse-macos-intel.zip
lan-mouse-macos-arm64/lan-mouse-macos-arm64.zip
lan-mouse-windows-x86_64/lan-mouse-windows-x86_64.zip

View File

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

146
.github/workflows/tagged-release.yml vendored Normal file
View File

@@ -0,0 +1,146 @@
name: "Tagged Release"
on:
push:
tags:
- v**
jobs:
linux-release-build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: install dependencies
run: |
sudo apt-get update
sudo apt-get install libx11-dev libxtst-dev
sudo apt-get install libadwaita-1-dev libgtk-4-dev
- name: Release Build
run: cargo build --release
- name: Upload build artifact
uses: actions/upload-artifact@v4
with:
name: lan-mouse-linux
path: target/release/lan-mouse
windows-release-build:
runs-on: windows-latest
steps:
- uses: actions/setup-python@v5
with:
python-version: '3.11'
# needed for cache restore
- name: create gtk dir
run: mkdir C:\gtk-build\gtk\x64\release
- uses: actions/cache@v3
id: cache
with:
path: c:/gtk-build/gtk/x64/release/**
key: gtk-windows-build
restore-keys: gtk-windows-build
- name: Update path
run: |
echo "PKG_CONFIG=C:\gtk-build\gtk\x64\release\bin\pkgconf.exe" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append
echo "C:\pkg-config-lite-0.28-1\bin" | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append
echo "C:\gtk-build\gtk\x64\release\bin" | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append
echo $env:GITHUB_PATH
echo $env:PATH
- name: Install dependencies
if: steps.cache.outputs.cache-hit != 'true'
run: |
# choco install msys2
# choco install visualstudio2022-workload-vctools
# choco install pkgconfiglite
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@v4
- name: Release Build
run: cargo build --release
- name: Create Archive
run: |
mkdir "lan-mouse-windows"
Get-Childitem -Path "C:\\gtk-build\\gtk\\x64\\release\\bin\\*.dll" -File -Recurse | Copy-Item -Destination "lan-mouse-windows"
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@v4
with:
name: lan-mouse-windows
path: lan-mouse-windows.zip
macos-release-build:
runs-on: macos-13
steps:
- uses: actions/checkout@v4
- name: install dependencies
run: brew install gtk4 libadwaita imagemagick
- name: Release Build
run: |
cargo build --release
cp target/release/lan-mouse lan-mouse-macos-intel
- name: Make icns
run: scripts/makeicns.sh
- name: Install cargo bundle
run: cargo install cargo-bundle
- name: Bundle
run: cargo bundle --release
- name: Zip bundle
run: |
cd target/release/bundle/osx
zip -r "lan-mouse-macos-intel.zip" "Lan Mouse.app"
- name: Upload build artifact
uses: actions/upload-artifact@v4
with:
name: lan-mouse-macos-intel.zip
path: target/release/bundle/osx/lan-mouse-macos-intel.zip
macos-aarch64-release-build:
runs-on: macos-14
steps:
- uses: actions/checkout@v4
- name: install dependencies
run: brew install gtk4 libadwaita imagemagick
- name: Release Build
run: |
cargo build --release
cp target/release/lan-mouse lan-mouse-macos-aarch64
- name: Make icns
run: scripts/makeicns.sh
- name: Install cargo bundle
run: cargo install cargo-bundle
- name: Bundle
run: cargo bundle --release
- name: Zip bundle
run: |
cd target/release/bundle/osx
zip -r "lan-mouse-macos-aarch64.zip" "Lan Mouse.app"
- name: Upload build artifact
uses: actions/upload-artifact@v4
with:
name: lan-mouse-macos-aarch64.zip
path: target/release/bundle/osx/lan-mouse-macos-aarch64.zip
tagged-release:
name: "Tagged Release"
needs: [windows-release-build, linux-release-build, macos-release-build, macos-aarch64-release-build]
runs-on: "ubuntu-latest"
steps:
- name: Download build artifacts
uses: actions/download-artifact@v4
- name: Create Release
uses: "marvinpinto/action-automatic-releases@latest"
with:
repo_token: "${{ secrets.GITHUB_TOKEN }}"
prerelease: false
files: |
lan-mouse-linux/lan-mouse
lan-mouse-macos-intel/lan-mouse-macos-intel.zip
lan-mouse-macos-aarch64/lan-mouse-macos-aarch64.zip
lan-mouse-windows/lan-mouse-windows.zip

4
.gitignore vendored
View File

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

View File

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

View File

@@ -1,65 +0,0 @@
# Lan Mouse Agent Instructions
## Overview
Lan Mouse is an open-source Software KVM sharing mouse/keyboard input across local networks. The Rust workspace combines a GTK frontend, CLI/daemon mode, and multi-OS capture/emulation backends for Linux, Windows, and macOS.
## Core principles
- **Scope discipline.** Only implement what was requested; describe follow-up work instead of absorbing it.
- **Clarify OS behavior.** Ask when requirements touch OS-specific capture/emulation (they differ significantly).
- **Docs stay current.** Update [README.md](README.md) or [DOC.md](DOC.md) when touching public APIs or platform support.
- **Rust idioms.** Use `Result`/`Option`, `thiserror` for errors, descriptive logs, and concise comments for non-obvious invariants.
## Terminology
- **Client:** A remote machine that can receive or send input events. Each client is either _active_ (receiving events) or _inactive_ (can send events back). This mutual exclusion prevents feedback loops.
- **Backend:** OS-specific implementation for capture or emulation (e.g., libei, layer-shell, wlroots, X11, Windows, macOS).
- **Handle:** A per-client identifier used to route events and track state (pressed keys, position).
## Architecture
**Pipeline:** `input-capture``lan-mouse-ipc``input-emulation`
- **input-capture:** Reads OS events into a `Stream<CaptureEvent>`. Backends tried in priority order (libei → layer-shell → X11 → fallback). Tracks `pressed_keys` to avoid stuck modifiers. `position_map` queues events when multiple clients share a screen edge.
- **input-emulation:** Replays events via the `Emulation` trait (`consume`, `create`, `destroy`, `terminate`). Maintains `pressed_keys` and releases them on disconnect.
- **lan-mouse-ipc / lan-mouse-proto:** Protocol glue and serialization. Events are UDP; connection requests are TCP on the same port. Version bumps required when serialization changes.
- **input-event:** Shared scancode enums and abstract event types—extend here, don't duplicate translations.
## Feature & cfg discipline
- Feature flags live in root `Cargo.toml`. Gate OS-specific modules with tight cfgs (e.g., `cfg(all(unix, feature = "layer_shell", not(target_os = "macos")))`).
- Prefer module-level gating over per-function cfgs to avoid empty stubs.
- New backends: add feature in `Cargo.toml`, create gated module, log backend selection.
## Async patterns
- Tokio runtime with `futures` streams and `async_trait`. Model new flows as streams or async methods.
- Avoid blocking; use `spawn_blocking` if needed. Prefer existing single-threaded stream handling.
- `InputCapture` implements `Stream` and manually pumps backends—don't short-circuit this logic.
## Commands
```sh
cargo build --workspace # full build
cargo build -p <crate> # single crate
cargo test --workspace # all tests
cargo fmt && cargo clippy --workspace --all-targets --all-features # lint
RUST_LOG=lan_mouse=debug cargo run # debug logging
```
Run from repo root—no `cd` in scripts.
## Testing
- Unit tests for utilities; integration tests for protocol behavior.
- OS-specific backends: test via GTK/CLI on target OS or document manual verification.
- Dummy backend exercises pipeline without real dependencies.
- Verify `terminate()` releases keys on unexpected disconnect.
## Workflow
1. Clarify ambiguous requirements, especially OS-specific behavior.
2. Implement minimal change; flag follow-up work.
3. Add proportional tests; run `cargo test` on affected crates.
4. Run `cargo fmt` and `cargo clippy --workspace --all-targets --all-features`.

2168
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

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

101
README.md
View File

@@ -1,9 +1,4 @@
# Lan Mouse # Lan Mouse
[![CI](https://github.com/feschber/lan-mouse/actions/workflows/rust.yml/badge.svg)](https://github.com/feschber/lan-mouse/actions/workflows/rust.yml) [![Cachix](https://github.com/feschber/lan-mouse/actions/workflows/cachix.yml/badge.svg)](https://github.com/feschber/lan-mouse/actions/workflows/cachix.yml) [![Release](https://github.com/feschber/lan-mouse/actions/workflows/release.yml/badge.svg)](https://github.com/feschber/lan-mouse/actions/workflows/release.yml)
[![crates.io](https://img.shields.io/crates/v/lan-mouse.svg)](https://crates.io/crates/lan-mouse) [![license](https://img.shields.io/crates/l/lan-mouse.svg)](https://github.com/feschber/lan-mouse/blob/main/Cargo.toml)
Lan Mouse is a *cross-platform* mouse and keyboard sharing software similar to universal-control on Apple devices. Lan Mouse is a *cross-platform* mouse and keyboard sharing software similar to universal-control on Apple devices.
It allows for using multiple PCs via a single set of mouse and keyboard. It allows for using multiple PCs via a single set of mouse and keyboard.
This is also known as a Software KVM switch. This is also known as a Software KVM switch.
@@ -86,37 +81,15 @@ paru -S lan-mouse-git
- flake: [README.md](./nix/README.md) - flake: [README.md](./nix/README.md)
</details> </details>
<details>
<summary>Fedora</summary>
You can install Lan Mouse from the [Terra Repository](https://terra.fyralabs.com).
After enabling Terra:
```sh
dnf install lan-mouse
```
</details>
<details>
<summary>MacOS</summary>
- Download the package for your Mac (Intel or ARM) from the releases page
- Unzip it
- Remove the quarantine with `xattr -rd com.apple.quarantine "Lan Mouse.app"`
- Launch the app
- Grant accessibility permissions in System Preferences
</details>
<details> <details>
<summary>Manual Installation</summary> <summary>Manual Installation</summary>
First make sure to [install the necessary dependencies](#installing-dependencies-for-development--compiling-from-source). First make sure to [install the necessary dependencies](#installing-dependencies).
Precompiled release binaries for Windows, MacOS and Linux are available in the [releases section](https://github.com/feschber/lan-mouse/releases). Precompiled release binaries for Windows, MacOS and Linux are available in the [releases section](https://github.com/feschber/lan-mouse/releases).
For Windows, the depenedencies are included in the .zip file, for other operating systems see [Installing Dependencies](#installing-dependencies-for-development--compiling-from-source). For Windows, the depenedencies are included in the .zip file, for other operating systems see [Installing Dependencies](#installing-dependencies).
Alternatively, the `lan-mouse` binary can be compiled from source (see below). Alternatively, the `lan-mouse` binary can be compiled from source (see below).
@@ -182,40 +155,13 @@ For a detailed list of available features, checkout the [Cargo.toml](./Cargo.tom
## Development
### Git pre-commit hook ## Installing Dependencies for Development / Compiling from Source
This repository includes a local git hooks directory `.githooks/` with a `pre-commit` script that enforces formatting, lints, and tests before allowing a commit. It is optional to enable it, but it will prevent you from committing code with failing unit tests or that needs clippy/fmt fixes. To enable the hook locally:
1. Make the hook executable:
```sh
chmod +x .githooks/pre-commit
```
2. Point git to the hooks directory (one-time per clone):
```sh
git config core.hooksPath .githooks
```
The `pre-commit` script runs `cargo fmt --all` (and fails if files were modified), `cargo clippy --workspace --all-targets --all-features -- -D warnings`, and `cargo test --workspace --all-features`.
### Dependencies & Compiling from Source
<details> <details>
<summary>MacOS</summary> <summary>MacOS</summary>
```sh ```sh
# Install dependencies brew install libadwaita pkg-config
brew install libadwaita pkg-config imagemagick
cargo install cargo-bundle
# Create the macOS icon file
scripts/makeicns.sh
# Create the .app bundle
cargo bundle
# Copy all dynamic libraries into the bundle, and update the bundle to find them there
scripts/copy-macos-dylib.sh
``` ```
</details> </details>
@@ -322,17 +268,19 @@ If the device still can not be entered, make sure you have UDP port `4242` (or t
<details> <details>
<summary>Command Line Interface</summary> <summary>Command Line Interface</summary>
The cli interface can be accessed by passing `cli` as a commandline argument. The cli interface can be enabled using `--frontend cli` as commandline arguments.
Use Type `help` to list the available commands.
```sh
lan-mouse cli help
```
to list the available commands and
```sh
lan-mouse cli <cmd> help
```
for information on how to use a specific command.
E.g.:
```sh
$ cargo run --release -- --frontend cli
(...)
> connect <host> left|right|top|bottom
(...)
> list
(...)
> activate 0
```
</details> </details>
<details> <details>
@@ -345,9 +293,6 @@ To do so, use the `daemon` subcommand:
```sh ```sh
lan-mouse daemon lan-mouse daemon
``` ```
</details>
## Systemd Service
In order to start lan-mouse with a graphical session automatically, In order to start lan-mouse with a graphical session automatically,
the [systemd-service](service/lan-mouse.service) can be used: the [systemd-service](service/lan-mouse.service) can be used:
@@ -359,9 +304,7 @@ cp service/lan-mouse.service ~/.config/systemd/user
systemctl --user daemon-reload systemctl --user daemon-reload
systemctl --user enable --now lan-mouse.service systemctl --user enable --now lan-mouse.service
``` ```
> [!Important] </details>
> Make sure to point `ExecStart=/usr/bin/lan-mouse daemon` to the actual `lan-mouse` binary (in case it is not under `/usr/bin`, e.g. when installed manually.
## Configuration ## Configuration
To automatically load clients on startup, the file `$XDG_CONFIG_HOME/lan-mouse/config.toml` is parsed. To automatically load clients on startup, the file `$XDG_CONFIG_HOME/lan-mouse/config.toml` is parsed.
@@ -383,6 +326,9 @@ release_bind = [ "KeyA", "KeyS", "KeyD", "KeyF" ]
# optional port (defaults to 4242) # optional port (defaults to 4242)
port = 4242 port = 4242
# # optional frontend -> defaults to gtk if available
# # possible values are "cli" and "gtk"
# frontend = "gtk"
# list of authorized tls certificate fingerprints that # list of authorized tls certificate fingerprints that
# are accepted for incoming traffic # are accepted for incoming traffic
@@ -390,9 +336,7 @@ port = 4242
"bc:05:ab:7a:a4:de:88:8c:2f:92:ac:bc:b8:49:b8:24:0d:44:b3:e6:a4:ef:d7:0b:6c:69:6d:77:53:0b:14:80" = "iridium" "bc:05:ab:7a:a4:de:88:8c:2f:92:ac:bc:b8:49:b8:24:0d:44:b3:e6:a4:ef:d7:0b:6c:69:6d:77:53:0b:14:80" = "iridium"
# define a client on the right side with host name "iridium" # define a client on the right side with host name "iridium"
[[clients]] [right]
# position (left | right | top | bottom)
position = "right"
# hostname # hostname
hostname = "iridium" hostname = "iridium"
# activate this client immediately when lan-mouse is started # activate this client immediately when lan-mouse is started
@@ -401,8 +345,7 @@ activate_on_startup = true
ips = ["192.168.178.156"] ips = ["192.168.178.156"]
# define a client on the left side with IP address 192.168.178.189 # define a client on the left side with IP address 192.168.178.189
[[clients]] [left]
position = "left"
# The hostname is optional: When no hostname is specified, # The hostname is optional: When no hostname is specified,
# at least one ip address needs to be specified. # at least one ip address needs to be specified.
hostname = "thorium" hostname = "thorium"

View File

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

View File

@@ -1,10 +1,14 @@
# example configuration # example configuration
# configure release bind # capture_backend = "LayerShell"
release_bind = [ "KeyA", "KeyS", "KeyD", "KeyF" ]
# release bind
release_bind = ["KeyA", "KeyS", "KeyD", "KeyF"]
# optional port (defaults to 4242) # optional port (defaults to 4242)
port = 4242 port = 4242
# optional frontend -> defaults to gtk if available
# frontend = "gtk"
# list of authorized tls certificate fingerprints that # list of authorized tls certificate fingerprints that
# are accepted for incoming traffic # are accepted for incoming traffic
@@ -12,19 +16,14 @@ port = 4242
"bc:05:ab:7a:a4:de:88:8c:2f:92:ac:bc:b8:49:b8:24:0d:44:b3:e6:a4:ef:d7:0b:6c:69:6d:77:53:0b:14:80" = "iridium" "bc:05:ab:7a:a4:de:88:8c:2f:92:ac:bc:b8:49:b8:24:0d:44:b3:e6:a4:ef:d7:0b:6c:69:6d:77:53:0b:14:80" = "iridium"
# define a client on the right side with host name "iridium" # define a client on the right side with host name "iridium"
[[clients]] [right]
# position (left | right | top | bottom)
position = "right"
# hostname # hostname
hostname = "iridium" hostname = "iridium"
# activate this client immediately when lan-mouse is started
activate_on_startup = true
# optional list of (known) ip addresses # optional list of (known) ip addresses
ips = ["192.168.178.156"] ips = ["192.168.178.156"]
# define a client on the left side with IP address 192.168.178.189 # define a client on the left side with IP address 192.168.178.189
[[clients]] [left]
position = "left"
# The hostname is optional: When no hostname is specified, # The hostname is optional: When no hostname is specified,
# at least one ip address needs to be specified. # at least one ip address needs to be specified.
hostname = "thorium" hostname = "thorium"

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

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

1
dylibs/.gitignore vendored
View File

@@ -1 +0,0 @@
*

12
flake.lock generated
View File

@@ -2,11 +2,11 @@
"nodes": { "nodes": {
"nixpkgs": { "nixpkgs": {
"locked": { "locked": {
"lastModified": 1772963539, "lastModified": 1740560979,
"narHash": "sha256-9jVDGZnvCckTGdYT53d/EfznygLskyLQXYwJLKMPsZs=", "narHash": "sha256-Vr3Qi346M+8CjedtbyUevIGDZW8LcA1fTG0ugPY/Hic=",
"owner": "nixos", "owner": "nixos",
"repo": "nixpkgs", "repo": "nixpkgs",
"rev": "9dcb002ca1690658be4a04645215baea8b95f31d", "rev": "5135c59491985879812717f4c9fea69604e7f26f",
"type": "github" "type": "github"
}, },
"original": { "original": {
@@ -29,11 +29,11 @@
] ]
}, },
"locked": { "locked": {
"lastModified": 1773025773, "lastModified": 1740623427,
"narHash": "sha256-Wik8+xApNfldpUFjPmJkPdg0RrvUPSWGIZis+A/0N1w=", "narHash": "sha256-3SdPQrZoa4odlScFDUHd4CUPQ/R1gtH4Mq9u8CBiK8M=",
"owner": "oxalica", "owner": "oxalica",
"repo": "rust-overlay", "repo": "rust-overlay",
"rev": "3c06fdbbd36ff60386a1e590ee0cd52dcd1892bf", "rev": "d342e8b5fd88421ff982f383c853f0fc78a847ab",
"type": "github" "type": "github"
}, },
"original": { "original": {

101
flake.nix
View File

@@ -7,87 +7,60 @@
inputs.nixpkgs.follows = "nixpkgs"; inputs.nixpkgs.follows = "nixpkgs";
}; };
}; };
outputs = outputs = {
{ self,
nixpkgs, nixpkgs,
rust-overlay, rust-overlay,
self,
... ...
}: }: let
let
inherit (nixpkgs) lib; inherit (nixpkgs) lib;
forEachPkgs = genSystems = lib.genAttrs [
f:
lib.genAttrs
[
"aarch64-darwin" "aarch64-darwin"
"aarch64-linux" "aarch64-linux"
"x86_64-darwin" "x86_64-darwin"
"x86_64-linux" "x86_64-linux"
] ];
( pkgsFor = system:
system: import nixpkgs {
let
pkgs = import nixpkgs {
inherit system; inherit system;
overlays = [ rust-overlay.overlays.default ];
}; overlays = [
# Default toolchain for devshell rust-overlay.overlays.default
rustToolchain = pkgs.rust-bin.stable.latest.default.override {
extensions = [
# includes already:
# rustc
# cargo
# rust-std
# rust-docs
# rustfmt-preview
# clippy-preview
"rust-analyzer"
"rust-src"
]; ];
}; };
# Minimal toolchain for builds (rustc + cargo + rust-std only) mkRustToolchain = pkgs:
rustToolchainForBuild = pkgs.rust-bin.stable.latest.minimal; pkgs.rust-bin.stable.latest.default.override {
in extensions = ["rust-src"];
f { inherit pkgs rustToolchain rustToolchainForBuild; }
);
in
{
packages = forEachPkgs (
{ pkgs, rustToolchainForBuild, ... }:
let
customRustPlatform = pkgs.makeRustPlatform {
cargo = rustToolchainForBuild;
rustc = rustToolchainForBuild;
}; };
lan-mouse = pkgs.callPackage ./nix { rustPlatform = customRustPlatform; }; pkgs = genSystems (system: import nixpkgs {inherit system;});
in in {
{ packages = genSystems (system: rec {
default = lan-mouse; default = pkgs.${system}.callPackage ./nix {};
inherit lan-mouse; lan-mouse = default;
} });
); homeManagerModules.default = import ./nix/hm-module.nix self;
devShells = forEachPkgs ( devShells = genSystems (system: let
{ pkgs, rustToolchain, ... }: pkgs = pkgsFor system;
{ rust = mkRustToolchain pkgs;
in {
default = pkgs.mkShell { default = pkgs.mkShell {
packages = packages = with pkgs; [
with pkgs; rust
[ rust-analyzer-unwrapped
rustToolchain
pkg-config pkg-config
xorg.libX11
gtk4 gtk4
libadwaita libadwaita
librsvg librsvg
] xorg.libXtst
++ lib.optionals pkgs.stdenv.isLinux [ ] ++ lib.optionals stdenv.isDarwin
libX11 (with darwin.apple_sdk_11_0.frameworks; [
libXtst CoreGraphics
]; ApplicationServices
env.RUST_SRC_PATH = "${rustToolchain}/lib/rustlib/src/rust/library"; ]);
RUST_SRC_PATH = "${rust}/lib/rustlib/src/rust/library";
}; };
} });
);
homeManagerModules.default = import ./nix/hm-module.nix self;
}; };
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,102 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<interface>
<requires lib="gtk" version="4.0"/>
<requires lib="libadwaita" version="1.0"/>
<template class="AuthorizationWindow" parent="AdwWindow">
<property name="modal">True</property>
<property name="width-request">180</property>
<property name="default-width">180</property>
<property name="height-request">180</property>
<property name="default-height">180</property>
<property name="title" translatable="yes">Unauthorized Device</property>
<property name="content">
<object class="GtkBox">
<property name="orientation">vertical</property>
<property name="vexpand">True</property>
<child type="top">
<object class="AdwHeaderBar">
<style>
<class name="flat"/>
</style>
</object>
</child>
<child>
<object class="GtkBox">
<property name="orientation">vertical</property>
<property name="spacing">30</property>
<property name="margin-start">30</property>
<property name="margin-end">30</property>
<property name="margin-top">30</property>
<property name="margin-bottom">30</property>
<child>
<object class="GtkLabel">
<property name="label">An unauthorized Device is trying to connect. Do you want to authorize this Device?</property>
<property name="width-request">100</property>
<property name="wrap">word-wrap</property>
</object>
</child>
<child>
<object class="AdwPreferencesGroup">
<property name="title">sha256 fingerprint</property>
<child>
<object class="AdwActionRow">
<property name="child">
<object class="GtkLabel" id="fingerprint">
<property name="ellipsize">PANGO_ELLIPSIZE_NONE</property>
<property name="vexpand">True</property>
<property name="hexpand">False</property>
<property name="wrap">True</property>
<property name="wrap-mode">word-char</property>
<property name="justify">center</property>
<property name="xalign">0.5</property>
<property name="margin-top">10</property>
<property name="margin-bottom">10</property>
<property name="margin-start">10</property>
<property name="margin-end">10</property>
<property name="width-chars">64</property>
</object>
</property>
</object>
</child>
</object>
</child>
</object>
</child>
<child>
<object class="GtkBox">
<property name="margin-start">30</property>
<property name="margin-end">30</property>
<property name="margin-top">30</property>
<property name="margin-bottom">30</property>
<property name="orientation">horizontal</property>
<property name="spacing">30</property>
<property name="hexpand">True</property>
<property name="vexpand">True</property>
<property name="valign">end</property>
<child>
<object class="GtkButton" id="cancel_button">
<signal name="clicked" handler="handle_cancel" swapped="true"/>
<property name="label" translatable="yes">Cancel</property>
<property name="can-shrink">True</property>
<property name="height-request">50</property>
<property name="hexpand">True</property>
</object>
</child>
<child>
<object class="GtkButton" id="confirm_button">
<signal name="clicked" handler="handle_confirm" swapped="true"/>
<property name="label" translatable="yes">Authorize</property>
<property name="can-shrink">True</property>
<property name="height-request">50</property>
<property name="hexpand">True</property>
<style>
<class name="destructive-action"/>
</style>
</object>
</child>
</object>
</child>
</object>
</property>
</template>
</interface>

View File

@@ -2,7 +2,6 @@
<gresources> <gresources>
<gresource prefix="/de/feschber/LanMouse"> <gresource prefix="/de/feschber/LanMouse">
<file compressed="true" preprocess="xml-stripblanks">window.ui</file> <file compressed="true" preprocess="xml-stripblanks">window.ui</file>
<file compressed="true" preprocess="xml-stripblanks">authorization_window.ui</file>
<file compressed="true" preprocess="xml-stripblanks">fingerprint_window.ui</file> <file compressed="true" preprocess="xml-stripblanks">fingerprint_window.ui</file>
<file compressed="true" preprocess="xml-stripblanks">client_row.ui</file> <file compressed="true" preprocess="xml-stripblanks">client_row.ui</file>
<file compressed="true" preprocess="xml-stripblanks">key_row.ui</file> <file compressed="true" preprocess="xml-stripblanks">key_row.ui</file>

View File

@@ -1,19 +0,0 @@
mod imp;
use glib::Object;
use gtk::{gio, glib, subclass::prelude::ObjectSubclassIsExt};
glib::wrapper! {
pub struct AuthorizationWindow(ObjectSubclass<imp::AuthorizationWindow>)
@extends adw::Window, gtk::Window, gtk::Widget,
@implements gio::ActionGroup, gio::ActionMap, gtk::Accessible, gtk::Buildable,
gtk::ConstraintTarget, gtk::Native, gtk::Root, gtk::ShortcutManager;
}
impl AuthorizationWindow {
pub(crate) fn new(fingerprint: &str) -> Self {
let window: Self = Object::builder().build();
window.imp().set_fingerprint(fingerprint);
window
}
}

View File

@@ -1,76 +0,0 @@
use std::sync::OnceLock;
use adw::prelude::*;
use adw::subclass::prelude::*;
use glib::subclass::InitializingObject;
use gtk::{
Button, CompositeTemplate, Label,
glib::{self, subclass::Signal},
template_callbacks,
};
#[derive(CompositeTemplate, Default)]
#[template(resource = "/de/feschber/LanMouse/authorization_window.ui")]
pub struct AuthorizationWindow {
#[template_child]
pub fingerprint: TemplateChild<Label>,
#[template_child]
pub cancel_button: TemplateChild<Button>,
#[template_child]
pub confirm_button: TemplateChild<Button>,
}
#[glib::object_subclass]
impl ObjectSubclass for AuthorizationWindow {
const NAME: &'static str = "AuthorizationWindow";
const ABSTRACT: bool = false;
type Type = super::AuthorizationWindow;
type ParentType = adw::Window;
fn class_init(klass: &mut Self::Class) {
klass.bind_template();
klass.bind_template_callbacks();
}
fn instance_init(obj: &InitializingObject<Self>) {
obj.init_template();
}
}
#[template_callbacks]
impl AuthorizationWindow {
#[template_callback]
fn handle_confirm(&self, _button: Button) {
let fp = self.fingerprint.text().as_str().trim().to_owned();
self.obj().emit_by_name("confirm-clicked", &[&fp])
}
#[template_callback]
fn handle_cancel(&self, _: Button) {
self.obj().emit_by_name("cancel-clicked", &[])
}
pub(super) fn set_fingerprint(&self, fingerprint: &str) {
self.fingerprint.set_text(fingerprint);
}
}
impl ObjectImpl for AuthorizationWindow {
fn signals() -> &'static [Signal] {
static SIGNALS: OnceLock<Vec<Signal>> = OnceLock::new();
SIGNALS.get_or_init(|| {
vec![
Signal::builder("confirm-clicked")
.param_types([String::static_type()])
.build(),
Signal::builder("cancel-clicked").build(),
]
})
}
}
impl WidgetImpl for AuthorizationWindow {}
impl WindowImpl for AuthorizationWindow {}
impl ApplicationWindowImpl for AuthorizationWindow {}
impl AdwWindowImpl for AuthorizationWindow {}

View File

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

View File

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

View File

@@ -1,7 +1,7 @@
mod imp; mod imp;
use glib::Object; use glib::Object;
use gtk::{gio, glib, prelude::ObjectExt, subclass::prelude::ObjectSubclassIsExt}; use gtk::{gio, glib};
glib::wrapper! { glib::wrapper! {
pub struct FingerprintWindow(ObjectSubclass<imp::FingerprintWindow>) pub struct FingerprintWindow(ObjectSubclass<imp::FingerprintWindow>)
@@ -11,12 +11,8 @@ glib::wrapper! {
} }
impl FingerprintWindow { impl FingerprintWindow {
pub(crate) fn new(fingerprint: Option<String>) -> Self { pub(crate) fn new() -> Self {
let window: Self = Object::builder().build(); let window: Self = Object::builder().build();
if let Some(fp) = fingerprint {
window.imp().fingerprint.set_property("text", fp);
window.imp().fingerprint.set_property("editable", false);
}
window window
} }
} }

View File

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

View File

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

View File

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

View File

@@ -1,4 +1,3 @@
mod authorization_window;
mod client_object; mod client_object;
mod client_row; mod client_row;
mod fingerprint_window; mod fingerprint_window;
@@ -13,7 +12,7 @@ use window::Window;
use lan_mouse_ipc::FrontendEvent; use lan_mouse_ipc::FrontendEvent;
use adw::Application; use adw::Application;
use gtk::{IconTheme, gdk::Display, glib::clone, prelude::*}; use gtk::{gdk::Display, glib::clone, prelude::*, IconTheme};
use gtk::{gio, glib, prelude::ApplicationExt}; use gtk::{gio, glib, prelude::ApplicationExt};
use self::client_object::ClientObject; use self::client_object::ClientObject;
@@ -147,21 +146,8 @@ fn build_ui(app: &Application) {
FrontendEvent::EmulationStatus(s) => window.set_emulation(s.into()), FrontendEvent::EmulationStatus(s) => window.set_emulation(s.into()),
FrontendEvent::AuthorizedUpdated(keys) => window.set_authorized_keys(keys), FrontendEvent::AuthorizedUpdated(keys) => window.set_authorized_keys(keys),
FrontendEvent::PublicKeyFingerprint(fp) => window.set_pk_fp(&fp), FrontendEvent::PublicKeyFingerprint(fp) => window.set_pk_fp(&fp),
FrontendEvent::ConnectionAttempt { fingerprint } => { FrontendEvent::IncomingConnected(_fingerprint, addr, pos) => {
window.request_authorization(&fingerprint); window.show_toast(format!("device connected: {addr} ({pos})").as_str());
}
FrontendEvent::DeviceConnected {
fingerprint: _,
addr,
} => {
window.show_toast(format!("device connected: {addr}").as_str());
}
FrontendEvent::DeviceEntered {
fingerprint: _,
addr,
pos,
} => {
window.show_toast(format!("device entered: {addr} ({pos})").as_str());
} }
FrontendEvent::IncomingDisconnected(addr) => { FrontendEvent::IncomingDisconnected(addr) => {
window.show_toast(format!("{addr} disconnected").as_str()); window.show_toast(format!("{addr} disconnected").as_str());

View File

@@ -4,21 +4,19 @@ use std::collections::HashMap;
use adw::prelude::*; use adw::prelude::*;
use adw::subclass::prelude::*; use adw::subclass::prelude::*;
use glib::{Object, clone}; use glib::{clone, Object};
use gtk::{ use gtk::{
NoSelection, gio, gio,
glib::{self, closure_local}, glib::{self, closure_local},
NoSelection,
}; };
use lan_mouse_ipc::{ use lan_mouse_ipc::{
ClientConfig, ClientHandle, ClientState, DEFAULT_PORT, FrontendRequest, FrontendRequestWriter, ClientConfig, ClientHandle, ClientState, FrontendRequest, FrontendRequestWriter, Position,
Position, DEFAULT_PORT,
}; };
use crate::{ use crate::{fingerprint_window::FingerprintWindow, key_object::KeyObject, key_row::KeyRow};
authorization_window::AuthorizationWindow, fingerprint_window::FingerprintWindow,
key_object::KeyObject, key_row::KeyRow,
};
use super::{client_object::ClientObject, client_row::ClientRow}; use super::{client_object::ClientObject, client_row::ClientRow};
@@ -323,7 +321,7 @@ impl Window {
pub(super) fn update_client_config(&self, handle: ClientHandle, client: ClientConfig) { pub(super) fn update_client_config(&self, handle: ClientHandle, client: ClientConfig) {
let Some(row) = self.row_for_handle(handle) else { let Some(row) = self.row_for_handle(handle) else {
log::warn!("could not find row for handle {handle}"); log::warn!("could not find row for handle {}", handle);
return; return;
}; };
row.set_hostname(client.hostname); row.set_hostname(client.hostname);
@@ -333,11 +331,11 @@ impl Window {
pub(super) fn update_client_state(&self, handle: ClientHandle, state: ClientState) { pub(super) fn update_client_state(&self, handle: ClientHandle, state: ClientState) {
let Some(row) = self.row_for_handle(handle) else { let Some(row) = self.row_for_handle(handle) else {
log::warn!("could not find row for handle {handle}"); log::warn!("could not find row for handle {}", handle);
return; return;
}; };
let Some(client_object) = self.client_object_for_handle(handle) else { let Some(client_object) = self.client_object_for_handle(handle) else {
log::warn!("could not find row for handle {handle}"); log::warn!("could not find row for handle {}", handle);
return; return;
}; };
@@ -396,8 +394,8 @@ impl Window {
self.request(FrontendRequest::Create); self.request(FrontendRequest::Create);
} }
fn open_fingerprint_dialog(&self, fp: Option<String>) { fn open_fingerprint_dialog(&self) {
let window = FingerprintWindow::new(fp); let window = FingerprintWindow::new();
window.set_transient_for(Some(self)); window.set_transient_for(Some(self));
window.connect_closure( window.connect_closure(
"confirm-clicked", "confirm-clicked",
@@ -471,33 +469,4 @@ impl Window {
pub(super) fn set_pk_fp(&self, fingerprint: &str) { pub(super) fn set_pk_fp(&self, fingerprint: &str) {
self.imp().fingerprint_row.set_subtitle(fingerprint); self.imp().fingerprint_row.set_subtitle(fingerprint);
} }
pub(super) fn request_authorization(&self, fingerprint: &str) {
if let Some(w) = self.imp().authorization_window.borrow_mut().take() {
w.close();
}
let window = AuthorizationWindow::new(fingerprint);
window.set_transient_for(Some(self));
window.connect_closure(
"confirm-clicked",
false,
closure_local!(
#[strong(rename_to = parent)]
self,
move |w: AuthorizationWindow, fp: String| {
w.close();
parent.open_fingerprint_dialog(Some(fp));
}
),
);
window.connect_closure(
"cancel-clicked",
false,
closure_local!(move |w: AuthorizationWindow| {
w.close();
}),
);
window.present();
self.imp().authorization_window.replace(Some(window));
}
} }

View File

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

View File

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

View File

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

View File

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

View File

@@ -20,8 +20,8 @@ mod connect;
mod connect_async; mod connect_async;
mod listen; mod listen;
pub use connect::{FrontendEventReader, FrontendRequestWriter, connect}; pub use connect::{connect, FrontendEventReader, FrontendRequestWriter};
pub use connect_async::{AsyncFrontendEventReader, AsyncFrontendRequestWriter, connect_async}; pub use connect_async::{connect_async, AsyncFrontendEventReader, AsyncFrontendRequestWriter};
pub use listen::AsyncFrontendListener; pub use listen::AsyncFrontendListener;
#[derive(Debug, Error)] #[derive(Debug, Error)]
@@ -59,7 +59,6 @@ pub enum IpcError {
pub const DEFAULT_PORT: u16 = 4242; pub const DEFAULT_PORT: u16 = 4242;
#[derive(Debug, Default, Eq, Hash, PartialEq, Clone, Copy, Serialize, Deserialize)] #[derive(Debug, Default, Eq, Hash, PartialEq, Clone, Copy, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum Position { pub enum Position {
#[default] #[default]
Left, Left,
@@ -202,21 +201,10 @@ pub enum FrontendEvent {
AuthorizedUpdated(HashMap<String, String>), AuthorizedUpdated(HashMap<String, String>),
/// public key fingerprint of this device /// public key fingerprint of this device
PublicKeyFingerprint(String), PublicKeyFingerprint(String),
/// new device connected /// incoming connected
DeviceConnected { IncomingConnected(String, SocketAddr, Position),
addr: SocketAddr,
fingerprint: String,
},
/// incoming device entered the screen
DeviceEntered {
fingerprint: String,
addr: SocketAddr,
pos: Position,
},
/// incoming disconnected /// incoming disconnected
IncomingDisconnected(SocketAddr), IncomingDisconnected(SocketAddr),
/// failed connection attempt (approval for fingerprint required)
ConnectionAttempt { fingerprint: String },
} }
#[derive(Debug, Eq, PartialEq, Clone, Serialize, Deserialize)] #[derive(Debug, Eq, PartialEq, Clone, Serialize, Deserialize)]
@@ -253,8 +241,6 @@ pub enum FrontendRequest {
RemoveAuthorizedKey(String), RemoveAuthorizedKey(String),
/// change the hook command /// change the hook command
UpdateEnterHook(u64, Option<String>), UpdateEnterHook(u64, Option<String>),
/// save config file
SaveConfiguration,
} }
#[derive(Clone, Copy, PartialEq, Eq, Debug, Default, Serialize, Deserialize)] #[derive(Clone, Copy, PartialEq, Eq, Debug, Default, Serialize, Deserialize)]

View File

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

View File

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

View File

@@ -1,40 +1,34 @@
{ {
stdenv,
rustPlatform, rustPlatform,
lib, lib,
pkg-config, pkgs,
libX11, }: let
gtk4, cargoToml = builtins.fromTOML (builtins.readFile ../Cargo.toml);
libadwaita,
libXtst,
wrapGAppsHook4,
librsvg,
git,
}:
let
cargoToml = fromTOML (builtins.readFile ../Cargo.toml);
pname = cargoToml.package.name; pname = cargoToml.package.name;
version = cargoToml.package.version; version = cargoToml.package.version;
in in
rustPlatform.buildRustPackage { rustPlatform.buildRustPackage {
inherit pname; pname = pname;
inherit version; version = version;
nativeBuildInputs = [ nativeBuildInputs = with pkgs; [
pkg-config
wrapGAppsHook4
git git
pkg-config
cmake
makeWrapper
buildPackages.gtk4
]; ];
buildInputs = [ buildInputs = with pkgs; [
xorg.libX11
gtk4 gtk4
libadwaita libadwaita
librsvg xorg.libXtst
] ] ++ lib.optionals stdenv.isDarwin
++ lib.optionals stdenv.isLinux [ (with darwin.apple_sdk_11_0.frameworks; [
libX11 CoreGraphics
libXtst ApplicationServices
]; ]);
src = builtins.path { src = builtins.path {
name = pname; name = pname;
@@ -46,7 +40,11 @@ rustPlatform.buildRustPackage {
# Set Environment Variables # Set Environment Variables
RUST_BACKTRACE = "full"; RUST_BACKTRACE = "full";
# Needed to enable support for SVG icons in GTK
postInstall = '' 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 *.desktop -t $out/share/applications
install -Dm444 lan-mouse-gtk/resources/*.svg -t $out/share/icons/hicolor/scalable/apps install -Dm444 lan-mouse-gtk/resources/*.svg -t $out/share/icons/hicolor/scalable/apps
''; '';

View File

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

View File

@@ -29,13 +29,13 @@ iconset="${3:-./target/icon.iconset}"
set -u set -u
mkdir -p "$iconset" mkdir -p "$iconset"
magick "$svg" -background none -resize 1024x1024 "$iconset"/icon_512x512@2x.png magick convert -background none -resize 1024x1024 "$svg" "$iconset"/icon_512x512@2x.png
magick "$svg" -background none -resize 512x512 "$iconset"/icon_512x512.png magick convert -background none -resize 512x512 "$svg" "$iconset"/icon_512x512.png
magick "$svg" -background none -resize 256x256 "$iconset"/icon_256x256.png magick convert -background none -resize 256x256 "$svg" "$iconset"/icon_256x256.png
magick "$svg" -background none -resize 128x128 "$iconset"/icon_128x128.png magick convert -background none -resize 128x128 "$svg" "$iconset"/icon_128x128.png
magick "$svg" -background none -resize 64x64 "$iconset"/icon_32x32@2x.png magick convert -background none -resize 64x64 "$svg" "$iconset"/icon_32x32@2x.png
magick "$svg" -background none -resize 32x32 "$iconset"/icon_32x32.png magick convert -background none -resize 32x32 "$svg" "$iconset"/icon_32x32.png
magick "$svg" -background none -resize 16x16 "$iconset"/icon_16x16.png magick convert -background none -resize 16x16 "$svg" "$iconset"/icon_16x16.png
cp "$iconset"/icon_512x512.png "$iconset"/icon_256x256@2x.png cp "$iconset"/icon_512x512.png "$iconset"/icon_256x256@2x.png
cp "$iconset"/icon_256x256.png "$iconset"/icon_128x128@2x.png cp "$iconset"/icon_256x256.png "$iconset"/icon_128x128@2x.png
cp "$iconset"/icon_32x32.png "$iconset"/icon_16x16@2x.png cp "$iconset"/icon_32x32.png "$iconset"/icon_16x16@2x.png

View File

@@ -10,8 +10,8 @@ use input_capture::{
}; };
use input_event::scancode; use input_event::scancode;
use lan_mouse_proto::ProtoEvent; use lan_mouse_proto::ProtoEvent;
use local_channel::mpsc::{Receiver, Sender, channel}; use local_channel::mpsc::{channel, Receiver, Sender};
use tokio::task::{JoinHandle, spawn_local}; use tokio::task::{spawn_local, JoinHandle};
use tokio_util::sync::CancellationToken; use tokio_util::sync::CancellationToken;
use crate::connect::LanMouseConnection; use crate::connect::LanMouseConnection;
@@ -49,7 +49,7 @@ pub(crate) enum CaptureType {
EnterOnly, EnterOnly,
} }
#[derive(Clone, Debug)] #[derive(Clone, Copy, Debug)]
enum CaptureRequest { enum CaptureRequest {
/// capture must release the mouse /// capture must release the mouse
Release, Release,
@@ -59,8 +59,6 @@ enum CaptureRequest {
Destroy(CaptureHandle), Destroy(CaptureHandle),
/// reenable input capture /// reenable input capture
Reenable, Reenable,
/// set release bind
SetReleaseBind(Vec<scancode::Linux>),
} }
impl Capture { impl Capture {
@@ -133,10 +131,6 @@ impl Capture {
pub(crate) async fn event(&mut self) -> ICaptureEvent { pub(crate) async fn event(&mut self) -> ICaptureEvent {
self.event_rx.recv().await.expect("channel closed") self.event_rx.recv().await.expect("channel closed")
} }
pub(crate) fn set_release_bind(&mut self, bind: Vec<scancode::Linux>) {
let _ = self.request_tx.send(CaptureRequest::SetReleaseBind(bind));
}
} }
/// debounce a statement `$st`, i.e. the statement is executed only if the /// debounce a statement `$st`, i.e. the statement is executed only if the
@@ -211,9 +205,6 @@ impl CaptureTask {
CaptureRequest::Create(h, p, t) => self.add_capture(h, p, t), CaptureRequest::Create(h, p, t) => self.add_capture(h, p, t),
CaptureRequest::Destroy(h) => self.remove_capture(h), CaptureRequest::Destroy(h) => self.remove_capture(h),
CaptureRequest::Release => { /* nothing to do */ } CaptureRequest::Release => { /* nothing to do */ }
CaptureRequest::SetReleaseBind(bind) => {
self.release_bind.borrow_mut().clone_from(&bind);
}
}, },
_ = self.cancellation_token.cancelled() => return, _ = self.cancellation_token.cancelled() => return,
} }
@@ -304,9 +295,6 @@ impl CaptureTask {
self.remove_capture(h); self.remove_capture(h);
capture.destroy(h).await?; capture.destroy(h).await?;
} }
CaptureRequest::SetReleaseBind(bind) => {
self.release_bind.borrow_mut().clone_from(&bind);
}
}, },
_ = self.cancellation_token.cancelled() => break, _ = self.cancellation_token.cancelled() => break,
} }
@@ -374,13 +362,7 @@ impl CaptureTask {
} }
async fn release_capture(&mut self, capture: &mut InputCapture) -> Result<(), CaptureError> { async fn release_capture(&mut self, capture: &mut InputCapture) -> Result<(), CaptureError> {
// If we have an active client, notify them we're leaving self.active_client.take();
if let Some(handle) = self.active_client.take() {
log::info!("sending Leave event to client {handle}");
if let Err(e) = self.conn.send(ProtoEvent::Leave(0), handle).await {
log::warn!("failed to send Leave to client {handle}: {e}");
}
}
capture.release().await capture.release().await
} }
} }

View File

@@ -9,42 +9,12 @@ use slab::Slab;
use lan_mouse_ipc::{ClientConfig, ClientHandle, ClientState, Position}; use lan_mouse_ipc::{ClientConfig, ClientHandle, ClientState, Position};
use crate::config::ConfigClient;
#[derive(Clone, Default)] #[derive(Clone, Default)]
pub struct ClientManager { pub struct ClientManager {
clients: Rc<RefCell<Slab<(ClientConfig, ClientState)>>>, clients: Rc<RefCell<Slab<(ClientConfig, ClientState)>>>,
} }
impl ClientManager { impl ClientManager {
/// get all clients
pub fn clients(&self) -> Vec<(ClientConfig, ClientState)> {
self.clients
.borrow()
.iter()
.map(|(_, c)| c.clone())
.collect::<Vec<_>>()
}
pub fn add_with_config(&self, config_client: ConfigClient) -> ClientHandle {
let config = ClientConfig {
hostname: config_client.hostname,
fix_ips: config_client.ips.into_iter().collect(),
port: config_client.port,
pos: config_client.pos,
cmd: config_client.enter_hook,
};
let state = ClientState {
active: config_client.active,
ips: HashSet::from_iter(config.fix_ips.iter().cloned()),
..Default::default()
};
let handle = self.add_client();
self.set_config(handle, config);
self.set_state(handle, state);
handle
}
/// add a new client to this manager /// add a new client to this manager
pub fn add_client(&self) -> ClientHandle { pub fn add_client(&self) -> ClientHandle {
self.clients.borrow_mut().insert(Default::default()) as ClientHandle self.clients.borrow_mut().insert(Default::default()) as ClientHandle
@@ -251,15 +221,6 @@ impl ClientManager {
.and_then(|(c, _)| c.cmd.clone()) .and_then(|(c, _)| c.cmd.clone())
} }
/// returns all clients that are currently registered
pub(crate) fn registered_clients(&self) -> Vec<ClientHandle> {
self.clients
.borrow()
.iter()
.map(|(h, _)| h as ClientHandle)
.collect()
}
/// returns all clients that are currently active /// returns all clients that are currently active
pub(crate) fn active_clients(&self) -> Vec<ClientHandle> { pub(crate) fn active_clients(&self) -> Vec<ClientHandle> {
self.clients self.clients

View File

@@ -1,22 +1,19 @@
use crate::capture_test::TestCaptureArgs; use crate::capture_test::TestCaptureArgs;
use crate::emulation_test::TestEmulationArgs; use crate::emulation_test::TestEmulationArgs;
use clap::{Parser, Subcommand, ValueEnum}; use clap::{Parser, Subcommand, ValueEnum};
use notify::{EventKind, RecommendedWatcher, Watcher};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::collections::HashMap; use std::collections::HashMap;
use std::env::{self, VarError}; use std::env::{self, VarError};
use std::fmt::Display; use std::fmt::Display;
use std::fs::{self, File}; use std::fs;
use std::io::Write;
use std::net::IpAddr; use std::net::IpAddr;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use std::{collections::HashSet, io}; use std::{collections::HashSet, io};
use thiserror::Error; use thiserror::Error;
use toml; use toml;
use toml_edit::{self, DocumentMut};
use lan_mouse_cli::CliArgs; use lan_mouse_cli::CliArgs;
use lan_mouse_ipc::{DEFAULT_PORT, Position}; use lan_mouse_ipc::{Position, DEFAULT_PORT};
use input_event::scancode::{ use input_event::scancode::{
self, self,
@@ -47,14 +44,14 @@ fn default_path() -> Result<PathBuf, VarError> {
Ok(PathBuf::from(default_path)) Ok(PathBuf::from(default_path))
} }
#[derive(Serialize, Deserialize, Clone, Debug, Default, PartialEq)] #[derive(Serialize, Deserialize, Debug)]
struct ConfigToml { struct ConfigToml {
capture_backend: Option<CaptureBackend>, capture_backend: Option<CaptureBackend>,
emulation_backend: Option<EmulationBackend>, emulation_backend: Option<EmulationBackend>,
port: Option<u16>, port: Option<u16>,
release_bind: Option<Vec<scancode::Linux>>, release_bind: Option<Vec<scancode::Linux>>,
cert_path: Option<PathBuf>, cert_path: Option<PathBuf>,
clients: Option<Vec<TomlClient>>, clients: Vec<TomlClient>,
authorized_fingerprints: Option<HashMap<String, String>>, authorized_fingerprints: Option<HashMap<String, String>>,
} }
@@ -64,7 +61,7 @@ struct TomlClient {
host_name: Option<String>, host_name: Option<String>,
ips: Option<Vec<IpAddr>>, ips: Option<Vec<IpAddr>>,
port: Option<u16>, port: Option<u16>,
position: Option<Position>, pos: Option<Position>,
activate_on_startup: Option<bool>, activate_on_startup: Option<bool>,
enter_hook: Option<String>, enter_hook: Option<String>,
} }
@@ -245,14 +242,8 @@ pub struct Config {
cert_path: PathBuf, cert_path: PathBuf,
/// path to the config file used /// path to the config file used
config_path: PathBuf, config_path: PathBuf,
/// path to config directory (parent of above)
config_dir: PathBuf,
/// the (optional) toml config and it's path /// the (optional) toml config and it's path
config_toml: Option<ConfigToml>, config_toml: Option<ConfigToml>,
// filesystem watcher
watcher: notify::RecommendedWatcher,
// channel for filesystem events
watch_rx: tokio::sync::mpsc::Receiver<Result<notify::Event, notify::Error>>,
} }
pub struct ConfigClient { pub struct ConfigClient {
@@ -271,7 +262,7 @@ impl From<TomlClient> for ConfigClient {
let hostname = toml.hostname; let hostname = toml.hostname;
let ips = HashSet::from_iter(toml.ips.into_iter().flatten()); let ips = HashSet::from_iter(toml.ips.into_iter().flatten());
let port = toml.port.unwrap_or(DEFAULT_PORT); let port = toml.port.unwrap_or(DEFAULT_PORT);
let pos = toml.position.unwrap_or_default(); let pos = toml.pos.unwrap_or_default();
Self { Self {
ips, ips,
hostname, hostname,
@@ -283,33 +274,6 @@ impl From<TomlClient> for ConfigClient {
} }
} }
impl From<ConfigClient> for TomlClient {
fn from(client: ConfigClient) -> Self {
let hostname = client.hostname;
let host_name = None;
let mut ips = client.ips.into_iter().collect::<Vec<_>>();
ips.sort();
let ips = Some(ips);
let port = if client.port == DEFAULT_PORT {
None
} else {
Some(client.port)
};
let position = Some(client.pos);
let activate_on_startup = if client.active { Some(true) } else { None };
let enter_hook = client.enter_hook;
Self {
hostname,
host_name,
ips,
port,
position,
activate_on_startup,
enter_hook,
}
}
}
#[derive(Debug, Error)] #[derive(Debug, Error)]
pub enum ConfigError { pub enum ConfigError {
#[error(transparent)] #[error(transparent)]
@@ -318,8 +282,6 @@ pub enum ConfigError {
Io(#[from] io::Error), Io(#[from] io::Error),
#[error(transparent)] #[error(transparent)]
Var(#[from] VarError), Var(#[from] VarError),
#[error(transparent)]
Watcher(#[from] notify::Error),
} }
const DEFAULT_RELEASE_KEYS: [scancode::Linux; 4] = const DEFAULT_RELEASE_KEYS: [scancode::Linux; 4] =
@@ -351,55 +313,12 @@ impl Config {
.or(config_toml.as_ref().and_then(|c| c.cert_path.clone())) .or(config_toml.as_ref().and_then(|c| c.cert_path.clone()))
.unwrap_or(default_path()?.join(CERT_FILE_NAME)); .unwrap_or(default_path()?.join(CERT_FILE_NAME));
let (tx, watch_rx) = tokio::sync::mpsc::channel(16); Ok(Config {
let watcher = RecommendedWatcher::new(
move |res| {
let _ = tx.blocking_send(res);
},
notify::Config::default(),
)?;
let config_dir = config_path
.parent()
.expect("config directory")
.to_path_buf();
let mut config = Config {
args, args,
cert_path, cert_path,
config_path, config_path,
config_dir,
config_toml, config_toml,
watcher, })
watch_rx,
};
config.watch()?;
Ok(config)
}
fn watch(&mut self) -> Result<(), notify::Error> {
self.watcher
.watch(&self.config_dir, notify::RecursiveMode::NonRecursive)?;
Ok(())
}
fn unwatch(&mut self) -> Result<(), notify::Error> {
self.watcher.unwatch(&self.config_dir)?;
Ok(())
}
pub async fn changed(&mut self) -> Result<(), notify::Error> {
loop {
let event = self.watch_rx.recv().await.expect("channel closed");
let event = event.expect("filesystem event");
if event.paths.contains(&self.config_path)
&& matches!(
event.kind,
EventKind::Create(_) | EventKind::Modify(_) | EventKind::Remove(_)
)
&& self.read_from_disk()?
{
return Ok(());
}
}
} }
/// the command to run /// the command to run
@@ -451,7 +370,6 @@ impl Config {
self.config_toml self.config_toml
.as_ref() .as_ref()
.map(|c| c.clients.clone()) .map(|c| c.clients.clone())
.unwrap_or_default()
.into_iter() .into_iter()
.flatten() .flatten()
.map(From::<TomlClient>::from) .map(From::<TomlClient>::from)
@@ -465,83 +383,4 @@ impl Config {
.and_then(|c| c.release_bind.clone()) .and_then(|c| c.release_bind.clone())
.unwrap_or(Vec::from_iter(DEFAULT_RELEASE_KEYS.iter().cloned())) .unwrap_or(Vec::from_iter(DEFAULT_RELEASE_KEYS.iter().cloned()))
} }
/// set configured clients
pub fn set_clients(&mut self, clients: Vec<ConfigClient>) {
if clients.is_empty() {
return;
}
if self.config_toml.is_none() {
self.config_toml = Some(Default::default());
}
self.config_toml.as_mut().expect("config").clients =
Some(clients.into_iter().map(|c| c.into()).collect::<Vec<_>>());
}
/// set authorized keys
pub fn set_authorized_keys(&mut self, fingerprints: HashMap<String, String>) {
if self.config_toml.is_none() {
self.config_toml = Default::default();
}
self.config_toml
.as_mut()
.expect("config")
.authorized_fingerprints = Some(fingerprints);
}
pub fn read_from_disk(&mut self) -> Result<bool, io::Error> {
log::info!("reading config from {:?}", &self.config_path);
let current_config = fs::read_to_string(&self.config_path)?;
let current_config = match current_config.parse::<DocumentMut>() {
Ok(c) => c,
Err(e) => {
log::warn!("{:?} {e}", self.config_path());
return Ok(false);
}
};
let mut changed = false;
match toml_edit::de::from_document::<ConfigToml>(current_config) {
Ok(current_config) => {
changed = self
.config_toml
.as_ref()
.is_none_or(|c| c != &current_config);
self.config_toml.replace(current_config);
}
Err(e) => log::warn!("{:?} {e}", self.config_path()),
};
Ok(changed)
}
pub fn write_back(&mut self) -> Result<(), io::Error> {
log::info!("writing config to {:?}", &self.config_path);
/* the new config */
let new_config = self.config_toml.clone().unwrap_or_default();
let new_config = toml_edit::ser::to_string_pretty(&new_config).expect("config");
/*
* TODO merge with current config file to preserve comments
* => eventually we might want to split this up into clients configured
* via the config file and clients managed through the GUI / frontend.
* The latter should be saved to $XDG_DATA_HOME instead of $XDG_CONFIG_HOME,
* and clients configured through .config could be made permanent.
* For now we just override the config file.
*/
let _ = self.unwatch();
/* write new config to file */
if let Some(p) = self.config_path().parent() {
fs::create_dir_all(p)?;
}
{
let mut f = File::create(self.config_path())?;
f.write_all(new_config.as_bytes())?;
f.sync_all()?;
}
let _ = self.watch();
Ok(())
}
} }

View File

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

View File

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

View File

@@ -1,9 +1,9 @@
use crate::listen::{LanMouseListener, ListenEvent, ListenerCreationError}; use crate::listen::{LanMouseListener, ListenerCreationError};
use futures::StreamExt; use futures::StreamExt;
use input_emulation::{EmulationHandle, InputEmulation, InputEmulationError}; use input_emulation::{EmulationHandle, InputEmulation, InputEmulationError};
use input_event::Event; use input_event::Event;
use lan_mouse_proto::{Position, ProtoEvent}; use lan_mouse_proto::{Position, ProtoEvent};
use local_channel::mpsc::{Receiver, Sender, channel}; use local_channel::mpsc::{channel, Receiver, Sender};
use std::{ use std::{
cell::Cell, cell::Cell,
collections::HashMap, collections::HashMap,
@@ -13,7 +13,7 @@ use std::{
}; };
use tokio::{ use tokio::{
select, select,
task::{JoinHandle, spawn_local}, task::{spawn_local, JoinHandle},
}; };
/// emulation handling events received from a listener /// emulation handling events received from a listener
@@ -24,15 +24,8 @@ pub(crate) struct Emulation {
} }
pub(crate) enum EmulationEvent { pub(crate) enum EmulationEvent {
Connected {
addr: SocketAddr,
fingerprint: String,
},
ConnectionAttempt {
fingerprint: String,
},
/// new connection /// new connection
Entered { Connected {
/// address of the connection /// address of the connection
addr: SocketAddr, addr: SocketAddr,
/// position of the connection /// position of the connection
@@ -41,9 +34,7 @@ pub(crate) enum EmulationEvent {
fingerprint: String, fingerprint: String,
}, },
/// connection closed /// connection closed
Disconnected { Disconnected { addr: SocketAddr },
addr: SocketAddr,
},
/// the port of the listener has changed /// the port of the listener has changed
PortChanged(Result<u16, ListenerCreationError>), PortChanged(Result<u16, ListenerCreationError>),
/// emulation was disabled /// emulation was disabled
@@ -128,11 +119,13 @@ impl ListenTask {
async fn run(mut self) { async fn run(mut self) {
let mut interval = tokio::time::interval(Duration::from_secs(5)); let mut interval = tokio::time::interval(Duration::from_secs(5));
let mut last_response = HashMap::new(); let mut last_response = HashMap::new();
let mut rejected_connections = HashMap::new();
loop { loop {
select! { select! {
e = self.listener.next() => {match e { e = self.listener.next() => {
Some(ListenEvent::Msg { event, addr }) => { let (event, addr) = match e {
Some(e) => e,
None => break,
};
log::trace!("{event} <-<-<-<-<- {addr}"); log::trace!("{event} <-<-<-<-<- {addr}");
last_response.insert(addr, Instant::now()); last_response.insert(addr, Instant::now());
match event { match event {
@@ -141,7 +134,7 @@ impl ListenTask {
log::info!("releasing capture: {addr} entered this device"); log::info!("releasing capture: {addr} entered this device");
self.event_tx.send(EmulationEvent::ReleaseNotify).expect("channel closed"); self.event_tx.send(EmulationEvent::ReleaseNotify).expect("channel closed");
self.listener.reply(addr, ProtoEvent::Ack(0)).await; self.listener.reply(addr, ProtoEvent::Ack(0)).await;
self.event_tx.send(EmulationEvent::Entered{addr, pos: to_ipc_pos(pos), fingerprint}).expect("channel closed"); self.event_tx.send(EmulationEvent::Connected{addr, pos: to_ipc_pos(pos), fingerprint}).expect("channel closed");
} }
} }
ProtoEvent::Leave(_) => { ProtoEvent::Leave(_) => {
@@ -153,17 +146,6 @@ impl ListenTask {
_ => {} _ => {}
} }
} }
Some(ListenEvent::Accept { addr, fingerprint }) => {
self.event_tx.send(EmulationEvent::Connected { addr, fingerprint }).expect("channel closed");
}
Some(ListenEvent::Rejected { fingerprint }) => {
if rejected_connections.insert(fingerprint.clone(), Instant::now())
.is_none_or(|i| i.elapsed() >= Duration::from_secs(2)) {
self.event_tx.send(EmulationEvent::ConnectionAttempt { fingerprint }).expect("channel closed");
}
}
None => break
}}
event = self.emulation_proxy.event() => { event = self.emulation_proxy.event() => {
self.event_tx.send(event).expect("channel closed"); self.event_tx.send(event).expect("channel closed");
} }

View File

@@ -1,18 +1,18 @@
use futures::{Stream, StreamExt}; use futures::{Stream, StreamExt};
use lan_mouse_proto::{MAX_EVENT_SIZE, ProtoEvent}; use lan_mouse_proto::{ProtoEvent, MAX_EVENT_SIZE};
use local_channel::mpsc::{Receiver, Sender, channel}; use local_channel::mpsc::{channel, Receiver, Sender};
use rustls::pki_types::CertificateDer; use rustls::pki_types::CertificateDer;
use std::{ use std::{
collections::{HashMap, VecDeque}, collections::HashMap,
net::SocketAddr, net::SocketAddr,
rc::Rc, rc::Rc,
sync::{Arc, Mutex, RwLock}, sync::{Arc, RwLock},
time::Duration, time::Duration,
}; };
use thiserror::Error; use thiserror::Error;
use tokio::{ use tokio::{
sync::Mutex as AsyncMutex, sync::Mutex,
task::{JoinHandle, spawn_local}, task::{spawn_local, JoinHandle},
}; };
use webrtc_dtls::{ use webrtc_dtls::{
config::{ClientAuthType::RequireAnyClientCert, Config, ExtendedMasterSecretType}, config::{ClientAuthType::RequireAnyClientCert, Config, ExtendedMasterSecretType},
@@ -20,7 +20,7 @@ use webrtc_dtls::{
crypto::Certificate, crypto::Certificate,
listener::listen, listener::listen,
}; };
use webrtc_util::{Conn, Error, conn::Listener}; use webrtc_util::{conn::Listener, Conn, Error};
use crate::crypto; use crate::crypto;
@@ -34,25 +34,11 @@ pub enum ListenerCreationError {
type ArcConn = Arc<dyn Conn + Send + Sync>; type ArcConn = Arc<dyn Conn + Send + Sync>;
pub(crate) enum ListenEvent {
Msg {
event: ProtoEvent,
addr: SocketAddr,
},
Accept {
addr: SocketAddr,
fingerprint: String,
},
Rejected {
fingerprint: String,
},
}
pub(crate) struct LanMouseListener { pub(crate) struct LanMouseListener {
listen_rx: Receiver<ListenEvent>, listen_rx: Receiver<(ProtoEvent, SocketAddr)>,
listen_tx: Sender<ListenEvent>, listen_tx: Sender<(ProtoEvent, SocketAddr)>,
listen_task: JoinHandle<()>, listen_task: JoinHandle<()>,
conns: Rc<AsyncMutex<Vec<(SocketAddr, ArcConn)>>>, conns: Rc<Mutex<Vec<(SocketAddr, ArcConn)>>>,
request_port_change: Sender<u16>, request_port_change: Sender<u16>,
port_changed: Receiver<Result<u16, ListenerCreationError>>, port_changed: Receiver<Result<u16, ListenerCreationError>>,
} }
@@ -72,12 +58,9 @@ impl LanMouseListener {
let (listen_tx, listen_rx) = channel(); let (listen_tx, listen_rx) = channel();
let (request_port_change, mut request_port_change_rx) = channel(); let (request_port_change, mut request_port_change_rx) = channel();
let (port_changed_tx, port_changed) = channel(); let (port_changed_tx, port_changed) = channel();
let connection_attempts: Arc<Mutex<VecDeque<String>>> = Default::default();
let authorized = authorized_keys.clone(); let authorized = authorized_keys.clone();
let verify_peer_certificate: Option<VerifyPeerCertificateFn> = { let verify_peer_certificate: Option<VerifyPeerCertificateFn> = Some(Arc::new(
let connection_attempts = connection_attempts.clone();
Some(Arc::new(
move |certs: &[Vec<u8>], _chains: &[CertificateDer<'static>]| { move |certs: &[Vec<u8>], _chains: &[CertificateDer<'static>]| {
assert!(certs.len() == 1); assert!(certs.len() == 1);
let fingerprints = certs let fingerprints = certs
@@ -91,16 +74,10 @@ impl LanMouseListener {
{ {
Ok(()) Ok(())
} else { } else {
let fingerprint = fingerprints.into_iter().next().expect("fingerprint");
connection_attempts
.lock()
.expect("lock")
.push_back(fingerprint);
Err(webrtc_dtls::Error::ErrVerifyDataMismatch) Err(webrtc_dtls::Error::ErrVerifyDataMismatch)
} }
}, },
)) ));
};
let cfg = Config { let cfg = Config {
certificates: vec![cert.clone()], certificates: vec![cert.clone()],
extended_master_secret: ExtendedMasterSecretType::Require, extended_master_secret: ExtendedMasterSecretType::Require,
@@ -112,14 +89,11 @@ impl LanMouseListener {
let listen_addr = SocketAddr::new("0.0.0.0".parse().expect("invalid ip"), port); let listen_addr = SocketAddr::new("0.0.0.0".parse().expect("invalid ip"), port);
let mut listener = listen(listen_addr, cfg.clone()).await?; let mut listener = listen(listen_addr, cfg.clone()).await?;
let conns: Rc<AsyncMutex<Vec<(SocketAddr, ArcConn)>>> = let conns: Rc<Mutex<Vec<(SocketAddr, ArcConn)>>> = Rc::new(Mutex::new(Vec::new()));
Rc::new(AsyncMutex::new(Vec::new()));
let conns_clone = conns.clone(); let conns_clone = conns.clone();
let listen_task: JoinHandle<()> = { let tx = listen_tx.clone();
let listen_tx = listen_tx.clone(); let listen_task: JoinHandle<()> = spawn_local(async move {
let connection_attempts = connection_attempts.clone();
spawn_local(async move {
loop { loop {
let sleep = tokio::time::sleep(Duration::from_secs(2)); let sleep = tokio::time::sleep(Duration::from_secs(2));
tokio::select! { tokio::select! {
@@ -130,31 +104,9 @@ impl LanMouseListener {
log::info!("dtls client connected, ip: {addr}"); log::info!("dtls client connected, ip: {addr}");
let mut conns = conns_clone.lock().await; let mut conns = conns_clone.lock().await;
conns.push((addr, conn.clone())); conns.push((addr, conn.clone()));
let dtls_conn: &DTLSConn = conn.as_any().downcast_ref().expect("dtls conn"); spawn_local(read_loop(conns_clone.clone(), addr, conn, tx.clone()));
let certs = dtls_conn.connection_state().await.peer_certificates;
let cert = certs.first().expect("cert");
let fingerprint = crypto::generate_fingerprint(cert);
listen_tx.send(ListenEvent::Accept { addr, fingerprint }).expect("channel closed");
spawn_local(read_loop(conns_clone.clone(), addr, conn, listen_tx.clone()));
}, },
Err(e) => { Err(e) => log::warn!("accept: {e}"),
if let Error::Std(ref e) = e {
if let Some(e) = e.0.downcast_ref::<webrtc_dtls::Error>() {
match e {
webrtc_dtls::Error::ErrVerifyDataMismatch => {
if let Some(fingerprint) = connection_attempts.lock().expect("lock").pop_front() {
listen_tx.send(ListenEvent::Rejected { fingerprint }).expect("channel closed");
}
}
_ => log::warn!("accept: {e}"),
}
} else {
log::warn!("accept: {e:?}");
}
} else {
log::warn!("accept: {e:?}");
}
}
}, },
port = request_port_change_rx.recv() => { port = request_port_change_rx.recv() => {
let port = port.expect("channel closed"); let port = port.expect("channel closed");
@@ -173,8 +125,7 @@ impl LanMouseListener {
}, },
}; };
} }
}) });
};
Ok(Self { Ok(Self {
conns, conns,
@@ -235,7 +186,7 @@ impl LanMouseListener {
} }
impl Stream for LanMouseListener { impl Stream for LanMouseListener {
type Item = ListenEvent; type Item = (ProtoEvent, SocketAddr);
fn poll_next( fn poll_next(
mut self: std::pin::Pin<&mut Self>, mut self: std::pin::Pin<&mut Self>,
@@ -246,25 +197,23 @@ impl Stream for LanMouseListener {
} }
async fn read_loop( async fn read_loop(
conns: Rc<AsyncMutex<Vec<(SocketAddr, ArcConn)>>>, conns: Rc<Mutex<Vec<(SocketAddr, ArcConn)>>>,
addr: SocketAddr, addr: SocketAddr,
conn: ArcConn, conn: ArcConn,
dtls_tx: Sender<ListenEvent>, dtls_tx: Sender<(ProtoEvent, SocketAddr)>,
) -> Result<(), Error> { ) -> Result<(), Error> {
let mut b = [0u8; MAX_EVENT_SIZE]; let mut b = [0u8; MAX_EVENT_SIZE];
while conn.recv(&mut b).await.is_ok() { while conn.recv(&mut b).await.is_ok() {
match b.try_into() { match b.try_into() {
Ok(event) => dtls_tx Ok(event) => dtls_tx.send((event, addr)).expect("channel closed"),
.send(ListenEvent::Msg { event, addr })
.expect("channel closed"),
Err(e) => { Err(e) => {
log::warn!("error receiving event: {e}"); log::warn!("error receiving event: {e}");
break; break;
} }
} }
} }
log::info!("dtls client disconnected {addr:?}"); log::info!("dtls client disconnected {:?}", addr);
let mut conns = conns.lock().await; let mut conns = conns.lock().await;
let index = conns let index = conns
.iter() .iter()

View File

@@ -1,7 +1,7 @@
use crate::{ use crate::{
capture::{Capture, CaptureType, ICaptureEvent}, capture::{Capture, CaptureType, ICaptureEvent},
client::ClientManager, client::ClientManager,
config::{Config, ConfigClient}, config::Config,
connect::LanMouseConnection, connect::LanMouseConnection,
crypto, crypto,
dns::{DnsEvent, DnsResolver}, dns::{DnsEvent, DnsResolver},
@@ -9,10 +9,10 @@ use crate::{
listen::{LanMouseListener, ListenerCreationError}, listen::{LanMouseListener, ListenerCreationError},
}; };
use futures::StreamExt; use futures::StreamExt;
use hickory_resolver::ResolveError; use hickory_resolver::error::ResolveError;
use lan_mouse_ipc::{ use lan_mouse_ipc::{
AsyncFrontendListener, ClientHandle, FrontendEvent, FrontendRequest, IpcError, AsyncFrontendListener, ClientConfig, ClientHandle, ClientState, FrontendEvent, FrontendRequest,
IpcListenerCreationError, Position, Status, IpcError, IpcListenerCreationError, Position, Status,
}; };
use log; use log;
use std::{ use std::{
@@ -39,8 +39,6 @@ pub enum ServiceError {
} }
pub struct Service { pub struct Service {
/// configuration
config: Config,
/// input capture /// input capture
capture: Capture, capture: Capture,
/// input emulation /// input emulation
@@ -83,7 +81,21 @@ impl Service {
pub async fn new(config: Config) -> Result<Self, ServiceError> { pub async fn new(config: Config) -> Result<Self, ServiceError> {
let client_manager = ClientManager::default(); let client_manager = ClientManager::default();
for client in config.clients() { for client in config.clients() {
client_manager.add_with_config(client); let config = ClientConfig {
hostname: client.hostname,
fix_ips: client.ips.into_iter().collect(),
port: client.port,
pos: client.pos,
cmd: client.enter_hook,
};
let state = ClientState {
active: client.active,
ips: HashSet::from_iter(config.fix_ips.iter().cloned()),
..Default::default()
};
let handle = client_manager.add_client();
client_manager.set_config(handle, config);
client_manager.set_state(handle, state);
} }
// load certificate // load certificate
@@ -110,7 +122,6 @@ impl Service {
let port = config.port(); let port = config.port();
let service = Self { let service = Self {
config,
capture, capture,
emulation, emulation,
frontend_listener, frontend_listener,
@@ -150,7 +161,6 @@ impl Service {
event = self.emulation.event() => self.handle_emulation_event(event), event = self.emulation.event() => self.handle_emulation_event(event),
event = self.capture.event() => self.handle_capture_event(event), event = self.capture.event() => self.handle_capture_event(event),
event = self.resolver.event() => self.handle_resolver_event(event), event = self.resolver.event() => self.handle_resolver_event(event),
_ = self.config.changed() => self.handle_config_change(),
r = signal::ctrl_c() => break r.expect("failed to wait for CTRL+C"), r = signal::ctrl_c() => break r.expect("failed to wait for CTRL+C"),
} }
} }
@@ -172,100 +182,27 @@ impl Service {
Err(e) => return log::error!("error receiving request: {e}"), Err(e) => return log::error!("error receiving request: {e}"),
}; };
match request { match request {
FrontendRequest::Activate(handle, active) => { FrontendRequest::Activate(handle, active) => self.set_client_active(handle, active),
self.set_client_active(handle, active); FrontendRequest::AuthorizeKey(desc, fp) => self.add_authorized_key(desc, fp),
self.save_config();
}
FrontendRequest::AuthorizeKey(desc, fp) => {
self.add_authorized_key(desc, fp);
self.save_config();
}
FrontendRequest::ChangePort(port) => self.change_port(port), FrontendRequest::ChangePort(port) => self.change_port(port),
FrontendRequest::Create => { FrontendRequest::Create => self.add_client(),
self.add_client(); FrontendRequest::Delete(handle) => self.remove_client(handle),
self.save_config();
}
FrontendRequest::Delete(handle) => {
self.remove_client(handle);
self.save_config();
}
FrontendRequest::EnableCapture => self.capture.reenable(), FrontendRequest::EnableCapture => self.capture.reenable(),
FrontendRequest::EnableEmulation => self.emulation.reenable(), FrontendRequest::EnableEmulation => self.emulation.reenable(),
FrontendRequest::Enumerate() => self.enumerate(), FrontendRequest::Enumerate() => self.enumerate(),
FrontendRequest::UpdateFixIps(handle, fix_ips) => { FrontendRequest::UpdateFixIps(handle, fix_ips) => self.update_fix_ips(handle, fix_ips),
self.update_fix_ips(handle, fix_ips); FrontendRequest::UpdateHostname(handle, host) => self.update_hostname(handle, host),
self.save_config(); FrontendRequest::UpdatePort(handle, port) => self.update_port(handle, port),
} FrontendRequest::UpdatePosition(handle, pos) => self.update_pos(handle, pos),
FrontendRequest::UpdateHostname(handle, host) => {
self.update_hostname(handle, host);
self.save_config();
}
FrontendRequest::UpdatePort(handle, port) => {
self.update_port(handle, port);
self.save_config();
}
FrontendRequest::UpdatePosition(handle, pos) => {
self.update_pos(handle, pos);
self.save_config();
}
FrontendRequest::ResolveDns(handle) => self.resolve(handle), FrontendRequest::ResolveDns(handle) => self.resolve(handle),
FrontendRequest::Sync => self.sync_frontend(), FrontendRequest::Sync => self.sync_frontend(),
FrontendRequest::RemoveAuthorizedKey(key) => { FrontendRequest::RemoveAuthorizedKey(key) => self.remove_authorized_key(key),
self.remove_authorized_key(key);
self.save_config();
}
FrontendRequest::UpdateEnterHook(handle, enter_hook) => { FrontendRequest::UpdateEnterHook(handle, enter_hook) => {
self.update_enter_hook(handle, enter_hook) self.update_enter_hook(handle, enter_hook)
} }
FrontendRequest::SaveConfiguration => self.save_config(),
} }
} }
fn save_config(&mut self) {
let clients = self.client_manager.clients();
let clients = clients
.into_iter()
.map(|(c, s)| ConfigClient {
ips: HashSet::from_iter(c.fix_ips),
hostname: c.hostname,
port: c.port,
pos: c.pos,
active: s.active,
enter_hook: c.cmd,
})
.collect();
self.config.set_clients(clients);
let authorized_keys = self.authorized_keys.read().expect("lock").clone();
self.config.set_authorized_keys(authorized_keys);
if let Err(e) = self.config.write_back() {
log::warn!("failed to write config: {e}");
}
}
fn handle_config_change(&mut self) {
for h in self.client_manager.registered_clients() {
self.remove_client(h);
}
for c in self.config.clients() {
let handle = self.client_manager.add_with_config(c);
log::info!("added client {handle}");
let (c, s) = self.client_manager.get_state(handle).unwrap();
if s.active {
self.client_manager.deactivate_client(handle);
self.activate_client(handle);
}
self.notify_frontend(FrontendEvent::Created(handle, c, s));
}
let release_bind = self.config.release_bind();
self.capture.set_release_bind(release_bind);
let authorized_keys = self.config.authorized_fingerprints();
self.authorized_keys
.write()
.unwrap()
.clone_from(&authorized_keys);
self.sync_frontend();
}
async fn handle_frontend_pending(&mut self) { async fn handle_frontend_pending(&mut self) {
while let Some(event) = self.pending_frontend_events.pop_front() { while let Some(event) = self.pending_frontend_events.pop_front() {
self.frontend_listener.broadcast(event).await; self.frontend_listener.broadcast(event).await;
@@ -274,10 +211,7 @@ impl Service {
fn handle_emulation_event(&mut self, event: EmulationEvent) { fn handle_emulation_event(&mut self, event: EmulationEvent) {
match event { match event {
EmulationEvent::ConnectionAttempt { fingerprint } => { EmulationEvent::Connected {
self.notify_frontend(FrontendEvent::ConnectionAttempt { fingerprint });
}
EmulationEvent::Entered {
addr, addr,
pos, pos,
fingerprint, fingerprint,
@@ -285,11 +219,7 @@ impl Service {
// check if already registered // check if already registered
if !self.incoming_conns.contains(&addr) { if !self.incoming_conns.contains(&addr) {
self.add_incoming(addr, pos, fingerprint.clone()); self.add_incoming(addr, pos, fingerprint.clone());
self.notify_frontend(FrontendEvent::DeviceEntered { self.notify_frontend(FrontendEvent::IncomingConnected(fingerprint, addr, pos));
fingerprint,
addr,
pos,
});
} else { } else {
self.update_incoming(addr, pos, fingerprint); self.update_incoming(addr, pos, fingerprint);
} }
@@ -316,9 +246,6 @@ impl Service {
self.notify_frontend(FrontendEvent::EmulationStatus(self.emulation_status)); self.notify_frontend(FrontendEvent::EmulationStatus(self.emulation_status));
} }
EmulationEvent::ReleaseNotify => self.capture.release(), EmulationEvent::ReleaseNotify => self.capture.release(),
EmulationEvent::Connected { addr, fingerprint } => {
self.notify_frontend(FrontendEvent::DeviceConnected { addr, fingerprint });
}
} }
} }
@@ -420,11 +347,7 @@ impl Service {
self.remove_incoming(addr); self.remove_incoming(addr);
self.add_incoming(addr, pos, fingerprint.clone()); self.add_incoming(addr, pos, fingerprint.clone());
self.notify_frontend(FrontendEvent::IncomingDisconnected(addr)); self.notify_frontend(FrontendEvent::IncomingDisconnected(addr));
self.notify_frontend(FrontendEvent::DeviceEntered { self.notify_frontend(FrontendEvent::IncomingConnected(fingerprint, addr, pos));
fingerprint,
addr,
pos,
});
} }
} }
@@ -488,7 +411,7 @@ impl Service {
} }
fn activate_client(&mut self, handle: ClientHandle) { fn activate_client(&mut self, handle: ClientHandle) {
log::debug!("activating client {handle}"); log::debug!("activating client");
/* resolve dns on activate */ /* resolve dns on activate */
self.resolve(handle); self.resolve(handle);