The response has been limited to 50k tokens of the smallest files in the repo. You can remove this limitation by removing the max tokens filter.
├── .envrc
├── .github
    ├── FUNDING.yml
    └── workflows
    │   ├── cachix.yml
    │   ├── pre-release.yml
    │   ├── rust.yml
    │   └── tagged-release.yml
├── .gitignore
├── Cargo.lock
├── Cargo.toml
├── DOC.md
├── LICENSE
├── README.md
├── build.rs
├── config.toml
├── de.feschber.LanMouse.desktop
├── de.feschber.LanMouse.yml
├── default.nix
├── deny.toml
├── firewall
    └── lan-mouse.xml
├── flake.lock
├── flake.nix
├── input-capture
    ├── Cargo.toml
    └── src
    │   ├── dummy.rs
    │   ├── error.rs
    │   ├── layer_shell.rs
    │   ├── lib.rs
    │   ├── libei.rs
    │   ├── macos.rs
    │   ├── windows.rs
    │   ├── windows
    │       ├── display_util.rs
    │       └── event_thread.rs
    │   └── x11.rs
├── input-emulation
    ├── Cargo.toml
    └── src
    │   ├── dummy.rs
    │   ├── error.rs
    │   ├── lib.rs
    │   ├── libei.rs
    │   ├── macos.rs
    │   ├── windows.rs
    │   ├── wlroots.rs
    │   ├── x11.rs
    │   └── xdg_desktop_portal.rs
├── input-event
    ├── Cargo.toml
    └── src
    │   ├── error.rs
    │   ├── lib.rs
    │   ├── libei.rs
    │   └── scancode.rs
├── lan-mouse-cli
    ├── Cargo.toml
    └── src
    │   └── lib.rs
├── lan-mouse-gtk
    ├── Cargo.toml
    ├── build.rs
    ├── resources
    │   ├── authorization_window.ui
    │   ├── client_row.ui
    │   ├── de.feschber.LanMouse.svg
    │   ├── fingerprint_window.ui
    │   ├── key_row.ui
    │   ├── resources.gresource.xml
    │   └── window.ui
    └── src
    │   ├── authorization_window.rs
    │   ├── authorization_window
    │       └── imp.rs
    │   ├── client_object.rs
    │   ├── client_object
    │       └── imp.rs
    │   ├── client_row.rs
    │   ├── client_row
    │       └── imp.rs
    │   ├── fingerprint_window.rs
    │   ├── fingerprint_window
    │       └── imp.rs
    │   ├── key_object.rs
    │   ├── key_object
    │       └── imp.rs
    │   ├── key_row.rs
    │   ├── key_row
    │       └── imp.rs
    │   ├── lib.rs
    │   ├── window.rs
    │   └── window
    │       └── imp.rs
├── lan-mouse-ipc
    ├── Cargo.toml
    └── src
    │   ├── connect.rs
    │   ├── connect_async.rs
    │   ├── lib.rs
    │   └── listen.rs
├── lan-mouse-proto
    ├── Cargo.toml
    └── src
    │   └── lib.rs
├── nix
    ├── README.md
    ├── default.nix
    └── hm-module.nix
├── screenshots
    ├── dark.png
    └── light.png
├── scripts
    └── makeicns.sh
├── service
    └── lan-mouse.service
├── shell.nix
└── src
    ├── capture.rs
    ├── capture_test.rs
    ├── client.rs
    ├── config.rs
    ├── connect.rs
    ├── crypto.rs
    ├── dns.rs
    ├── emulation.rs
    ├── emulation_test.rs
    ├── lib.rs
    ├── listen.rs
    ├── main.rs
    └── service.rs


/.envrc:
--------------------------------------------------------------------------------
1 | use flake


--------------------------------------------------------------------------------
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | github: [feschber]
2 | ko_fi: feschber
3 | 


--------------------------------------------------------------------------------
/.github/workflows/cachix.yml:
--------------------------------------------------------------------------------
 1 | name: Binary Cache
 2 | 
 3 | on: [push, pull_request, workflow_dispatch]
 4 | jobs:
 5 |   nix:
 6 |     strategy:
 7 |       matrix:
 8 |        os: 
 9 |         - ubuntu-latest
10 |         - macos-13
11 |         - macos-14
12 |     name: "Build"
13 |     runs-on: ${{ matrix.os }} 
14 |     steps:
15 |     - name: Checkout
16 |       uses: actions/checkout@v4
17 |       with:
18 |         submodules: recursive
19 | 
20 |     - uses: DeterminateSystems/nix-installer-action@main
21 |       with:
22 |         logger: pretty
23 |     - uses: DeterminateSystems/magic-nix-cache-action@main
24 |     - uses: cachix/cachix-action@v14
25 |       with:
26 |         name: lan-mouse
27 |         authToken: '${{ secrets.CACHIX_AUTH_TOKEN }}'
28 | 
29 |     - name: Build lan-mouse (x86_64-linux)
30 |       if: matrix.os == 'ubuntu-latest'
31 |       run: nix build --print-build-logs --show-trace .#packages.x86_64-linux.lan-mouse
32 | 
33 |     - name: Build lan-mouse (x86_64-darwin)
34 |       if: matrix.os == 'macos-13'
35 |       run: nix build --print-build-logs --show-trace .#packages.x86_64-darwin.lan-mouse
36 | 
37 |     - name: Build lan-mouse (aarch64-darwin)
38 |       if: matrix.os == 'macos-14'
39 |       run: nix build --print-build-logs --show-trace .#packages.aarch64-darwin.lan-mouse
40 | 
41 | 


--------------------------------------------------------------------------------
/.github/workflows/pre-release.yml:
--------------------------------------------------------------------------------
  1 | name: "pre-release"
  2 | 
  3 | on:
  4 |   push:
  5 |     branches: [ "main" ]
  6 | 
  7 | 
  8 | 
  9 | env:
 10 |   CARGO_TERM_COLOR: always
 11 | 
 12 | jobs:
 13 |   linux-release-build:
 14 |     runs-on: ubuntu-latest
 15 |     steps:
 16 |     - uses: actions/checkout@v4
 17 |     - name: install dependencies
 18 |       run: |
 19 |         sudo apt-get update
 20 |         sudo apt-get install libx11-dev libxtst-dev
 21 |         sudo apt-get install libadwaita-1-dev libgtk-4-dev
 22 |     - name: Release Build
 23 |       run: cargo build --release
 24 |     - name: Upload build artifact
 25 |       uses: actions/upload-artifact@v4
 26 |       with:
 27 |         name: lan-mouse-linux
 28 |         path: target/release/lan-mouse
 29 | 
 30 |   windows-release-build:
 31 |     runs-on: windows-latest
 32 |     steps:
 33 |     - uses: actions/setup-python@v5
 34 |       with:
 35 |         python-version: '3.11'
 36 |     # needed for cache restore
 37 |     - name: create gtk dir
 38 |       run: mkdir C:\gtk-build\gtk\x64\release
 39 |     - uses: actions/cache@v3
 40 |       id: cache
 41 |       with:
 42 |         path: c:/gtk-build/gtk/x64/release/**
 43 |         key: gtk-windows-build
 44 |         restore-keys: gtk-windows-build
 45 |     - name: Update path
 46 |       run: |
 47 |         echo "PKG_CONFIG=C:\gtk-build\gtk\x64\release\bin\pkgconf.exe" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append
 48 |         echo "C:\pkg-config-lite-0.28-1\bin" | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append
 49 |         echo "C:\gtk-build\gtk\x64\release\bin" | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append
 50 |         echo $env:GITHUB_PATH
 51 |         echo $env:PATH
 52 |     - name: Install dependencies
 53 |       if: steps.cache.outputs.cache-hit != 'true'
 54 |       run: |
 55 |         # choco install msys2
 56 |         # choco install visualstudio2022-workload-vctools
 57 |         # choco install pkgconfiglite
 58 |         py -m venv .venv
 59 |         .venv\Scripts\activate.ps1
 60 |         py -m pip install gvsbuild
 61 |         # see https://github.com/wingtk/gvsbuild/pull/1004
 62 |         Move-Item "C:\Program Files\Git\usr\bin" "C:\Program Files\Git\usr\notbin"
 63 |         Move-Item "C:\Program Files\Git\bin" "C:\Program Files\Git\notbin"
 64 |         gvsbuild build --msys-dir=C:\msys64 gtk4 libadwaita librsvg
 65 |         Move-Item "C:\Program Files\Git\usr\notbin" "C:\Program Files\Git\usr\bin"
 66 |         Move-Item "C:\Program Files\Git\notbin" "C:\Program Files\Git\bin"
 67 |     - uses: actions/checkout@v4
 68 |     - name: Release Build
 69 |       run: cargo build --release
 70 |     - name: Create Archive
 71 |       run: |
 72 |         mkdir "lan-mouse-windows"
 73 |         Get-Childitem -Path "C:\\gtk-build\\gtk\\x64\\release\\bin\\*.dll" -File -Recurse | Copy-Item -Destination "lan-mouse-windows"
 74 |         Copy-Item -Path "target\release\lan-mouse.exe" -Destination "lan-mouse-windows"
 75 |         Compress-Archive -Path "lan-mouse-windows\*" -DestinationPath lan-mouse-windows.zip
 76 |     - name: Upload build artifact
 77 |       uses: actions/upload-artifact@v4
 78 |       with:
 79 |         name: lan-mouse-windows
 80 |         path: lan-mouse-windows.zip
 81 | 
 82 |   macos-release-build:
 83 |     runs-on: macos-13
 84 |     steps:
 85 |     - uses: actions/checkout@v4
 86 |     - name: install dependencies
 87 |       run: brew install gtk4 libadwaita imagemagick
 88 |     - name: Release Build
 89 |       run: |
 90 |         cargo build --release
 91 |         cp target/release/lan-mouse lan-mouse-macos-intel
 92 |     - name: Make icns
 93 |       run: scripts/makeicns.sh
 94 |     - name: Install cargo bundle
 95 |       run: cargo install cargo-bundle
 96 |     - name: Bundle
 97 |       run: cargo bundle --release
 98 |     - name: Zip bundle
 99 |       run: |
100 |         cd target/release/bundle/osx
101 |         zip -r "lan-mouse-macos-intel.zip" "Lan Mouse.app"
102 |     - name: Upload build artifact
103 |       uses: actions/upload-artifact@v4
104 |       with:
105 |         name: lan-mouse-macos-intel
106 |         path: target/release/bundle/osx/lan-mouse-macos-intel.zip
107 | 
108 |   macos-aarch64-release-build:
109 |     runs-on: macos-14
110 |     steps:
111 |     - uses: actions/checkout@v4
112 |     - name: install dependencies
113 |       run: brew install gtk4 libadwaita imagemagick
114 |     - name: Release Build
115 |       run: |
116 |         cargo build --release
117 |         cp target/release/lan-mouse lan-mouse-macos-aarch64
118 |     - name: Make icns
119 |       run: scripts/makeicns.sh
120 |     - name: Install cargo bundle
121 |       run: cargo install cargo-bundle
122 |     - name: Bundle
123 |       run: cargo bundle --release
124 |     - name: Zip bundle
125 |       run: |
126 |         cd target/release/bundle/osx
127 |         zip -r "lan-mouse-macos-aarch64.zip" "Lan Mouse.app"
128 |     - name: Upload build artifact
129 |       uses: actions/upload-artifact@v4
130 |       with:
131 |         name: lan-mouse-macos-aarch64
132 |         path: target/release/bundle/osx/lan-mouse-macos-aarch64.zip
133 | 
134 |   pre-release:
135 |     name: "Pre Release"
136 |     needs: [windows-release-build, linux-release-build, macos-release-build, macos-aarch64-release-build]
137 |     runs-on: "ubuntu-latest"
138 |     steps:
139 |     - name: Download build artifacts
140 |       uses: actions/download-artifact@v4
141 |     - name: Create Release
142 |       uses: "marvinpinto/action-automatic-releases@latest"
143 |       with:
144 |         repo_token: "${{ secrets.GITHUB_TOKEN }}"
145 |         automatic_release_tag: "latest"
146 |         prerelease: true
147 |         title: "Development Build"
148 |         files: |
149 |           lan-mouse-linux/lan-mouse
150 |           lan-mouse-macos-intel/lan-mouse-macos-intel.zip
151 |           lan-mouse-macos-aarch64/lan-mouse-macos-aarch64.zip
152 |           lan-mouse-windows/lan-mouse-windows.zip
153 | 


--------------------------------------------------------------------------------
/.github/workflows/rust.yml:
--------------------------------------------------------------------------------
  1 | name: Rust
  2 | 
  3 | on:
  4 |   push:
  5 |     branches: [ "main" ]
  6 |   pull_request:
  7 |     branches: [ "main" ]
  8 | 
  9 | env:
 10 |   CARGO_TERM_COLOR: always
 11 | 
 12 | jobs:
 13 |   build-linux:
 14 | 
 15 |     runs-on: ubuntu-latest
 16 | 
 17 |     steps:
 18 |     - uses: actions/checkout@v4
 19 |     - name: install dependencies
 20 |       run: |
 21 |         sudo apt-get update
 22 |         sudo apt-get install libx11-dev libxtst-dev
 23 |         sudo apt-get install libadwaita-1-dev libgtk-4-dev
 24 |     - name: Build
 25 |       run: cargo build --verbose
 26 |     - name: Run tests
 27 |       run: cargo test --verbose
 28 |     - name: Check Formatting
 29 |       run: cargo fmt --check
 30 |     - name: Clippy
 31 |       run: cargo clippy --all-features --all-targets -- --deny warnings
 32 |     - name: Upload build artifact
 33 |       uses: actions/upload-artifact@v4
 34 |       with:
 35 |         name: lan-mouse
 36 |         path: target/debug/lan-mouse
 37 | 
 38 |   build-windows:
 39 | 
 40 |     runs-on: windows-latest
 41 | 
 42 |     steps:
 43 |     - uses: actions/checkout@v4
 44 |     - uses: actions/setup-python@v5
 45 |       with:
 46 |         python-version: '3.11'
 47 |     # needed for cache restore
 48 |     - name: create gtk dir
 49 |       run: mkdir C:\gtk-build\gtk\x64\release
 50 |     - uses: actions/cache@v3
 51 |       id: cache
 52 |       with:
 53 |         path: c:/gtk-build/gtk/x64/release/**
 54 |         key: gtk-windows-build
 55 |         restore-keys: gtk-windows-build
 56 |     - name: Update path
 57 |       run: |
 58 |         echo "PKG_CONFIG=C:\gtk-build\gtk\x64\release\bin\pkgconf.exe" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append
 59 |         echo "C:\pkg-config-lite-0.28-1\bin" | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append
 60 |         echo "C:\gtk-build\gtk\x64\release\bin" | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append
 61 |         echo $env:GITHUB_PATH
 62 |         echo $env:PATH
 63 |     - name: Install dependencies
 64 |       if: steps.cache.outputs.cache-hit != 'true'
 65 |       run: |
 66 |         # choco install msys2
 67 |         # choco install visualstudio2022-workload-vctools
 68 |         # choco install pkgconfiglite
 69 |         py -m venv .venv
 70 |         .venv\Scripts\activate.ps1
 71 |         py -m pip install gvsbuild
 72 |         # see https://github.com/wingtk/gvsbuild/pull/1004
 73 |         Move-Item "C:\Program Files\Git\usr\bin" "C:\Program Files\Git\usr\notbin"
 74 |         Move-Item "C:\Program Files\Git\bin" "C:\Program Files\Git\notbin"
 75 |         gvsbuild build --msys-dir=C:\msys64 gtk4 libadwaita librsvg
 76 |         Move-Item "C:\Program Files\Git\usr\notbin" "C:\Program Files\Git\usr\bin"
 77 |         Move-Item "C:\Program Files\Git\notbin" "C:\Program Files\Git\bin"
 78 |     - name: Build
 79 |       run: cargo build --verbose
 80 |     - name: Run tests
 81 |       run: cargo test --verbose
 82 |     - name: Check Formatting
 83 |       run: cargo fmt --check
 84 |     - name: Clippy
 85 |       run: cargo clippy --all-features --all-targets -- --deny warnings
 86 |     - name: Copy Gtk Dlls
 87 |       run: Get-Childitem -Path "C:\\gtk-build\\gtk\\x64\\release\\bin\\*.dll" -File -Recurse | Copy-Item -Destination "target\\debug"
 88 |     - name: Upload build artifact
 89 |       uses: actions/upload-artifact@v4
 90 |       with:
 91 |         name: lan-mouse-windows
 92 |         path: |
 93 |           target/debug/lan-mouse.exe
 94 |           target/debug/*.dll
 95 | 
 96 |   build-macos:
 97 |     runs-on: macos-13
 98 |     steps:
 99 |     - uses: actions/checkout@v4
100 |     - name: install dependencies
101 |       run: brew install gtk4 libadwaita imagemagick
102 |     - name: Build
103 |       run: cargo build --verbose
104 |     - name: Run tests
105 |       run: cargo test --verbose
106 |     - name: Check Formatting
107 |       run: cargo fmt --check
108 |     - name: Clippy
109 |       run: cargo clippy --all-features --all-targets -- --deny warnings
110 |     - name: Make icns
111 |       run: scripts/makeicns.sh
112 |     - name: Install cargo bundle
113 |       run: cargo install cargo-bundle
114 |     - name: Bundle
115 |       run: cargo bundle
116 |     - name: Zip bundle
117 |       run: |
118 |         cd target/debug/bundle/osx
119 |         zip -r "Lan Mouse macOS (Intel).zip" "Lan Mouse.app"
120 |     - name: Upload build artifact
121 |       uses: actions/upload-artifact@v4
122 |       with:
123 |         name: Lan Mouse macOS (Intel)
124 |         path: target/debug/bundle/osx/Lan Mouse macOS (Intel).zip
125 | 
126 |   build-macos-aarch64:
127 |     runs-on: macos-14
128 |     steps:
129 |     - uses: actions/checkout@v4
130 |     - name: install dependencies
131 |       run: brew install gtk4 libadwaita imagemagick
132 |     - name: Build
133 |       run: cargo build --verbose
134 |     - name: Run tests
135 |       run: cargo test --verbose
136 |     - name: Check Formatting
137 |       run: cargo fmt --check
138 |     - name: Clippy
139 |       run: cargo clippy --all-features --all-targets -- --deny warnings
140 |     - name: Make icns
141 |       run: scripts/makeicns.sh
142 |     - name: Install cargo bundle
143 |       run: cargo install cargo-bundle
144 |     - name: Bundle
145 |       run: cargo bundle
146 |     - name: Zip bundle
147 |       run: |
148 |         cd target/debug/bundle/osx
149 |         zip -r "Lan Mouse macOS (ARM).zip" "Lan Mouse.app"
150 |     - name: Upload build artifact
151 |       uses: actions/upload-artifact@v4
152 |       with:
153 |         name: Lan Mouse macOS (ARM)
154 |         path: target/debug/bundle/osx/Lan Mouse macOS (ARM).zip
155 | 


--------------------------------------------------------------------------------
/.github/workflows/tagged-release.yml:
--------------------------------------------------------------------------------
  1 | name: "Tagged Release"
  2 | 
  3 | on:
  4 |   push:
  5 |     tags:
  6 |       - v**
  7 | 
  8 | jobs:
  9 |   linux-release-build:
 10 |     runs-on: ubuntu-latest
 11 |     steps:
 12 |     - uses: actions/checkout@v4
 13 |     - name: install dependencies
 14 |       run: |
 15 |         sudo apt-get update
 16 |         sudo apt-get install libx11-dev libxtst-dev
 17 |         sudo apt-get install libadwaita-1-dev libgtk-4-dev
 18 |     - name: Release Build
 19 |       run: cargo build --release
 20 |     - name: Upload build artifact
 21 |       uses: actions/upload-artifact@v4
 22 |       with:
 23 |         name: lan-mouse-linux
 24 |         path: target/release/lan-mouse
 25 | 
 26 |   windows-release-build:
 27 |     runs-on: windows-latest
 28 |     steps:
 29 |     - uses: actions/setup-python@v5
 30 |       with:
 31 |         python-version: '3.11'
 32 |     # needed for cache restore
 33 |     - name: create gtk dir
 34 |       run: mkdir C:\gtk-build\gtk\x64\release
 35 |     - uses: actions/cache@v3
 36 |       id: cache
 37 |       with:
 38 |         path: c:/gtk-build/gtk/x64/release/**
 39 |         key: gtk-windows-build
 40 |         restore-keys: gtk-windows-build
 41 |     - name: Update path
 42 |       run: |
 43 |         echo "PKG_CONFIG=C:\gtk-build\gtk\x64\release\bin\pkgconf.exe" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append
 44 |         echo "C:\pkg-config-lite-0.28-1\bin" | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append
 45 |         echo "C:\gtk-build\gtk\x64\release\bin" | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append
 46 |         echo $env:GITHUB_PATH
 47 |         echo $env:PATH
 48 |     - name: Install dependencies
 49 |       if: steps.cache.outputs.cache-hit != 'true'
 50 |       run: |
 51 |         # choco install msys2
 52 |         # choco install visualstudio2022-workload-vctools
 53 |         # choco install pkgconfiglite
 54 |         py -m venv .venv
 55 |         .venv\Scripts\activate.ps1
 56 |         py -m pip install gvsbuild
 57 |         # see https://github.com/wingtk/gvsbuild/pull/1004
 58 |         Move-Item "C:\Program Files\Git\usr\bin" "C:\Program Files\Git\usr\notbin"
 59 |         Move-Item "C:\Program Files\Git\bin" "C:\Program Files\Git\notbin"
 60 |         gvsbuild build --msys-dir=C:\msys64 gtk4 libadwaita librsvg
 61 |         Move-Item "C:\Program Files\Git\usr\notbin" "C:\Program Files\Git\usr\bin"
 62 |         Move-Item "C:\Program Files\Git\notbin" "C:\Program Files\Git\bin"
 63 |     - uses: actions/checkout@v4
 64 |     - name: Release Build
 65 |       run: cargo build --release
 66 |     - name: Create Archive
 67 |       run: |
 68 |         mkdir "lan-mouse-windows"
 69 |         Get-Childitem -Path "C:\\gtk-build\\gtk\\x64\\release\\bin\\*.dll" -File -Recurse | Copy-Item -Destination "lan-mouse-windows"
 70 |         Copy-Item -Path "target\release\lan-mouse.exe" -Destination "lan-mouse-windows"
 71 |         Compress-Archive -Path "lan-mouse-windows\*" -DestinationPath lan-mouse-windows.zip
 72 |     - name: Upload build artifact
 73 |       uses: actions/upload-artifact@v4
 74 |       with:
 75 |         name: lan-mouse-windows
 76 |         path: lan-mouse-windows.zip
 77 | 
 78 |   macos-release-build:
 79 |     runs-on: macos-13
 80 |     steps:
 81 |     - uses: actions/checkout@v4
 82 |     - name: install dependencies
 83 |       run: brew install gtk4 libadwaita imagemagick
 84 |     - name: Release Build
 85 |       run: |
 86 |         cargo build --release
 87 |         cp target/release/lan-mouse lan-mouse-macos-intel
 88 |     - name: Make icns
 89 |       run: scripts/makeicns.sh
 90 |     - name: Install cargo bundle
 91 |       run: cargo install cargo-bundle
 92 |     - name: Bundle
 93 |       run: cargo bundle --release
 94 |     - name: Zip bundle
 95 |       run: |
 96 |         cd target/release/bundle/osx
 97 |         zip -r "lan-mouse-macos-intel.zip" "Lan Mouse.app"
 98 |     - name: Upload build artifact
 99 |       uses: actions/upload-artifact@v4
100 |       with:
101 |         name: lan-mouse-macos-intel.zip
102 |         path: target/release/bundle/osx/lan-mouse-macos-intel.zip
103 | 
104 |   macos-aarch64-release-build:
105 |     runs-on: macos-14
106 |     steps:
107 |     - uses: actions/checkout@v4
108 |     - name: install dependencies
109 |       run: brew install gtk4 libadwaita imagemagick
110 |     - name: Release Build
111 |       run: |
112 |         cargo build --release
113 |         cp target/release/lan-mouse lan-mouse-macos-aarch64
114 |     - name: Make icns
115 |       run: scripts/makeicns.sh
116 |     - name: Install cargo bundle
117 |       run: cargo install cargo-bundle
118 |     - name: Bundle
119 |       run: cargo bundle --release
120 |     - name: Zip bundle
121 |       run: |
122 |         cd target/release/bundle/osx
123 |         zip -r "lan-mouse-macos-aarch64.zip" "Lan Mouse.app"
124 |     - name: Upload build artifact
125 |       uses: actions/upload-artifact@v4
126 |       with:
127 |         name: lan-mouse-macos-aarch64.zip
128 |         path: target/release/bundle/osx/lan-mouse-macos-aarch64.zip
129 | 
130 |   tagged-release:
131 |     name: "Tagged Release"
132 |     needs: [windows-release-build, linux-release-build, macos-release-build, macos-aarch64-release-build]
133 |     runs-on: "ubuntu-latest"
134 |     steps:
135 |     - name: Download build artifacts
136 |       uses: actions/download-artifact@v4
137 |     - name: Create Release
138 |       uses: "marvinpinto/action-automatic-releases@latest"
139 |       with:
140 |         repo_token: "${{ secrets.GITHUB_TOKEN }}"
141 |         prerelease: false
142 |         files: |
143 |           lan-mouse-linux/lan-mouse
144 |           lan-mouse-macos-intel/lan-mouse-macos-intel.zip
145 |           lan-mouse-macos-aarch64/lan-mouse-macos-aarch64.zip
146 |           lan-mouse-windows/lan-mouse-windows.zip
147 | 


--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
 1 | /target
 2 | .gdbinit
 3 | .idea/
 4 | .vs/
 5 | .vscode/
 6 | .direnv/
 7 | result
 8 | *.pem
 9 | *.csr
10 | extfile.conf
11 | 


--------------------------------------------------------------------------------
/Cargo.toml:
--------------------------------------------------------------------------------
 1 | [workspace]
 2 | members = [
 3 |     "input-capture",
 4 |     "input-emulation",
 5 |     "input-event",
 6 |     "lan-mouse-ipc",
 7 |     "lan-mouse-cli",
 8 |     "lan-mouse-gtk",
 9 |     "lan-mouse-proto",
10 | ]
11 | 
12 | [package]
13 | name = "lan-mouse"
14 | description = "Software KVM Switch / mouse & keyboard sharing software for Local Area Networks"
15 | version = "0.10.0"
16 | edition = "2021"
17 | license = "GPL-3.0-or-later"
18 | repository = "https://github.com/feschber/lan-mouse"
19 | 
20 | [profile.release]
21 | codegen-units = 1
22 | lto = "fat"
23 | strip = true
24 | panic = "abort"
25 | 
26 | [build-dependencies]
27 | shadow-rs = "0.38.0"
28 | 
29 | [dependencies]
30 | input-event = { path = "input-event", version = "0.3.0" }
31 | input-emulation = { path = "input-emulation", version = "0.3.0", default-features = false }
32 | input-capture = { path = "input-capture", version = "0.3.0", default-features = false }
33 | lan-mouse-cli = { path = "lan-mouse-cli", version = "0.2.0" }
34 | lan-mouse-gtk = { path = "lan-mouse-gtk", version = "0.2.0", optional = true }
35 | lan-mouse-ipc = { path = "lan-mouse-ipc", version = "0.2.0" }
36 | lan-mouse-proto = { path = "lan-mouse-proto", version = "0.2.0" }
37 | shadow-rs = { version = "0.38.0", features = ["metadata"] }
38 | 
39 | hickory-resolver = "0.24.1"
40 | toml = "0.8"
41 | serde = { version = "1.0", features = ["derive"] }
42 | log = "0.4.20"
43 | env_logger = "0.11.3"
44 | serde_json = "1.0.107"
45 | tokio = { version = "1.32.0", features = [
46 |     "io-util",
47 |     "io-std",
48 |     "macros",
49 |     "net",
50 |     "process",
51 |     "rt",
52 |     "sync",
53 |     "signal",
54 | ] }
55 | futures = "0.3.28"
56 | clap = { version = "4.4.11", features = ["derive"] }
57 | slab = "0.4.9"
58 | thiserror = "2.0.0"
59 | tokio-util = "0.7.11"
60 | local-channel = "0.1.5"
61 | webrtc-dtls = { version = "0.10.0", features = ["pem"] }
62 | webrtc-util = "0.9.0"
63 | rustls = { version = "0.23.12", default-features = false, features = [
64 |     "std",
65 |     "ring",
66 | ] }
67 | rcgen = "0.13.1"
68 | sha2 = "0.10.8"
69 | 
70 | [target.'cfg(unix)'.dependencies]
71 | libc = "0.2.148"
72 | 
73 | [features]
74 | default = [
75 |     "gtk",
76 |     "layer_shell_capture",
77 |     "x11_capture",
78 |     "libei_capture",
79 |     "wlroots_emulation",
80 |     "libei_emulation",
81 |     "rdp_emulation",
82 |     "x11_emulation",
83 | ]
84 | gtk = ["dep:lan-mouse-gtk"]
85 | layer_shell_capture = ["input-capture/layer_shell"]
86 | x11_capture = ["input-capture/x11"]
87 | libei_capture = ["input-event/libei", "input-capture/libei"]
88 | libei_emulation = ["input-event/libei", "input-emulation/libei"]
89 | wlroots_emulation = ["input-emulation/wlroots"]
90 | x11_emulation = ["input-emulation/x11"]
91 | rdp_emulation = ["input-emulation/remote_desktop_portal"]
92 | 
93 | [package.metadata.bundle]
94 | name = "Lan Mouse"
95 | icon = ["target/icon.icns"]
96 | identifier = "de.feschber.LanMouse"
97 | 


--------------------------------------------------------------------------------
/DOC.md:
--------------------------------------------------------------------------------
 1 | # General Software Architecture
 2 | 
 3 | ## Events
 4 | 
 5 | Each instance of lan-mouse can emit and receive events, where
 6 | an event is either a mouse or keyboard event for now.
 7 | 
 8 | The general Architecture is shown in the following flow chart:
 9 | ```mermaid
10 | graph TD
11 |     A[Wayland Backend] -->|WaylandEvent| D{Input}
12 |     B[X11 Backend] -->|X11Event| D{Input}
13 |     C[Windows Backend] -->|WindowsEvent| D{Input}
14 |     D -->|Abstract Event| E[Emitter]
15 |     E -->|Udp Event| F[Receiver]
16 |     F -->|Abstract Event| G{Dispatcher}
17 |     G -->|Wayland Event| H[Wayland Backend]
18 |     G -->|X11 Event| I[X11 Backend]
19 |     G -->|Windows Event| J[Windows Backend]
20 | ```
21 | 
22 | ### Input
23 | The input component is responsible for translating inputs from a given backend
24 | to a standardized format and passing them to the event emitter.
25 | 
26 | ### Emitter
27 | The event emitter serializes events and sends them over the network
28 | to the correct client.
29 | 
30 | ### Receiver
31 | The receiver receives events over the network and deserializes them into
32 | the standardized event format.
33 | 
34 | ### Dispatcher
35 | The dispatcher component takes events from the event receiver and passes them
36 | to the correct backend corresponding to the type of client.
37 | 
38 | 
39 | ## Requests
40 | 
41 | // TODO this currently works differently
42 | 
43 | Aside from events, requests can be sent via a simple protocol.
44 | For this, a simple tcp server is listening on the same port as the udp
45 | event receiver and accepts requests for connecting to a device or to
46 | request the keymap of a device.
47 | 
48 | ```mermaid
49 | sequenceDiagram
50 |     Alice->>+Bob: Request Connection (secret)
51 |     Bob-->>-Alice: Ack (Keyboard Layout)
52 | ```
53 | 
54 | ## Problems
55 | The general Idea is to have a bidirectional connection by default, meaning
56 | any connected device can not only receive events but also send events back.
57 | 
58 | This way when connecting e.g. a PC to a Laptop, either device can be used
59 | to control the other.
60 | 
61 | It needs to be ensured, that whenever a device is controlled the controlled
62 | device does not transmit the events back to the original sender.
63 | Otherwise events are multiplied and either one of the instances crashes.
64 | 
65 | To keep the implementation of input backends simple this needs to be handled
66 | on the server level.
67 | 
68 | ## Device State - Active and Inactive
69 | To solve this problem, each device can be in exactly two states:
70 | 
71 | Either events are sent or received.
72 | 
73 | This ensures that
74 | - a) Events can never result in a feedback loop.
75 | - b) As soon as a virtual input enters another client, lan-mouse will stop receiving events,
76 | which ensures clients can only be controlled directly and not indirectly through other clients.
77 | 
78 | 


--------------------------------------------------------------------------------
/build.rs:
--------------------------------------------------------------------------------
1 | use shadow_rs::ShadowBuilder;
2 | 
3 | fn main() {
4 |     ShadowBuilder::builder()
5 |         .deny_const(Default::default())
6 |         .build()
7 |         .expect("shadow build");
8 | }
9 | 


--------------------------------------------------------------------------------
/config.toml:
--------------------------------------------------------------------------------
 1 | # example configuration
 2 | 
 3 | # configure release bind
 4 | release_bind = [ "KeyA", "KeyS", "KeyD", "KeyF" ]
 5 | 
 6 | # optional port (defaults to 4242)
 7 | port = 4242
 8 | 
 9 | # list of authorized tls certificate fingerprints that
10 | # are accepted for incoming traffic
11 | [authorized_fingerprints]
12 | "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"
13 | 
14 | # define a client on the right side with host name "iridium"
15 | [[clients]]
16 | # position (left | right | top | bottom)
17 | position = "right"
18 | # hostname
19 | hostname = "iridium"
20 | # activate this client immediately when lan-mouse is started
21 | activate_on_startup = true
22 | # optional list of (known) ip addresses
23 | ips = ["192.168.178.156"]
24 | 
25 | # define a client on the left side with IP address 192.168.178.189
26 | [[clients]]
27 | position = "left"
28 | # The hostname is optional: When no hostname is specified,
29 | # at least one ip address needs to be specified.
30 | hostname = "thorium"
31 | # ips for ethernet and wifi
32 | ips = ["192.168.178.189", "192.168.178.172"]
33 | # optional port
34 | port = 4242
35 | 


--------------------------------------------------------------------------------
/de.feschber.LanMouse.desktop:
--------------------------------------------------------------------------------
 1 | [Desktop Entry]
 2 | Categories=Utility;
 3 | Comment[en_US]=Mouse & Keyboard sharing via LAN
 4 | Comment=Mouse & Keyboard sharing via LAN
 5 | Comment[de_DE]=Maus- und Tastaturfreigabe über LAN
 6 | Exec=lan-mouse
 7 | Icon=de.feschber.LanMouse
 8 | Name[en_US]=Lan Mouse
 9 | Name=Lan Mouse
10 | StartupNotify=true
11 | Terminal=false
12 | Type=Application
13 | 


--------------------------------------------------------------------------------
/de.feschber.LanMouse.yml:
--------------------------------------------------------------------------------
 1 | app-id: de.feschber.LanMouse
 2 | runtime: org.freedesktop.Platform
 3 | runtime-version: '22.08'
 4 | sdk: org.freedesktop.Sdk
 5 | command: target/release/lan-mouse
 6 | modules:
 7 |   - name: hello
 8 |     buildsystem: simple
 9 |     build-commands:
10 |       - cargo build --release
11 |       - install -D lan-mouse /app/bin/lan-mouse
12 |     sources:
13 |       - type: file
14 |         path: target/release/lan-mouse
15 | 


--------------------------------------------------------------------------------
/default.nix:
--------------------------------------------------------------------------------
1 | { pkgs ? import <nixpkgs> { }
2 | }:
3 | pkgs.callPackage nix/default.nix { }
4 | 


--------------------------------------------------------------------------------
/firewall/lan-mouse.xml:
--------------------------------------------------------------------------------
1 | <?xml version="1.0" encoding="utf-8"?>
2 | <!-- for packaging: /usr/lib/firewalld/services/lan-mouse.xml -->
3 | <!-- configure manually: /etc/firewalld/services/lan-mouse.xml -->
4 | <service>
5 |   <short>LAN Mouse</short>
6 |   <description>mouse and keyboard sharing via LAN</description>
7 |   <port port="4242" protocol="udp"/>
8 | </service>
9 | 


--------------------------------------------------------------------------------
/flake.lock:
--------------------------------------------------------------------------------
 1 | {
 2 |   "nodes": {
 3 |     "nixpkgs": {
 4 |       "locked": {
 5 |         "lastModified": 1740560979,
 6 |         "narHash": "sha256-Vr3Qi346M+8CjedtbyUevIGDZW8LcA1fTG0ugPY/Hic=",
 7 |         "owner": "nixos",
 8 |         "repo": "nixpkgs",
 9 |         "rev": "5135c59491985879812717f4c9fea69604e7f26f",
10 |         "type": "github"
11 |       },
12 |       "original": {
13 |         "owner": "nixos",
14 |         "ref": "nixos-unstable",
15 |         "repo": "nixpkgs",
16 |         "type": "github"
17 |       }
18 |     },
19 |     "root": {
20 |       "inputs": {
21 |         "nixpkgs": "nixpkgs",
22 |         "rust-overlay": "rust-overlay"
23 |       }
24 |     },
25 |     "rust-overlay": {
26 |       "inputs": {
27 |         "nixpkgs": [
28 |           "nixpkgs"
29 |         ]
30 |       },
31 |       "locked": {
32 |         "lastModified": 1740623427,
33 |         "narHash": "sha256-3SdPQrZoa4odlScFDUHd4CUPQ/R1gtH4Mq9u8CBiK8M=",
34 |         "owner": "oxalica",
35 |         "repo": "rust-overlay",
36 |         "rev": "d342e8b5fd88421ff982f383c853f0fc78a847ab",
37 |         "type": "github"
38 |       },
39 |       "original": {
40 |         "owner": "oxalica",
41 |         "repo": "rust-overlay",
42 |         "type": "github"
43 |       }
44 |     }
45 |   },
46 |   "root": "root",
47 |   "version": 7
48 | }
49 | 


--------------------------------------------------------------------------------
/flake.nix:
--------------------------------------------------------------------------------
 1 | {
 2 |   description = "Nix Flake for lan-mouse";
 3 |   inputs = {
 4 |     nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable";
 5 |     rust-overlay = {
 6 |       url = "github:oxalica/rust-overlay";
 7 |       inputs.nixpkgs.follows = "nixpkgs";
 8 |     };
 9 |   };
10 |   outputs = {
11 |     self,
12 |     nixpkgs,
13 |     rust-overlay,
14 |     ...
15 |   }: let
16 |     inherit (nixpkgs) lib;
17 |     genSystems = lib.genAttrs [
18 |       "aarch64-darwin"
19 |       "aarch64-linux"
20 |       "x86_64-darwin"
21 |       "x86_64-linux"
22 |     ];
23 |     pkgsFor = system:
24 |       import nixpkgs {
25 |         inherit system;
26 | 
27 |         overlays = [
28 |           rust-overlay.overlays.default
29 |         ];
30 |       };
31 |     mkRustToolchain = pkgs:
32 |       pkgs.rust-bin.stable.latest.default.override {
33 |         extensions = ["rust-src"];
34 |       };
35 |     pkgs = genSystems (system: import nixpkgs {inherit system;});
36 |   in {
37 |     packages = genSystems (system: rec {
38 |       default = pkgs.${system}.callPackage ./nix {};
39 |       lan-mouse = default;
40 |     });
41 |     homeManagerModules.default = import ./nix/hm-module.nix self;
42 |     devShells = genSystems (system: let
43 |       pkgs = pkgsFor system;
44 |       rust = mkRustToolchain pkgs;
45 |     in {
46 |       default = pkgs.mkShell {
47 |         packages = with pkgs; [
48 |           rust
49 |           rust-analyzer-unwrapped
50 |           pkg-config
51 |           xorg.libX11
52 |           gtk4
53 |           libadwaita
54 |           librsvg
55 |           xorg.libXtst
56 |         ] ++ lib.optionals stdenv.isDarwin
57 |         (with darwin.apple_sdk_11_0.frameworks; [
58 |           CoreGraphics
59 |           ApplicationServices
60 |         ]);
61 | 
62 |         RUST_SRC_PATH = "${rust}/lib/rustlib/src/rust/library";
63 |       };
64 |     });
65 |   };
66 | }
67 | 


--------------------------------------------------------------------------------
/input-capture/Cargo.toml:
--------------------------------------------------------------------------------
 1 | [package]
 2 | name = "input-capture"
 3 | description = "cross-platform input-capture library used by lan-mouse"
 4 | version = "0.3.0"
 5 | edition = "2021"
 6 | license = "GPL-3.0-or-later"
 7 | repository = "https://github.com/feschber/lan-mouse"
 8 | 
 9 | [dependencies]
10 | futures = "0.3.28"
11 | futures-core = "0.3.30"
12 | log = "0.4.22"
13 | input-event = { path = "../input-event", version = "0.3.0" }
14 | memmap = "0.7"
15 | tempfile = "3.8"
16 | thiserror = "2.0.0"
17 | tokio = { version = "1.32.0", features = [
18 |     "io-util",
19 |     "io-std",
20 |     "macros",
21 |     "net",
22 |     "process",
23 |     "rt",
24 |     "sync",
25 |     "signal",
26 | ] }
27 | once_cell = "1.19.0"
28 | async-trait = "0.1.81"
29 | tokio-util = "0.7.11"
30 | 
31 | 
32 | [target.'cfg(all(unix, not(target_os="macos")))'.dependencies]
33 | wayland-client = { version = "0.31.1", optional = true }
34 | wayland-protocols = { version = "0.32.1", features = [
35 |     "client",
36 |     "staging",
37 |     "unstable",
38 | ], optional = true }
39 | wayland-protocols-wlr = { version = "0.3.1", features = [
40 |     "client",
41 | ], optional = true }
42 | x11 = { version = "2.21.0", features = ["xlib", "xtest"], optional = true }
43 | ashpd = { version = "0.10", default-features = false, features = [
44 |     "tokio",
45 | ], optional = true }
46 | reis = { version = "0.4", features = ["tokio"], optional = true }
47 | 
48 | [target.'cfg(target_os="macos")'.dependencies]
49 | core-graphics = { version = "0.24.0", features = ["highsierra"] }
50 | core-foundation = "0.10.0"
51 | core-foundation-sys = "0.8.6"
52 | libc = "0.2.155"
53 | keycode = "0.4.0"
54 | bitflags = "2.6.0"
55 | 
56 | [target.'cfg(windows)'.dependencies]
57 | windows = { version = "0.58.0", features = [
58 |     "Win32_System_LibraryLoader",
59 |     "Win32_System_Threading",
60 |     "Win32_Foundation",
61 |     "Win32_Graphics",
62 |     "Win32_Graphics_Gdi",
63 |     "Win32_UI_Input_KeyboardAndMouse",
64 |     "Win32_UI_WindowsAndMessaging",
65 | ] }
66 | 
67 | [features]
68 | default = ["layer_shell", "x11", "libei"]
69 | layer_shell = [
70 |     "dep:wayland-client",
71 |     "dep:wayland-protocols",
72 |     "dep:wayland-protocols-wlr",
73 | ]
74 | x11 = ["dep:x11"]
75 | libei = ["dep:reis", "dep:ashpd"]
76 | 


--------------------------------------------------------------------------------
/input-capture/src/dummy.rs:
--------------------------------------------------------------------------------
 1 | use std::f64::consts::PI;
 2 | use std::pin::Pin;
 3 | use std::task::{ready, Context, Poll};
 4 | use std::time::Duration;
 5 | 
 6 | use async_trait::async_trait;
 7 | use futures_core::Stream;
 8 | use input_event::PointerEvent;
 9 | use tokio::time::{self, Instant, Interval};
10 | 
11 | use super::{Capture, CaptureError, CaptureEvent, Position};
12 | 
13 | pub struct DummyInputCapture {
14 |     start: Option<Instant>,
15 |     interval: Interval,
16 |     offset: (i32, i32),
17 | }
18 | 
19 | impl DummyInputCapture {
20 |     pub fn new() -> Self {
21 |         Self {
22 |             start: None,
23 |             interval: time::interval(Duration::from_millis(1)),
24 |             offset: (0, 0),
25 |         }
26 |     }
27 | }
28 | 
29 | impl Default for DummyInputCapture {
30 |     fn default() -> Self {
31 |         Self::new()
32 |     }
33 | }
34 | 
35 | #[async_trait]
36 | impl Capture for DummyInputCapture {
37 |     async fn create(&mut self, _pos: Position) -> Result<(), CaptureError> {
38 |         Ok(())
39 |     }
40 | 
41 |     async fn destroy(&mut self, _pos: Position) -> Result<(), CaptureError> {
42 |         Ok(())
43 |     }
44 | 
45 |     async fn release(&mut self) -> Result<(), CaptureError> {
46 |         Ok(())
47 |     }
48 | 
49 |     async fn terminate(&mut self) -> Result<(), CaptureError> {
50 |         Ok(())
51 |     }
52 | }
53 | 
54 | const FREQUENCY_HZ: f64 = 1.0;
55 | const RADIUS: f64 = 100.0;
56 | 
57 | impl Stream for DummyInputCapture {
58 |     type Item = Result<(Position, CaptureEvent), CaptureError>;
59 | 
60 |     fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
61 |         let current = ready!(self.interval.poll_tick(cx));
62 |         let event = match self.start {
63 |             None => {
64 |                 self.start.replace(current);
65 |                 CaptureEvent::Begin
66 |             }
67 |             Some(start) => {
68 |                 let elapsed = start.elapsed();
69 |                 let elapsed_sec_f64 = elapsed.as_secs_f64();
70 |                 let second_fraction = elapsed_sec_f64 - elapsed_sec_f64 as u64 as f64;
71 |                 let radians = second_fraction * 2. * PI * FREQUENCY_HZ;
72 |                 let offset = (radians.cos() * RADIUS * 2., (radians * 2.).sin() * RADIUS);
73 |                 let offset = (offset.0 as i32, offset.1 as i32);
74 |                 let relative_motion = (offset.0 - self.offset.0, offset.1 - self.offset.1);
75 |                 self.offset = offset;
76 |                 let (dx, dy) = (relative_motion.0 as f64, relative_motion.1 as f64);
77 |                 CaptureEvent::Input(input_event::Event::Pointer(PointerEvent::Motion {
78 |                     time: 0,
79 |                     dx,
80 |                     dy,
81 |                 }))
82 |             }
83 |         };
84 |         Poll::Ready(Some(Ok((Position::Left, event))))
85 |     }
86 | }
87 | 


--------------------------------------------------------------------------------
/input-capture/src/error.rs:
--------------------------------------------------------------------------------
  1 | use thiserror::Error;
  2 | 
  3 | #[derive(Debug, Error)]
  4 | pub enum InputCaptureError {
  5 |     #[error("error creating input-capture: `{0}`")]
  6 |     Create(#[from] CaptureCreationError),
  7 |     #[error("error while capturing input: `{0}`")]
  8 |     Capture(#[from] CaptureError),
  9 | }
 10 | 
 11 | #[cfg(all(unix, feature = "layer_shell", not(target_os = "macos")))]
 12 | use std::io;
 13 | #[cfg(all(unix, feature = "layer_shell", not(target_os = "macos")))]
 14 | use wayland_client::{
 15 |     backend::WaylandError,
 16 |     globals::{BindError, GlobalError},
 17 |     ConnectError, DispatchError,
 18 | };
 19 | 
 20 | #[cfg(all(unix, feature = "libei", not(target_os = "macos")))]
 21 | use ashpd::desktop::ResponseError;
 22 | 
 23 | #[cfg(target_os = "macos")]
 24 | use core_graphics::base::CGError;
 25 | 
 26 | #[derive(Debug, Error)]
 27 | pub enum CaptureError {
 28 |     #[error("activation stream closed unexpectedly")]
 29 |     ActivationClosed,
 30 |     #[error("libei stream was closed")]
 31 |     EndOfStream,
 32 |     #[error("io error: `{0}`")]
 33 |     Io(#[from] std::io::Error),
 34 |     #[cfg(all(unix, feature = "libei", not(target_os = "macos")))]
 35 |     #[error("libei error: `{0}`")]
 36 |     Reis(#[from] reis::Error),
 37 |     #[cfg(all(unix, feature = "libei", not(target_os = "macos")))]
 38 |     #[error(transparent)]
 39 |     Portal(#[from] ashpd::Error),
 40 |     #[cfg(all(unix, feature = "libei", not(target_os = "macos")))]
 41 |     #[error("libei disconnected - reason: `{0}`")]
 42 |     Disconnected(String),
 43 |     #[cfg(target_os = "macos")]
 44 |     #[error("failed to warp mouse cursor: `{0}`")]
 45 |     WarpCursor(CGError),
 46 |     #[cfg(target_os = "macos")]
 47 |     #[error("reset_mouse_position called without a connected client")]
 48 |     ResetMouseWithoutClient,
 49 |     #[cfg(target_os = "macos")]
 50 |     #[error("core-graphics error: {0}")]
 51 |     CoreGraphics(CGError),
 52 |     #[cfg(target_os = "macos")]
 53 |     #[error("unable to map key event: {0}")]
 54 |     KeyMapError(i64),
 55 |     #[cfg(target_os = "macos")]
 56 |     #[error("Event tap disabled")]
 57 |     EventTapDisabled,
 58 | }
 59 | 
 60 | #[derive(Debug, Error)]
 61 | pub enum CaptureCreationError {
 62 |     #[error("no backend available")]
 63 |     NoAvailableBackend,
 64 |     #[cfg(all(unix, feature = "libei", not(target_os = "macos")))]
 65 |     #[error("error creating input-capture-portal backend: `{0}`")]
 66 |     Libei(#[from] LibeiCaptureCreationError),
 67 |     #[cfg(all(unix, feature = "layer_shell", not(target_os = "macos")))]
 68 |     #[error("error creating layer-shell capture backend: `{0}`")]
 69 |     LayerShell(#[from] LayerShellCaptureCreationError),
 70 |     #[cfg(all(unix, feature = "x11", not(target_os = "macos")))]
 71 |     #[error("error creating x11 capture backend: `{0}`")]
 72 |     X11(#[from] X11InputCaptureCreationError),
 73 |     #[cfg(windows)]
 74 |     #[error("error creating windows capture backend")]
 75 |     Windows,
 76 |     #[cfg(target_os = "macos")]
 77 |     #[error("error creating macos capture backend: `{0}`")]
 78 |     MacOS(#[from] MacosCaptureCreationError),
 79 | }
 80 | 
 81 | impl CaptureCreationError {
 82 |     /// request was intentionally denied by the user
 83 |     #[cfg(all(unix, feature = "libei", not(target_os = "macos")))]
 84 |     pub(crate) fn cancelled_by_user(&self) -> bool {
 85 |         matches!(
 86 |             self,
 87 |             CaptureCreationError::Libei(LibeiCaptureCreationError::Ashpd(ashpd::Error::Response(
 88 |                 ResponseError::Cancelled
 89 |             )))
 90 |         )
 91 |     }
 92 |     #[cfg(not(all(unix, feature = "libei", not(target_os = "macos"))))]
 93 |     pub(crate) fn cancelled_by_user(&self) -> bool {
 94 |         false
 95 |     }
 96 | }
 97 | 
 98 | #[cfg(all(unix, feature = "libei", not(target_os = "macos")))]
 99 | #[derive(Debug, Error)]
100 | pub enum LibeiCaptureCreationError {
101 |     #[error("xdg-desktop-portal: `{0}`")]
102 |     Ashpd(#[from] ashpd::Error),
103 | }
104 | 
105 | #[cfg(all(unix, feature = "layer_shell", not(target_os = "macos")))]
106 | #[derive(Debug, Error)]
107 | #[error("{protocol} protocol not supported: {inner}")]
108 | pub struct WaylandBindError {
109 |     inner: BindError,
110 |     protocol: &'static str,
111 | }
112 | 
113 | #[cfg(all(unix, feature = "layer_shell", not(target_os = "macos")))]
114 | impl WaylandBindError {
115 |     pub(crate) fn new(inner: BindError, protocol: &'static str) -> Self {
116 |         Self { inner, protocol }
117 |     }
118 | }
119 | 
120 | #[cfg(all(unix, feature = "layer_shell", not(target_os = "macos")))]
121 | #[derive(Debug, Error)]
122 | pub enum LayerShellCaptureCreationError {
123 |     #[error(transparent)]
124 |     Connect(#[from] ConnectError),
125 |     #[error(transparent)]
126 |     Global(#[from] GlobalError),
127 |     #[error(transparent)]
128 |     Wayland(#[from] WaylandError),
129 |     #[error(transparent)]
130 |     Bind(#[from] WaylandBindError),
131 |     #[error(transparent)]
132 |     Dispatch(#[from] DispatchError),
133 |     #[error(transparent)]
134 |     Io(#[from] io::Error),
135 | }
136 | 
137 | #[cfg(all(unix, feature = "x11", not(target_os = "macos")))]
138 | #[derive(Debug, Error)]
139 | pub enum X11InputCaptureCreationError {
140 |     #[error("X11 input capture is not yet implemented :(")]
141 |     NotImplemented,
142 | }
143 | 
144 | #[cfg(target_os = "macos")]
145 | #[derive(Debug, Error)]
146 | pub enum MacosCaptureCreationError {
147 |     #[error("event source creation failed!")]
148 |     EventSourceCreation,
149 |     #[cfg(target_os = "macos")]
150 |     #[error("event tap creation failed")]
151 |     EventTapCreation,
152 |     #[error("failed to set CG Cursor property")]
153 |     CGCursorProperty,
154 |     #[cfg(target_os = "macos")]
155 |     #[error("failed to get display ids: {0}")]
156 |     ActiveDisplays(CGError),
157 | }
158 | 


--------------------------------------------------------------------------------
/input-capture/src/windows.rs:
--------------------------------------------------------------------------------
 1 | use async_trait::async_trait;
 2 | use core::task::{Context, Poll};
 3 | use event_thread::EventThread;
 4 | use futures::Stream;
 5 | use std::pin::Pin;
 6 | 
 7 | use std::task::ready;
 8 | use tokio::sync::mpsc::{channel, Receiver};
 9 | 
10 | use super::{Capture, CaptureError, CaptureEvent, Position};
11 | 
12 | mod display_util;
13 | mod event_thread;
14 | 
15 | pub struct WindowsInputCapture {
16 |     event_rx: Receiver<(Position, CaptureEvent)>,
17 |     event_thread: EventThread,
18 | }
19 | 
20 | #[async_trait]
21 | impl Capture for WindowsInputCapture {
22 |     async fn create(&mut self, pos: Position) -> Result<(), CaptureError> {
23 |         self.event_thread.create(pos);
24 |         Ok(())
25 |     }
26 | 
27 |     async fn destroy(&mut self, pos: Position) -> Result<(), CaptureError> {
28 |         self.event_thread.destroy(pos);
29 |         Ok(())
30 |     }
31 | 
32 |     async fn release(&mut self) -> Result<(), CaptureError> {
33 |         self.event_thread.release_capture();
34 |         Ok(())
35 |     }
36 | 
37 |     async fn terminate(&mut self) -> Result<(), CaptureError> {
38 |         Ok(())
39 |     }
40 | }
41 | 
42 | impl WindowsInputCapture {
43 |     pub(crate) fn new() -> Self {
44 |         let (event_tx, event_rx) = channel(10);
45 |         let event_thread = EventThread::new(event_tx);
46 |         Self {
47 |             event_thread,
48 |             event_rx,
49 |         }
50 |     }
51 | }
52 | 
53 | impl Stream for WindowsInputCapture {
54 |     type Item = Result<(Position, CaptureEvent), CaptureError>;
55 |     fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
56 |         match ready!(self.event_rx.poll_recv(cx)) {
57 |             None => Poll::Ready(None),
58 |             Some(e) => Poll::Ready(Some(Ok(e))),
59 |         }
60 |     }
61 | }
62 | 


--------------------------------------------------------------------------------
/input-capture/src/windows/display_util.rs:
--------------------------------------------------------------------------------
  1 | use windows::Win32::Foundation::RECT;
  2 | 
  3 | use crate::Position;
  4 | 
  5 | fn is_within_dp_region(point: (i32, i32), display: &RECT) -> bool {
  6 |     [
  7 |         Position::Left,
  8 |         Position::Right,
  9 |         Position::Top,
 10 |         Position::Bottom,
 11 |     ]
 12 |     .iter()
 13 |     .all(|&pos| is_within_dp_boundary(point, display, pos))
 14 | }
 15 | 
 16 | fn is_within_dp_boundary(point: (i32, i32), display: &RECT, pos: Position) -> bool {
 17 |     let (x, y) = point;
 18 |     match pos {
 19 |         Position::Left => display.left <= x,
 20 |         Position::Right => display.right > x,
 21 |         Position::Top => display.top <= y,
 22 |         Position::Bottom => display.bottom > y,
 23 |     }
 24 | }
 25 | 
 26 | /// returns whether the given position is within the display bounds with respect to the given
 27 | /// barrier position
 28 | ///
 29 | /// # Arguments
 30 | ///
 31 | /// * `x`:
 32 | /// * `y`:
 33 | /// * `displays`:
 34 | /// * `pos`:
 35 | ///
 36 | /// returns: bool
 37 | ///
 38 | fn in_bounds(point: (i32, i32), displays: &[RECT], pos: Position) -> bool {
 39 |     displays
 40 |         .iter()
 41 |         .any(|d| is_within_dp_boundary(point, d, pos))
 42 | }
 43 | 
 44 | fn in_display_region(point: (i32, i32), displays: &[RECT]) -> bool {
 45 |     displays.iter().any(|d| is_within_dp_region(point, d))
 46 | }
 47 | 
 48 | fn moved_across_boundary(
 49 |     prev_pos: (i32, i32),
 50 |     curr_pos: (i32, i32),
 51 |     displays: &[RECT],
 52 |     pos: Position,
 53 | ) -> bool {
 54 |     /* was within bounds, but is not anymore */
 55 |     in_display_region(prev_pos, displays) && !in_bounds(curr_pos, displays, pos)
 56 | }
 57 | 
 58 | pub(crate) fn entered_barrier(
 59 |     prev_pos: (i32, i32),
 60 |     curr_pos: (i32, i32),
 61 |     displays: &[RECT],
 62 | ) -> Option<Position> {
 63 |     [
 64 |         Position::Left,
 65 |         Position::Right,
 66 |         Position::Top,
 67 |         Position::Bottom,
 68 |     ]
 69 |     .into_iter()
 70 |     .find(|&pos| moved_across_boundary(prev_pos, curr_pos, displays, pos))
 71 | }
 72 | 
 73 | ///
 74 | /// clamp point to display bounds
 75 | ///
 76 | /// # Arguments
 77 | ///
 78 | /// * `prev_point`: coordinates, the cursor was before entering, within bounds of a display
 79 | /// * `entry_point`: point to clamp
 80 | ///
 81 | /// returns: (i32, i32), the corrected entry point
 82 | ///
 83 | pub(crate) fn clamp_to_display_bounds(
 84 |     display_regions: &[RECT],
 85 |     prev_point: (i32, i32),
 86 |     point: (i32, i32),
 87 | ) -> (i32, i32) {
 88 |     /* find display where movement came from */
 89 |     let display = display_regions
 90 |         .iter()
 91 |         .find(|&d| is_within_dp_region(prev_point, d))
 92 |         .unwrap();
 93 | 
 94 |     /* clamp to bounds (inclusive) */
 95 |     let (x, y) = point;
 96 |     let (min_x, max_x) = (display.left, display.right - 1);
 97 |     let (min_y, max_y) = (display.top, display.bottom - 1);
 98 |     (x.clamp(min_x, max_x), y.clamp(min_y, max_y))
 99 | }
100 | 


--------------------------------------------------------------------------------
/input-capture/src/x11.rs:
--------------------------------------------------------------------------------
 1 | use std::task::Poll;
 2 | 
 3 | use async_trait::async_trait;
 4 | use futures_core::Stream;
 5 | 
 6 | use super::{error::X11InputCaptureCreationError, Capture, CaptureError, CaptureEvent, Position};
 7 | 
 8 | pub struct X11InputCapture {}
 9 | 
10 | impl X11InputCapture {
11 |     pub fn new() -> std::result::Result<Self, X11InputCaptureCreationError> {
12 |         Err(X11InputCaptureCreationError::NotImplemented)
13 |     }
14 | }
15 | 
16 | #[async_trait]
17 | impl Capture for X11InputCapture {
18 |     async fn create(&mut self, _pos: Position) -> Result<(), CaptureError> {
19 |         Ok(())
20 |     }
21 | 
22 |     async fn destroy(&mut self, _pos: Position) -> Result<(), CaptureError> {
23 |         Ok(())
24 |     }
25 | 
26 |     async fn release(&mut self) -> Result<(), CaptureError> {
27 |         Ok(())
28 |     }
29 | 
30 |     async fn terminate(&mut self) -> Result<(), CaptureError> {
31 |         Ok(())
32 |     }
33 | }
34 | 
35 | impl Stream for X11InputCapture {
36 |     type Item = Result<(Position, CaptureEvent), CaptureError>;
37 | 
38 |     fn poll_next(
39 |         self: std::pin::Pin<&mut Self>,
40 |         _cx: &mut std::task::Context<'_>,
41 |     ) -> std::task::Poll<Option<Self::Item>> {
42 |         Poll::Pending
43 |     }
44 | }
45 | 


--------------------------------------------------------------------------------
/input-emulation/Cargo.toml:
--------------------------------------------------------------------------------
 1 | [package]
 2 | name = "input-emulation"
 3 | description = "cross-platform input emulation library used by lan-mouse"
 4 | version = "0.3.0"
 5 | edition = "2021"
 6 | license = "GPL-3.0-or-later"
 7 | repository = "https://github.com/feschber/lan-mouse"
 8 | 
 9 | [dependencies]
10 | async-trait = "0.1.80"
11 | futures = "0.3.28"
12 | log = "0.4.22"
13 | input-event = { path = "../input-event", version = "0.3.0" }
14 | thiserror = "2.0.0"
15 | tokio = { version = "1.32.0", features = [
16 |     "io-util",
17 |     "io-std",
18 |     "macros",
19 |     "net",
20 |     "process",
21 |     "rt",
22 |     "sync",
23 |     "signal",
24 | ] }
25 | once_cell = "1.19.0"
26 | 
27 | [target.'cfg(all(unix, not(target_os="macos")))'.dependencies]
28 | bitflags = "2.6.0"
29 | wayland-client = { version = "0.31.1", optional = true }
30 | wayland-protocols = { version = "0.32.1", features = [
31 |     "client",
32 |     "staging",
33 |     "unstable",
34 | ], optional = true }
35 | wayland-protocols-wlr = { version = "0.3.1", features = [
36 |     "client",
37 | ], optional = true }
38 | wayland-protocols-misc = { version = "0.3.1", features = [
39 |     "client",
40 | ], optional = true }
41 | x11 = { version = "2.21.0", features = ["xlib", "xtest"], optional = true }
42 | ashpd = { version = "0.10", default-features = false, features = [
43 |     "tokio",
44 | ], optional = true }
45 | reis = { version = "0.4", features = ["tokio"], optional = true }
46 | 
47 | [target.'cfg(target_os="macos")'.dependencies]
48 | bitflags = "2.6.0"
49 | core-graphics = { version = "0.24.0", features = ["highsierra"] }
50 | keycode = "0.4.0"
51 | 
52 | [target.'cfg(windows)'.dependencies]
53 | windows = { version = "0.58.0", features = [
54 |     "Win32_System_LibraryLoader",
55 |     "Win32_System_Threading",
56 |     "Win32_Foundation",
57 |     "Win32_Graphics",
58 |     "Win32_Graphics_Gdi",
59 |     "Win32_UI_Input_KeyboardAndMouse",
60 |     "Win32_UI_WindowsAndMessaging",
61 | ] }
62 | 
63 | [features]
64 | default = ["wlroots", "x11", "remote_desktop_portal", "libei"]
65 | wlroots = [
66 |     "dep:wayland-client",
67 |     "dep:wayland-protocols",
68 |     "dep:wayland-protocols-wlr",
69 |     "dep:wayland-protocols-misc",
70 | ]
71 | x11 = ["dep:x11"]
72 | remote_desktop_portal = ["dep:ashpd"]
73 | libei = ["dep:reis", "dep:ashpd"]
74 | 


--------------------------------------------------------------------------------
/input-emulation/src/dummy.rs:
--------------------------------------------------------------------------------
 1 | use async_trait::async_trait;
 2 | use input_event::Event;
 3 | 
 4 | use crate::error::EmulationError;
 5 | 
 6 | use super::{Emulation, EmulationHandle};
 7 | 
 8 | #[derive(Default)]
 9 | pub(crate) struct DummyEmulation;
10 | 
11 | impl DummyEmulation {
12 |     pub(crate) fn new() -> Self {
13 |         Self {}
14 |     }
15 | }
16 | 
17 | #[async_trait]
18 | impl Emulation for DummyEmulation {
19 |     async fn consume(
20 |         &mut self,
21 |         event: Event,
22 |         client_handle: EmulationHandle,
23 |     ) -> Result<(), EmulationError> {
24 |         log::info!("received event: ({client_handle}) {event}");
25 |         Ok(())
26 |     }
27 |     async fn create(&mut self, _: EmulationHandle) {}
28 |     async fn destroy(&mut self, _: EmulationHandle) {}
29 |     async fn terminate(&mut self) {
30 |         /* nothing to do */
31 |     }
32 | }
33 | 


--------------------------------------------------------------------------------
/input-emulation/src/error.rs:
--------------------------------------------------------------------------------
  1 | #[derive(Debug, Error)]
  2 | pub enum InputEmulationError {
  3 |     #[error("error creating input-emulation: `{0}`")]
  4 |     Create(#[from] EmulationCreationError),
  5 |     #[error("error emulating input: `{0}`")]
  6 |     Emulate(#[from] EmulationError),
  7 | }
  8 | 
  9 | #[cfg(all(
 10 |     unix,
 11 |     any(feature = "remote_desktop_portal", feature = "libei"),
 12 |     not(target_os = "macos")
 13 | ))]
 14 | use ashpd::{desktop::ResponseError, Error::Response};
 15 | use std::io;
 16 | use thiserror::Error;
 17 | 
 18 | #[cfg(all(unix, feature = "wlroots", not(target_os = "macos")))]
 19 | use wayland_client::{
 20 |     backend::WaylandError,
 21 |     globals::{BindError, GlobalError},
 22 |     ConnectError, DispatchError,
 23 | };
 24 | 
 25 | #[derive(Debug, Error)]
 26 | pub enum EmulationError {
 27 |     #[error("event stream closed")]
 28 |     EndOfStream,
 29 |     #[cfg(all(unix, feature = "libei", not(target_os = "macos")))]
 30 |     #[error("libei error: `{0}`")]
 31 |     Libei(#[from] reis::Error),
 32 |     #[cfg(all(unix, feature = "wlroots", not(target_os = "macos")))]
 33 |     #[error("wayland error: `{0}`")]
 34 |     Wayland(#[from] wayland_client::backend::WaylandError),
 35 |     #[cfg(all(
 36 |         unix,
 37 |         any(feature = "remote_desktop_portal", feature = "libei"),
 38 |         not(target_os = "macos")
 39 |     ))]
 40 |     #[error("xdg-desktop-portal: `{0}`")]
 41 |     Ashpd(#[from] ashpd::Error),
 42 |     #[error("io error: `{0}`")]
 43 |     Io(#[from] io::Error),
 44 | }
 45 | 
 46 | #[derive(Debug, Error)]
 47 | pub enum EmulationCreationError {
 48 |     #[cfg(all(unix, feature = "wlroots", not(target_os = "macos")))]
 49 |     #[error("wlroots backend: `{0}`")]
 50 |     Wlroots(#[from] WlrootsEmulationCreationError),
 51 |     #[cfg(all(unix, feature = "libei", not(target_os = "macos")))]
 52 |     #[error("libei backend: `{0}`")]
 53 |     Libei(#[from] LibeiEmulationCreationError),
 54 |     #[cfg(all(unix, feature = "remote_desktop_portal", not(target_os = "macos")))]
 55 |     #[error("xdg-desktop-portal: `{0}`")]
 56 |     Xdp(#[from] XdpEmulationCreationError),
 57 |     #[cfg(all(unix, feature = "x11", not(target_os = "macos")))]
 58 |     #[error("x11: `{0}`")]
 59 |     X11(#[from] X11EmulationCreationError),
 60 |     #[cfg(target_os = "macos")]
 61 |     #[error("macos: `{0}`")]
 62 |     MacOs(#[from] MacOSEmulationCreationError),
 63 |     #[cfg(windows)]
 64 |     #[error("windows: `{0}`")]
 65 |     Windows(#[from] WindowsEmulationCreationError),
 66 |     #[error("capture error")]
 67 |     NoAvailableBackend,
 68 | }
 69 | 
 70 | impl EmulationCreationError {
 71 |     /// request was intentionally denied by the user
 72 |     pub(crate) fn cancelled_by_user(&self) -> bool {
 73 |         #[cfg(all(unix, feature = "libei", not(target_os = "macos")))]
 74 |         if matches!(
 75 |             self,
 76 |             EmulationCreationError::Libei(LibeiEmulationCreationError::Ashpd(Response(
 77 |                 ResponseError::Cancelled,
 78 |             )))
 79 |         ) {
 80 |             return true;
 81 |         }
 82 |         #[cfg(all(unix, feature = "remote_desktop_portal", not(target_os = "macos")))]
 83 |         if matches!(
 84 |             self,
 85 |             EmulationCreationError::Xdp(XdpEmulationCreationError::Ashpd(Response(
 86 |                 ResponseError::Cancelled,
 87 |             )))
 88 |         ) {
 89 |             return true;
 90 |         }
 91 |         false
 92 |     }
 93 | }
 94 | 
 95 | #[cfg(all(unix, feature = "wlroots", not(target_os = "macos")))]
 96 | #[derive(Debug, Error)]
 97 | pub enum WlrootsEmulationCreationError {
 98 |     #[error(transparent)]
 99 |     Connect(#[from] ConnectError),
100 |     #[error(transparent)]
101 |     Global(#[from] GlobalError),
102 |     #[error(transparent)]
103 |     Wayland(#[from] WaylandError),
104 |     #[error(transparent)]
105 |     Bind(#[from] WaylandBindError),
106 |     #[error(transparent)]
107 |     Dispatch(#[from] DispatchError),
108 |     #[error(transparent)]
109 |     Io(#[from] std::io::Error),
110 | }
111 | 
112 | #[cfg(all(unix, feature = "wlroots", not(target_os = "macos")))]
113 | #[derive(Debug, Error)]
114 | #[error("wayland protocol \"{protocol}\" not supported: {inner}")]
115 | pub struct WaylandBindError {
116 |     inner: BindError,
117 |     protocol: &'static str,
118 | }
119 | 
120 | #[cfg(all(unix, feature = "wlroots", not(target_os = "macos")))]
121 | impl WaylandBindError {
122 |     pub(crate) fn new(inner: BindError, protocol: &'static str) -> Self {
123 |         Self { inner, protocol }
124 |     }
125 | }
126 | 
127 | #[cfg(all(unix, feature = "libei", not(target_os = "macos")))]
128 | #[derive(Debug, Error)]
129 | pub enum LibeiEmulationCreationError {
130 |     #[error(transparent)]
131 |     Ashpd(#[from] ashpd::Error),
132 |     #[error(transparent)]
133 |     Io(#[from] std::io::Error),
134 |     #[error(transparent)]
135 |     Reis(#[from] reis::Error),
136 | }
137 | 
138 | #[cfg(all(unix, feature = "remote_desktop_portal", not(target_os = "macos")))]
139 | #[derive(Debug, Error)]
140 | pub enum XdpEmulationCreationError {
141 |     #[error(transparent)]
142 |     Ashpd(#[from] ashpd::Error),
143 | }
144 | 
145 | #[cfg(all(unix, feature = "x11", not(target_os = "macos")))]
146 | #[derive(Debug, Error)]
147 | pub enum X11EmulationCreationError {
148 |     #[error("could not open display")]
149 |     OpenDisplay,
150 | }
151 | 
152 | #[cfg(target_os = "macos")]
153 | #[derive(Debug, Error)]
154 | pub enum MacOSEmulationCreationError {
155 |     #[error("could not create event source")]
156 |     EventSourceCreation,
157 | }
158 | 
159 | #[cfg(windows)]
160 | #[derive(Debug, Error)]
161 | pub enum WindowsEmulationCreationError {}
162 | 


--------------------------------------------------------------------------------
/input-emulation/src/lib.rs:
--------------------------------------------------------------------------------
  1 | use async_trait::async_trait;
  2 | use std::{
  3 |     collections::{HashMap, HashSet},
  4 |     fmt::Display,
  5 | };
  6 | 
  7 | use input_event::{Event, KeyboardEvent};
  8 | 
  9 | pub use self::error::{EmulationCreationError, EmulationError, InputEmulationError};
 10 | 
 11 | #[cfg(windows)]
 12 | mod windows;
 13 | 
 14 | #[cfg(all(unix, feature = "x11", not(target_os = "macos")))]
 15 | mod x11;
 16 | 
 17 | #[cfg(all(unix, feature = "wlroots", not(target_os = "macos")))]
 18 | mod wlroots;
 19 | 
 20 | #[cfg(all(unix, feature = "remote_desktop_portal", not(target_os = "macos")))]
 21 | mod xdg_desktop_portal;
 22 | 
 23 | #[cfg(all(unix, feature = "libei", not(target_os = "macos")))]
 24 | mod libei;
 25 | 
 26 | #[cfg(target_os = "macos")]
 27 | mod macos;
 28 | 
 29 | /// fallback input emulation (logs events)
 30 | mod dummy;
 31 | mod error;
 32 | 
 33 | pub type EmulationHandle = u64;
 34 | 
 35 | #[derive(Clone, Copy, Debug, Eq, PartialEq)]
 36 | pub enum Backend {
 37 |     #[cfg(all(unix, feature = "wlroots", not(target_os = "macos")))]
 38 |     Wlroots,
 39 |     #[cfg(all(unix, feature = "libei", not(target_os = "macos")))]
 40 |     Libei,
 41 |     #[cfg(all(unix, feature = "remote_desktop_portal", not(target_os = "macos")))]
 42 |     Xdp,
 43 |     #[cfg(all(unix, feature = "x11", not(target_os = "macos")))]
 44 |     X11,
 45 |     #[cfg(windows)]
 46 |     Windows,
 47 |     #[cfg(target_os = "macos")]
 48 |     MacOs,
 49 |     Dummy,
 50 | }
 51 | 
 52 | impl Display for Backend {
 53 |     fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
 54 |         match self {
 55 |             #[cfg(all(unix, feature = "wlroots", not(target_os = "macos")))]
 56 |             Backend::Wlroots => write!(f, "wlroots"),
 57 |             #[cfg(all(unix, feature = "libei", not(target_os = "macos")))]
 58 |             Backend::Libei => write!(f, "libei"),
 59 |             #[cfg(all(unix, feature = "remote_desktop_portal", not(target_os = "macos")))]
 60 |             Backend::Xdp => write!(f, "xdg-desktop-portal"),
 61 |             #[cfg(all(unix, feature = "x11", not(target_os = "macos")))]
 62 |             Backend::X11 => write!(f, "X11"),
 63 |             #[cfg(windows)]
 64 |             Backend::Windows => write!(f, "windows"),
 65 |             #[cfg(target_os = "macos")]
 66 |             Backend::MacOs => write!(f, "macos"),
 67 |             Backend::Dummy => write!(f, "dummy"),
 68 |         }
 69 |     }
 70 | }
 71 | 
 72 | pub struct InputEmulation {
 73 |     emulation: Box<dyn Emulation>,
 74 |     handles: HashSet<EmulationHandle>,
 75 |     pressed_keys: HashMap<EmulationHandle, HashSet<u32>>,
 76 | }
 77 | 
 78 | impl InputEmulation {
 79 |     async fn with_backend(backend: Backend) -> Result<InputEmulation, EmulationCreationError> {
 80 |         let emulation: Box<dyn Emulation> = match backend {
 81 |             #[cfg(all(unix, feature = "wlroots", not(target_os = "macos")))]
 82 |             Backend::Wlroots => Box::new(wlroots::WlrootsEmulation::new()?),
 83 |             #[cfg(all(unix, feature = "libei", not(target_os = "macos")))]
 84 |             Backend::Libei => Box::new(libei::LibeiEmulation::new().await?),
 85 |             #[cfg(all(unix, feature = "x11", not(target_os = "macos")))]
 86 |             Backend::X11 => Box::new(x11::X11Emulation::new()?),
 87 |             #[cfg(all(unix, feature = "remote_desktop_portal", not(target_os = "macos")))]
 88 |             Backend::Xdp => Box::new(xdg_desktop_portal::DesktopPortalEmulation::new().await?),
 89 |             #[cfg(windows)]
 90 |             Backend::Windows => Box::new(windows::WindowsEmulation::new()?),
 91 |             #[cfg(target_os = "macos")]
 92 |             Backend::MacOs => Box::new(macos::MacOSEmulation::new()?),
 93 |             Backend::Dummy => Box::new(dummy::DummyEmulation::new()),
 94 |         };
 95 |         Ok(Self {
 96 |             emulation,
 97 |             handles: HashSet::new(),
 98 |             pressed_keys: HashMap::new(),
 99 |         })
100 |     }
101 | 
102 |     pub async fn new(backend: Option<Backend>) -> Result<InputEmulation, EmulationCreationError> {
103 |         if let Some(backend) = backend {
104 |             let b = Self::with_backend(backend).await;
105 |             if b.is_ok() {
106 |                 log::info!("using emulation backend: {backend}");
107 |             }
108 |             return b;
109 |         }
110 | 
111 |         for backend in [
112 |             #[cfg(all(unix, feature = "wlroots", not(target_os = "macos")))]
113 |             Backend::Wlroots,
114 |             #[cfg(all(unix, feature = "libei", not(target_os = "macos")))]
115 |             Backend::Libei,
116 |             #[cfg(all(unix, feature = "remote_desktop_portal", not(target_os = "macos")))]
117 |             Backend::Xdp,
118 |             #[cfg(all(unix, feature = "x11", not(target_os = "macos")))]
119 |             Backend::X11,
120 |             #[cfg(windows)]
121 |             Backend::Windows,
122 |             #[cfg(target_os = "macos")]
123 |             Backend::MacOs,
124 |             Backend::Dummy,
125 |         ] {
126 |             match Self::with_backend(backend).await {
127 |                 Ok(b) => {
128 |                     log::info!("using emulation backend: {backend}");
129 |                     return Ok(b);
130 |                 }
131 |                 Err(e) if e.cancelled_by_user() => return Err(e),
132 |                 Err(e) => log::warn!("{e}"),
133 |             }
134 |         }
135 | 
136 |         Err(EmulationCreationError::NoAvailableBackend)
137 |     }
138 | 
139 |     pub async fn consume(
140 |         &mut self,
141 |         event: Event,
142 |         handle: EmulationHandle,
143 |     ) -> Result<(), EmulationError> {
144 |         match event {
145 |             Event::Keyboard(KeyboardEvent::Key { key, state, .. }) => {
146 |                 // prevent double pressed / released keys
147 |                 if self.update_pressed_keys(handle, key, state) {
148 |                     self.emulation.consume(event, handle).await?;
149 |                 }
150 |                 Ok(())
151 |             }
152 |             _ => self.emulation.consume(event, handle).await,
153 |         }
154 |     }
155 | 
156 |     pub async fn create(&mut self, handle: EmulationHandle) -> bool {
157 |         if self.handles.insert(handle) {
158 |             self.pressed_keys.insert(handle, HashSet::new());
159 |             self.emulation.create(handle).await;
160 |             true
161 |         } else {
162 |             false
163 |         }
164 |     }
165 | 
166 |     pub async fn destroy(&mut self, handle: EmulationHandle) {
167 |         let _ = self.release_keys(handle).await;
168 |         if self.handles.remove(&handle) {
169 |             self.pressed_keys.remove(&handle);
170 |             self.emulation.destroy(handle).await
171 |         }
172 |     }
173 | 
174 |     pub async fn terminate(&mut self) {
175 |         for handle in self.handles.iter().cloned().collect::<Vec<_>>() {
176 |             self.destroy(handle).await
177 |         }
178 |         self.emulation.terminate().await
179 |     }
180 | 
181 |     pub async fn release_keys(&mut self, handle: EmulationHandle) -> Result<(), EmulationError> {
182 |         if let Some(keys) = self.pressed_keys.get_mut(&handle) {
183 |             let keys = keys.drain().collect::<Vec<_>>();
184 |             for key in keys {
185 |                 let event = Event::Keyboard(KeyboardEvent::Key {
186 |                     time: 0,
187 |                     key,
188 |                     state: 0,
189 |                 });
190 |                 self.emulation.consume(event, handle).await?;
191 |                 if let Ok(key) = input_event::scancode::Linux::try_from(key) {
192 |                     log::warn!("releasing stuck key: {key:?}");
193 |                 }
194 |             }
195 |         }
196 | 
197 |         let event = Event::Keyboard(KeyboardEvent::Modifiers {
198 |             depressed: 0,
199 |             latched: 0,
200 |             locked: 0,
201 |             group: 0,
202 |         });
203 |         self.emulation.consume(event, handle).await?;
204 |         Ok(())
205 |     }
206 | 
207 |     pub fn has_pressed_keys(&self, handle: EmulationHandle) -> bool {
208 |         self.pressed_keys
209 |             .get(&handle)
210 |             .is_some_and(|p| !p.is_empty())
211 |     }
212 | 
213 |     /// update the pressed_keys for the given handle
214 |     /// returns whether the event should be processed
215 |     fn update_pressed_keys(&mut self, handle: EmulationHandle, key: u32, state: u8) -> bool {
216 |         let Some(pressed_keys) = self.pressed_keys.get_mut(&handle) else {
217 |             return false;
218 |         };
219 | 
220 |         if state == 0 {
221 |             // currently pressed => can release
222 |             pressed_keys.remove(&key)
223 |         } else {
224 |             // currently not pressed => can press
225 |             pressed_keys.insert(key)
226 |         }
227 |     }
228 | }
229 | 
230 | #[async_trait]
231 | trait Emulation: Send {
232 |     async fn consume(
233 |         &mut self,
234 |         event: Event,
235 |         handle: EmulationHandle,
236 |     ) -> Result<(), EmulationError>;
237 |     async fn create(&mut self, handle: EmulationHandle);
238 |     async fn destroy(&mut self, handle: EmulationHandle);
239 |     async fn terminate(&mut self);
240 | }
241 | 


--------------------------------------------------------------------------------
/input-emulation/src/windows.rs:
--------------------------------------------------------------------------------
  1 | use super::error::{EmulationError, WindowsEmulationCreationError};
  2 | use input_event::{
  3 |     scancode, Event, KeyboardEvent, PointerEvent, BTN_BACK, BTN_FORWARD, BTN_LEFT, BTN_MIDDLE,
  4 |     BTN_RIGHT,
  5 | };
  6 | 
  7 | use async_trait::async_trait;
  8 | use std::ops::BitOrAssign;
  9 | use std::time::Duration;
 10 | use tokio::task::AbortHandle;
 11 | use windows::Win32::UI::Input::KeyboardAndMouse::{
 12 |     SendInput, INPUT_0, KEYEVENTF_EXTENDEDKEY, MOUSEEVENTF_XDOWN, MOUSEEVENTF_XUP,
 13 | };
 14 | use windows::Win32::UI::Input::KeyboardAndMouse::{
 15 |     INPUT, INPUT_KEYBOARD, INPUT_MOUSE, KEYBDINPUT, KEYEVENTF_KEYUP, KEYEVENTF_SCANCODE,
 16 |     MOUSEEVENTF_HWHEEL, MOUSEEVENTF_LEFTDOWN, MOUSEEVENTF_LEFTUP, MOUSEEVENTF_MIDDLEDOWN,
 17 |     MOUSEEVENTF_MIDDLEUP, MOUSEEVENTF_MOVE, MOUSEEVENTF_RIGHTDOWN, MOUSEEVENTF_RIGHTUP,
 18 |     MOUSEEVENTF_WHEEL, MOUSEINPUT,
 19 | };
 20 | use windows::Win32::UI::WindowsAndMessaging::{XBUTTON1, XBUTTON2};
 21 | 
 22 | use super::{Emulation, EmulationHandle};
 23 | 
 24 | const DEFAULT_REPEAT_DELAY: Duration = Duration::from_millis(500);
 25 | const DEFAULT_REPEAT_INTERVAL: Duration = Duration::from_millis(32);
 26 | 
 27 | pub(crate) struct WindowsEmulation {
 28 |     repeat_task: Option<AbortHandle>,
 29 | }
 30 | 
 31 | impl WindowsEmulation {
 32 |     pub(crate) fn new() -> Result<Self, WindowsEmulationCreationError> {
 33 |         Ok(Self { repeat_task: None })
 34 |     }
 35 | }
 36 | 
 37 | #[async_trait]
 38 | impl Emulation for WindowsEmulation {
 39 |     async fn consume(&mut self, event: Event, _: EmulationHandle) -> Result<(), EmulationError> {
 40 |         match event {
 41 |             Event::Pointer(pointer_event) => match pointer_event {
 42 |                 PointerEvent::Motion { time: _, dx, dy } => {
 43 |                     rel_mouse(dx as i32, dy as i32);
 44 |                 }
 45 |                 PointerEvent::Button {
 46 |                     time: _,
 47 |                     button,
 48 |                     state,
 49 |                 } => mouse_button(button, state),
 50 |                 PointerEvent::Axis {
 51 |                     time: _,
 52 |                     axis,
 53 |                     value,
 54 |                 } => scroll(axis, value as i32),
 55 |                 PointerEvent::AxisDiscrete120 { axis, value } => scroll(axis, value),
 56 |             },
 57 |             Event::Keyboard(keyboard_event) => match keyboard_event {
 58 |                 KeyboardEvent::Key {
 59 |                     time: _,
 60 |                     key,
 61 |                     state,
 62 |                 } => {
 63 |                     match state {
 64 |                         // pressed
 65 |                         0 => self.kill_repeat_task(),
 66 |                         1 => self.spawn_repeat_task(key).await,
 67 |                         _ => {}
 68 |                     }
 69 |                     key_event(key, state)
 70 |                 }
 71 |                 KeyboardEvent::Modifiers { .. } => {}
 72 |             },
 73 |         }
 74 |         // FIXME
 75 |         Ok(())
 76 |     }
 77 | 
 78 |     async fn create(&mut self, _handle: EmulationHandle) {}
 79 | 
 80 |     async fn destroy(&mut self, _handle: EmulationHandle) {}
 81 | 
 82 |     async fn terminate(&mut self) {}
 83 | }
 84 | 
 85 | impl WindowsEmulation {
 86 |     async fn spawn_repeat_task(&mut self, key: u32) {
 87 |         // there can only be one repeating key and it's
 88 |         // always the last to be pressed
 89 |         self.kill_repeat_task();
 90 |         let repeat_task = tokio::task::spawn_local(async move {
 91 |             tokio::time::sleep(DEFAULT_REPEAT_DELAY).await;
 92 |             loop {
 93 |                 key_event(key, 1);
 94 |                 tokio::time::sleep(DEFAULT_REPEAT_INTERVAL).await;
 95 |             }
 96 |         });
 97 |         self.repeat_task = Some(repeat_task.abort_handle());
 98 |     }
 99 |     fn kill_repeat_task(&mut self) {
100 |         if let Some(task) = self.repeat_task.take() {
101 |             task.abort();
102 |         }
103 |     }
104 | }
105 | 
106 | fn send_input_safe(input: INPUT) {
107 |     unsafe {
108 |         loop {
109 |             /* retval = number of successfully submitted events */
110 |             if SendInput(&[input], std::mem::size_of::<INPUT>() as i32) > 0 {
111 |                 break;
112 |             }
113 |         }
114 |     }
115 | }
116 | 
117 | fn send_mouse_input(mi: MOUSEINPUT) {
118 |     send_input_safe(INPUT {
119 |         r#type: INPUT_MOUSE,
120 |         Anonymous: INPUT_0 { mi },
121 |     });
122 | }
123 | 
124 | fn send_keyboard_input(ki: KEYBDINPUT) {
125 |     send_input_safe(INPUT {
126 |         r#type: INPUT_KEYBOARD,
127 |         Anonymous: INPUT_0 { ki },
128 |     });
129 | }
130 | fn rel_mouse(dx: i32, dy: i32) {
131 |     let mi = MOUSEINPUT {
132 |         dx,
133 |         dy,
134 |         mouseData: 0,
135 |         dwFlags: MOUSEEVENTF_MOVE,
136 |         time: 0,
137 |         dwExtraInfo: 0,
138 |     };
139 |     send_mouse_input(mi);
140 | }
141 | 
142 | fn mouse_button(button: u32, state: u32) {
143 |     let dw_flags = match state {
144 |         0 => match button {
145 |             BTN_LEFT => MOUSEEVENTF_LEFTUP,
146 |             BTN_RIGHT => MOUSEEVENTF_RIGHTUP,
147 |             BTN_MIDDLE => MOUSEEVENTF_MIDDLEUP,
148 |             BTN_BACK => MOUSEEVENTF_XUP,
149 |             BTN_FORWARD => MOUSEEVENTF_XUP,
150 |             _ => return,
151 |         },
152 |         1 => match button {
153 |             BTN_LEFT => MOUSEEVENTF_LEFTDOWN,
154 |             BTN_RIGHT => MOUSEEVENTF_RIGHTDOWN,
155 |             BTN_MIDDLE => MOUSEEVENTF_MIDDLEDOWN,
156 |             BTN_BACK => MOUSEEVENTF_XDOWN,
157 |             BTN_FORWARD => MOUSEEVENTF_XDOWN,
158 |             _ => return,
159 |         },
160 |         _ => return,
161 |     };
162 |     let mouse_data = match button {
163 |         BTN_BACK => XBUTTON1 as u32,
164 |         BTN_FORWARD => XBUTTON2 as u32,
165 |         _ => 0,
166 |     };
167 |     let mi = MOUSEINPUT {
168 |         dx: 0,
169 |         dy: 0, // no movement
170 |         mouseData: mouse_data,
171 |         dwFlags: dw_flags,
172 |         time: 0,
173 |         dwExtraInfo: 0,
174 |     };
175 |     send_mouse_input(mi);
176 | }
177 | 
178 | fn scroll(axis: u8, value: i32) {
179 |     let event_type = match axis {
180 |         0 => MOUSEEVENTF_WHEEL,
181 |         1 => MOUSEEVENTF_HWHEEL,
182 |         _ => return,
183 |     };
184 |     let mi = MOUSEINPUT {
185 |         dx: 0,
186 |         dy: 0,
187 |         mouseData: -value as u32,
188 |         dwFlags: event_type,
189 |         time: 0,
190 |         dwExtraInfo: 0,
191 |     };
192 |     send_mouse_input(mi);
193 | }
194 | 
195 | fn key_event(key: u32, state: u8) {
196 |     let scancode = match linux_keycode_to_windows_scancode(key) {
197 |         Some(code) => code,
198 |         None => return,
199 |     };
200 |     let extended = scancode > 0xff;
201 |     let scancode = scancode & 0xff;
202 |     let mut flags = KEYEVENTF_SCANCODE;
203 |     if extended {
204 |         flags.bitor_assign(KEYEVENTF_EXTENDEDKEY);
205 |     }
206 |     if state == 0 {
207 |         flags.bitor_assign(KEYEVENTF_KEYUP);
208 |     }
209 |     let ki = KEYBDINPUT {
210 |         wVk: Default::default(),
211 |         wScan: scancode,
212 |         dwFlags: flags,
213 |         time: 0,
214 |         dwExtraInfo: 0,
215 |     };
216 |     send_keyboard_input(ki);
217 | }
218 | 
219 | fn linux_keycode_to_windows_scancode(linux_keycode: u32) -> Option<u16> {
220 |     let linux_scancode = match scancode::Linux::try_from(linux_keycode) {
221 |         Ok(s) => s,
222 |         Err(_) => {
223 |             log::warn!("unknown keycode: {linux_keycode}");
224 |             return None;
225 |         }
226 |     };
227 |     log::trace!("linux code: {linux_scancode:?}");
228 |     let windows_scancode = match scancode::Windows::try_from(linux_scancode) {
229 |         Ok(s) => s,
230 |         Err(_) => {
231 |             log::warn!("failed to translate linux code into windows scancode: {linux_scancode:?}");
232 |             return None;
233 |         }
234 |     };
235 |     log::trace!("windows code: {windows_scancode:?}");
236 |     Some(windows_scancode as u16)
237 | }
238 | 


--------------------------------------------------------------------------------
/input-emulation/src/x11.rs:
--------------------------------------------------------------------------------
  1 | use async_trait::async_trait;
  2 | use std::ptr;
  3 | use x11::{
  4 |     xlib::{self, XCloseDisplay},
  5 |     xtest,
  6 | };
  7 | 
  8 | use input_event::{
  9 |     Event, KeyboardEvent, PointerEvent, BTN_BACK, BTN_FORWARD, BTN_LEFT, BTN_MIDDLE, BTN_RIGHT,
 10 | };
 11 | 
 12 | use crate::error::EmulationError;
 13 | 
 14 | use super::{error::X11EmulationCreationError, Emulation, EmulationHandle};
 15 | 
 16 | pub(crate) struct X11Emulation {
 17 |     display: *mut xlib::Display,
 18 | }
 19 | 
 20 | unsafe impl Send for X11Emulation {}
 21 | 
 22 | impl X11Emulation {
 23 |     pub(crate) fn new() -> Result<Self, X11EmulationCreationError> {
 24 |         let display = unsafe {
 25 |             match xlib::XOpenDisplay(ptr::null()) {
 26 |                 d if std::ptr::eq(d, ptr::null_mut::<xlib::Display>()) => {
 27 |                     Err(X11EmulationCreationError::OpenDisplay)
 28 |                 }
 29 |                 display => Ok(display),
 30 |             }
 31 |         }?;
 32 |         Ok(Self { display })
 33 |     }
 34 | 
 35 |     fn relative_motion(&self, dx: i32, dy: i32) {
 36 |         unsafe {
 37 |             xtest::XTestFakeRelativeMotionEvent(self.display, dx, dy, 0, 0);
 38 |         }
 39 |     }
 40 | 
 41 |     fn emulate_mouse_button(&self, button: u32, state: u32) {
 42 |         unsafe {
 43 |             let x11_button = match button {
 44 |                 BTN_RIGHT => 3,
 45 |                 BTN_MIDDLE => 2,
 46 |                 BTN_BACK => 8,
 47 |                 BTN_FORWARD => 9,
 48 |                 BTN_LEFT => 1,
 49 |                 _ => 1,
 50 |             };
 51 |             xtest::XTestFakeButtonEvent(self.display, x11_button, state as i32, 0);
 52 |         };
 53 |     }
 54 | 
 55 |     const SCROLL_UP: u32 = 4;
 56 |     const SCROLL_DOWN: u32 = 5;
 57 |     const SCROLL_LEFT: u32 = 6;
 58 |     const SCROLL_RIGHT: u32 = 7;
 59 | 
 60 |     fn emulate_scroll(&self, axis: u8, value: f64) {
 61 |         let direction = match axis {
 62 |             1 => {
 63 |                 if value < 0.0 {
 64 |                     Self::SCROLL_LEFT
 65 |                 } else {
 66 |                     Self::SCROLL_RIGHT
 67 |                 }
 68 |             }
 69 |             _ => {
 70 |                 if value < 0.0 {
 71 |                     Self::SCROLL_UP
 72 |                 } else {
 73 |                     Self::SCROLL_DOWN
 74 |                 }
 75 |             }
 76 |         };
 77 | 
 78 |         unsafe {
 79 |             xtest::XTestFakeButtonEvent(self.display, direction, 1, 0);
 80 |             xtest::XTestFakeButtonEvent(self.display, direction, 0, 0);
 81 |         }
 82 |     }
 83 | 
 84 |     #[allow(dead_code)]
 85 |     fn emulate_key(&self, key: u32, state: u8) {
 86 |         let key = key + 8; // xorg keycodes are shifted by 8
 87 |         unsafe {
 88 |             xtest::XTestFakeKeyEvent(self.display, key, state as i32, 0);
 89 |         }
 90 |     }
 91 | }
 92 | 
 93 | impl Drop for X11Emulation {
 94 |     fn drop(&mut self) {
 95 |         unsafe {
 96 |             XCloseDisplay(self.display);
 97 |         }
 98 |     }
 99 | }
100 | 
101 | #[async_trait]
102 | impl Emulation for X11Emulation {
103 |     async fn consume(&mut self, event: Event, _: EmulationHandle) -> Result<(), EmulationError> {
104 |         match event {
105 |             Event::Pointer(pointer_event) => match pointer_event {
106 |                 PointerEvent::Motion { time: _, dx, dy } => {
107 |                     self.relative_motion(dx as i32, dy as i32);
108 |                 }
109 |                 PointerEvent::Button {
110 |                     time: _,
111 |                     button,
112 |                     state,
113 |                 } => {
114 |                     self.emulate_mouse_button(button, state);
115 |                 }
116 |                 PointerEvent::Axis {
117 |                     time: _,
118 |                     axis,
119 |                     value,
120 |                 } => {
121 |                     self.emulate_scroll(axis, value);
122 |                 }
123 |                 PointerEvent::AxisDiscrete120 { axis, value } => {
124 |                     self.emulate_scroll(axis, value as f64);
125 |                 }
126 |             },
127 |             Event::Keyboard(KeyboardEvent::Key {
128 |                 time: _,
129 |                 key,
130 |                 state,
131 |             }) => {
132 |                 self.emulate_key(key, state);
133 |             }
134 |             _ => {}
135 |         }
136 |         unsafe {
137 |             xlib::XFlush(self.display);
138 |         }
139 |         // FIXME
140 |         Ok(())
141 |     }
142 | 
143 |     async fn create(&mut self, _: EmulationHandle) {
144 |         // for our purposes it does not matter what client sent the event
145 |     }
146 | 
147 |     async fn destroy(&mut self, _: EmulationHandle) {
148 |         // for our purposes it does not matter what client sent the event
149 |     }
150 | 
151 |     async fn terminate(&mut self) {
152 |         /* nothing to do */
153 |     }
154 | }
155 | 


--------------------------------------------------------------------------------
/input-emulation/src/xdg_desktop_portal.rs:
--------------------------------------------------------------------------------
  1 | use ashpd::{
  2 |     desktop::{
  3 |         remote_desktop::{Axis, DeviceType, KeyState, RemoteDesktop},
  4 |         PersistMode, Session,
  5 |     },
  6 |     zbus::AsyncDrop,
  7 | };
  8 | use async_trait::async_trait;
  9 | 
 10 | use futures::FutureExt;
 11 | use input_event::{
 12 |     Event::{Keyboard, Pointer},
 13 |     KeyboardEvent, PointerEvent,
 14 | };
 15 | 
 16 | use crate::error::EmulationError;
 17 | 
 18 | use super::{error::XdpEmulationCreationError, Emulation, EmulationHandle};
 19 | 
 20 | pub(crate) struct DesktopPortalEmulation<'a> {
 21 |     proxy: RemoteDesktop<'a>,
 22 |     session: Session<'a, RemoteDesktop<'a>>,
 23 | }
 24 | 
 25 | impl<'a> DesktopPortalEmulation<'a> {
 26 |     pub(crate) async fn new() -> Result<DesktopPortalEmulation<'a>, XdpEmulationCreationError> {
 27 |         log::debug!("connecting to org.freedesktop.portal.RemoteDesktop portal ...");
 28 |         let proxy = RemoteDesktop::new().await?;
 29 | 
 30 |         // retry when user presses the cancel button
 31 |         log::debug!("creating session ...");
 32 |         let session = proxy.create_session().await?;
 33 | 
 34 |         log::debug!("selecting devices ...");
 35 |         proxy
 36 |             .select_devices(
 37 |                 &session,
 38 |                 DeviceType::Keyboard | DeviceType::Pointer,
 39 |                 None,
 40 |                 PersistMode::ExplicitlyRevoked,
 41 |             )
 42 |             .await?;
 43 | 
 44 |         log::info!("requesting permission for input emulation");
 45 |         let _devices = proxy.start(&session, None).await?.response()?;
 46 | 
 47 |         log::debug!("started session");
 48 |         let session = session;
 49 | 
 50 |         Ok(Self { proxy, session })
 51 |     }
 52 | }
 53 | 
 54 | #[async_trait]
 55 | impl Emulation for DesktopPortalEmulation<'_> {
 56 |     async fn consume(
 57 |         &mut self,
 58 |         event: input_event::Event,
 59 |         _client: EmulationHandle,
 60 |     ) -> Result<(), EmulationError> {
 61 |         match event {
 62 |             Pointer(p) => match p {
 63 |                 PointerEvent::Motion { time: _, dx, dy } => {
 64 |                     self.proxy
 65 |                         .notify_pointer_motion(&self.session, dx, dy)
 66 |                         .await?;
 67 |                 }
 68 |                 PointerEvent::Button {
 69 |                     time: _,
 70 |                     button,
 71 |                     state,
 72 |                 } => {
 73 |                     let state = match state {
 74 |                         0 => KeyState::Released,
 75 |                         _ => KeyState::Pressed,
 76 |                     };
 77 |                     self.proxy
 78 |                         .notify_pointer_button(&self.session, button as i32, state)
 79 |                         .await?;
 80 |                 }
 81 |                 PointerEvent::AxisDiscrete120 { axis, value } => {
 82 |                     let axis = match axis {
 83 |                         0 => Axis::Vertical,
 84 |                         _ => Axis::Horizontal,
 85 |                     };
 86 |                     self.proxy
 87 |                         .notify_pointer_axis_discrete(&self.session, axis, value / 120)
 88 |                         .await?;
 89 |                 }
 90 |                 PointerEvent::Axis {
 91 |                     time: _,
 92 |                     axis,
 93 |                     value,
 94 |                 } => {
 95 |                     let axis = match axis {
 96 |                         0 => Axis::Vertical,
 97 |                         _ => Axis::Horizontal,
 98 |                     };
 99 |                     let (dx, dy) = match axis {
100 |                         Axis::Vertical => (0., value),
101 |                         Axis::Horizontal => (value, 0.),
102 |                     };
103 |                     self.proxy
104 |                         .notify_pointer_axis(&self.session, dx, dy, true)
105 |                         .await?;
106 |                 }
107 |             },
108 |             Keyboard(k) => {
109 |                 match k {
110 |                     KeyboardEvent::Key {
111 |                         time: _,
112 |                         key,
113 |                         state,
114 |                     } => {
115 |                         let state = match state {
116 |                             0 => KeyState::Released,
117 |                             _ => KeyState::Pressed,
118 |                         };
119 |                         self.proxy
120 |                             .notify_keyboard_keycode(&self.session, key as i32, state)
121 |                             .await?;
122 |                     }
123 |                     KeyboardEvent::Modifiers { .. } => {
124 |                         // ignore
125 |                     }
126 |                 }
127 |             }
128 |         }
129 |         Ok(())
130 |     }
131 | 
132 |     async fn create(&mut self, _client: EmulationHandle) {}
133 |     async fn destroy(&mut self, _client: EmulationHandle) {}
134 |     async fn terminate(&mut self) {
135 |         if let Err(e) = self.session.close().await {
136 |             log::warn!("session.close(): {e}");
137 |         };
138 |         if let Err(e) = self.session.receive_closed().await {
139 |             log::warn!("session.receive_closed(): {e}");
140 |         };
141 |     }
142 | }
143 | 
144 | impl AsyncDrop for DesktopPortalEmulation<'_> {
145 |     #[doc = r" Perform the async cleanup."]
146 |     #[allow(clippy::type_complexity, clippy::type_repetition_in_bounds)]
147 |     fn async_drop<'async_trait>(
148 |         self,
149 |     ) -> ::core::pin::Pin<
150 |         Box<dyn ::core::future::Future<Output = ()> + ::core::marker::Send + 'async_trait>,
151 |     >
152 |     where
153 |         Self: 'async_trait,
154 |     {
155 |         async move {
156 |             let _ = self.session.close().await;
157 |         }
158 |         .boxed()
159 |     }
160 | }
161 | 


--------------------------------------------------------------------------------
/input-event/Cargo.toml:
--------------------------------------------------------------------------------
 1 | [package]
 2 | name = "input-event"
 3 | description = "cross-platform input-event types for input-capture / input-emulation"
 4 | version = "0.3.0"
 5 | edition = "2021"
 6 | license = "GPL-3.0-or-later"
 7 | repository = "https://github.com/feschber/lan-mouse"
 8 | 
 9 | [dependencies]
10 | futures-core = "0.3.30"
11 | log = "0.4.22"
12 | num_enum = "0.7.2"
13 | serde = { version = "1.0", features = ["derive"] }
14 | thiserror = "2.0.0"
15 | 
16 | [target.'cfg(all(unix, not(target_os="macos")))'.dependencies]
17 | reis = { version = "0.4", optional = true }
18 | 
19 | [features]
20 | default = ["libei"]
21 | libei = ["dep:reis"]
22 | 


--------------------------------------------------------------------------------
/input-event/src/error.rs:
--------------------------------------------------------------------------------
1 | 
2 | 


--------------------------------------------------------------------------------
/input-event/src/lib.rs:
--------------------------------------------------------------------------------
  1 | use std::fmt::{self, Display};
  2 | 
  3 | pub mod error;
  4 | pub mod scancode;
  5 | 
  6 | #[cfg(all(unix, feature = "libei", not(target_os = "macos")))]
  7 | mod libei;
  8 | 
  9 | // FIXME
 10 | pub const BTN_LEFT: u32 = 0x110;
 11 | pub const BTN_RIGHT: u32 = 0x111;
 12 | pub const BTN_MIDDLE: u32 = 0x112;
 13 | pub const BTN_BACK: u32 = 0x113;
 14 | pub const BTN_FORWARD: u32 = 0x114;
 15 | 
 16 | #[derive(Debug, PartialEq, Clone, Copy)]
 17 | pub enum PointerEvent {
 18 |     /// relative motion event
 19 |     Motion { time: u32, dx: f64, dy: f64 },
 20 |     /// mouse button event
 21 |     Button { time: u32, button: u32, state: u32 },
 22 |     /// axis event, scroll event for touchpads
 23 |     Axis { time: u32, axis: u8, value: f64 },
 24 |     /// discrete axis event, scroll event for mice - 120 = one scroll tick
 25 |     AxisDiscrete120 { axis: u8, value: i32 },
 26 | }
 27 | 
 28 | #[derive(Debug, PartialEq, Clone, Copy)]
 29 | pub enum KeyboardEvent {
 30 |     /// a key press / release event
 31 |     Key { time: u32, key: u32, state: u8 },
 32 |     /// modifiers changed state
 33 |     Modifiers {
 34 |         depressed: u32,
 35 |         latched: u32,
 36 |         locked: u32,
 37 |         group: u32,
 38 |     },
 39 | }
 40 | 
 41 | #[derive(PartialEq, Debug, Clone, Copy)]
 42 | pub enum Event {
 43 |     /// pointer event (motion / button / axis)
 44 |     Pointer(PointerEvent),
 45 |     /// keyboard events (key / modifiers)
 46 |     Keyboard(KeyboardEvent),
 47 | }
 48 | 
 49 | impl Display for PointerEvent {
 50 |     fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
 51 |         match self {
 52 |             PointerEvent::Motion { time: _, dx, dy } => write!(f, "motion({dx},{dy})"),
 53 |             PointerEvent::Button {
 54 |                 time: _,
 55 |                 button,
 56 |                 state,
 57 |             } => {
 58 |                 let str = match *button {
 59 |                     BTN_LEFT => Some("left"),
 60 |                     BTN_RIGHT => Some("right"),
 61 |                     BTN_MIDDLE => Some("middle"),
 62 |                     BTN_FORWARD => Some("forward"),
 63 |                     BTN_BACK => Some("back"),
 64 |                     _ => None,
 65 |                 };
 66 |                 if let Some(button) = str {
 67 |                     write!(f, "button({button}, {state})")
 68 |                 } else {
 69 |                     write!(f, "button({button}, {state}")
 70 |                 }
 71 |             }
 72 |             PointerEvent::Axis {
 73 |                 time: _,
 74 |                 axis,
 75 |                 value,
 76 |             } => write!(f, "scroll({axis}, {value})"),
 77 |             PointerEvent::AxisDiscrete120 { axis, value } => {
 78 |                 write!(f, "scroll-120 ({axis}, {value})")
 79 |             }
 80 |         }
 81 |     }
 82 | }
 83 | 
 84 | impl Display for KeyboardEvent {
 85 |     fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
 86 |         match self {
 87 |             KeyboardEvent::Key {
 88 |                 time: _,
 89 |                 key,
 90 |                 state,
 91 |             } => {
 92 |                 let scan = scancode::Linux::try_from(*key);
 93 |                 if let Ok(scan) = scan {
 94 |                     write!(f, "key({scan:?}, {state})")
 95 |                 } else {
 96 |                     write!(f, "key({key}, {state})")
 97 |                 }
 98 |             }
 99 |             KeyboardEvent::Modifiers {
100 |                 depressed: mods_depressed,
101 |                 latched: mods_latched,
102 |                 locked: mods_locked,
103 |                 group,
104 |             } => write!(
105 |                 f,
106 |                 "modifiers({mods_depressed},{mods_latched},{mods_locked},{group})"
107 |             ),
108 |         }
109 |     }
110 | }
111 | 
112 | impl Display for Event {
113 |     fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
114 |         match self {
115 |             Event::Pointer(p) => write!(f, "{}", p),
116 |             Event::Keyboard(k) => write!(f, "{}", k),
117 |         }
118 |     }
119 | }
120 | 


--------------------------------------------------------------------------------
/input-event/src/libei.rs:
--------------------------------------------------------------------------------
  1 | use reis::{
  2 |     ei::{button::ButtonState, keyboard::KeyState},
  3 |     event::EiEvent,
  4 | };
  5 | 
  6 | use crate::{Event, KeyboardEvent, PointerEvent};
  7 | 
  8 | impl Event {
  9 |     pub fn from_ei_event(ei_event: EiEvent) -> impl Iterator<Item = Self> {
 10 |         to_input_events(ei_event).into_iter()
 11 |     }
 12 | }
 13 | 
 14 | enum Events {
 15 |     None,
 16 |     One(Event),
 17 |     Two(Event, Event),
 18 | }
 19 | 
 20 | impl Events {
 21 |     fn into_iter(self) -> impl Iterator<Item = Event> {
 22 |         EventIterator::new(self)
 23 |     }
 24 | }
 25 | 
 26 | struct EventIterator {
 27 |     events: [Option<Event>; 2],
 28 |     pos: usize,
 29 | }
 30 | 
 31 | impl EventIterator {
 32 |     fn new(events: Events) -> Self {
 33 |         let events = match events {
 34 |             Events::None => [None, None],
 35 |             Events::One(e) => [Some(e), None],
 36 |             Events::Two(e, f) => [Some(e), Some(f)],
 37 |         };
 38 |         Self { events, pos: 0 }
 39 |     }
 40 | }
 41 | 
 42 | impl Iterator for EventIterator {
 43 |     type Item = Event;
 44 | 
 45 |     fn next(&mut self) -> Option<Self::Item> {
 46 |         let res = if self.pos >= self.events.len() {
 47 |             None
 48 |         } else {
 49 |             self.events[self.pos]
 50 |         };
 51 |         self.pos += 1;
 52 |         res
 53 |     }
 54 | }
 55 | 
 56 | fn to_input_events(ei_event: EiEvent) -> Events {
 57 |     match ei_event {
 58 |         EiEvent::KeyboardModifiers(mods) => {
 59 |             let modifier_event = KeyboardEvent::Modifiers {
 60 |                 depressed: mods.depressed,
 61 |                 latched: mods.latched,
 62 |                 locked: mods.locked,
 63 |                 group: mods.group,
 64 |             };
 65 |             Events::One(Event::Keyboard(modifier_event))
 66 |         }
 67 |         EiEvent::Frame(_) => Events::None, /* FIXME */
 68 |         EiEvent::PointerMotion(motion) => {
 69 |             let motion_event = PointerEvent::Motion {
 70 |                 time: motion.time as u32,
 71 |                 dx: motion.dx as f64,
 72 |                 dy: motion.dy as f64,
 73 |             };
 74 |             Events::One(Event::Pointer(motion_event))
 75 |         }
 76 |         EiEvent::PointerMotionAbsolute(_) => Events::None,
 77 |         EiEvent::Button(button) => {
 78 |             let button_event = PointerEvent::Button {
 79 |                 time: button.time as u32,
 80 |                 button: button.button,
 81 |                 state: match button.state {
 82 |                     ButtonState::Released => 0,
 83 |                     ButtonState::Press => 1,
 84 |                 },
 85 |             };
 86 |             Events::One(Event::Pointer(button_event))
 87 |         }
 88 |         EiEvent::ScrollDelta(delta) => {
 89 |             let dy = Event::Pointer(PointerEvent::Axis {
 90 |                 time: 0,
 91 |                 axis: 0,
 92 |                 value: delta.dy as f64,
 93 |             });
 94 |             let dx = Event::Pointer(PointerEvent::Axis {
 95 |                 time: 0,
 96 |                 axis: 1,
 97 |                 value: delta.dx as f64,
 98 |             });
 99 |             if delta.dy != 0. && delta.dx != 0. {
100 |                 Events::Two(dy, dx)
101 |             } else if delta.dy != 0. {
102 |                 Events::One(dy)
103 |             } else if delta.dx != 0. {
104 |                 Events::One(dx)
105 |             } else {
106 |                 Events::None
107 |             }
108 |         }
109 |         EiEvent::ScrollStop(_) => Events::None,   /* TODO */
110 |         EiEvent::ScrollCancel(_) => Events::None, /* TODO */
111 |         EiEvent::ScrollDiscrete(scroll) => {
112 |             let dy = Event::Pointer(PointerEvent::AxisDiscrete120 {
113 |                 axis: 0,
114 |                 value: scroll.discrete_dy,
115 |             });
116 |             let dx = Event::Pointer(PointerEvent::AxisDiscrete120 {
117 |                 axis: 1,
118 |                 value: scroll.discrete_dx,
119 |             });
120 |             if scroll.discrete_dy != 0 && scroll.discrete_dx != 0 {
121 |                 Events::Two(dy, dx)
122 |             } else if scroll.discrete_dy != 0 {
123 |                 Events::One(dy)
124 |             } else if scroll.discrete_dx != 0 {
125 |                 Events::One(dx)
126 |             } else {
127 |                 Events::None
128 |             }
129 |         }
130 |         EiEvent::KeyboardKey(key) => {
131 |             let key_event = KeyboardEvent::Key {
132 |                 key: key.key,
133 |                 state: match key.state {
134 |                     KeyState::Press => 1,
135 |                     KeyState::Released => 0,
136 |                 },
137 |                 time: key.time as u32,
138 |             };
139 |             Events::One(Event::Keyboard(key_event))
140 |         }
141 |         EiEvent::TouchDown(_) => Events::None,   /* TODO */
142 |         EiEvent::TouchUp(_) => Events::None,     /* TODO */
143 |         EiEvent::TouchMotion(_) => Events::None, /* TODO */
144 |         _ => Events::None,
145 |     }
146 | }
147 | 


--------------------------------------------------------------------------------
/lan-mouse-cli/Cargo.toml:
--------------------------------------------------------------------------------
 1 | [package]
 2 | name = "lan-mouse-cli"
 3 | description = "CLI Frontend for lan-mouse"
 4 | version = "0.2.0"
 5 | edition = "2021"
 6 | license = "GPL-3.0-or-later"
 7 | repository = "https://github.com/feschber/lan-mouse"
 8 | 
 9 | [dependencies]
10 | futures = "0.3.30"
11 | lan-mouse-ipc = { path = "../lan-mouse-ipc", version = "0.2.0" }
12 | clap = { version = "4.4.11", features = ["derive"] }
13 | thiserror = "2.0.0"
14 | tokio = { version = "1.32.0", features = [
15 |     "io-util",
16 |     "io-std",
17 |     "macros",
18 |     "net",
19 |     "rt",
20 | ] }
21 | 


--------------------------------------------------------------------------------
/lan-mouse-cli/src/lib.rs:
--------------------------------------------------------------------------------
  1 | use clap::{Args, Parser, Subcommand};
  2 | use futures::StreamExt;
  3 | 
  4 | use std::{net::IpAddr, time::Duration};
  5 | use thiserror::Error;
  6 | 
  7 | use lan_mouse_ipc::{
  8 |     connect_async, ClientHandle, ConnectionError, FrontendEvent, FrontendRequest, IpcError,
  9 |     Position,
 10 | };
 11 | 
 12 | #[derive(Debug, Error)]
 13 | pub enum CliError {
 14 |     /// is the service running?
 15 |     #[error("could not connect: `{0}` - is the service running?")]
 16 |     ServiceNotRunning(#[from] ConnectionError),
 17 |     #[error("error communicating with service: {0}")]
 18 |     Ipc(#[from] IpcError),
 19 | }
 20 | 
 21 | #[derive(Parser, Clone, Debug, PartialEq, Eq)]
 22 | #[command(name = "lan-mouse-cli", about = "LanMouse CLI interface")]
 23 | pub struct CliArgs {
 24 |     #[command(subcommand)]
 25 |     command: CliSubcommand,
 26 | }
 27 | 
 28 | #[derive(Args, Clone, Debug, PartialEq, Eq)]
 29 | struct Client {
 30 |     #[arg(long)]
 31 |     hostname: Option<String>,
 32 |     #[arg(long)]
 33 |     port: Option<u16>,
 34 |     #[arg(long)]
 35 |     ips: Option<Vec<IpAddr>>,
 36 |     #[arg(long)]
 37 |     enter_hook: Option<String>,
 38 | }
 39 | 
 40 | #[derive(Clone, Subcommand, Debug, PartialEq, Eq)]
 41 | enum CliSubcommand {
 42 |     /// add a new client
 43 |     AddClient(Client),
 44 |     /// remove an existing client
 45 |     RemoveClient { id: ClientHandle },
 46 |     /// activate a client
 47 |     Activate { id: ClientHandle },
 48 |     /// deactivate a client
 49 |     Deactivate { id: ClientHandle },
 50 |     /// list configured clients
 51 |     List,
 52 |     /// change hostname
 53 |     SetHost {
 54 |         id: ClientHandle,
 55 |         host: Option<String>,
 56 |     },
 57 |     /// change port
 58 |     SetPort { id: ClientHandle, port: u16 },
 59 |     /// set position
 60 |     SetPosition { id: ClientHandle, pos: Position },
 61 |     /// set ips
 62 |     SetIps { id: ClientHandle, ips: Vec<IpAddr> },
 63 |     /// re-enable capture
 64 |     EnableCapture,
 65 |     /// re-enable emulation
 66 |     EnableEmulation,
 67 |     /// authorize a public key
 68 |     AuthorizeKey {
 69 |         description: String,
 70 |         sha256_fingerprint: String,
 71 |     },
 72 |     /// deauthorize a public key
 73 |     RemoveAuthorizedKey { sha256_fingerprint: String },
 74 | }
 75 | 
 76 | pub async fn run(args: CliArgs) -> Result<(), CliError> {
 77 |     execute(args.command).await?;
 78 |     Ok(())
 79 | }
 80 | 
 81 | async fn execute(cmd: CliSubcommand) -> Result<(), CliError> {
 82 |     let (mut rx, mut tx) = connect_async(Some(Duration::from_millis(500))).await?;
 83 |     match cmd {
 84 |         CliSubcommand::AddClient(Client {
 85 |             hostname,
 86 |             port,
 87 |             ips,
 88 |             enter_hook,
 89 |         }) => {
 90 |             tx.request(FrontendRequest::Create).await?;
 91 |             while let Some(e) = rx.next().await {
 92 |                 if let FrontendEvent::Created(handle, _, _) = e? {
 93 |                     if let Some(hostname) = hostname {
 94 |                         tx.request(FrontendRequest::UpdateHostname(handle, Some(hostname)))
 95 |                             .await?;
 96 |                     }
 97 |                     if let Some(port) = port {
 98 |                         tx.request(FrontendRequest::UpdatePort(handle, port))
 99 |                             .await?;
100 |                     }
101 |                     if let Some(ips) = ips {
102 |                         tx.request(FrontendRequest::UpdateFixIps(handle, ips))
103 |                             .await?;
104 |                     }
105 |                     if let Some(enter_hook) = enter_hook {
106 |                         tx.request(FrontendRequest::UpdateEnterHook(handle, Some(enter_hook)))
107 |                             .await?;
108 |                     }
109 |                     break;
110 |                 }
111 |             }
112 |         }
113 |         CliSubcommand::RemoveClient { id } => tx.request(FrontendRequest::Delete(id)).await?,
114 |         CliSubcommand::Activate { id } => tx.request(FrontendRequest::Activate(id, true)).await?,
115 |         CliSubcommand::Deactivate { id } => {
116 |             tx.request(FrontendRequest::Activate(id, false)).await?
117 |         }
118 |         CliSubcommand::List => {
119 |             tx.request(FrontendRequest::Enumerate()).await?;
120 |             while let Some(e) = rx.next().await {
121 |                 if let FrontendEvent::Enumerate(clients) = e? {
122 |                     for (handle, config, state) in clients {
123 |                         let host = config.hostname.unwrap_or("unknown".to_owned());
124 |                         let port = config.port;
125 |                         let pos = config.pos;
126 |                         let active = state.active;
127 |                         let ips = state.ips;
128 |                         println!(
129 |                             "id {handle}: {host}:{port} ({pos}) active: {active}, ips: {ips:?}"
130 |                         );
131 |                     }
132 |                     break;
133 |                 }
134 |             }
135 |         }
136 |         CliSubcommand::SetHost { id, host } => {
137 |             tx.request(FrontendRequest::UpdateHostname(id, host))
138 |                 .await?
139 |         }
140 |         CliSubcommand::SetPort { id, port } => {
141 |             tx.request(FrontendRequest::UpdatePort(id, port)).await?
142 |         }
143 |         CliSubcommand::SetPosition { id, pos } => {
144 |             tx.request(FrontendRequest::UpdatePosition(id, pos)).await?
145 |         }
146 |         CliSubcommand::SetIps { id, ips } => {
147 |             tx.request(FrontendRequest::UpdateFixIps(id, ips)).await?
148 |         }
149 |         CliSubcommand::EnableCapture => tx.request(FrontendRequest::EnableCapture).await?,
150 |         CliSubcommand::EnableEmulation => tx.request(FrontendRequest::EnableEmulation).await?,
151 |         CliSubcommand::AuthorizeKey {
152 |             description,
153 |             sha256_fingerprint,
154 |         } => {
155 |             tx.request(FrontendRequest::AuthorizeKey(
156 |                 description,
157 |                 sha256_fingerprint,
158 |             ))
159 |             .await?
160 |         }
161 |         CliSubcommand::RemoveAuthorizedKey { sha256_fingerprint } => {
162 |             tx.request(FrontendRequest::RemoveAuthorizedKey(sha256_fingerprint))
163 |                 .await?
164 |         }
165 |     }
166 |     Ok(())
167 | }
168 | 


--------------------------------------------------------------------------------
/lan-mouse-gtk/Cargo.toml:
--------------------------------------------------------------------------------
 1 | [package]
 2 | name = "lan-mouse-gtk"
 3 | description = "GTK4 / Libadwaita Frontend for lan-mouse"
 4 | version = "0.2.0"
 5 | edition = "2021"
 6 | license = "GPL-3.0-or-later"
 7 | repository = "https://github.com/feschber/lan-mouse"
 8 | 
 9 | [dependencies]
10 | gtk = { package = "gtk4", version = "0.9.0", features = ["v4_2"] }
11 | adw = { package = "libadwaita", version = "0.7.0", features = ["v1_1"] }
12 | async-channel = { version = "2.1.1" }
13 | hostname = "0.4.0"
14 | log = "0.4.20"
15 | lan-mouse-ipc = { path = "../lan-mouse-ipc", version = "0.2.0" }
16 | thiserror = "2.0.0"
17 | 
18 | [build-dependencies]
19 | glib-build-tools = { version = "0.20.0" }
20 | 


--------------------------------------------------------------------------------
/lan-mouse-gtk/build.rs:
--------------------------------------------------------------------------------
1 | fn main() {
2 |     // composite_templates
3 |     glib_build_tools::compile_resources(
4 |         &["resources"],
5 |         "resources/resources.gresource.xml",
6 |         "lan-mouse.gresource",
7 |     );
8 | }
9 | 


--------------------------------------------------------------------------------
/lan-mouse-gtk/resources/authorization_window.ui:
--------------------------------------------------------------------------------
  1 | <?xml version="1.0" encoding="UTF-8"?>
  2 | <interface>
  3 |   <requires lib="gtk" version="4.0"/>
  4 |   <requires lib="libadwaita" version="1.0"/>
  5 |   <template class="AuthorizationWindow" parent="AdwWindow">
  6 |     <property name="modal">True</property>
  7 |     <property name="width-request">180</property>
  8 |     <property name="default-width">180</property>
  9 |     <property name="height-request">180</property>
 10 |     <property name="default-height">180</property>
 11 |     <property name="title" translatable="yes">Unauthorized Device</property>
 12 |     <property name="content">
 13 |       <object class="GtkBox">
 14 |         <property name="orientation">vertical</property>
 15 |         <property name="vexpand">True</property>
 16 |         <child type="top">
 17 |           <object class="AdwHeaderBar">
 18 |             <style>
 19 |               <class name="flat"/>
 20 |             </style>
 21 |           </object>
 22 |         </child>
 23 |         <child>
 24 |           <object class="GtkBox">
 25 |             <property name="orientation">vertical</property>
 26 |             <property name="spacing">30</property>
 27 |             <property name="margin-start">30</property>
 28 |             <property name="margin-end">30</property>
 29 |             <property name="margin-top">30</property>
 30 |             <property name="margin-bottom">30</property>
 31 |             <child>
 32 |               <object class="GtkLabel">
 33 |                 <property name="label">An unauthorized Device is trying to connect. Do you want to authorize this Device?</property>
 34 |                 <property name="width-request">100</property>
 35 |                 <property name="wrap">word-wrap</property>
 36 |               </object>
 37 |             </child>
 38 |             <child>
 39 |               <object class="AdwPreferencesGroup">
 40 |                 <property name="title">sha256 fingerprint</property>
 41 |                 <child>
 42 |                   <object class="AdwActionRow">
 43 |                     <property name="child">
 44 |                       <object class="GtkLabel" id="fingerprint">
 45 |                         <property name="ellipsize">PANGO_ELLIPSIZE_NONE</property>
 46 |                         <property name="vexpand">True</property>
 47 |                         <property name="hexpand">False</property>
 48 |                         <property name="wrap">True</property>
 49 |                         <property name="wrap-mode">word-char</property>
 50 |                         <property name="justify">center</property>
 51 |                         <property name="xalign">0.5</property>
 52 |                         <property name="margin-top">10</property>
 53 |                         <property name="margin-bottom">10</property>
 54 |                         <property name="margin-start">10</property>
 55 |                         <property name="margin-end">10</property>
 56 |                         <property name="width-chars">64</property>
 57 |                       </object>
 58 |                     </property>
 59 |                   </object>
 60 |                 </child>
 61 |               </object>
 62 |             </child>
 63 |           </object>
 64 |         </child>
 65 |         <child>
 66 |           <object class="GtkBox">
 67 |             <property name="margin-start">30</property>
 68 |             <property name="margin-end">30</property>
 69 |             <property name="margin-top">30</property>
 70 |             <property name="margin-bottom">30</property>
 71 |             <property name="orientation">horizontal</property>
 72 |             <property name="spacing">30</property>
 73 |             <property name="hexpand">True</property>
 74 |             <property name="vexpand">True</property>
 75 |             <property name="valign">end</property>
 76 |             <child>
 77 |               <object class="GtkButton" id="cancel_button">
 78 |                 <signal name="clicked" handler="handle_cancel" swapped="true"/>
 79 |                 <property name="label" translatable="yes">Cancel</property>
 80 |                 <property name="can-shrink">True</property>
 81 |                 <property name="height-request">50</property>
 82 |                 <property name="hexpand">True</property>
 83 |               </object>
 84 |             </child>
 85 |             <child>
 86 |               <object class="GtkButton" id="confirm_button">
 87 |                 <signal name="clicked" handler="handle_confirm" swapped="true"/>
 88 |                 <property name="label" translatable="yes">Authorize</property>
 89 |                 <property name="can-shrink">True</property>
 90 |                 <property name="height-request">50</property>
 91 |                 <property name="hexpand">True</property>
 92 |                 <style>
 93 |                   <class name="destructive-action"/>
 94 |                 </style>
 95 |               </object>
 96 |             </child>
 97 |           </object>
 98 |         </child>
 99 |       </object>
100 |     </property>
101 |   </template>
102 | </interface>
103 | 


--------------------------------------------------------------------------------
/lan-mouse-gtk/resources/client_row.ui:
--------------------------------------------------------------------------------
 1 | <?xml version="1.0" encoding="UTF-8"?>
 2 | <interface>
 3 | 	<template class="ClientRow" parent="AdwExpanderRow">
 4 | 		<property name="title">hostname</property>
 5 | 		<!-- enabled -->
 6 | 		<child type="prefix">
 7 | 			<object class="GtkSwitch" id="enable_switch">
 8 | 				<property name="valign">center</property>
 9 | 				<property name="halign">end</property>
10 | 				<property name="tooltip-text" translatable="yes">enable</property>
11 | 			</object>
12 | 		</child>
13 | 		<child type="suffix">
14 | 			<object class="GtkButton" id="dns_button">
15 | 				<signal name="clicked" handler="handle_request_dns" swapped="true"/>
16 | 				<!--<property name="icon-name">network-wired-disconnected-symbolic</property>-->
17 | 				<property name="icon-name">network-wired-symbolic</property>
18 | 				<property name="valign">center</property>
19 | 				<property name="halign">end</property>
20 | 				<property name="tooltip-text" translatable="yes">resolve host</property>
21 | 			</object>
22 | 		</child>
23 | 		<child type="suffix">
24 | 			<object class="GtkSpinner" id="dns_loading_indicator">
25 | 			</object>
26 | 		</child>
27 | 		<!-- host -->
28 | 		<child>
29 | 			<object class="AdwActionRow">
30 | 				<property name="title">hostname</property>
31 | 				<property name="subtitle">port</property>
32 | 				<!-- hostname -->
33 | 				<child>
34 | 					<object class="GtkEntry" id="hostname">
35 | 						<!-- <property name="title" translatable="yes">hostname</property> -->
36 | 						<property name="xalign">0.5</property>
37 | 						<property name="valign">center</property>
38 | 						<property name="placeholder-text">hostname</property>
39 | 						<property name="width-chars">-1</property>
40 | 					</object>
41 | 				</child>
42 | 				<!-- port -->
43 | 				<child>
44 | 					<object class="GtkEntry" id="port">
45 | 						<!-- <property name="title" translatable="yes">port</property> -->
46 |                         <property name="max-width-chars">5</property>
47 | 						<property name="input_purpose">GTK_INPUT_PURPOSE_NUMBER</property>
48 | 						<property name="xalign">0.5</property>
49 | 						<property name="valign">center</property>
50 | 						<property name="placeholder-text">4242</property>
51 | 						<property name="width-chars">5</property>
52 | 					</object>
53 | 				</child>
54 | 			</object>
55 | 		</child>
56 | 		<!-- position -->
57 | 		<child>
58 | 			<object class="AdwComboRow" id="position">
59 | 				<property name="title" translatable="yes">position</property>
60 | 				<property name="model">
61 | 					<object class="GtkStringList">
62 | 						<items>
63 | 							<item>Left</item>
64 | 							<item>Right</item>
65 | 							<item>Top</item>
66 | 							<item>Bottom</item>
67 | 						</items>
68 | 					</object>
69 | 				</property>
70 | 			</object>
71 | 		</child>
72 | 		<!-- delete button -->
73 | 		<child>
74 | 			<object class="AdwActionRow" id="delete_row">
75 | 				<property name="title">delete this client</property>
76 | 				<child>
77 | 					<object class="GtkButton" id="delete_button">
78 | 						<signal name="activate" handler="handle_client_delete" object="delete_row" swapped="true"/>
79 | 						<property name="icon-name">user-trash-symbolic</property>
80 | 						<property name="valign">center</property>
81 | 						<property name="halign">center</property>
82 | 						<property name="name">delete-button</property>
83 | 						<style><class name="error"/></style>
84 | 					</object>
85 | 				</child>
86 | 			</object>
87 | 		</child>
88 | 	</template>
89 | </interface>
90 | 


--------------------------------------------------------------------------------
/lan-mouse-gtk/resources/de.feschber.LanMouse.svg:
--------------------------------------------------------------------------------
  1 | <?xml version="1.0" encoding="UTF-8" standalone="no"?>
  2 | <!-- Created with Inkscape (http://www.inkscape.org/) -->
  3 | 
  4 | <svg
  5 |    width="48"
  6 |    height="48"
  7 |    viewBox="0 0 12.7 12.7"
  8 |    version="1.1"
  9 |    id="svg1"
 10 |    inkscape:version="1.3 (0e150ed6c4, 2023-07-21)"
 11 |    sodipodi:docname="mouse-icon.svg"
 12 |    xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
 13 |    xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
 14 |    xmlns="http://www.w3.org/2000/svg"
 15 |    xmlns:svg="http://www.w3.org/2000/svg">
 16 |   <sodipodi:namedview
 17 |      id="namedview1"
 18 |      pagecolor="#ffffff"
 19 |      bordercolor="#000000"
 20 |      borderopacity="0.25"
 21 |      inkscape:showpageshadow="2"
 22 |      inkscape:pageopacity="0.0"
 23 |      inkscape:pagecheckerboard="0"
 24 |      inkscape:deskcolor="#d1d1d1"
 25 |      inkscape:document-units="mm"
 26 |      inkscape:zoom="22.737887"
 27 |      inkscape:cx="19.54887"
 28 |      inkscape:cy="26.167778"
 29 |      inkscape:window-width="2560"
 30 |      inkscape:window-height="1374"
 31 |      inkscape:window-x="0"
 32 |      inkscape:window-y="0"
 33 |      inkscape:window-maximized="1"
 34 |      inkscape:current-layer="layer1" />
 35 |   <defs
 36 |      id="defs1" />
 37 |   <g
 38 |      inkscape:label="Layer 1"
 39 |      inkscape:groupmode="layer"
 40 |      id="layer1">
 41 |     <g
 42 |        id="g20"
 43 |        transform="translate(1.1586889,0.39019296)">
 44 |       <g
 45 |          id="g8"
 46 |          transform="translate(-0.11519282,-3.9659242)">
 47 |         <g
 48 |            id="g6"
 49 |            transform="translate(0.67275315,0.39959697)">
 50 |           <g
 51 |              id="g5">
 52 |             <rect
 53 |                style="fill:none;stroke:#000000;stroke-width:0.264999;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;paint-order:normal"
 54 |                id="rect4"
 55 |                width="1.3032579"
 56 |                height="1.3032579"
 57 |                x="1.7199994"
 58 |                y="7.5408325"
 59 |                ry="0.3373504" />
 60 |             <rect
 61 |                style="fill:none;stroke:#000000;stroke-width:0.264999;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;paint-order:normal"
 62 |                id="rect4-2"
 63 |                width="1.3032579"
 64 |                height="1.3032579"
 65 |                x="3.8428385"
 66 |                y="7.5408325"
 67 |                ry="0.3373504" />
 68 |           </g>
 69 |           <rect
 70 |              style="fill:none;stroke:#000000;stroke-width:0.264999;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;paint-order:normal"
 71 |              id="rect4-3"
 72 |              width="1.3032579"
 73 |              height="1.3032579"
 74 |              x="2.781419"
 75 |              y="5.1382394"
 76 |              ry="0.3373504" />
 77 |         </g>
 78 |         <path
 79 |            style="fill:none;stroke:#000000;stroke-width:0.264999;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;paint-order:normal"
 80 |            d="M 1.1519282,7.3907619 H 7.059674"
 81 |            id="path5" />
 82 |         <path
 83 |            style="fill:none;stroke:#000000;stroke-width:0.264999;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;paint-order:normal"
 84 |            d="M 4.1058009,6.8410941 V 7.3907617"
 85 |            id="path6" />
 86 |         <path
 87 |            style="fill:none;stroke:#000000;stroke-width:0.264999;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;paint-order:normal"
 88 |            d="m 5.1672204,7.9404294 2e-7,-0.5496677"
 89 |            id="path7" />
 90 |         <path
 91 |            style="fill:none;stroke:#000000;stroke-width:0.264999;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;paint-order:normal"
 92 |            d="M 3.0443815,7.9404294 V 7.3907617"
 93 |            id="path8" />
 94 |       </g>
 95 |       <path
 96 |          style="fill:none;stroke:#000000;stroke-width:0.264999;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;paint-order:normal"
 97 |          d="M 6.9444811,3.4248375 Z"
 98 |          id="path9" />
 99 |       <path
100 |          style="color:#000000;fill:#000000;stroke-linecap:round;stroke-linejoin:round;-inkscape-stroke:none"
101 |          d="m 6.9840449,3.4464199 c -0.072714,-0.0035 -0.1209639,-0.2113583 -0.125,-0.1386718 -0.0035,0.072714 0.052314,0.1346357 0.125,0.1386718 0,0 0.6614057,0.034643 1.3535156,0.4765625 0.6921097,0.4419191 1.4111567,1.2803292 1.5136717,2.9433594 0.05132,0.832563 -0.07521,1.3855916 -0.279297,1.75 -0.20409,0.3644084 -0.482943,0.5482749 -0.777343,0.640625 -0.5888014,0.1847002 -1.2265629,-0.021484 -1.2265629,-0.021484 -0.069024,-0.023541 -0.144095,0.013122 -0.1679688,0.082031 -0.023366,0.069587 0.014295,0.1449093 0.083984,0.1679687 0,0 0.6961634,0.2406696 1.3886717,0.023437 C 9.2189712,9.4003039 9.5672292,9.1706004 9.8043572,8.7472012 10.041486,8.323802 10.170261,7.7150888 10.116858,6.8487637 10.009921,5.1140179 9.2320232,4.3532014 8.4801387,3.8731154 7.7282538,3.3930294 6.9840449,3.4464198 6.9840449,3.4464199 Z"
102 |          id="path18"
103 |          sodipodi:nodetypes="cccsssscccssssc" />
104 |       <g
105 |          id="g19"
106 |          transform="matrix(1.8148709,0,0,1.8148709,-4.1533763,-7.8818885)">
107 |         <g
108 |            id="g17"
109 |            transform="translate(0.01163623,0.23038484)">
110 |           <ellipse
111 |              style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.264999;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;paint-order:normal"
112 |              id="path10"
113 |              cx="3.9823804"
114 |              cy="8.17869"
115 |              rx="0.49368349"
116 |              ry="0.62533247" />
117 |           <ellipse
118 |              style="fill:#3d3d3d;fill-opacity:1;stroke:none;stroke-width:0.168876;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;paint-order:normal"
119 |              id="ellipse17"
120 |              cx="3.9823804"
121 |              cy="8.17869"
122 |              rx="0.31096464"
123 |              ry="0.40317491" />
124 |         </g>
125 |         <path
126 |            id="path11"
127 |            style="stroke-width:0.264999;stroke-linecap:round;stroke-linejoin:round"
128 |            d="M 7.479305,9.4704944 C 7.4964603,9.9336885 6.9306558,9.9678313 5.3811502,10.087599 3.2109768,10.255341 2.4751992,9.6707727 2.4355055,9.5280908 2.3112754,9.0815374 3.8270232,8.4090748 5.3811502,8.4090748 c 1.5633309,0 2.0816988,0.6171052 2.0981548,1.0614196 z"
129 |            sodipodi:nodetypes="sssss" />
130 |         <circle
131 |            style="fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:0.264999;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;paint-order:normal"
132 |            id="path12"
133 |            cx="3.5281858"
134 |            cy="9.0632057"
135 |            r="0.18513133" />
136 |         <g
137 |            id="g18"
138 |            transform="translate(0.01163623,0.23038484)">
139 |           <ellipse
140 |              style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.264999;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;paint-order:normal"
141 |              id="path10-2"
142 |              cx="4.6085634"
143 |              cy="8.17869"
144 |              rx="0.49368349"
145 |              ry="0.62533247" />
146 |           <ellipse
147 |              style="fill:#3d3d3d;fill-opacity:1;stroke:none;stroke-width:0.168876;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;paint-order:normal"
148 |              id="ellipse16"
149 |              cx="4.6085634"
150 |              cy="8.17869"
151 |              rx="0.31096464"
152 |              ry="0.40317491" />
153 |         </g>
154 |         <ellipse
155 |            style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.112226;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;paint-order:normal"
156 |            id="circle18"
157 |            cx="3.5003331"
158 |            cy="9.0344076"
159 |            rx="0.078639306"
160 |            ry="0.07816644" />
161 |         <ellipse
162 |            style="fill:#4f4f4f;fill-opacity:1;stroke:none;stroke-width:0.264999;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1;paint-order:normal"
163 |            id="path19"
164 |            cx="2.4818404"
165 |            cy="9.4499254"
166 |            rx="0.05348238"
167 |            ry="0.11930636" />
168 |       </g>
169 |     </g>
170 |   </g>
171 | </svg>
172 | 


--------------------------------------------------------------------------------
/lan-mouse-gtk/resources/fingerprint_window.ui:
--------------------------------------------------------------------------------
  1 | <?xml version="1.0" encoding="UTF-8"?>
  2 | <interface>
  3 |   <requires lib="gtk" version="4.0"/>
  4 |   <requires lib="libadwaita" version="1.0"/>
  5 |   <template class="FingerprintWindow" parent="AdwWindow">
  6 |     <property name="modal">True</property>
  7 |     <property name="width-request">880</property>
  8 |     <property name="default-width">880</property>
  9 |     <property name="height-request">380</property>
 10 |     <property name="default-height">380</property>
 11 |     <property name="title" translatable="yes">Add Certificate Fingerprint</property>
 12 |     <property name="content">
 13 |       <object class="AdwToolbarView">
 14 |         <child type="top">
 15 |             <object class="AdwHeaderBar"/>
 16 |         </child>
 17 |         <property name="content">
 18 |           <object class="AdwClamp">
 19 |             <property name="maximum-size">770</property>
 20 |             <property name="tightening-threshold">0</property>
 21 |             <property name="child">
 22 |               <object class="GtkBox">
 23 |                 <property name="orientation">vertical</property>
 24 |                 <property name="spacing">18</property>
 25 |                 <child>
 26 |                   <object class="GtkLabel">
 27 |                     <property name="label">The certificate fingerprint serves as a unique identifier for your device.</property>
 28 |                   </object>
 29 |                 </child>
 30 |                 <child>
 31 |                   <object class="GtkLabel">
 32 |                     <property name="label">You can find it under the `General` section of the device you want to connect</property>
 33 |                   </object>
 34 |                 </child>
 35 |                 <child>
 36 |                   <object class="AdwPreferencesGroup">
 37 |                     <property name="title">description</property>
 38 |                     <child>
 39 |                       <object class="AdwActionRow">
 40 |                         <property name="child">
 41 |                           <object class="GtkText" id="description">
 42 |                             <property name="margin-top">10</property>
 43 |                             <property name="margin-bottom">10</property>
 44 |                             <property name="margin-start">10</property>
 45 |                             <property name="margin-end">10</property>
 46 |                             <property name="enable-undo">True</property>
 47 |                             <property name="hexpand">True</property>
 48 |                             <property name="vexpand">True</property>
 49 |                             <property name="max-length">0</property>
 50 |                           </object>
 51 |                         </property>
 52 |                       </object>
 53 |                     </child>
 54 |                   </object>
 55 |                 </child>
 56 |                 <child>
 57 |                   <object class="AdwPreferencesGroup">
 58 |                     <property name="title">sha256 fingerprint</property>
 59 |                     <child>
 60 |                       <object class="AdwActionRow">
 61 |                         <property name="child">
 62 |                           <object class="GtkText" id="fingerprint">
 63 |                             <property name="margin-top">10</property>
 64 |                             <property name="margin-bottom">10</property>
 65 |                             <property name="margin-start">10</property>
 66 |                             <property name="margin-end">10</property>
 67 |                             <property name="enable-undo">True</property>
 68 |                             <property name="hexpand">True</property>
 69 |                             <property name="vexpand">True</property>
 70 |                             <property name="max-length">0</property>
 71 |                           </object>
 72 |                         </property>
 73 |                       </object>
 74 |                     </child>
 75 |                   </object>
 76 |                 </child>
 77 |                 <child>
 78 |                   <object class="GtkBox">
 79 |                     <property name="orientation">vertical</property>
 80 |                     <property name="halign">center</property>
 81 |                     <child>
 82 |                       <object class="GtkButton" id="confirm_button">
 83 |                         <signal name="clicked" handler="handle_confirm" swapped="true"/>
 84 |                         <property name="label" translatable="yes">Confirm</property>
 85 |                         <property name="can-shrink">True</property>
 86 |                         <style>
 87 |                             <class name="pill"/>
 88 |                             <class name="suggested-action"/>
 89 |                         </style>
 90 |                       </object>
 91 |                     </child>
 92 |                   </object>
 93 |                 </child>
 94 |               </object>
 95 |             </property>
 96 |           </object>
 97 |         </property>
 98 |       </object>
 99 |     </property>
100 |   </template>
101 | </interface>
102 | 


--------------------------------------------------------------------------------
/lan-mouse-gtk/resources/key_row.ui:
--------------------------------------------------------------------------------
 1 | <?xml version="1.0" encoding="UTF-8"?>
 2 | <interface>
 3 | 	<template class="KeyRow" parent="AdwActionRow">
 4 | 		<child type="prefix">
 5 | 			<object class="GtkButton" id="delete_button">
 6 | 				<property name="valign">center</property>
 7 | 				<property name="halign">end</property>
 8 | 				<property name="tooltip-text" translatable="yes">revoke authorization</property>
 9 |                 <property name="icon-name">edit-delete-symbolic</property>
10 | 				<style>
11 |                     <class name="flat"/>
12 | 				</style>
13 | 			</object>
14 | 		</child>
15 | 	</template>
16 | </interface>
17 | 


--------------------------------------------------------------------------------
/lan-mouse-gtk/resources/resources.gresource.xml:
--------------------------------------------------------------------------------
 1 | <?xml version="1.0" encoding="UTF-8"?>
 2 | <gresources>
 3 |   <gresource prefix="/de/feschber/LanMouse">
 4 |     <file compressed="true" preprocess="xml-stripblanks">window.ui</file>
 5 |     <file compressed="true" preprocess="xml-stripblanks">authorization_window.ui</file>
 6 |     <file compressed="true" preprocess="xml-stripblanks">fingerprint_window.ui</file>
 7 |     <file compressed="true" preprocess="xml-stripblanks">client_row.ui</file>
 8 |     <file compressed="true" preprocess="xml-stripblanks">key_row.ui</file>
 9 |   </gresource>
10 |   <gresource prefix="/de/feschber/LanMouse/icons">
11 |     <file compressed="true" preprocess="xml-stripblanks">de.feschber.LanMouse.svg</file>
12 |   </gresource>
13 | </gresources>
14 | 


--------------------------------------------------------------------------------
/lan-mouse-gtk/src/authorization_window.rs:
--------------------------------------------------------------------------------
 1 | mod imp;
 2 | 
 3 | use glib::Object;
 4 | use gtk::{gio, glib, subclass::prelude::ObjectSubclassIsExt};
 5 | 
 6 | glib::wrapper! {
 7 |     pub struct AuthorizationWindow(ObjectSubclass<imp::AuthorizationWindow>)
 8 |     @extends adw::Window, gtk::Window, gtk::Widget,
 9 |     @implements gio::ActionGroup, gio::ActionMap, gtk::Accessible, gtk::Buildable,
10 |                 gtk::ConstraintTarget, gtk::Native, gtk::Root, gtk::ShortcutManager;
11 | }
12 | 
13 | impl AuthorizationWindow {
14 |     pub(crate) fn new(fingerprint: &str) -> Self {
15 |         let window: Self = Object::builder().build();
16 |         window.imp().set_fingerprint(fingerprint);
17 |         window
18 |     }
19 | }
20 | 


--------------------------------------------------------------------------------
/lan-mouse-gtk/src/authorization_window/imp.rs:
--------------------------------------------------------------------------------
 1 | use std::sync::OnceLock;
 2 | 
 3 | use adw::prelude::*;
 4 | use adw::subclass::prelude::*;
 5 | use glib::subclass::InitializingObject;
 6 | use gtk::{
 7 |     glib::{self, subclass::Signal},
 8 |     template_callbacks, Button, CompositeTemplate, Label,
 9 | };
10 | 
11 | #[derive(CompositeTemplate, Default)]
12 | #[template(resource = "/de/feschber/LanMouse/authorization_window.ui")]
13 | pub struct AuthorizationWindow {
14 |     #[template_child]
15 |     pub fingerprint: TemplateChild<Label>,
16 |     #[template_child]
17 |     pub cancel_button: TemplateChild<Button>,
18 |     #[template_child]
19 |     pub confirm_button: TemplateChild<Button>,
20 | }
21 | 
22 | #[glib::object_subclass]
23 | impl ObjectSubclass for AuthorizationWindow {
24 |     const NAME: &'static str = "AuthorizationWindow";
25 |     const ABSTRACT: bool = false;
26 | 
27 |     type Type = super::AuthorizationWindow;
28 |     type ParentType = adw::Window;
29 | 
30 |     fn class_init(klass: &mut Self::Class) {
31 |         klass.bind_template();
32 |         klass.bind_template_callbacks();
33 |     }
34 | 
35 |     fn instance_init(obj: &InitializingObject<Self>) {
36 |         obj.init_template();
37 |     }
38 | }
39 | 
40 | #[template_callbacks]
41 | impl AuthorizationWindow {
42 |     #[template_callback]
43 |     fn handle_confirm(&self, _button: Button) {
44 |         let fp = self.fingerprint.text().as_str().trim().to_owned();
45 |         self.obj().emit_by_name("confirm-clicked", &[&fp])
46 |     }
47 | 
48 |     #[template_callback]
49 |     fn handle_cancel(&self, _: Button) {
50 |         self.obj().emit_by_name("cancel-clicked", &[])
51 |     }
52 | 
53 |     pub(super) fn set_fingerprint(&self, fingerprint: &str) {
54 |         self.fingerprint.set_text(fingerprint);
55 |     }
56 | }
57 | 
58 | impl ObjectImpl for AuthorizationWindow {
59 |     fn signals() -> &'static [Signal] {
60 |         static SIGNALS: OnceLock<Vec<Signal>> = OnceLock::new();
61 |         SIGNALS.get_or_init(|| {
62 |             vec![
63 |                 Signal::builder("confirm-clicked")
64 |                     .param_types([String::static_type()])
65 |                     .build(),
66 |                 Signal::builder("cancel-clicked").build(),
67 |             ]
68 |         })
69 |     }
70 | }
71 | 
72 | impl WidgetImpl for AuthorizationWindow {}
73 | impl WindowImpl for AuthorizationWindow {}
74 | impl ApplicationWindowImpl for AuthorizationWindow {}
75 | impl AdwWindowImpl for AuthorizationWindow {}
76 | 


--------------------------------------------------------------------------------
/lan-mouse-gtk/src/client_object.rs:
--------------------------------------------------------------------------------
 1 | mod imp;
 2 | 
 3 | use adw::subclass::prelude::*;
 4 | use gtk::glib::{self, Object};
 5 | 
 6 | use lan_mouse_ipc::{ClientConfig, ClientHandle, ClientState};
 7 | 
 8 | glib::wrapper! {
 9 |     pub struct ClientObject(ObjectSubclass<imp::ClientObject>);
10 | }
11 | 
12 | impl ClientObject {
13 |     pub fn new(handle: ClientHandle, client: ClientConfig, state: ClientState) -> Self {
14 |         Object::builder()
15 |             .property("handle", handle)
16 |             .property("hostname", client.hostname)
17 |             .property("port", client.port as u32)
18 |             .property("position", client.pos.to_string())
19 |             .property("active", state.active)
20 |             .property(
21 |                 "ips",
22 |                 state
23 |                     .ips
24 |                     .iter()
25 |                     .map(|ip| ip.to_string())
26 |                     .collect::<Vec<_>>(),
27 |             )
28 |             .property("resolving", state.resolving)
29 |             .build()
30 |     }
31 | 
32 |     pub fn get_data(&self) -> ClientData {
33 |         self.imp().data.borrow().clone()
34 |     }
35 | }
36 | 
37 | #[derive(Default, Clone)]
38 | pub struct ClientData {
39 |     pub handle: ClientHandle,
40 |     pub hostname: Option<String>,
41 |     pub port: u32,
42 |     pub active: bool,
43 |     pub position: String,
44 |     pub resolving: bool,
45 |     pub ips: Vec<String>,
46 | }
47 | 


--------------------------------------------------------------------------------
/lan-mouse-gtk/src/client_object/imp.rs:
--------------------------------------------------------------------------------
 1 | use std::cell::RefCell;
 2 | 
 3 | use glib::Properties;
 4 | use gtk::glib;
 5 | use gtk::prelude::*;
 6 | use gtk::subclass::prelude::*;
 7 | 
 8 | use lan_mouse_ipc::ClientHandle;
 9 | 
10 | use super::ClientData;
11 | 
12 | #[derive(Properties, Default)]
13 | #[properties(wrapper_type = super::ClientObject)]
14 | pub struct ClientObject {
15 |     #[property(name = "handle", get, set, type = ClientHandle, member = handle)]
16 |     #[property(name = "hostname", get, set, type = Option<String>, member = hostname)]
17 |     #[property(name = "port", get, set, type = u32, member = port, maximum = u16::MAX as u32)]
18 |     #[property(name = "active", get, set, type = bool, member = active)]
19 |     #[property(name = "position", get, set, type = String, member = position)]
20 |     #[property(name = "resolving", get, set, type = bool, member = resolving)]
21 |     #[property(name = "ips", get, set, type = Vec<String>, member = ips)]
22 |     pub data: RefCell<ClientData>,
23 | }
24 | 
25 | #[glib::object_subclass]
26 | impl ObjectSubclass for ClientObject {
27 |     const NAME: &'static str = "ClientObject";
28 |     type Type = super::ClientObject;
29 | }
30 | 
31 | #[glib::derived_properties]
32 | impl ObjectImpl for ClientObject {}
33 | 


--------------------------------------------------------------------------------
/lan-mouse-gtk/src/client_row.rs:
--------------------------------------------------------------------------------
  1 | mod imp;
  2 | 
  3 | use adw::prelude::*;
  4 | use adw::subclass::prelude::*;
  5 | use gtk::glib::{self, Object};
  6 | 
  7 | use lan_mouse_ipc::{Position, DEFAULT_PORT};
  8 | 
  9 | use super::ClientObject;
 10 | 
 11 | glib::wrapper! {
 12 |     pub struct ClientRow(ObjectSubclass<imp::ClientRow>)
 13 |     @extends gtk::ListBoxRow, gtk::Widget, adw::PreferencesRow, adw::ExpanderRow,
 14 |     @implements gtk::Accessible, gtk::Actionable, gtk::Buildable, gtk::ConstraintTarget;
 15 | }
 16 | 
 17 | impl ClientRow {
 18 |     pub fn new(client_object: &ClientObject) -> Self {
 19 |         let client_row: Self = Object::builder().build();
 20 |         client_row
 21 |             .imp()
 22 |             .client_object
 23 |             .borrow_mut()
 24 |             .replace(client_object.clone());
 25 |         client_row
 26 |     }
 27 | 
 28 |     pub fn bind(&self, client_object: &ClientObject) {
 29 |         let mut bindings = self.imp().bindings.borrow_mut();
 30 | 
 31 |         // bind client active to switch state
 32 |         let active_binding = client_object
 33 |             .bind_property("active", &self.imp().enable_switch.get(), "state")
 34 |             .sync_create()
 35 |             .build();
 36 | 
 37 |         // bind client active to switch position
 38 |         let switch_position_binding = client_object
 39 |             .bind_property("active", &self.imp().enable_switch.get(), "active")
 40 |             .sync_create()
 41 |             .build();
 42 | 
 43 |         // bind hostname to hostname edit field
 44 |         let hostname_binding = client_object
 45 |             .bind_property("hostname", &self.imp().hostname.get(), "text")
 46 |             .transform_to(|_, v: Option<String>| {
 47 |                 if let Some(hostname) = v {
 48 |                     Some(hostname)
 49 |                 } else {
 50 |                     Some("".to_string())
 51 |                 }
 52 |             })
 53 |             .sync_create()
 54 |             .build();
 55 | 
 56 |         // bind hostname to title
 57 |         let title_binding = client_object
 58 |             .bind_property("hostname", self, "title")
 59 |             .transform_to(|_, v: Option<String>| v.or(Some("<span font_style=\"italic\" font_weight=\"light\" foreground=\"darkgrey\">no hostname!</span>".to_string())))
 60 |             .sync_create()
 61 |             .build();
 62 | 
 63 |         // bind port to port edit field
 64 |         let port_binding = client_object
 65 |             .bind_property("port", &self.imp().port.get(), "text")
 66 |             .transform_to(|_, v: u32| {
 67 |                 if v == DEFAULT_PORT as u32 {
 68 |                     Some("".to_string())
 69 |                 } else {
 70 |                     Some(v.to_string())
 71 |                 }
 72 |             })
 73 |             .sync_create()
 74 |             .build();
 75 | 
 76 |         // bind port to subtitle
 77 |         let subtitle_binding = client_object
 78 |             .bind_property("port", self, "subtitle")
 79 |             .sync_create()
 80 |             .build();
 81 | 
 82 |         // bind position to selected position
 83 |         let position_binding = client_object
 84 |             .bind_property("position", &self.imp().position.get(), "selected")
 85 |             .transform_to(|_, v: String| match v.as_str() {
 86 |                 "right" => Some(1u32),
 87 |                 "top" => Some(2u32),
 88 |                 "bottom" => Some(3u32),
 89 |                 _ => Some(0u32),
 90 |             })
 91 |             .sync_create()
 92 |             .build();
 93 | 
 94 |         // bind resolving status to spinner visibility
 95 |         let resolve_binding = client_object
 96 |             .bind_property(
 97 |                 "resolving",
 98 |                 &self.imp().dns_loading_indicator.get(),
 99 |                 "spinning",
100 |             )
101 |             .sync_create()
102 |             .build();
103 | 
104 |         // bind ips to tooltip-text
105 |         let ip_binding = client_object
106 |             .bind_property("ips", &self.imp().dns_button.get(), "tooltip-text")
107 |             .transform_to(|_, ips: Vec<String>| {
108 |                 if ips.is_empty() {
109 |                     Some("no ip addresses associated with this client".into())
110 |                 } else {
111 |                     Some(ips.join("\n"))
112 |                 }
113 |             })
114 |             .sync_create()
115 |             .build();
116 | 
117 |         bindings.push(active_binding);
118 |         bindings.push(switch_position_binding);
119 |         bindings.push(hostname_binding);
120 |         bindings.push(title_binding);
121 |         bindings.push(port_binding);
122 |         bindings.push(subtitle_binding);
123 |         bindings.push(position_binding);
124 |         bindings.push(resolve_binding);
125 |         bindings.push(ip_binding);
126 |     }
127 | 
128 |     pub fn unbind(&self) {
129 |         for binding in self.imp().bindings.borrow_mut().drain(..) {
130 |             binding.unbind();
131 |         }
132 |     }
133 | 
134 |     pub fn set_active(&self, active: bool) {
135 |         self.imp().set_active(active);
136 |     }
137 | 
138 |     pub fn set_hostname(&self, hostname: Option<String>) {
139 |         self.imp().set_hostname(hostname);
140 |     }
141 | 
142 |     pub fn set_port(&self, port: u16) {
143 |         self.imp().set_port(port);
144 |     }
145 | 
146 |     pub fn set_position(&self, pos: Position) {
147 |         self.imp().set_pos(pos);
148 |     }
149 | 
150 |     pub fn set_dns_state(&self, resolved: bool) {
151 |         self.imp().set_dns_state(resolved);
152 |     }
153 | }
154 | 


--------------------------------------------------------------------------------
/lan-mouse-gtk/src/client_row/imp.rs:
--------------------------------------------------------------------------------
  1 | use std::cell::RefCell;
  2 | 
  3 | use adw::subclass::prelude::*;
  4 | use adw::{prelude::*, ActionRow, ComboRow};
  5 | use glib::{subclass::InitializingObject, Binding};
  6 | use gtk::glib::subclass::Signal;
  7 | use gtk::glib::{clone, SignalHandlerId};
  8 | use gtk::{glib, Button, CompositeTemplate, Entry, Switch};
  9 | use lan_mouse_ipc::Position;
 10 | use std::sync::OnceLock;
 11 | 
 12 | use crate::client_object::ClientObject;
 13 | 
 14 | #[derive(CompositeTemplate, Default)]
 15 | #[template(resource = "/de/feschber/LanMouse/client_row.ui")]
 16 | pub struct ClientRow {
 17 |     #[template_child]
 18 |     pub enable_switch: TemplateChild<gtk::Switch>,
 19 |     #[template_child]
 20 |     pub dns_button: TemplateChild<gtk::Button>,
 21 |     #[template_child]
 22 |     pub hostname: TemplateChild<gtk::Entry>,
 23 |     #[template_child]
 24 |     pub port: TemplateChild<gtk::Entry>,
 25 |     #[template_child]
 26 |     pub position: TemplateChild<ComboRow>,
 27 |     #[template_child]
 28 |     pub delete_row: TemplateChild<ActionRow>,
 29 |     #[template_child]
 30 |     pub delete_button: TemplateChild<gtk::Button>,
 31 |     #[template_child]
 32 |     pub dns_loading_indicator: TemplateChild<gtk::Spinner>,
 33 |     pub bindings: RefCell<Vec<Binding>>,
 34 |     hostname_change_handler: RefCell<Option<SignalHandlerId>>,
 35 |     port_change_handler: RefCell<Option<SignalHandlerId>>,
 36 |     position_change_handler: RefCell<Option<SignalHandlerId>>,
 37 |     set_state_handler: RefCell<Option<SignalHandlerId>>,
 38 |     pub client_object: RefCell<Option<ClientObject>>,
 39 | }
 40 | 
 41 | #[glib::object_subclass]
 42 | impl ObjectSubclass for ClientRow {
 43 |     // `NAME` needs to match `class` attribute of template
 44 |     const NAME: &'static str = "ClientRow";
 45 |     const ABSTRACT: bool = false;
 46 | 
 47 |     type Type = super::ClientRow;
 48 |     type ParentType = adw::ExpanderRow;
 49 | 
 50 |     fn class_init(klass: &mut Self::Class) {
 51 |         klass.bind_template();
 52 |         klass.bind_template_callbacks();
 53 |     }
 54 | 
 55 |     fn instance_init(obj: &InitializingObject<Self>) {
 56 |         obj.init_template();
 57 |     }
 58 | }
 59 | 
 60 | impl ObjectImpl for ClientRow {
 61 |     fn constructed(&self) {
 62 |         self.parent_constructed();
 63 |         self.delete_button.connect_clicked(clone!(
 64 |             #[weak(rename_to = row)]
 65 |             self,
 66 |             move |button| {
 67 |                 row.handle_client_delete(button);
 68 |             }
 69 |         ));
 70 |         let handler = self.hostname.connect_changed(clone!(
 71 |             #[weak(rename_to = row)]
 72 |             self,
 73 |             move |entry| {
 74 |                 row.handle_hostname_changed(entry);
 75 |             }
 76 |         ));
 77 |         self.hostname_change_handler.replace(Some(handler));
 78 |         let handler = self.port.connect_changed(clone!(
 79 |             #[weak(rename_to = row)]
 80 |             self,
 81 |             move |entry| {
 82 |                 row.handle_port_changed(entry);
 83 |             }
 84 |         ));
 85 |         self.port_change_handler.replace(Some(handler));
 86 |         let handler = self.position.connect_selected_notify(clone!(
 87 |             #[weak(rename_to = row)]
 88 |             self,
 89 |             move |position| {
 90 |                 row.handle_position_changed(position);
 91 |             }
 92 |         ));
 93 |         self.position_change_handler.replace(Some(handler));
 94 |         let handler = self.enable_switch.connect_state_set(clone!(
 95 |             #[weak(rename_to = row)]
 96 |             self,
 97 |             #[upgrade_or]
 98 |             glib::Propagation::Proceed,
 99 |             move |switch, state| {
100 |                 row.handle_activate_switch(state, switch);
101 |                 glib::Propagation::Proceed
102 |             }
103 |         ));
104 |         self.set_state_handler.replace(Some(handler));
105 |     }
106 | 
107 |     fn signals() -> &'static [glib::subclass::Signal] {
108 |         static SIGNALS: OnceLock<Vec<Signal>> = OnceLock::new();
109 |         SIGNALS.get_or_init(|| {
110 |             vec![
111 |                 Signal::builder("request-activate")
112 |                     .param_types([bool::static_type()])
113 |                     .build(),
114 |                 Signal::builder("request-delete").build(),
115 |                 Signal::builder("request-dns").build(),
116 |                 Signal::builder("request-hostname-change")
117 |                     .param_types([String::static_type()])
118 |                     .build(),
119 |                 Signal::builder("request-port-change")
120 |                     .param_types([u32::static_type()])
121 |                     .build(),
122 |                 Signal::builder("request-position-change")
123 |                     .param_types([u32::static_type()])
124 |                     .build(),
125 |             ]
126 |         })
127 |     }
128 | }
129 | 
130 | #[gtk::template_callbacks]
131 | impl ClientRow {
132 |     #[template_callback]
133 |     fn handle_activate_switch(&self, state: bool, _switch: &Switch) -> bool {
134 |         self.obj().emit_by_name::<()>("request-activate", &[&state]);
135 |         true // dont run default handler
136 |     }
137 | 
138 |     #[template_callback]
139 |     fn handle_request_dns(&self, _: &Button) {
140 |         self.obj().emit_by_name::<()>("request-dns", &[]);
141 |     }
142 | 
143 |     #[template_callback]
144 |     fn handle_client_delete(&self, _button: &Button) {
145 |         self.obj().emit_by_name::<()>("request-delete", &[]);
146 |     }
147 | 
148 |     fn handle_port_changed(&self, port_entry: &Entry) {
149 |         if let Ok(port) = port_entry.text().parse::<u16>() {
150 |             self.obj()
151 |                 .emit_by_name::<()>("request-port-change", &[&(port as u32)]);
152 |         }
153 |     }
154 | 
155 |     fn handle_hostname_changed(&self, hostname_entry: &Entry) {
156 |         self.obj()
157 |             .emit_by_name::<()>("request-hostname-change", &[&hostname_entry.text()]);
158 |     }
159 | 
160 |     fn handle_position_changed(&self, position: &ComboRow) {
161 |         self.obj()
162 |             .emit_by_name("request-position-change", &[&position.selected()])
163 |     }
164 | 
165 |     pub(super) fn set_hostname(&self, hostname: Option<String>) {
166 |         let position = self.hostname.position();
167 |         let handler = self.hostname_change_handler.borrow();
168 |         let handler = handler.as_ref().expect("signal handler");
169 |         self.hostname.block_signal(handler);
170 |         self.client_object
171 |             .borrow_mut()
172 |             .as_mut()
173 |             .expect("client object")
174 |             .set_property("hostname", hostname);
175 |         self.hostname.unblock_signal(handler);
176 |         self.hostname.set_position(position);
177 |     }
178 | 
179 |     pub(super) fn set_port(&self, port: u16) {
180 |         let position = self.port.position();
181 |         let handler = self.port_change_handler.borrow();
182 |         let handler = handler.as_ref().expect("signal handler");
183 |         self.port.block_signal(handler);
184 |         self.client_object
185 |             .borrow_mut()
186 |             .as_mut()
187 |             .expect("client object")
188 |             .set_port(port as u32);
189 |         self.port.unblock_signal(handler);
190 |         self.port.set_position(position);
191 |     }
192 | 
193 |     pub(super) fn set_pos(&self, pos: Position) {
194 |         let handler = self.position_change_handler.borrow();
195 |         let handler = handler.as_ref().expect("signal handler");
196 |         self.position.block_signal(handler);
197 |         self.client_object
198 |             .borrow_mut()
199 |             .as_mut()
200 |             .expect("client object")
201 |             .set_position(pos.to_string());
202 |         self.position.unblock_signal(handler);
203 |     }
204 | 
205 |     pub(super) fn set_active(&self, active: bool) {
206 |         let handler = self.set_state_handler.borrow();
207 |         let handler = handler.as_ref().expect("signal handler");
208 |         self.enable_switch.block_signal(handler);
209 |         self.client_object
210 |             .borrow_mut()
211 |             .as_mut()
212 |             .expect("client object")
213 |             .set_active(active);
214 |         self.enable_switch.unblock_signal(handler);
215 |     }
216 | 
217 |     pub(super) fn set_dns_state(&self, resolved: bool) {
218 |         if resolved {
219 |             self.dns_button.set_css_classes(&["success"])
220 |         } else {
221 |             self.dns_button.set_css_classes(&["warning"])
222 |         }
223 |     }
224 | }
225 | 
226 | impl WidgetImpl for ClientRow {}
227 | impl BoxImpl for ClientRow {}
228 | impl ListBoxRowImpl for ClientRow {}
229 | impl PreferencesRowImpl for ClientRow {}
230 | impl ExpanderRowImpl for ClientRow {}
231 | 


--------------------------------------------------------------------------------
/lan-mouse-gtk/src/fingerprint_window.rs:
--------------------------------------------------------------------------------
 1 | mod imp;
 2 | 
 3 | use glib::Object;
 4 | use gtk::{gio, glib, prelude::ObjectExt, subclass::prelude::ObjectSubclassIsExt};
 5 | 
 6 | glib::wrapper! {
 7 |     pub struct FingerprintWindow(ObjectSubclass<imp::FingerprintWindow>)
 8 |     @extends adw::Window, gtk::Window, gtk::Widget,
 9 |     @implements gio::ActionGroup, gio::ActionMap, gtk::Accessible, gtk::Buildable,
10 |                 gtk::ConstraintTarget, gtk::Native, gtk::Root, gtk::ShortcutManager;
11 | }
12 | 
13 | impl FingerprintWindow {
14 |     pub(crate) fn new(fingerprint: Option<String>) -> Self {
15 |         let window: Self = Object::builder().build();
16 |         if let Some(fp) = fingerprint {
17 |             window.imp().fingerprint.set_property("text", fp);
18 |             window.imp().fingerprint.set_property("editable", false);
19 |         }
20 |         window
21 |     }
22 | }
23 | 


--------------------------------------------------------------------------------
/lan-mouse-gtk/src/fingerprint_window/imp.rs:
--------------------------------------------------------------------------------
 1 | use std::sync::OnceLock;
 2 | 
 3 | use adw::prelude::*;
 4 | use adw::subclass::prelude::*;
 5 | use glib::subclass::InitializingObject;
 6 | use gtk::{
 7 |     glib::{self, subclass::Signal},
 8 |     template_callbacks, Button, CompositeTemplate, Text,
 9 | };
10 | 
11 | #[derive(CompositeTemplate, Default)]
12 | #[template(resource = "/de/feschber/LanMouse/fingerprint_window.ui")]
13 | pub struct FingerprintWindow {
14 |     #[template_child]
15 |     pub description: TemplateChild<Text>,
16 |     #[template_child]
17 |     pub fingerprint: TemplateChild<Text>,
18 |     #[template_child]
19 |     pub confirm_button: TemplateChild<Button>,
20 | }
21 | 
22 | #[glib::object_subclass]
23 | impl ObjectSubclass for FingerprintWindow {
24 |     const NAME: &'static str = "FingerprintWindow";
25 |     const ABSTRACT: bool = false;
26 | 
27 |     type Type = super::FingerprintWindow;
28 |     type ParentType = adw::Window;
29 | 
30 |     fn class_init(klass: &mut Self::Class) {
31 |         klass.bind_template();
32 |         klass.bind_template_callbacks();
33 |     }
34 | 
35 |     fn instance_init(obj: &InitializingObject<Self>) {
36 |         obj.init_template();
37 |     }
38 | }
39 | 
40 | #[template_callbacks]
41 | impl FingerprintWindow {
42 |     #[template_callback]
43 |     fn handle_confirm(&self, _button: Button) {
44 |         let desc = self.description.text().as_str().trim().to_owned();
45 |         let fp = self.fingerprint.text().as_str().trim().to_owned();
46 |         self.obj().emit_by_name("confirm-clicked", &[&desc, &fp])
47 |     }
48 | }
49 | 
50 | impl ObjectImpl for FingerprintWindow {
51 |     fn signals() -> &'static [Signal] {
52 |         static SIGNALS: OnceLock<Vec<Signal>> = OnceLock::new();
53 |         SIGNALS.get_or_init(|| {
54 |             vec![Signal::builder("confirm-clicked")
55 |                 .param_types([String::static_type(), String::static_type()])
56 |                 .build()]
57 |         })
58 |     }
59 | }
60 | 
61 | impl WidgetImpl for FingerprintWindow {}
62 | impl WindowImpl for FingerprintWindow {}
63 | impl ApplicationWindowImpl for FingerprintWindow {}
64 | impl AdwWindowImpl for FingerprintWindow {}
65 | 


--------------------------------------------------------------------------------
/lan-mouse-gtk/src/key_object.rs:
--------------------------------------------------------------------------------
 1 | mod imp;
 2 | 
 3 | use adw::subclass::prelude::*;
 4 | use gtk::glib::{self, Object};
 5 | 
 6 | glib::wrapper! {
 7 |     pub struct KeyObject(ObjectSubclass<imp::KeyObject>);
 8 | }
 9 | 
10 | impl KeyObject {
11 |     pub fn new(desc: String, fp: String) -> Self {
12 |         Object::builder()
13 |             .property("description", desc)
14 |             .property("fingerprint", fp)
15 |             .build()
16 |     }
17 | 
18 |     pub fn get_description(&self) -> String {
19 |         self.imp().description.borrow().clone()
20 |     }
21 | 
22 |     pub fn get_fingerprint(&self) -> String {
23 |         self.imp().fingerprint.borrow().clone()
24 |     }
25 | }
26 | 


--------------------------------------------------------------------------------
/lan-mouse-gtk/src/key_object/imp.rs:
--------------------------------------------------------------------------------
 1 | use std::cell::RefCell;
 2 | 
 3 | use glib::Properties;
 4 | use gtk::glib;
 5 | use gtk::prelude::*;
 6 | use gtk::subclass::prelude::*;
 7 | 
 8 | #[derive(Properties, Default)]
 9 | #[properties(wrapper_type = super::KeyObject)]
10 | pub struct KeyObject {
11 |     #[property(name = "description", get, set, type = String)]
12 |     pub description: RefCell<String>,
13 |     #[property(name = "fingerprint", get, set, type = String)]
14 |     pub fingerprint: RefCell<String>,
15 | }
16 | 
17 | #[glib::object_subclass]
18 | impl ObjectSubclass for KeyObject {
19 |     const NAME: &'static str = "KeyObject";
20 |     type Type = super::KeyObject;
21 | }
22 | 
23 | #[glib::derived_properties]
24 | impl ObjectImpl for KeyObject {}
25 | 


--------------------------------------------------------------------------------
/lan-mouse-gtk/src/key_row.rs:
--------------------------------------------------------------------------------
 1 | mod imp;
 2 | 
 3 | use adw::prelude::*;
 4 | use adw::subclass::prelude::*;
 5 | use gtk::glib::{self, Object};
 6 | 
 7 | use super::KeyObject;
 8 | 
 9 | glib::wrapper! {
10 |     pub struct KeyRow(ObjectSubclass<imp::KeyRow>)
11 |     @extends gtk::ListBoxRow, gtk::Widget, adw::PreferencesRow, adw::ActionRow,
12 |     @implements gtk::Accessible, gtk::Actionable, gtk::Buildable, gtk::ConstraintTarget;
13 | }
14 | 
15 | impl Default for KeyRow {
16 |     fn default() -> Self {
17 |         Self::new()
18 |     }
19 | }
20 | 
21 | impl KeyRow {
22 |     pub fn new() -> Self {
23 |         Object::builder().build()
24 |     }
25 | 
26 |     pub fn bind(&self, key_object: &KeyObject) {
27 |         let mut bindings = self.imp().bindings.borrow_mut();
28 | 
29 |         let title_binding = key_object
30 |             .bind_property("description", self, "title")
31 |             .sync_create()
32 |             .build();
33 | 
34 |         let subtitle_binding = key_object
35 |             .bind_property("fingerprint", self, "subtitle")
36 |             .sync_create()
37 |             .build();
38 | 
39 |         bindings.push(title_binding);
40 |         bindings.push(subtitle_binding);
41 |     }
42 | 
43 |     pub fn unbind(&self) {
44 |         for binding in self.imp().bindings.borrow_mut().drain(..) {
45 |             binding.unbind();
46 |         }
47 |     }
48 | }
49 | 


--------------------------------------------------------------------------------
/lan-mouse-gtk/src/key_row/imp.rs:
--------------------------------------------------------------------------------
 1 | use std::cell::RefCell;
 2 | 
 3 | use adw::subclass::prelude::*;
 4 | use adw::{prelude::*, ActionRow};
 5 | use glib::{subclass::InitializingObject, Binding};
 6 | use gtk::glib::clone;
 7 | use gtk::glib::subclass::Signal;
 8 | use gtk::{glib, Button, CompositeTemplate};
 9 | use std::sync::OnceLock;
10 | 
11 | #[derive(CompositeTemplate, Default)]
12 | #[template(resource = "/de/feschber/LanMouse/key_row.ui")]
13 | pub struct KeyRow {
14 |     #[template_child]
15 |     pub delete_button: TemplateChild<gtk::Button>,
16 |     pub bindings: RefCell<Vec<Binding>>,
17 | }
18 | 
19 | #[glib::object_subclass]
20 | impl ObjectSubclass for KeyRow {
21 |     // `NAME` needs to match `class` attribute of template
22 |     const NAME: &'static str = "KeyRow";
23 |     const ABSTRACT: bool = false;
24 | 
25 |     type Type = super::KeyRow;
26 |     type ParentType = ActionRow;
27 | 
28 |     fn class_init(klass: &mut Self::Class) {
29 |         klass.bind_template();
30 |         klass.bind_template_callbacks();
31 |     }
32 | 
33 |     fn instance_init(obj: &InitializingObject<Self>) {
34 |         obj.init_template();
35 |     }
36 | }
37 | 
38 | impl ObjectImpl for KeyRow {
39 |     fn constructed(&self) {
40 |         self.parent_constructed();
41 |         self.delete_button.connect_clicked(clone!(
42 |             #[weak(rename_to = row)]
43 |             self,
44 |             move |button| {
45 |                 row.handle_delete(button);
46 |             }
47 |         ));
48 |     }
49 | 
50 |     fn signals() -> &'static [glib::subclass::Signal] {
51 |         static SIGNALS: OnceLock<Vec<Signal>> = OnceLock::new();
52 |         SIGNALS.get_or_init(|| vec![Signal::builder("request-delete").build()])
53 |     }
54 | }
55 | 
56 | #[gtk::template_callbacks]
57 | impl KeyRow {
58 |     #[template_callback]
59 |     fn handle_delete(&self, _button: &Button) {
60 |         self.obj().emit_by_name::<()>("request-delete", &[]);
61 |     }
62 | }
63 | 
64 | impl WidgetImpl for KeyRow {}
65 | impl BoxImpl for KeyRow {}
66 | impl ListBoxRowImpl for KeyRow {}
67 | impl PreferencesRowImpl for KeyRow {}
68 | impl ActionRowImpl for KeyRow {}
69 | 


--------------------------------------------------------------------------------
/lan-mouse-gtk/src/lib.rs:
--------------------------------------------------------------------------------
  1 | mod authorization_window;
  2 | mod client_object;
  3 | mod client_row;
  4 | mod fingerprint_window;
  5 | mod key_object;
  6 | mod key_row;
  7 | mod window;
  8 | 
  9 | use std::{env, process, str};
 10 | 
 11 | use window::Window;
 12 | 
 13 | use lan_mouse_ipc::FrontendEvent;
 14 | 
 15 | use adw::Application;
 16 | use gtk::{gdk::Display, glib::clone, prelude::*, IconTheme};
 17 | use gtk::{gio, glib, prelude::ApplicationExt};
 18 | 
 19 | use self::client_object::ClientObject;
 20 | use self::key_object::KeyObject;
 21 | 
 22 | use thiserror::Error;
 23 | 
 24 | #[derive(Error, Debug)]
 25 | pub enum GtkError {
 26 |     #[error("gtk frontend exited with non zero exit code: {0}")]
 27 |     NonZeroExitCode(i32),
 28 | }
 29 | 
 30 | pub fn run() -> Result<(), GtkError> {
 31 |     log::debug!("running gtk frontend");
 32 |     #[cfg(windows)]
 33 |     let ret = std::thread::Builder::new()
 34 |         .stack_size(8 * 1024 * 1024) // https://gitlab.gnome.org/GNOME/gtk/-/commit/52dbb3f372b2c3ea339e879689c1de535ba2c2c3 -> caused crash on windows
 35 |         .name("gtk".into())
 36 |         .spawn(gtk_main)
 37 |         .unwrap()
 38 |         .join()
 39 |         .unwrap();
 40 |     #[cfg(not(windows))]
 41 |     let ret = gtk_main();
 42 | 
 43 |     match ret {
 44 |         glib::ExitCode::SUCCESS => Ok(()),
 45 |         e => Err(GtkError::NonZeroExitCode(e.value())),
 46 |     }
 47 | }
 48 | 
 49 | fn gtk_main() -> glib::ExitCode {
 50 |     gio::resources_register_include!("lan-mouse.gresource").expect("Failed to register resources.");
 51 | 
 52 |     let app = Application::builder()
 53 |         .application_id("de.feschber.LanMouse")
 54 |         .build();
 55 | 
 56 |     app.connect_startup(|app| {
 57 |         load_icons();
 58 |         setup_actions(app);
 59 |         setup_menu(app);
 60 |     });
 61 |     app.connect_activate(build_ui);
 62 | 
 63 |     let args: Vec<&'static str> = vec![];
 64 |     app.run_with_args(&args)
 65 | }
 66 | 
 67 | fn load_icons() {
 68 |     let display = &Display::default().expect("Could not connect to a display.");
 69 |     let icon_theme = IconTheme::for_display(display);
 70 |     icon_theme.add_resource_path("/de/feschber/LanMouse/icons");
 71 | }
 72 | 
 73 | // Add application actions
 74 | fn setup_actions(app: &adw::Application) {
 75 |     // Quit action
 76 |     // This is important on macOS, where users expect a File->Quit action with a Cmd+Q shortcut.
 77 |     let quit_action = gio::SimpleAction::new("quit", None);
 78 |     quit_action.connect_activate({
 79 |         let app = app.clone();
 80 |         move |_, _| {
 81 |             app.quit();
 82 |         }
 83 |     });
 84 |     app.add_action(&quit_action);
 85 | }
 86 | 
 87 | // Set up a global menu
 88 | //
 89 | // Currently this is used only on macOS
 90 | fn setup_menu(app: &adw::Application) {
 91 |     let menu = gio::Menu::new();
 92 | 
 93 |     let file_menu = gio::Menu::new();
 94 |     file_menu.append(Some("Quit"), Some("app.quit"));
 95 |     menu.append_submenu(Some("_File"), &file_menu);
 96 | 
 97 |     app.set_menubar(Some(&menu))
 98 | }
 99 | 
100 | fn build_ui(app: &Application) {
101 |     log::debug!("connecting to lan-mouse-socket");
102 |     let (mut frontend_rx, frontend_tx) = match lan_mouse_ipc::connect() {
103 |         Ok(conn) => conn,
104 |         Err(e) => {
105 |             log::error!("{e}");
106 |             process::exit(1);
107 |         }
108 |     };
109 |     log::debug!("connected to lan-mouse-socket");
110 | 
111 |     let (sender, receiver) = async_channel::bounded(10);
112 | 
113 |     gio::spawn_blocking(move || {
114 |         while let Some(e) = frontend_rx.next_event() {
115 |             match e {
116 |                 Ok(e) => sender.send_blocking(e).unwrap(),
117 |                 Err(e) => {
118 |                     log::error!("{e}");
119 |                     break;
120 |                 }
121 |             }
122 |         }
123 |     });
124 | 
125 |     let window = Window::new(app, frontend_tx);
126 | 
127 |     glib::spawn_future_local(clone!(
128 |         #[weak]
129 |         window,
130 |         async move {
131 |             loop {
132 |                 let notify = receiver.recv().await.unwrap_or_else(|_| process::exit(1));
133 |                 match notify {
134 |                     FrontendEvent::Created(handle, client, state) => {
135 |                         window.new_client(handle, client, state)
136 |                     }
137 |                     FrontendEvent::Deleted(client) => window.delete_client(client),
138 |                     FrontendEvent::State(handle, config, state) => {
139 |                         window.update_client_config(handle, config);
140 |                         window.update_client_state(handle, state);
141 |                     }
142 |                     FrontendEvent::NoSuchClient(_) => {}
143 |                     FrontendEvent::Error(e) => window.show_toast(e.as_str()),
144 |                     FrontendEvent::Enumerate(clients) => window.update_client_list(clients),
145 |                     FrontendEvent::PortChanged(port, msg) => window.update_port(port, msg),
146 |                     FrontendEvent::CaptureStatus(s) => window.set_capture(s.into()),
147 |                     FrontendEvent::EmulationStatus(s) => window.set_emulation(s.into()),
148 |                     FrontendEvent::AuthorizedUpdated(keys) => window.set_authorized_keys(keys),
149 |                     FrontendEvent::PublicKeyFingerprint(fp) => window.set_pk_fp(&fp),
150 |                     FrontendEvent::ConnectionAttempt { fingerprint } => {
151 |                         window.request_authorization(&fingerprint);
152 |                     }
153 |                     FrontendEvent::DeviceConnected {
154 |                         fingerprint: _,
155 |                         addr,
156 |                     } => {
157 |                         window.show_toast(format!("device connected: {addr}").as_str());
158 |                     }
159 |                     FrontendEvent::DeviceEntered {
160 |                         fingerprint: _,
161 |                         addr,
162 |                         pos,
163 |                     } => {
164 |                         window.show_toast(format!("device entered: {addr} ({pos})").as_str());
165 |                     }
166 |                     FrontendEvent::IncomingDisconnected(addr) => {
167 |                         window.show_toast(format!("{addr} disconnected").as_str());
168 |                     }
169 |                 }
170 |             }
171 |         }
172 |     ));
173 | 
174 |     window.present();
175 | }
176 | 


--------------------------------------------------------------------------------
/lan-mouse-gtk/src/window/imp.rs:
--------------------------------------------------------------------------------
  1 | use std::cell::{Cell, RefCell};
  2 | 
  3 | use adw::subclass::prelude::*;
  4 | use adw::{prelude::*, ActionRow, PreferencesGroup, ToastOverlay};
  5 | use glib::subclass::InitializingObject;
  6 | use gtk::glib::clone;
  7 | use gtk::{gdk, gio, glib, Button, CompositeTemplate, Entry, Image, Label, ListBox};
  8 | 
  9 | use lan_mouse_ipc::{FrontendRequestWriter, DEFAULT_PORT};
 10 | 
 11 | #[derive(CompositeTemplate, Default)]
 12 | #[template(resource = "/de/feschber/LanMouse/window.ui")]
 13 | pub struct Window {
 14 |     #[template_child]
 15 |     pub authorized_placeholder: TemplateChild<ActionRow>,
 16 |     #[template_child]
 17 |     pub fingerprint_row: TemplateChild<ActionRow>,
 18 |     #[template_child]
 19 |     pub port_edit_apply: TemplateChild<Button>,
 20 |     #[template_child]
 21 |     pub port_edit_cancel: TemplateChild<Button>,
 22 |     #[template_child]
 23 |     pub client_list: TemplateChild<ListBox>,
 24 |     #[template_child]
 25 |     pub client_placeholder: TemplateChild<ActionRow>,
 26 |     #[template_child]
 27 |     pub port_entry: TemplateChild<Entry>,
 28 |     #[template_child]
 29 |     pub hostname_copy_icon: TemplateChild<Image>,
 30 |     #[template_child]
 31 |     pub hostname_label: TemplateChild<Label>,
 32 |     #[template_child]
 33 |     pub toast_overlay: TemplateChild<ToastOverlay>,
 34 |     #[template_child]
 35 |     pub capture_emulation_group: TemplateChild<PreferencesGroup>,
 36 |     #[template_child]
 37 |     pub capture_status_row: TemplateChild<ActionRow>,
 38 |     #[template_child]
 39 |     pub emulation_status_row: TemplateChild<ActionRow>,
 40 |     #[template_child]
 41 |     pub input_emulation_button: TemplateChild<Button>,
 42 |     #[template_child]
 43 |     pub input_capture_button: TemplateChild<Button>,
 44 |     #[template_child]
 45 |     pub authorized_list: TemplateChild<ListBox>,
 46 |     pub clients: RefCell<Option<gio::ListStore>>,
 47 |     pub authorized: RefCell<Option<gio::ListStore>>,
 48 |     pub frontend_request_writer: RefCell<Option<FrontendRequestWriter>>,
 49 |     pub port: Cell<u16>,
 50 |     pub capture_active: Cell<bool>,
 51 |     pub emulation_active: Cell<bool>,
 52 | }
 53 | 
 54 | #[glib::object_subclass]
 55 | impl ObjectSubclass for Window {
 56 |     // `NAME` needs to match `class` attribute of template
 57 |     const NAME: &'static str = "LanMouseWindow";
 58 |     const ABSTRACT: bool = false;
 59 | 
 60 |     type Type = super::Window;
 61 |     type ParentType = adw::ApplicationWindow;
 62 | 
 63 |     fn class_init(klass: &mut Self::Class) {
 64 |         klass.bind_template();
 65 |         klass.bind_template_callbacks();
 66 |     }
 67 | 
 68 |     fn instance_init(obj: &InitializingObject<Self>) {
 69 |         obj.init_template();
 70 |     }
 71 | }
 72 | 
 73 | #[gtk::template_callbacks]
 74 | impl Window {
 75 |     #[template_callback]
 76 |     fn handle_add_client_pressed(&self, _button: &Button) {
 77 |         self.obj().request_client_create();
 78 |     }
 79 | 
 80 |     #[template_callback]
 81 |     fn handle_copy_hostname(&self, _: &Button) {
 82 |         if let Ok(hostname) = hostname::get() {
 83 |             let display = gdk::Display::default().unwrap();
 84 |             let clipboard = display.clipboard();
 85 |             clipboard.set_text(hostname.to_str().expect("hostname: invalid utf8"));
 86 |             let icon = self.hostname_copy_icon.clone();
 87 |             icon.set_icon_name(Some("emblem-ok-symbolic"));
 88 |             icon.set_css_classes(&["success"]);
 89 |             glib::spawn_future_local(clone!(
 90 |                 #[weak]
 91 |                 icon,
 92 |                 async move {
 93 |                     glib::timeout_future_seconds(1).await;
 94 |                     icon.set_icon_name(Some("edit-copy-symbolic"));
 95 |                     icon.set_css_classes(&[]);
 96 |                 }
 97 |             ));
 98 |         }
 99 |     }
100 | 
101 |     #[template_callback]
102 |     fn handle_copy_fingerprint(&self, button: &Button) {
103 |         let fingerprint: String = self.fingerprint_row.property("subtitle");
104 |         let display = gdk::Display::default().unwrap();
105 |         let clipboard = display.clipboard();
106 |         clipboard.set_text(&fingerprint);
107 |         button.set_icon_name("emblem-ok-symbolic");
108 |         button.set_css_classes(&["success"]);
109 |         glib::spawn_future_local(clone!(
110 |             #[weak]
111 |             button,
112 |             async move {
113 |                 glib::timeout_future_seconds(1).await;
114 |                 button.set_icon_name("edit-copy-symbolic");
115 |                 button.set_css_classes(&[]);
116 |             }
117 |         ));
118 |     }
119 | 
120 |     #[template_callback]
121 |     fn handle_port_changed(&self, _entry: &Entry) {
122 |         self.port_edit_apply.set_visible(true);
123 |         self.port_edit_cancel.set_visible(true);
124 |     }
125 | 
126 |     #[template_callback]
127 |     fn handle_port_edit_apply(&self) {
128 |         self.obj().request_port_change();
129 |     }
130 | 
131 |     #[template_callback]
132 |     fn handle_port_edit_cancel(&self) {
133 |         log::debug!("cancel port edit");
134 |         self.port_entry
135 |             .set_text(self.port.get().to_string().as_str());
136 |         self.port_edit_apply.set_visible(false);
137 |         self.port_edit_cancel.set_visible(false);
138 |     }
139 | 
140 |     #[template_callback]
141 |     fn handle_emulation(&self) {
142 |         self.obj().request_emulation();
143 |     }
144 | 
145 |     #[template_callback]
146 |     fn handle_capture(&self) {
147 |         self.obj().request_capture();
148 |     }
149 | 
150 |     #[template_callback]
151 |     fn handle_add_cert_fingerprint(&self, _button: &Button) {
152 |         self.obj().open_fingerprint_dialog(None);
153 |     }
154 | 
155 |     pub fn set_port(&self, port: u16) {
156 |         self.port.set(port);
157 |         if port == DEFAULT_PORT {
158 |             self.port_entry.set_text("");
159 |         } else {
160 |             self.port_entry.set_text(format!("{port}").as_str());
161 |         }
162 |         self.port_edit_apply.set_visible(false);
163 |         self.port_edit_cancel.set_visible(false);
164 |     }
165 | }
166 | 
167 | impl ObjectImpl for Window {
168 |     fn constructed(&self) {
169 |         if let Ok(hostname) = hostname::get() {
170 |             self.hostname_label
171 |                 .set_text(hostname.to_str().expect("hostname: invalid utf8"));
172 |         }
173 |         self.parent_constructed();
174 |         self.set_port(DEFAULT_PORT);
175 |         let obj = self.obj();
176 |         obj.setup_icon();
177 |         obj.setup_clients();
178 |         obj.setup_authorized();
179 |     }
180 | }
181 | 
182 | impl WidgetImpl for Window {}
183 | impl WindowImpl for Window {}
184 | impl ApplicationWindowImpl for Window {}
185 | impl AdwApplicationWindowImpl for Window {}
186 | 


--------------------------------------------------------------------------------
/lan-mouse-ipc/Cargo.toml:
--------------------------------------------------------------------------------
 1 | [package]
 2 | name = "lan-mouse-ipc"
 3 | description = "library for communication between lan-mouse service and frontends"
 4 | version = "0.2.0"
 5 | edition = "2021"
 6 | license = "GPL-3.0-or-later"
 7 | repository = "https://github.com/feschber/lan-mouse"
 8 | 
 9 | [dependencies]
10 | futures = "0.3.30"
11 | log = "0.4.22"
12 | serde = { version = "1.0", features = ["derive"] }
13 | serde_json = "1.0.107"
14 | thiserror = "2.0.0"
15 | tokio = { version = "1.32.0", features = ["net", "io-util", "time"] }
16 | tokio-stream = { version = "0.1.15", features = ["io-util"] }
17 | 


--------------------------------------------------------------------------------
/lan-mouse-ipc/src/connect.rs:
--------------------------------------------------------------------------------
 1 | use crate::{ConnectionError, FrontendEvent, FrontendRequest, IpcError};
 2 | use std::{
 3 |     cmp::min,
 4 |     io::{self, prelude::*, BufReader, LineWriter, Lines},
 5 |     thread,
 6 |     time::Duration,
 7 | };
 8 | 
 9 | #[cfg(unix)]
10 | use std::os::unix::net::UnixStream;
11 | 
12 | #[cfg(windows)]
13 | use std::net::TcpStream;
14 | 
15 | pub struct FrontendEventReader {
16 |     #[cfg(unix)]
17 |     lines: Lines<BufReader<UnixStream>>,
18 |     #[cfg(windows)]
19 |     lines: Lines<BufReader<TcpStream>>,
20 | }
21 | 
22 | pub struct FrontendRequestWriter {
23 |     #[cfg(unix)]
24 |     line_writer: LineWriter<UnixStream>,
25 |     #[cfg(windows)]
26 |     line_writer: LineWriter<TcpStream>,
27 | }
28 | 
29 | impl FrontendEventReader {
30 |     pub fn next_event(&mut self) -> Option<Result<FrontendEvent, IpcError>> {
31 |         match self.lines.next()? {
32 |             Err(e) => Some(Err(e.into())),
33 |             Ok(l) => Some(serde_json::from_str(l.as_str()).map_err(|e| e.into())),
34 |         }
35 |     }
36 | }
37 | 
38 | impl FrontendRequestWriter {
39 |     pub fn request(&mut self, request: FrontendRequest) -> Result<(), io::Error> {
40 |         let mut json = serde_json::to_string(&request).unwrap();
41 |         log::debug!("requesting: {json}");
42 |         json.push('\n');
43 |         self.line_writer.write_all(json.as_bytes())?;
44 |         Ok(())
45 |     }
46 | }
47 | 
48 | pub fn connect() -> Result<(FrontendEventReader, FrontendRequestWriter), ConnectionError> {
49 |     let rx = wait_for_service()?;
50 |     let tx = rx.try_clone()?;
51 |     let buf_reader = BufReader::new(rx);
52 |     let lines = buf_reader.lines();
53 |     let line_writer = LineWriter::new(tx);
54 |     let reader = FrontendEventReader { lines };
55 |     let writer = FrontendRequestWriter { line_writer };
56 |     Ok((reader, writer))
57 | }
58 | 
59 | /// wait for the lan-mouse socket to come online
60 | #[cfg(unix)]
61 | fn wait_for_service() -> Result<UnixStream, ConnectionError> {
62 |     let socket_path = crate::default_socket_path()?;
63 |     let mut duration = Duration::from_millis(10);
64 |     loop {
65 |         if let Ok(stream) = UnixStream::connect(&socket_path) {
66 |             break Ok(stream);
67 |         }
68 |         // a signaling mechanism or inotify could be used to
69 |         // improve this
70 |         thread::sleep(exponential_back_off(&mut duration));
71 |     }
72 | }
73 | 
74 | #[cfg(windows)]
75 | fn wait_for_service() -> Result<TcpStream, ConnectionError> {
76 |     let mut duration = Duration::from_millis(10);
77 |     loop {
78 |         if let Ok(stream) = TcpStream::connect("127.0.0.1:5252") {
79 |             break Ok(stream);
80 |         }
81 |         thread::sleep(exponential_back_off(&mut duration));
82 |     }
83 | }
84 | 
85 | fn exponential_back_off(duration: &mut Duration) -> Duration {
86 |     let new = duration.saturating_mul(2);
87 |     *duration = min(new, Duration::from_secs(1));
88 |     *duration
89 | }
90 | 


--------------------------------------------------------------------------------
/lan-mouse-ipc/src/connect_async.rs:
--------------------------------------------------------------------------------
  1 | use crate::{ConnectionError, FrontendEvent, FrontendRequest, IpcError};
  2 | use std::{
  3 |     cmp::min,
  4 |     task::{ready, Poll},
  5 |     time::Duration,
  6 | };
  7 | 
  8 | use futures::{Stream, StreamExt};
  9 | use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader, ReadHalf, WriteHalf};
 10 | use tokio_stream::wrappers::LinesStream;
 11 | 
 12 | #[cfg(unix)]
 13 | use tokio::net::UnixStream;
 14 | 
 15 | #[cfg(windows)]
 16 | use tokio::net::TcpStream;
 17 | 
 18 | pub struct AsyncFrontendEventReader {
 19 |     #[cfg(unix)]
 20 |     lines_stream: LinesStream<BufReader<ReadHalf<UnixStream>>>,
 21 |     #[cfg(windows)]
 22 |     lines_stream: LinesStream<BufReader<ReadHalf<TcpStream>>>,
 23 | }
 24 | 
 25 | pub struct AsyncFrontendRequestWriter {
 26 |     #[cfg(unix)]
 27 |     tx: WriteHalf<UnixStream>,
 28 |     #[cfg(windows)]
 29 |     tx: WriteHalf<TcpStream>,
 30 | }
 31 | 
 32 | impl Stream for AsyncFrontendEventReader {
 33 |     type Item = Result<FrontendEvent, IpcError>;
 34 | 
 35 |     fn poll_next(
 36 |         mut self: std::pin::Pin<&mut Self>,
 37 |         cx: &mut std::task::Context<'_>,
 38 |     ) -> std::task::Poll<Option<Self::Item>> {
 39 |         let line = ready!(self.lines_stream.poll_next_unpin(cx));
 40 |         let event = line.map(|l| {
 41 |             l.map_err(Into::<IpcError>::into)
 42 |                 .and_then(|l| serde_json::from_str(l.as_str()).map_err(|e| e.into()))
 43 |         });
 44 |         Poll::Ready(event)
 45 |     }
 46 | }
 47 | 
 48 | impl AsyncFrontendRequestWriter {
 49 |     pub async fn request(&mut self, request: FrontendRequest) -> Result<(), IpcError> {
 50 |         let mut json = serde_json::to_string(&request).unwrap();
 51 |         log::debug!("requesting: {json}");
 52 |         json.push('\n');
 53 |         self.tx.write_all(json.as_bytes()).await?;
 54 |         Ok(())
 55 |     }
 56 | }
 57 | 
 58 | pub async fn connect_async(
 59 |     timeout: Option<Duration>,
 60 | ) -> Result<(AsyncFrontendEventReader, AsyncFrontendRequestWriter), ConnectionError> {
 61 |     let stream = if let Some(duration) = timeout {
 62 |         tokio::select! {
 63 |             s = wait_for_service() => s?,
 64 |             _ = tokio::time::sleep(duration) => return Err(ConnectionError::Timeout),
 65 |         }
 66 |     } else {
 67 |         wait_for_service().await?
 68 |     };
 69 |     #[cfg(unix)]
 70 |     let (rx, tx): (ReadHalf<UnixStream>, WriteHalf<UnixStream>) = tokio::io::split(stream);
 71 |     #[cfg(windows)]
 72 |     let (rx, tx): (ReadHalf<TcpStream>, WriteHalf<TcpStream>) = tokio::io::split(stream);
 73 |     let buf_reader = BufReader::new(rx);
 74 |     let lines = buf_reader.lines();
 75 |     let lines_stream = LinesStream::new(lines);
 76 |     let reader = AsyncFrontendEventReader { lines_stream };
 77 |     let writer = AsyncFrontendRequestWriter { tx };
 78 |     Ok((reader, writer))
 79 | }
 80 | 
 81 | /// wait for the lan-mouse socket to come online
 82 | #[cfg(unix)]
 83 | async fn wait_for_service() -> Result<UnixStream, ConnectionError> {
 84 |     let socket_path = crate::default_socket_path()?;
 85 |     let mut duration = Duration::from_millis(10);
 86 |     loop {
 87 |         if let Ok(stream) = UnixStream::connect(&socket_path).await {
 88 |             break Ok(stream);
 89 |         }
 90 |         // a signaling mechanism or inotify could be used to
 91 |         // improve this
 92 |         tokio::time::sleep(exponential_back_off(&mut duration)).await;
 93 |     }
 94 | }
 95 | 
 96 | #[cfg(windows)]
 97 | async fn wait_for_service() -> Result<TcpStream, ConnectionError> {
 98 |     let mut duration = Duration::from_millis(10);
 99 |     loop {
100 |         if let Ok(stream) = TcpStream::connect("127.0.0.1:5252").await {
101 |             break Ok(stream);
102 |         }
103 |         tokio::time::sleep(exponential_back_off(&mut duration)).await;
104 |     }
105 | }
106 | 
107 | fn exponential_back_off(duration: &mut Duration) -> Duration {
108 |     let new = duration.saturating_mul(2);
109 |     *duration = min(new, Duration::from_secs(1));
110 |     *duration
111 | }
112 | 


--------------------------------------------------------------------------------
/lan-mouse-ipc/src/lib.rs:
--------------------------------------------------------------------------------
  1 | use std::{
  2 |     collections::{HashMap, HashSet},
  3 |     env::VarError,
  4 |     fmt::Display,
  5 |     io,
  6 |     net::{IpAddr, SocketAddr},
  7 |     str::FromStr,
  8 | };
  9 | use thiserror::Error;
 10 | 
 11 | #[cfg(unix)]
 12 | use std::{
 13 |     env,
 14 |     path::{Path, PathBuf},
 15 | };
 16 | 
 17 | use serde::{Deserialize, Serialize};
 18 | 
 19 | mod connect;
 20 | mod connect_async;
 21 | mod listen;
 22 | 
 23 | pub use connect::{connect, FrontendEventReader, FrontendRequestWriter};
 24 | pub use connect_async::{connect_async, AsyncFrontendEventReader, AsyncFrontendRequestWriter};
 25 | pub use listen::AsyncFrontendListener;
 26 | 
 27 | #[derive(Debug, Error)]
 28 | pub enum ConnectionError {
 29 |     #[error(transparent)]
 30 |     SocketPath(#[from] SocketPathError),
 31 |     #[error(transparent)]
 32 |     Io(#[from] io::Error),
 33 |     #[error("connection timed out")]
 34 |     Timeout,
 35 | }
 36 | 
 37 | #[derive(Debug, Error)]
 38 | pub enum IpcListenerCreationError {
 39 |     #[error("could not determine socket-path: `{0}`")]
 40 |     SocketPath(#[from] SocketPathError),
 41 |     #[error("service already running!")]
 42 |     AlreadyRunning,
 43 |     #[error("failed to bind lan-mouse socket: `{0}`")]
 44 |     Bind(io::Error),
 45 | }
 46 | 
 47 | #[derive(Debug, Error)]
 48 | pub enum IpcError {
 49 |     #[error("io error occured: `{0}`")]
 50 |     Io(#[from] io::Error),
 51 |     #[error("invalid json: `{0}`")]
 52 |     Json(#[from] serde_json::Error),
 53 |     #[error(transparent)]
 54 |     Connection(#[from] ConnectionError),
 55 |     #[error(transparent)]
 56 |     Listen(#[from] IpcListenerCreationError),
 57 | }
 58 | 
 59 | pub const DEFAULT_PORT: u16 = 4242;
 60 | 
 61 | #[derive(Debug, Default, Eq, Hash, PartialEq, Clone, Copy, Serialize, Deserialize)]
 62 | #[serde(rename_all = "lowercase")]
 63 | pub enum Position {
 64 |     #[default]
 65 |     Left,
 66 |     Right,
 67 |     Top,
 68 |     Bottom,
 69 | }
 70 | 
 71 | impl Position {
 72 |     pub fn opposite(&self) -> Self {
 73 |         match self {
 74 |             Position::Left => Position::Right,
 75 |             Position::Right => Position::Left,
 76 |             Position::Top => Position::Bottom,
 77 |             Position::Bottom => Position::Top,
 78 |         }
 79 |     }
 80 | }
 81 | 
 82 | #[derive(Debug, Error)]
 83 | #[error("not a valid position: {pos}")]
 84 | pub struct PositionParseError {
 85 |     pos: String,
 86 | }
 87 | 
 88 | impl FromStr for Position {
 89 |     type Err = PositionParseError;
 90 | 
 91 |     fn from_str(s: &str) -> Result<Self, Self::Err> {
 92 |         match s {
 93 |             "left" => Ok(Self::Left),
 94 |             "right" => Ok(Self::Right),
 95 |             "top" => Ok(Self::Top),
 96 |             "bottom" => Ok(Self::Bottom),
 97 |             _ => Err(PositionParseError { pos: s.into() }),
 98 |         }
 99 |     }
100 | }
101 | 
102 | impl Display for Position {
103 |     fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
104 |         write!(
105 |             f,
106 |             "{}",
107 |             match self {
108 |                 Position::Left => "left",
109 |                 Position::Right => "right",
110 |                 Position::Top => "top",
111 |                 Position::Bottom => "bottom",
112 |             }
113 |         )
114 |     }
115 | }
116 | 
117 | impl TryFrom<&str> for Position {
118 |     type Error = ();
119 | 
120 |     fn try_from(s: &str) -> Result<Self, Self::Error> {
121 |         match s {
122 |             "left" => Ok(Position::Left),
123 |             "right" => Ok(Position::Right),
124 |             "top" => Ok(Position::Top),
125 |             "bottom" => Ok(Position::Bottom),
126 |             _ => Err(()),
127 |         }
128 |     }
129 | }
130 | 
131 | #[derive(Debug, Eq, PartialEq, Clone, Serialize, Deserialize)]
132 | pub struct ClientConfig {
133 |     /// hostname of this client
134 |     pub hostname: Option<String>,
135 |     /// fix ips, determined by the user
136 |     pub fix_ips: Vec<IpAddr>,
137 |     /// both active_addr and addrs can be None / empty so port needs to be stored seperately
138 |     pub port: u16,
139 |     /// position of a client on screen
140 |     pub pos: Position,
141 |     /// enter hook
142 |     pub cmd: Option<String>,
143 | }
144 | 
145 | impl Default for ClientConfig {
146 |     fn default() -> Self {
147 |         Self {
148 |             port: DEFAULT_PORT,
149 |             hostname: Default::default(),
150 |             fix_ips: Default::default(),
151 |             pos: Default::default(),
152 |             cmd: None,
153 |         }
154 |     }
155 | }
156 | 
157 | pub type ClientHandle = u64;
158 | 
159 | #[derive(Debug, Default, Clone, Serialize, Deserialize)]
160 | pub struct ClientState {
161 |     /// events should be sent to and received from the client
162 |     pub active: bool,
163 |     /// `active` address of the client, used to send data to.
164 |     /// This should generally be the socket address where data
165 |     /// was last received from.
166 |     pub active_addr: Option<SocketAddr>,
167 |     /// tracks whether or not the client is available for emulation
168 |     pub alive: bool,
169 |     /// ips from dns
170 |     pub dns_ips: Vec<IpAddr>,
171 |     /// all ip addresses associated with a particular client
172 |     /// e.g. Laptops usually have at least an ethernet and a wifi port
173 |     /// which have different ip addresses
174 |     pub ips: HashSet<IpAddr>,
175 |     /// client has pressed keys
176 |     pub has_pressed_keys: bool,
177 |     /// dns resolving in progress
178 |     pub resolving: bool,
179 | }
180 | 
181 | #[derive(Debug, Clone, Serialize, Deserialize)]
182 | pub enum FrontendEvent {
183 |     /// a client was created
184 |     Created(ClientHandle, ClientConfig, ClientState),
185 |     /// no such client
186 |     NoSuchClient(ClientHandle),
187 |     /// state changed
188 |     State(ClientHandle, ClientConfig, ClientState),
189 |     /// the client was deleted
190 |     Deleted(ClientHandle),
191 |     /// new port, reason of failure (if failed)
192 |     PortChanged(u16, Option<String>),
193 |     /// list of all clients, used for initial state synchronization
194 |     Enumerate(Vec<(ClientHandle, ClientConfig, ClientState)>),
195 |     /// an error occured
196 |     Error(String),
197 |     /// capture status
198 |     CaptureStatus(Status),
199 |     /// emulation status
200 |     EmulationStatus(Status),
201 |     /// authorized public key fingerprints have been updated
202 |     AuthorizedUpdated(HashMap<String, String>),
203 |     /// public key fingerprint of this device
204 |     PublicKeyFingerprint(String),
205 |     /// new device connected
206 |     DeviceConnected {
207 |         addr: SocketAddr,
208 |         fingerprint: String,
209 |     },
210 |     /// incoming device entered the screen
211 |     DeviceEntered {
212 |         fingerprint: String,
213 |         addr: SocketAddr,
214 |         pos: Position,
215 |     },
216 |     /// incoming disconnected
217 |     IncomingDisconnected(SocketAddr),
218 |     /// failed connection attempt (approval for fingerprint required)
219 |     ConnectionAttempt { fingerprint: String },
220 | }
221 | 
222 | #[derive(Debug, Eq, PartialEq, Clone, Serialize, Deserialize)]
223 | pub enum FrontendRequest {
224 |     /// activate/deactivate client
225 |     Activate(ClientHandle, bool),
226 |     /// add a new client
227 |     Create,
228 |     /// change the listen port (recreate udp listener)
229 |     ChangePort(u16),
230 |     /// remove a client
231 |     Delete(ClientHandle),
232 |     /// request an enumeration of all clients
233 |     Enumerate(),
234 |     /// resolve dns
235 |     ResolveDns(ClientHandle),
236 |     /// update hostname
237 |     UpdateHostname(ClientHandle, Option<String>),
238 |     /// update port
239 |     UpdatePort(ClientHandle, u16),
240 |     /// update position
241 |     UpdatePosition(ClientHandle, Position),
242 |     /// update fix-ips
243 |     UpdateFixIps(ClientHandle, Vec<IpAddr>),
244 |     /// request reenabling input capture
245 |     EnableCapture,
246 |     /// request reenabling input emulation
247 |     EnableEmulation,
248 |     /// synchronize all state
249 |     Sync,
250 |     /// authorize fingerprint (description, fingerprint)
251 |     AuthorizeKey(String, String),
252 |     /// remove fingerprint (fingerprint)
253 |     RemoveAuthorizedKey(String),
254 |     /// change the hook command
255 |     UpdateEnterHook(u64, Option<String>),
256 | }
257 | 
258 | #[derive(Clone, Copy, PartialEq, Eq, Debug, Default, Serialize, Deserialize)]
259 | pub enum Status {
260 |     #[default]
261 |     Disabled,
262 |     Enabled,
263 | }
264 | 
265 | impl From<Status> for bool {
266 |     fn from(status: Status) -> Self {
267 |         match status {
268 |             Status::Enabled => true,
269 |             Status::Disabled => false,
270 |         }
271 |     }
272 | }
273 | 
274 | #[cfg(unix)]
275 | const LAN_MOUSE_SOCKET_NAME: &str = "lan-mouse-socket.sock";
276 | 
277 | #[derive(Debug, Error)]
278 | pub enum SocketPathError {
279 |     #[error("could not determine $XDG_RUNTIME_DIR: `{0}`")]
280 |     XdgRuntimeDirNotFound(VarError),
281 |     #[error("could not determine $HOME: `{0}`")]
282 |     HomeDirNotFound(VarError),
283 | }
284 | 
285 | #[cfg(all(unix, not(target_os = "macos")))]
286 | pub fn default_socket_path() -> Result<PathBuf, SocketPathError> {
287 |     let xdg_runtime_dir =
288 |         env::var("XDG_RUNTIME_DIR").map_err(SocketPathError::XdgRuntimeDirNotFound)?;
289 |     Ok(Path::new(xdg_runtime_dir.as_str()).join(LAN_MOUSE_SOCKET_NAME))
290 | }
291 | 
292 | #[cfg(all(unix, target_os = "macos"))]
293 | pub fn default_socket_path() -> Result<PathBuf, SocketPathError> {
294 |     let home = env::var("HOME").map_err(SocketPathError::HomeDirNotFound)?;
295 |     Ok(Path::new(home.as_str())
296 |         .join("Library")
297 |         .join("Caches")
298 |         .join(LAN_MOUSE_SOCKET_NAME))
299 | }
300 | 


--------------------------------------------------------------------------------
/lan-mouse-ipc/src/listen.rs:
--------------------------------------------------------------------------------
  1 | use futures::{stream::SelectAll, Stream, StreamExt};
  2 | #[cfg(unix)]
  3 | use std::path::PathBuf;
  4 | use std::{
  5 |     io::ErrorKind,
  6 |     pin::Pin,
  7 |     task::{Context, Poll},
  8 | };
  9 | 
 10 | use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader, ReadHalf, WriteHalf};
 11 | use tokio_stream::wrappers::LinesStream;
 12 | 
 13 | #[cfg(unix)]
 14 | use tokio::net::UnixListener;
 15 | #[cfg(unix)]
 16 | use tokio::net::UnixStream;
 17 | 
 18 | #[cfg(windows)]
 19 | use tokio::net::TcpListener;
 20 | #[cfg(windows)]
 21 | use tokio::net::TcpStream;
 22 | 
 23 | use crate::{FrontendEvent, FrontendRequest, IpcError, IpcListenerCreationError};
 24 | 
 25 | pub struct AsyncFrontendListener {
 26 |     #[cfg(windows)]
 27 |     listener: TcpListener,
 28 |     #[cfg(unix)]
 29 |     listener: UnixListener,
 30 |     #[cfg(unix)]
 31 |     socket_path: PathBuf,
 32 |     #[cfg(unix)]
 33 |     line_streams: SelectAll<LinesStream<BufReader<ReadHalf<UnixStream>>>>,
 34 |     #[cfg(windows)]
 35 |     line_streams: SelectAll<LinesStream<BufReader<ReadHalf<TcpStream>>>>,
 36 |     #[cfg(unix)]
 37 |     tx_streams: Vec<WriteHalf<UnixStream>>,
 38 |     #[cfg(windows)]
 39 |     tx_streams: Vec<WriteHalf<TcpStream>>,
 40 | }
 41 | 
 42 | impl AsyncFrontendListener {
 43 |     pub async fn new() -> Result<Self, IpcListenerCreationError> {
 44 |         #[cfg(unix)]
 45 |         let (socket_path, listener) = {
 46 |             let socket_path = crate::default_socket_path()?;
 47 | 
 48 |             log::debug!("remove socket: {:?}", socket_path);
 49 |             if socket_path.exists() {
 50 |                 // try to connect to see if some other instance
 51 |                 // of lan-mouse is already running
 52 |                 match UnixStream::connect(&socket_path).await {
 53 |                     // connected -> lan-mouse is already running
 54 |                     Ok(_) => return Err(IpcListenerCreationError::AlreadyRunning),
 55 |                     // lan-mouse is not running but a socket was left behind
 56 |                     Err(e) => {
 57 |                         log::debug!("{socket_path:?}: {e} - removing left behind socket");
 58 |                         let _ = std::fs::remove_file(&socket_path);
 59 |                     }
 60 |                 }
 61 |             }
 62 |             let listener = match UnixListener::bind(&socket_path) {
 63 |                 Ok(ls) => ls,
 64 |                 // some other lan-mouse instance has bound the socket in the meantime
 65 |                 Err(e) if e.kind() == ErrorKind::AddrInUse => {
 66 |                     return Err(IpcListenerCreationError::AlreadyRunning)
 67 |                 }
 68 |                 Err(e) => return Err(IpcListenerCreationError::Bind(e)),
 69 |             };
 70 |             (socket_path, listener)
 71 |         };
 72 | 
 73 |         #[cfg(windows)]
 74 |         let listener = match TcpListener::bind("127.0.0.1:5252").await {
 75 |             Ok(ls) => ls,
 76 |             // some other lan-mouse instance has bound the socket in the meantime
 77 |             Err(e) if e.kind() == ErrorKind::AddrInUse => {
 78 |                 return Err(IpcListenerCreationError::AlreadyRunning)
 79 |             }
 80 |             Err(e) => return Err(IpcListenerCreationError::Bind(e)),
 81 |         };
 82 | 
 83 |         let adapter = Self {
 84 |             listener,
 85 |             #[cfg(unix)]
 86 |             socket_path,
 87 |             line_streams: SelectAll::new(),
 88 |             tx_streams: vec![],
 89 |         };
 90 | 
 91 |         Ok(adapter)
 92 |     }
 93 | 
 94 |     pub async fn broadcast(&mut self, notify: FrontendEvent) {
 95 |         // encode event
 96 |         let mut json = serde_json::to_string(&notify).unwrap();
 97 |         json.push('\n');
 98 | 
 99 |         let mut keep = vec![];
100 |         // TODO do simultaneously
101 |         for tx in self.tx_streams.iter_mut() {
102 |             // write len + payload
103 |             if tx.write(json.as_bytes()).await.is_err() {
104 |                 keep.push(false);
105 |                 continue;
106 |             }
107 |             keep.push(true);
108 |         }
109 | 
110 |         // could not find a better solution because async
111 |         let mut keep = keep.into_iter();
112 |         self.tx_streams.retain(|_| keep.next().unwrap());
113 |     }
114 | }
115 | 
116 | #[cfg(unix)]
117 | impl Drop for AsyncFrontendListener {
118 |     fn drop(&mut self) {
119 |         log::debug!("remove socket: {:?}", self.socket_path);
120 |         let _ = std::fs::remove_file(&self.socket_path);
121 |     }
122 | }
123 | 
124 | impl Stream for AsyncFrontendListener {
125 |     type Item = Result<FrontendRequest, IpcError>;
126 |     fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
127 |         if let Poll::Ready(Some(Ok(l))) = self.line_streams.poll_next_unpin(cx) {
128 |             let request = serde_json::from_str(l.as_str()).map_err(|e| e.into());
129 |             return Poll::Ready(Some(request));
130 |         }
131 |         let mut sync = false;
132 |         while let Poll::Ready(Ok((stream, _))) = self.listener.poll_accept(cx) {
133 |             let (rx, tx) = tokio::io::split(stream);
134 |             let buf_reader = BufReader::new(rx);
135 |             let lines = buf_reader.lines();
136 |             let lines = LinesStream::new(lines);
137 |             self.line_streams.push(lines);
138 |             self.tx_streams.push(tx);
139 |             sync = true;
140 |         }
141 |         if sync {
142 |             Poll::Ready(Some(Ok(FrontendRequest::Sync)))
143 |         } else {
144 |             Poll::Pending
145 |         }
146 |     }
147 | }
148 | 


--------------------------------------------------------------------------------
/lan-mouse-proto/Cargo.toml:
--------------------------------------------------------------------------------
 1 | [package]
 2 | name = "lan-mouse-proto"
 3 | description = "network protocol for lan-mouse"
 4 | version = "0.2.0"
 5 | edition = "2021"
 6 | license = "GPL-3.0-or-later"
 7 | repository = "https://github.com/feschber/lan-mouse"
 8 | 
 9 | [dependencies]
10 | num_enum = "0.7.2"
11 | thiserror = "2.0.0"
12 | input-event = { path = "../input-event", version = "0.3.0" }
13 | paste = "1.0"
14 | 


--------------------------------------------------------------------------------
/nix/README.md:
--------------------------------------------------------------------------------
 1 | # Nix Flake Usage
 2 | 
 3 | ## run
 4 | 
 5 | ```bash
 6 | nix run github:feschber/lan-mouse
 7 | 
 8 | # with params
 9 | nix run github:feschber/lan-mouse -- --help
10 | 
11 | ```
12 | 
13 | ## home-manager module
14 | 
15 | add input
16 | 
17 | ```nix
18 | inputs = {
19 |     lan-mouse.url = "github:feschber/lan-mouse";
20 | }
21 | ```
22 | 
23 | enable lan-mouse
24 | 
25 | ``` nix
26 | {
27 |   inputs,
28 |   ...
29 | }: {
30 |   # add the home manager module
31 |   imports = [inputs.lan-mouse.homeManagerModules.default];
32 | 
33 |   programs.lan-mouse = {
34 |     enable = true;
35 |     # systemd = false;
36 |     # package = inputs.lan-mouse.packages.${pkgs.stdenv.hostPlatform.system}.default
37 |     # Optional configuration in nix syntax, see config.toml for available options
38 |     # settings = { };
39 |     };
40 |   };
41 | }
42 | 
43 | ```
44 | 


--------------------------------------------------------------------------------
/nix/default.nix:
--------------------------------------------------------------------------------
 1 | {
 2 |   rustPlatform,
 3 |   lib,
 4 |   pkgs,
 5 | }: let
 6 |   cargoToml = builtins.fromTOML (builtins.readFile ../Cargo.toml);
 7 |   pname = cargoToml.package.name;
 8 |   version = cargoToml.package.version;
 9 | in
10 | rustPlatform.buildRustPackage {
11 |   pname = pname;
12 |   version = version;
13 | 
14 |   nativeBuildInputs = with pkgs; [
15 |     git
16 |     pkg-config
17 |     cmake
18 |     makeWrapper
19 |     buildPackages.gtk4
20 |   ];
21 | 
22 |   buildInputs = with pkgs; [
23 |     xorg.libX11
24 |     gtk4
25 |     libadwaita
26 |     xorg.libXtst
27 |   ] ++ lib.optionals stdenv.isDarwin
28 |   (with darwin.apple_sdk_11_0.frameworks; [
29 |     CoreGraphics
30 |     ApplicationServices
31 |   ]);
32 | 
33 |   src = builtins.path {
34 |     name = pname;
35 |     path = lib.cleanSource ../.;
36 |   };
37 | 
38 |   cargoLock.lockFile = ../Cargo.lock;
39 | 
40 |   # Set Environment Variables
41 |   RUST_BACKTRACE = "full";
42 | 
43 |   # Needed to enable support for SVG icons in GTK
44 |   postInstall = ''
45 |     wrapProgram "$out/bin/lan-mouse" \
46 |       --set GDK_PIXBUF_MODULE_FILE ${pkgs.librsvg.out}/lib/gdk-pixbuf-2.0/2.10.0/loaders.cache
47 | 
48 |     install -Dm444 *.desktop -t $out/share/applications
49 |     install -Dm444 lan-mouse-gtk/resources/*.svg -t $out/share/icons/hicolor/scalable/apps
50 |   '';
51 | 
52 |   meta = with lib; {
53 |     description = "Lan Mouse is a mouse and keyboard sharing software";
54 |     longDescription = ''
55 |       Lan Mouse is a mouse and keyboard sharing software similar to universal-control on Apple devices. It allows for using multiple pcs with a single set of mouse and keyboard. This is also known as a Software KVM switch.
56 |       The primary target is Wayland on Linux but Windows and MacOS and Linux on Xorg have partial support as well (see below for more details).
57 |     '';
58 |     mainProgram = pname;
59 |     platforms = platforms.all;
60 |   };
61 | }
62 | 


--------------------------------------------------------------------------------
/nix/hm-module.nix:
--------------------------------------------------------------------------------
 1 | self: {
 2 |   config,
 3 |   pkgs,
 4 |   lib,
 5 |   ...
 6 | }:
 7 | with lib; let
 8 |   cfg = config.programs.lan-mouse;
 9 |   defaultPackage = self.packages.${pkgs.stdenv.hostPlatform.system}.default;
10 |   tomlFormat = pkgs.formats.toml {};
11 | in {
12 |   options.programs.lan-mouse = with types; {
13 |     enable = mkEnableOption "Whether or not to enable lan-mouse.";
14 |     package = mkOption {
15 |       type = with types; nullOr package;
16 |       default = defaultPackage;
17 |       defaultText = literalExpression "inputs.lan-mouse.packages.${pkgs.stdenv.hostPlatform.system}.default";
18 |       description = ''
19 |         The lan-mouse package to use.
20 | 
21 |         By default, this option will use the `packages.default` as exposed by this flake.
22 |       '';
23 |     };
24 |     systemd = mkOption {
25 |       type = types.bool;
26 |       default = pkgs.stdenv.isLinux;
27 |       description = "Whether to enable to systemd service for lan-mouse on linux.";
28 |     };
29 |     launchd = mkOption {
30 |       type = types.bool;
31 |       default = pkgs.stdenv.isDarwin;
32 |       description = "Whether to enable to launchd service for lan-mouse on macOS.";
33 |     };
34 |     settings = lib.mkOption {
35 |       inherit (tomlFormat) type;
36 |       default = {};
37 |       example = builtins.fromTOML (builtins.readFile (self + /config.toml));
38 |       description = ''
39 |         Optional configuration written to {file}`$XDG_CONFIG_HOME/lan-mouse/config.toml`.
40 | 
41 |         See <https://github.com/feschber/lan-mouse/> for
42 |         available options and documentation.
43 |       '';
44 |     };
45 |   };
46 | 
47 |   config = mkIf cfg.enable {
48 |     systemd.user.services.lan-mouse = lib.mkIf cfg.systemd {
49 |       Unit = {
50 |         Description = "Systemd service for Lan Mouse";
51 |         Requires = ["graphical-session.target"];
52 |       };
53 |       Service = {
54 |         Type = "simple";
55 |         ExecStart = "${cfg.package}/bin/lan-mouse daemon";
56 |       };
57 |       Install.WantedBy = [
58 |         (lib.mkIf config.wayland.windowManager.hyprland.systemd.enable "hyprland-session.target")
59 |         (lib.mkIf config.wayland.windowManager.sway.systemd.enable "sway-session.target")
60 |       ];
61 |     };
62 | 
63 |     launchd.agents.lan-mouse = lib.mkIf cfg.launchd {
64 |       enable = true;
65 |       config = {
66 |         ProgramArguments = [
67 |           "${cfg.package}/bin/lan-mouse"
68 |           "daemon"
69 |         ];
70 |         KeepAlive = true;
71 |       };
72 |     };
73 | 
74 |     home.packages = [
75 |       cfg.package
76 |     ];
77 | 
78 |     xdg.configFile."lan-mouse/config.toml" = lib.mkIf (cfg.settings != {}) {
79 |       source = tomlFormat.generate "config.toml" cfg.settings;
80 |     };
81 |   };
82 | }
83 | 


--------------------------------------------------------------------------------
/screenshots/dark.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/feschber/lan-mouse/e46fe60b3e24be83de38701faa99a4fbd9186f08/screenshots/dark.png


--------------------------------------------------------------------------------
/screenshots/light.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/feschber/lan-mouse/e46fe60b3e24be83de38701faa99a4fbd9186f08/screenshots/light.png


--------------------------------------------------------------------------------
/scripts/makeicns.sh:
--------------------------------------------------------------------------------
 1 | #!/bin/sh
 2 | set -e
 3 | 
 4 | usage() {
 5 |     cat <<EOF
 6 | $0: Make a macOS icns file from an SVG with ImageMagick and iconutil.
 7 | usage: $0 [SVG [ICNS [ICONSET]]
 8 | 
 9 | ARGUMENTS
10 |     SVG     The SVG file to convert
11 |             Defaults to ./lan-mouse-gtk/resources/de.feschber.LanMouse.svg
12 |     ICNS    The icns file to create
13 |             Defaults to ./target/icon.icns
14 |     ICONSET The iconset directory to create
15 |             Defaults to ./target/icon.iconset
16 |             This is just a temporary directory
17 | EOF
18 | }
19 | 
20 | if [ "$1" = "-h" ] || [ "$1" = "--help" ]; then
21 |     usage
22 |     exit 0
23 | fi
24 | 
25 | svg="${1:-./lan-mouse-gtk/resources/de.feschber.LanMouse.svg}"
26 | icns="${2:-./target/icon.icns}"
27 | iconset="${3:-./target/icon.iconset}"
28 | 
29 | set -u
30 | 
31 | mkdir -p "$iconset"
32 | magick convert -background none -resize 1024x1024 "$svg" "$iconset"/icon_512x512@2x.png
33 | magick convert -background none -resize 512x512 "$svg" "$iconset"/icon_512x512.png
34 | magick convert -background none -resize 256x256 "$svg" "$iconset"/icon_256x256.png
35 | magick convert -background none -resize 128x128 "$svg" "$iconset"/icon_128x128.png
36 | magick convert -background none -resize 64x64 "$svg" "$iconset"/icon_32x32@2x.png
37 | magick convert -background none -resize 32x32 "$svg" "$iconset"/icon_32x32.png
38 | magick convert -background none -resize 16x16 "$svg" "$iconset"/icon_16x16.png
39 | cp "$iconset"/icon_512x512.png "$iconset"/icon_256x256@2x.png
40 | cp "$iconset"/icon_256x256.png "$iconset"/icon_128x128@2x.png
41 | cp "$iconset"/icon_32x32.png "$iconset"/icon_16x16@2x.png
42 | iconutil -c icns "$iconset" -o "$icns"
43 | 


--------------------------------------------------------------------------------
/service/lan-mouse.service:
--------------------------------------------------------------------------------
 1 | [Unit]
 2 | Description=Lan Mouse
 3 | # lan mouse needs an active graphical session
 4 | After=graphical-session.target
 5 | # make sure the service terminates with the graphical session
 6 | BindsTo=graphical-session.target
 7 | 
 8 | [Service]
 9 | ExecStart=/usr/bin/lan-mouse daemon
10 | Restart=on-failure
11 | 
12 | [Install]
13 | WantedBy=graphical-session.target
14 | 


--------------------------------------------------------------------------------
/shell.nix:
--------------------------------------------------------------------------------
1 | (builtins.getFlake ("git+file://" + toString ./.)).devShells.${builtins.currentSystem}.default
2 | 


--------------------------------------------------------------------------------
/src/capture_test.rs:
--------------------------------------------------------------------------------
 1 | use crate::config::Config;
 2 | use clap::Args;
 3 | use futures::StreamExt;
 4 | use input_capture::{self, CaptureError, CaptureEvent, InputCapture, InputCaptureError, Position};
 5 | use input_event::{Event, KeyboardEvent};
 6 | 
 7 | #[derive(Args, Clone, Debug, Eq, PartialEq)]
 8 | pub struct TestCaptureArgs {}
 9 | 
10 | pub async fn run(config: Config, _args: TestCaptureArgs) -> Result<(), InputCaptureError> {
11 |     log::info!("running input capture test");
12 |     log::info!("creating input capture");
13 |     let backend = config.capture_backend().map(|b| b.into());
14 |     loop {
15 |         let mut input_capture = InputCapture::new(backend).await?;
16 |         log::info!("creating clients");
17 |         input_capture.create(0, Position::Left).await?;
18 |         input_capture.create(4, Position::Left).await?;
19 |         input_capture.create(1, Position::Right).await?;
20 |         input_capture.create(2, Position::Top).await?;
21 |         input_capture.create(3, Position::Bottom).await?;
22 |         if let Err(e) = do_capture(&mut input_capture).await {
23 |             log::warn!("{e} - recreating capture");
24 |         }
25 |         let _ = input_capture.terminate().await;
26 |     }
27 | }
28 | 
29 | async fn do_capture(input_capture: &mut InputCapture) -> Result<(), CaptureError> {
30 |     loop {
31 |         let (client, event) = input_capture
32 |             .next()
33 |             .await
34 |             .ok_or(CaptureError::EndOfStream)??;
35 |         let pos = match client {
36 |             0 | 4 => Position::Left,
37 |             1 => Position::Right,
38 |             2 => Position::Top,
39 |             3 => Position::Bottom,
40 |             _ => panic!(),
41 |         };
42 |         log::info!("position: {client} ({pos}), event: {event}");
43 |         if let CaptureEvent::Input(Event::Keyboard(KeyboardEvent::Key { key: 1, .. })) = event {
44 |             input_capture.release().await?;
45 |             break Ok(());
46 |         }
47 |     }
48 | }
49 | 


--------------------------------------------------------------------------------
/src/client.rs:
--------------------------------------------------------------------------------
  1 | use std::{
  2 |     cell::RefCell,
  3 |     collections::HashSet,
  4 |     net::{IpAddr, SocketAddr},
  5 |     rc::Rc,
  6 | };
  7 | 
  8 | use slab::Slab;
  9 | 
 10 | use lan_mouse_ipc::{ClientConfig, ClientHandle, ClientState, Position};
 11 | 
 12 | #[derive(Clone, Default)]
 13 | pub struct ClientManager {
 14 |     clients: Rc<RefCell<Slab<(ClientConfig, ClientState)>>>,
 15 | }
 16 | 
 17 | impl ClientManager {
 18 |     /// add a new client to this manager
 19 |     pub fn add_client(&self) -> ClientHandle {
 20 |         self.clients.borrow_mut().insert(Default::default()) as ClientHandle
 21 |     }
 22 | 
 23 |     /// set the config of the given client
 24 |     pub fn set_config(&self, handle: ClientHandle, config: ClientConfig) {
 25 |         if let Some((c, _)) = self.clients.borrow_mut().get_mut(handle as usize) {
 26 |             *c = config;
 27 |         }
 28 |     }
 29 | 
 30 |     /// set the state of the given client
 31 |     pub fn set_state(&self, handle: ClientHandle, state: ClientState) {
 32 |         if let Some((_, s)) = self.clients.borrow_mut().get_mut(handle as usize) {
 33 |             *s = state;
 34 |         }
 35 |     }
 36 | 
 37 |     /// activate the given client
 38 |     /// returns, whether the client was activated
 39 |     pub fn activate_client(&self, handle: ClientHandle) -> bool {
 40 |         let mut clients = self.clients.borrow_mut();
 41 |         match clients.get_mut(handle as usize) {
 42 |             Some((_, s)) if !s.active => {
 43 |                 s.active = true;
 44 |                 true
 45 |             }
 46 |             _ => false,
 47 |         }
 48 |     }
 49 | 
 50 |     /// deactivate the given client
 51 |     /// returns, whether the client was deactivated
 52 |     pub fn deactivate_client(&self, handle: ClientHandle) -> bool {
 53 |         let mut clients = self.clients.borrow_mut();
 54 |         match clients.get_mut(handle as usize) {
 55 |             Some((_, s)) if s.active => {
 56 |                 s.active = false;
 57 |                 true
 58 |             }
 59 |             _ => false,
 60 |         }
 61 |     }
 62 | 
 63 |     /// find a client by its address
 64 |     pub fn get_client(&self, addr: SocketAddr) -> Option<ClientHandle> {
 65 |         // since there shouldn't be more than a handful of clients at any given
 66 |         // time this is likely faster than using a HashMap
 67 |         self.clients
 68 |             .borrow()
 69 |             .iter()
 70 |             .find_map(|(k, (_, s))| {
 71 |                 if s.active && s.ips.contains(&addr.ip()) {
 72 |                     Some(k)
 73 |                 } else {
 74 |                     None
 75 |                 }
 76 |             })
 77 |             .map(|p| p as ClientHandle)
 78 |     }
 79 | 
 80 |     /// get the client at the given position
 81 |     pub fn client_at(&self, pos: Position) -> Option<ClientHandle> {
 82 |         self.clients
 83 |             .borrow()
 84 |             .iter()
 85 |             .find_map(|(k, (c, s))| {
 86 |                 if s.active && c.pos == pos {
 87 |                     Some(k)
 88 |                 } else {
 89 |                     None
 90 |                 }
 91 |             })
 92 |             .map(|p| p as ClientHandle)
 93 |     }
 94 | 
 95 |     pub(crate) fn get_hostname(&self, handle: ClientHandle) -> Option<String> {
 96 |         self.clients
 97 |             .borrow_mut()
 98 |             .get_mut(handle as usize)
 99 |             .and_then(|(c, _)| c.hostname.clone())
100 |     }
101 | 
102 |     /// get the position of the corresponding client
103 |     pub(crate) fn get_pos(&self, handle: ClientHandle) -> Option<Position> {
104 |         self.clients
105 |             .borrow()
106 |             .get(handle as usize)
107 |             .map(|(c, _)| c.pos)
108 |     }
109 | 
110 |     /// remove a client from the list
111 |     pub fn remove_client(&self, client: ClientHandle) -> Option<(ClientConfig, ClientState)> {
112 |         // remove id from occupied ids
113 |         self.clients.borrow_mut().try_remove(client as usize)
114 |     }
115 | 
116 |     /// get the config & state of the given client
117 |     pub fn get_state(&self, handle: ClientHandle) -> Option<(ClientConfig, ClientState)> {
118 |         self.clients.borrow().get(handle as usize).cloned()
119 |     }
120 | 
121 |     /// get the current config & state of all clients
122 |     pub fn get_client_states(&self) -> Vec<(ClientHandle, ClientConfig, ClientState)> {
123 |         self.clients
124 |             .borrow()
125 |             .iter()
126 |             .map(|(k, v)| (k as ClientHandle, v.0.clone(), v.1.clone()))
127 |             .collect()
128 |     }
129 | 
130 |     /// update the fix ips of the client
131 |     pub fn set_fix_ips(&self, handle: ClientHandle, fix_ips: Vec<IpAddr>) {
132 |         if let Some((c, _)) = self.clients.borrow_mut().get_mut(handle as usize) {
133 |             c.fix_ips = fix_ips
134 |         }
135 |         self.update_ips(handle);
136 |     }
137 | 
138 |     /// update the dns-ips of the client
139 |     pub fn set_dns_ips(&self, handle: ClientHandle, dns_ips: Vec<IpAddr>) {
140 |         if let Some((_, s)) = self.clients.borrow_mut().get_mut(handle as usize) {
141 |             s.dns_ips = dns_ips
142 |         }
143 |         self.update_ips(handle);
144 |     }
145 | 
146 |     fn update_ips(&self, handle: ClientHandle) {
147 |         if let Some((c, s)) = self.clients.borrow_mut().get_mut(handle as usize) {
148 |             s.ips = c
149 |                 .fix_ips
150 |                 .iter()
151 |                 .cloned()
152 |                 .chain(s.dns_ips.iter().cloned())
153 |                 .collect::<HashSet<_>>();
154 |         }
155 |     }
156 | 
157 |     /// update the hostname of the given client
158 |     /// this automatically clears the active ip address and ips from dns
159 |     pub fn set_hostname(&self, handle: ClientHandle, hostname: Option<String>) -> bool {
160 |         let mut clients = self.clients.borrow_mut();
161 |         let Some((c, s)) = clients.get_mut(handle as usize) else {
162 |             return false;
163 |         };
164 | 
165 |         // hostname changed
166 |         if c.hostname != hostname {
167 |             c.hostname = hostname;
168 |             s.active_addr = None;
169 |             s.dns_ips.clear();
170 |             drop(clients);
171 |             self.update_ips(handle);
172 |             true
173 |         } else {
174 |             false
175 |         }
176 |     }
177 | 
178 |     /// update the port of the client
179 |     pub(crate) fn set_port(&self, handle: ClientHandle, port: u16) {
180 |         match self.clients.borrow_mut().get_mut(handle as usize) {
181 |             Some((c, s)) if c.port != port => {
182 |                 c.port = port;
183 |                 s.active_addr = s.active_addr.map(|a| SocketAddr::new(a.ip(), port));
184 |             }
185 |             _ => {}
186 |         };
187 |     }
188 | 
189 |     /// update the position of the client
190 |     /// returns true, if a change in capture position is required (pos changed & client is active)
191 |     pub(crate) fn set_pos(&self, handle: ClientHandle, pos: Position) -> bool {
192 |         match self.clients.borrow_mut().get_mut(handle as usize) {
193 |             Some((c, s)) if c.pos != pos => {
194 |                 log::info!("update pos {handle} {} -> {}", c.pos, pos);
195 |                 c.pos = pos;
196 |                 s.active
197 |             }
198 |             _ => false,
199 |         }
200 |     }
201 | 
202 |     /// update the enter hook command of the client
203 |     pub(crate) fn set_enter_hook(&self, handle: ClientHandle, enter_hook: Option<String>) {
204 |         if let Some((c, _s)) = self.clients.borrow_mut().get_mut(handle as usize) {
205 |             c.cmd = enter_hook;
206 |         }
207 |     }
208 | 
209 |     /// set resolving status of the client
210 |     pub(crate) fn set_resolving(&self, handle: ClientHandle, status: bool) {
211 |         if let Some((_, s)) = self.clients.borrow_mut().get_mut(handle as usize) {
212 |             s.resolving = status;
213 |         }
214 |     }
215 | 
216 |     /// get the enter hook command
217 |     pub(crate) fn get_enter_cmd(&self, handle: ClientHandle) -> Option<String> {
218 |         self.clients
219 |             .borrow()
220 |             .get(handle as usize)
221 |             .and_then(|(c, _)| c.cmd.clone())
222 |     }
223 | 
224 |     /// returns all clients that are currently active
225 |     pub(crate) fn active_clients(&self) -> Vec<ClientHandle> {
226 |         self.clients
227 |             .borrow()
228 |             .iter()
229 |             .filter(|(_, (_, s))| s.active)
230 |             .map(|(h, _)| h as ClientHandle)
231 |             .collect()
232 |     }
233 | 
234 |     pub(crate) fn set_active_addr(&self, handle: ClientHandle, addr: Option<SocketAddr>) {
235 |         if let Some((_, s)) = self.clients.borrow_mut().get_mut(handle as usize) {
236 |             s.active_addr = addr;
237 |         }
238 |     }
239 | 
240 |     pub(crate) fn set_alive(&self, handle: ClientHandle, alive: bool) {
241 |         if let Some((_, s)) = self.clients.borrow_mut().get_mut(handle as usize) {
242 |             s.alive = alive;
243 |         }
244 |     }
245 | 
246 |     pub(crate) fn active_addr(&self, handle: ClientHandle) -> Option<SocketAddr> {
247 |         self.clients
248 |             .borrow()
249 |             .get(handle as usize)
250 |             .and_then(|(_, s)| s.active_addr)
251 |     }
252 | 
253 |     pub(crate) fn alive(&self, handle: ClientHandle) -> bool {
254 |         self.clients
255 |             .borrow()
256 |             .get(handle as usize)
257 |             .map(|(_, s)| s.alive)
258 |             .unwrap_or(false)
259 |     }
260 | 
261 |     pub(crate) fn get_port(&self, handle: ClientHandle) -> Option<u16> {
262 |         self.clients
263 |             .borrow()
264 |             .get(handle as usize)
265 |             .map(|(c, _)| c.port)
266 |     }
267 | 
268 |     pub(crate) fn get_ips(&self, handle: ClientHandle) -> Option<HashSet<IpAddr>> {
269 |         self.clients
270 |             .borrow()
271 |             .get(handle as usize)
272 |             .map(|(_, s)| s.ips.clone())
273 |     }
274 | }
275 | 


--------------------------------------------------------------------------------
/src/crypto.rs:
--------------------------------------------------------------------------------
 1 | use std::fs;
 2 | use std::io::{self, BufWriter, Read, Write};
 3 | use std::path::Path;
 4 | use std::{fs::File, io::BufReader};
 5 | 
 6 | #[cfg(unix)]
 7 | use std::os::unix::fs::PermissionsExt;
 8 | 
 9 | use sha2::{Digest, Sha256};
10 | use thiserror::Error;
11 | use webrtc_dtls::crypto::Certificate;
12 | 
13 | #[derive(Debug, Error)]
14 | pub enum Error {
15 |     #[error(transparent)]
16 |     Io(#[from] io::Error),
17 |     #[error(transparent)]
18 |     Dtls(#[from] webrtc_dtls::Error),
19 | }
20 | 
21 | pub fn generate_fingerprint(cert: &[u8]) -> String {
22 |     let mut hash = Sha256::new();
23 |     hash.update(cert);
24 |     let bytes = hash
25 |         .finalize()
26 |         .iter()
27 |         .map(|x| format!("{x:02x}"))
28 |         .collect::<Vec<_>>();
29 |     bytes.join(":").to_lowercase()
30 | }
31 | 
32 | pub fn certificate_fingerprint(cert: &Certificate) -> String {
33 |     let certificate = cert.certificate.first().expect("certificate missing");
34 |     generate_fingerprint(certificate)
35 | }
36 | 
37 | /// load certificate from file
38 | pub fn load_certificate(path: &Path) -> Result<Certificate, Error> {
39 |     let f = File::open(path)?;
40 | 
41 |     let mut reader = BufReader::new(f);
42 |     let mut pem = String::new();
43 |     reader.read_to_string(&mut pem)?;
44 |     Ok(Certificate::from_pem(pem.as_str())?)
45 | }
46 | 
47 | pub(crate) fn load_or_generate_key_and_cert(path: &Path) -> Result<Certificate, Error> {
48 |     if path.exists() && path.is_file() {
49 |         Ok(load_certificate(path)?)
50 |     } else {
51 |         generate_key_and_cert(path)
52 |     }
53 | }
54 | 
55 | pub(crate) fn generate_key_and_cert(path: &Path) -> Result<Certificate, Error> {
56 |     let cert = Certificate::generate_self_signed(["ignored".to_owned()])?;
57 |     let serialized = cert.serialize_pem();
58 |     let parent = path.parent().expect("is a path");
59 |     fs::create_dir_all(parent)?;
60 |     let f = File::create(path)?;
61 |     #[cfg(unix)]
62 |     {
63 |         let mut perm = f.metadata()?.permissions();
64 |         perm.set_mode(0o400); /* r-- --- --- */
65 |         f.set_permissions(perm)?;
66 |     }
67 |     /* FIXME windows permissions */
68 |     let mut writer = BufWriter::new(f);
69 |     writer.write_all(serialized.as_bytes())?;
70 |     Ok(cert)
71 | }
72 | 


--------------------------------------------------------------------------------
/src/dns.rs:
--------------------------------------------------------------------------------
  1 | use std::{collections::HashMap, net::IpAddr};
  2 | 
  3 | use local_channel::mpsc::{channel, Receiver, Sender};
  4 | use tokio::task::{spawn_local, JoinHandle};
  5 | 
  6 | use hickory_resolver::{error::ResolveError, TokioAsyncResolver};
  7 | use tokio_util::sync::CancellationToken;
  8 | 
  9 | use lan_mouse_ipc::ClientHandle;
 10 | 
 11 | pub(crate) struct DnsResolver {
 12 |     cancellation_token: CancellationToken,
 13 |     task: Option<JoinHandle<()>>,
 14 |     request_tx: Sender<DnsRequest>,
 15 |     event_rx: Receiver<DnsEvent>,
 16 | }
 17 | 
 18 | struct DnsRequest {
 19 |     handle: ClientHandle,
 20 |     hostname: String,
 21 | }
 22 | 
 23 | pub(crate) enum DnsEvent {
 24 |     Resolving(ClientHandle),
 25 |     Resolved(ClientHandle, String, Result<Vec<IpAddr>, ResolveError>),
 26 | }
 27 | 
 28 | struct DnsTask {
 29 |     resolver: TokioAsyncResolver,
 30 |     request_rx: Receiver<DnsRequest>,
 31 |     event_tx: Sender<DnsEvent>,
 32 |     cancellation_token: CancellationToken,
 33 |     active_tasks: HashMap<ClientHandle, JoinHandle<()>>,
 34 | }
 35 | 
 36 | impl DnsResolver {
 37 |     pub(crate) fn new() -> Result<Self, ResolveError> {
 38 |         let resolver = TokioAsyncResolver::tokio_from_system_conf()?;
 39 |         let (request_tx, request_rx) = channel();
 40 |         let (event_tx, event_rx) = channel();
 41 |         let cancellation_token = CancellationToken::new();
 42 |         let dns_task = DnsTask {
 43 |             active_tasks: Default::default(),
 44 |             resolver,
 45 |             request_rx,
 46 |             event_tx,
 47 |             cancellation_token: cancellation_token.clone(),
 48 |         };
 49 |         let task = Some(spawn_local(dns_task.run()));
 50 |         Ok(Self {
 51 |             cancellation_token,
 52 |             task,
 53 |             event_rx,
 54 |             request_tx,
 55 |         })
 56 |     }
 57 | 
 58 |     pub(crate) fn resolve(&self, handle: ClientHandle, hostname: String) {
 59 |         let request = DnsRequest { handle, hostname };
 60 |         self.request_tx.send(request).expect("channel closed");
 61 |     }
 62 | 
 63 |     pub(crate) async fn event(&mut self) -> DnsEvent {
 64 |         self.event_rx.recv().await.expect("channel closed")
 65 |     }
 66 | 
 67 |     pub(crate) async fn terminate(&mut self) {
 68 |         self.cancellation_token.cancel();
 69 |         self.task.take().expect("task").await.expect("join error");
 70 |     }
 71 | }
 72 | 
 73 | impl DnsTask {
 74 |     async fn run(mut self) {
 75 |         let cancellation_token = self.cancellation_token.clone();
 76 |         tokio::select! {
 77 |             _ = self.do_dns() => {},
 78 |             _ = cancellation_token.cancelled() => {},
 79 |         }
 80 |     }
 81 | 
 82 |     async fn do_dns(&mut self) {
 83 |         while let Some(dns_request) = self.request_rx.recv().await {
 84 |             let DnsRequest { handle, hostname } = dns_request;
 85 | 
 86 |             /* abort previous dns task */
 87 |             let previous_task = self.active_tasks.remove(&handle);
 88 |             if let Some(task) = previous_task {
 89 |                 if !task.is_finished() {
 90 |                     task.abort();
 91 |                 }
 92 |             }
 93 | 
 94 |             self.event_tx
 95 |                 .send(DnsEvent::Resolving(handle))
 96 |                 .expect("channel closed");
 97 | 
 98 |             /* spawn task for dns request */
 99 |             let event_tx = self.event_tx.clone();
100 |             let resolver = self.resolver.clone();
101 |             let cancellation_token = self.cancellation_token.clone();
102 | 
103 |             let task = tokio::task::spawn_local(async move {
104 |                 tokio::select! {
105 |                     ips = resolver.lookup_ip(&hostname) => {
106 |                        let ips = ips.map(|ips| ips.iter().collect::<Vec<_>>());
107 |                        event_tx
108 |                            .send(DnsEvent::Resolved(handle, hostname, ips))
109 |                            .expect("channel closed");
110 |                     }
111 |                     _ = cancellation_token.cancelled() => {},
112 |                 }
113 |             });
114 |             self.active_tasks.insert(handle, task);
115 |         }
116 |     }
117 | }
118 | 


--------------------------------------------------------------------------------
/src/emulation_test.rs:
--------------------------------------------------------------------------------
 1 | use crate::config::Config;
 2 | use clap::Args;
 3 | use input_emulation::{InputEmulation, InputEmulationError};
 4 | use input_event::{Event, PointerEvent};
 5 | use std::f64::consts::PI;
 6 | use std::time::{Duration, Instant};
 7 | 
 8 | const FREQUENCY_HZ: f64 = 1.0;
 9 | const RADIUS: f64 = 100.0;
10 | 
11 | #[derive(Args, Clone, Debug, Eq, PartialEq)]
12 | pub struct TestEmulationArgs {
13 |     #[arg(long)]
14 |     mouse: bool,
15 |     #[arg(long)]
16 |     keyboard: bool,
17 |     #[arg(long)]
18 |     scroll: bool,
19 | }
20 | 
21 | pub async fn run(config: Config, _args: TestEmulationArgs) -> Result<(), InputEmulationError> {
22 |     log::info!("running input emulation test");
23 | 
24 |     let backend = config.emulation_backend().map(|b| b.into());
25 |     let mut emulation = InputEmulation::new(backend).await?;
26 |     emulation.create(0).await;
27 | 
28 |     let start = Instant::now();
29 |     let mut offset = (0, 0);
30 |     loop {
31 |         tokio::time::sleep(Duration::from_millis(1)).await;
32 |         let elapsed = start.elapsed();
33 |         let elapsed_sec_f64 = elapsed.as_secs_f64();
34 |         let second_fraction = elapsed_sec_f64 - elapsed_sec_f64 as u64 as f64;
35 |         let radians = second_fraction * 2. * PI * FREQUENCY_HZ;
36 |         let new_offset_f = (radians.cos() * RADIUS * 2., (radians * 2.).sin() * RADIUS);
37 |         let new_offset = (new_offset_f.0 as i32, new_offset_f.1 as i32);
38 |         if new_offset != offset {
39 |             let relative_motion = (new_offset.0 - offset.0, new_offset.1 - offset.1);
40 |             offset = new_offset;
41 |             let (dx, dy) = (relative_motion.0 as f64, relative_motion.1 as f64);
42 |             let event = Event::Pointer(PointerEvent::Motion { time: 0, dx, dy });
43 |             emulation.consume(event, 0).await?;
44 |         }
45 |     }
46 | }
47 | 


--------------------------------------------------------------------------------
/src/lib.rs:
--------------------------------------------------------------------------------
 1 | mod capture;
 2 | pub mod capture_test;
 3 | pub mod client;
 4 | pub mod config;
 5 | mod connect;
 6 | mod crypto;
 7 | mod dns;
 8 | mod emulation;
 9 | pub mod emulation_test;
10 | mod listen;
11 | pub mod service;
12 | 


--------------------------------------------------------------------------------
/src/main.rs:
--------------------------------------------------------------------------------
  1 | use env_logger::Env;
  2 | use input_capture::InputCaptureError;
  3 | use input_emulation::InputEmulationError;
  4 | use lan_mouse::{
  5 |     capture_test,
  6 |     config::{self, Command, Config, ConfigError},
  7 |     emulation_test,
  8 |     service::{Service, ServiceError},
  9 | };
 10 | use lan_mouse_cli::CliError;
 11 | #[cfg(feature = "gtk")]
 12 | use lan_mouse_gtk::GtkError;
 13 | use lan_mouse_ipc::{IpcError, IpcListenerCreationError};
 14 | use std::{
 15 |     future::Future,
 16 |     io,
 17 |     process::{self, Child},
 18 | };
 19 | use thiserror::Error;
 20 | use tokio::task::LocalSet;
 21 | 
 22 | #[derive(Debug, Error)]
 23 | enum LanMouseError {
 24 |     #[error(transparent)]
 25 |     Service(#[from] ServiceError),
 26 |     #[error(transparent)]
 27 |     IpcError(#[from] IpcError),
 28 |     #[error(transparent)]
 29 |     Config(#[from] ConfigError),
 30 |     #[error(transparent)]
 31 |     Io(#[from] io::Error),
 32 |     #[error(transparent)]
 33 |     Capture(#[from] InputCaptureError),
 34 |     #[error(transparent)]
 35 |     Emulation(#[from] InputEmulationError),
 36 |     #[cfg(feature = "gtk")]
 37 |     #[error(transparent)]
 38 |     Gtk(#[from] GtkError),
 39 |     #[error(transparent)]
 40 |     Cli(#[from] CliError),
 41 | }
 42 | 
 43 | fn main() {
 44 |     // init logging
 45 |     let env = Env::default().filter_or("LAN_MOUSE_LOG_LEVEL", "info");
 46 |     env_logger::init_from_env(env);
 47 | 
 48 |     if let Err(e) = run() {
 49 |         log::error!("{e}");
 50 |         process::exit(1);
 51 |     }
 52 | }
 53 | 
 54 | fn run() -> Result<(), LanMouseError> {
 55 |     let config = config::Config::new()?;
 56 |     match config.command() {
 57 |         Some(command) => match command {
 58 |             Command::TestEmulation(args) => run_async(emulation_test::run(config, args))?,
 59 |             Command::TestCapture(args) => run_async(capture_test::run(config, args))?,
 60 |             Command::Cli(cli_args) => run_async(lan_mouse_cli::run(cli_args))?,
 61 |             Command::Daemon => {
 62 |                 // if daemon is specified we run the service
 63 |                 match run_async(run_service(config)) {
 64 |                     Err(LanMouseError::Service(ServiceError::IpcListen(
 65 |                         IpcListenerCreationError::AlreadyRunning,
 66 |                     ))) => log::info!("service already running!"),
 67 |                     r => r?,
 68 |                 }
 69 |             }
 70 |         },
 71 |         None => {
 72 |             //  otherwise start the service as a child process and
 73 |             //  run a frontend
 74 |             #[cfg(feature = "gtk")]
 75 |             {
 76 |                 let mut service = start_service()?;
 77 |                 let res = lan_mouse_gtk::run();
 78 |                 #[cfg(unix)]
 79 |                 {
 80 |                     // on unix we give the service a chance to terminate gracefully
 81 |                     let pid = service.id() as libc::pid_t;
 82 |                     unsafe {
 83 |                         libc::kill(pid, libc::SIGINT);
 84 |                     }
 85 |                     service.wait()?;
 86 |                 }
 87 |                 service.kill()?;
 88 |                 res?;
 89 |             }
 90 |             #[cfg(not(feature = "gtk"))]
 91 |             {
 92 |                 // run daemon if gtk is diabled
 93 |                 match run_async(run_service(config)) {
 94 |                     Err(LanMouseError::Service(ServiceError::IpcListen(
 95 |                         IpcListenerCreationError::AlreadyRunning,
 96 |                     ))) => log::info!("service already running!"),
 97 |                     r => r?,
 98 |                 }
 99 |             }
100 |         }
101 |     }
102 | 
103 |     Ok(())
104 | }
105 | 
106 | fn run_async<F, E>(f: F) -> Result<(), LanMouseError>
107 | where
108 |     F: Future<Output = Result<(), E>>,
109 |     LanMouseError: From<E>,
110 | {
111 |     // create single threaded tokio runtime
112 |     let runtime = tokio::runtime::Builder::new_current_thread()
113 |         .enable_io()
114 |         .enable_time()
115 |         .build()?;
116 | 
117 |     // run async event loop
118 |     Ok(runtime.block_on(LocalSet::new().run_until(f))?)
119 | }
120 | 
121 | fn start_service() -> Result<Child, io::Error> {
122 |     let child = process::Command::new(std::env::current_exe()?)
123 |         .args(std::env::args().skip(1))
124 |         .arg("daemon")
125 |         .spawn()?;
126 |     Ok(child)
127 | }
128 | 
129 | async fn run_service(config: Config) -> Result<(), ServiceError> {
130 |     let release_bind = config.release_bind();
131 |     let config_path = config.config_path().to_owned();
132 |     let mut service = Service::new(config).await?;
133 |     log::info!("using config: {config_path:?}");
134 |     log::info!("Press {release_bind:?} to release the mouse");
135 |     service.run().await?;
136 |     log::info!("service exited!");
137 |     Ok(())
138 | }
139 | 


--------------------------------------------------------------------------------