From 9af5f9452efe30a715823320088b6d70fd31b83d Mon Sep 17 00:00:00 2001 From: Ferdinand Schober Date: Tue, 24 Mar 2026 15:06:03 +0100 Subject: [PATCH 01/33] fix icon build (#399) --- .github/workflows/pre-release.yml | 4 ++-- .github/workflows/tagged-release.yml | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/pre-release.yml b/.github/workflows/pre-release.yml index cf70278..81260af 100644 --- a/.github/workflows/pre-release.yml +++ b/.github/workflows/pre-release.yml @@ -84,7 +84,7 @@ jobs: steps: - uses: actions/checkout@v4 - name: install dependencies - run: brew install gtk4 libadwaita imagemagick + run: brew install gtk4 libadwaita imagemagick librsvg - name: Release Build run: | cargo build --release @@ -112,7 +112,7 @@ jobs: steps: - uses: actions/checkout@v4 - name: install dependencies - run: brew install gtk4 libadwaita imagemagick + run: brew install gtk4 libadwaita imagemagick librsvg - name: Release Build run: | cargo build --release diff --git a/.github/workflows/tagged-release.yml b/.github/workflows/tagged-release.yml index 73075a8..a1e77cb 100644 --- a/.github/workflows/tagged-release.yml +++ b/.github/workflows/tagged-release.yml @@ -80,7 +80,7 @@ jobs: steps: - uses: actions/checkout@v4 - name: install dependencies - run: brew install gtk4 libadwaita imagemagick + run: brew install gtk4 libadwaita imagemagick librsvg - name: Release Build run: | cargo build --release @@ -108,7 +108,7 @@ jobs: steps: - uses: actions/checkout@v4 - name: install dependencies - run: brew install gtk4 libadwaita imagemagick + run: brew install gtk4 libadwaita imagemagick librsvg - name: Release Build run: | cargo build --release From 810e25a7fc74136cd08f9975341065e3292ead75 Mon Sep 17 00:00:00 2001 From: Ferdinand Schober Date: Wed, 25 Mar 2026 11:46:35 +0100 Subject: [PATCH 02/33] Fix CI (#400) apparently inkscape is now required on macos --- .github/workflows/pre-release.yml | 8 ++++++-- .github/workflows/tagged-release.yml | 8 ++++++-- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/.github/workflows/pre-release.yml b/.github/workflows/pre-release.yml index 81260af..fc28d5f 100644 --- a/.github/workflows/pre-release.yml +++ b/.github/workflows/pre-release.yml @@ -84,7 +84,9 @@ jobs: steps: - uses: actions/checkout@v4 - name: install dependencies - run: brew install gtk4 libadwaita imagemagick librsvg + run: | + brew install --cask inkscape + brew install gtk4 libadwaita imagemagick librsvg - name: Release Build run: | cargo build --release @@ -112,7 +114,9 @@ jobs: steps: - uses: actions/checkout@v4 - name: install dependencies - run: brew install gtk4 libadwaita imagemagick librsvg + run: | + brew install --cask inkscape + brew install gtk4 libadwaita imagemagick librsvg - name: Release Build run: | cargo build --release diff --git a/.github/workflows/tagged-release.yml b/.github/workflows/tagged-release.yml index a1e77cb..6759617 100644 --- a/.github/workflows/tagged-release.yml +++ b/.github/workflows/tagged-release.yml @@ -80,7 +80,9 @@ jobs: steps: - uses: actions/checkout@v4 - name: install dependencies - run: brew install gtk4 libadwaita imagemagick librsvg + run: | + brew install --cask inkscape + brew install gtk4 libadwaita imagemagick librsvg - name: Release Build run: | cargo build --release @@ -108,7 +110,9 @@ jobs: steps: - uses: actions/checkout@v4 - name: install dependencies - run: brew install gtk4 libadwaita imagemagick librsvg + run: | + brew install --cask inkscape + brew install gtk4 libadwaita imagemagick librsvg - name: Release Build run: | cargo build --release From 9540739d89ed47b319808535432e9259c9069e71 Mon Sep 17 00:00:00 2001 From: Ferdinand Schober Date: Wed, 25 Mar 2026 12:44:59 +0100 Subject: [PATCH 03/33] add cancel in progress for CI --- .github/workflows/cachix.yml | 4 ++++ .github/workflows/rust.yml | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/.github/workflows/cachix.yml b/.github/workflows/cachix.yml index 494c803..521f390 100644 --- a/.github/workflows/cachix.yml +++ b/.github/workflows/cachix.yml @@ -7,6 +7,10 @@ on: branches: ["main"] workflow_dispatch: +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + jobs: nix: strategy: diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index ecef06e..d7bfc57 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -9,6 +9,10 @@ on: env: CARGO_TERM_COLOR: always +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + jobs: fmt: name: Formatting From 3eba50a0d30839b507c7924e7f0c10fe30ee7a23 Mon Sep 17 00:00:00 2001 From: Jon Stelly <967068+jonstelly@users.noreply.github.com> Date: Wed, 25 Mar 2026 07:09:28 -0500 Subject: [PATCH 04/33] feat: workflow and build updates (#372) * feat: workflow and build updates - macos 15 runners - add linux arm build - combine pre-release and tagged-release workflows * remove old release job * rename x86-64 -> x86_64 * rename arm -> arm64 * downgrade arm runner to ubuntu-22.04 --------- Co-authored-by: Ferdinand Schober Co-authored-by: Ferdinand Schober --- .../{pre-release.yml => release.yml} | 96 ++++++++--- .github/workflows/tagged-release.yml | 154 ------------------ README.md | 5 + 3 files changed, 75 insertions(+), 180 deletions(-) rename .github/workflows/{pre-release.yml => release.yml} (62%) delete mode 100644 .github/workflows/tagged-release.yml diff --git a/.github/workflows/pre-release.yml b/.github/workflows/release.yml similarity index 62% rename from .github/workflows/pre-release.yml rename to .github/workflows/release.yml index fc28d5f..6c3cdcd 100644 --- a/.github/workflows/pre-release.yml +++ b/.github/workflows/release.yml @@ -1,10 +1,17 @@ -name: "pre-release" +name: "Release" +run-name: "Release - ${{ startsWith(github.ref, 'refs/tags/') && github.ref_name || github.event.inputs.name || github.ref_name }}" on: push: branches: [ "main" ] - - + tags: + - v** + workflow_dispatch: + inputs: + name: + description: 'Development release name' + required: false + default: '' env: CARGO_TERM_COLOR: always @@ -20,12 +27,33 @@ jobs: sudo apt-get install libx11-dev libxtst-dev sudo apt-get install libadwaita-1-dev libgtk-4-dev - name: Release Build - run: cargo build --release + run: | + cargo build --release + cp target/release/lan-mouse lan-mouse-linux-x86_64 - name: Upload build artifact uses: actions/upload-artifact@v4 with: - name: lan-mouse-linux - path: target/release/lan-mouse + name: lan-mouse-linux-x86_64 + path: lan-mouse-linux-x86_64 + + linux-arm64-release-build: + runs-on: ubuntu-22.04-arm + steps: + - uses: actions/checkout@v4 + - name: install dependencies + run: | + sudo apt-get update + sudo apt-get install libx11-dev libxtst-dev + sudo apt-get install libadwaita-1-dev libgtk-4-dev + - name: Release Build + run: | + cargo build --release + cp target/release/lan-mouse lan-mouse-linux-arm64 + - name: Upload build artifact + uses: actions/upload-artifact@v4 + with: + name: lan-mouse-linux-arm64 + path: lan-mouse-linux-arm64 windows-release-build: runs-on: windows-latest @@ -72,12 +100,12 @@ jobs: mkdir "lan-mouse-windows" Get-Childitem -Path "C:\\gtk-build\\gtk\\x64\\release\\bin\\*.dll" -File -Recurse | Copy-Item -Destination "lan-mouse-windows" Copy-Item -Path "target\release\lan-mouse.exe" -Destination "lan-mouse-windows" - Compress-Archive -Path "lan-mouse-windows\*" -DestinationPath lan-mouse-windows.zip + Compress-Archive -Path "lan-mouse-windows\*" -DestinationPath lan-mouse-windows-x86_64.zip - name: Upload build artifact uses: actions/upload-artifact@v4 with: - name: lan-mouse-windows - path: lan-mouse-windows.zip + name: lan-mouse-windows-x86_64 + path: lan-mouse-windows-x86_64.zip macos-release-build: runs-on: macos-15-intel @@ -109,8 +137,8 @@ jobs: name: lan-mouse-macos-intel path: target/release/bundle/osx/lan-mouse-macos-intel.zip - macos-aarch64-release-build: - runs-on: macos-14 + macos-arm64-release-build: + runs-on: macos-15 steps: - uses: actions/checkout@v4 - name: install dependencies @@ -120,7 +148,7 @@ jobs: - name: Release Build run: | cargo build --release - cp target/release/lan-mouse lan-mouse-macos-aarch64 + cp target/release/lan-mouse lan-mouse-macos-arm64 - name: Make icns run: scripts/makeicns.sh - name: Install cargo bundle @@ -132,29 +160,45 @@ jobs: - name: Zip bundle run: | cd target/release/bundle/osx - zip -r "lan-mouse-macos-aarch64.zip" "Lan Mouse.app" + zip -r "lan-mouse-macos-arm64.zip" "Lan Mouse.app" - name: Upload build artifact uses: actions/upload-artifact@v4 with: - name: lan-mouse-macos-aarch64 - path: target/release/bundle/osx/lan-mouse-macos-aarch64.zip + name: lan-mouse-macos-arm64 + path: target/release/bundle/osx/lan-mouse-macos-arm64.zip - pre-release: - name: "Pre Release" - needs: [windows-release-build, linux-release-build, macos-release-build, macos-aarch64-release-build] + release: + name: "Release" + needs: [windows-release-build, linux-release-build, linux-arm64-release-build, macos-release-build, macos-arm64-release-build] runs-on: "ubuntu-latest" steps: - name: Download build artifacts uses: actions/download-artifact@v4 - - name: Create Release - uses: "marvinpinto/action-automatic-releases@latest" + - name: Create Pre-Release + if: ${{ !startsWith(github.ref, 'refs/tags/') }} + uses: softprops/action-gh-release@v2 with: - repo_token: "${{ secrets.GITHUB_TOKEN }}" - automatic_release_tag: "latest" + token: ${{ secrets.GITHUB_TOKEN }} + tag_name: ${{ github.event.inputs.name || github.ref_name }} + name: ${{ github.event.inputs.name || github.ref_name }} prerelease: true - title: "Development Build" + generate_release_notes: true files: | - lan-mouse-linux/lan-mouse + lan-mouse-linux-x86_64/lan-mouse-linux-x86_64 + lan-mouse-linux-arm64/lan-mouse-linux-arm64 lan-mouse-macos-intel/lan-mouse-macos-intel.zip - lan-mouse-macos-aarch64/lan-mouse-macos-aarch64.zip - lan-mouse-windows/lan-mouse-windows.zip + lan-mouse-macos-arm64/lan-mouse-macos-arm64.zip + lan-mouse-windows-x86_64/lan-mouse-windows-x86_64.zip + - name: Create Tagged Release + if: startsWith(github.ref, 'refs/tags/') + uses: softprops/action-gh-release@v2 + with: + token: ${{ secrets.GITHUB_TOKEN }} + name: ${{ github.ref_name }} + generate_release_notes: true + files: | + lan-mouse-linux-x86_64/lan-mouse-linux-x86_64 + lan-mouse-linux-arm64/lan-mouse-linux-arm64 + lan-mouse-macos-intel/lan-mouse-macos-intel.zip + lan-mouse-macos-arm64/lan-mouse-macos-arm64.zip + lan-mouse-windows-x86_64/lan-mouse-windows-x86_64.zip diff --git a/.github/workflows/tagged-release.yml b/.github/workflows/tagged-release.yml deleted file mode 100644 index 6759617..0000000 --- a/.github/workflows/tagged-release.yml +++ /dev/null @@ -1,154 +0,0 @@ -name: "Tagged Release" - -on: - push: - tags: - - v** - -jobs: - linux-release-build: - runs-on: ubuntu-22.04 - steps: - - uses: actions/checkout@v4 - - name: install dependencies - run: | - sudo apt-get update - sudo apt-get install libx11-dev libxtst-dev - sudo apt-get install libadwaita-1-dev libgtk-4-dev - - name: Release Build - run: cargo build --release - - name: Upload build artifact - uses: actions/upload-artifact@v4 - with: - name: lan-mouse-linux - path: target/release/lan-mouse - - windows-release-build: - runs-on: windows-latest - steps: - - uses: actions/setup-python@v5 - with: - python-version: '3.11' - # needed for cache restore - - name: create gtk dir - run: mkdir C:\gtk-build\gtk\x64\release - - uses: actions/cache@v3 - id: cache - with: - path: c:/gtk-build/gtk/x64/release/** - key: gtk-windows-build - restore-keys: gtk-windows-build - - name: Update path - run: | - echo "PKG_CONFIG=C:\gtk-build\gtk\x64\release\bin\pkgconf.exe" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append - echo "C:\pkg-config-lite-0.28-1\bin" | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append - echo "C:\gtk-build\gtk\x64\release\bin" | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append - echo $env:GITHUB_PATH - echo $env:PATH - - name: Install dependencies - if: steps.cache.outputs.cache-hit != 'true' - run: | - # choco install msys2 - # choco install visualstudio2022-workload-vctools - # choco install pkgconfiglite - py -m venv .venv - .venv\Scripts\activate.ps1 - py -m pip install gvsbuild - # see https://github.com/wingtk/gvsbuild/pull/1004 - Move-Item "C:\Program Files\Git\usr\bin" "C:\Program Files\Git\usr\notbin" - Move-Item "C:\Program Files\Git\bin" "C:\Program Files\Git\notbin" - gvsbuild build --msys-dir=C:\msys64 gtk4 libadwaita librsvg - Move-Item "C:\Program Files\Git\usr\notbin" "C:\Program Files\Git\usr\bin" - Move-Item "C:\Program Files\Git\notbin" "C:\Program Files\Git\bin" - - uses: actions/checkout@v4 - - name: Release Build - run: cargo build --release - - name: Create Archive - run: | - mkdir "lan-mouse-windows" - Get-Childitem -Path "C:\\gtk-build\\gtk\\x64\\release\\bin\\*.dll" -File -Recurse | Copy-Item -Destination "lan-mouse-windows" - Copy-Item -Path "target\release\lan-mouse.exe" -Destination "lan-mouse-windows" - Compress-Archive -Path "lan-mouse-windows\*" -DestinationPath lan-mouse-windows.zip - - name: Upload build artifact - uses: actions/upload-artifact@v4 - with: - name: lan-mouse-windows - path: lan-mouse-windows.zip - - macos-release-build: - runs-on: macos-15-intel - steps: - - uses: actions/checkout@v4 - - name: install dependencies - run: | - brew install --cask inkscape - brew install gtk4 libadwaita imagemagick librsvg - - name: Release Build - run: | - cargo build --release - cp target/release/lan-mouse lan-mouse-macos-intel - - name: Make icns - run: scripts/makeicns.sh - - name: Install cargo bundle - run: cargo install cargo-bundle - - name: Bundle - run: | - cargo bundle --release - scripts/copy-macos-dylib.sh "target/release/bundle/osx/Lan Mouse.app/Contents/MacOS/lan-mouse" - - name: Zip bundle - run: | - cd target/release/bundle/osx - zip -r "lan-mouse-macos-intel.zip" "Lan Mouse.app" - - name: Upload build artifact - uses: actions/upload-artifact@v4 - with: - name: lan-mouse-macos-intel.zip - path: target/release/bundle/osx/lan-mouse-macos-intel.zip - - macos-aarch64-release-build: - runs-on: macos-14 - steps: - - uses: actions/checkout@v4 - - name: install dependencies - run: | - brew install --cask inkscape - brew install gtk4 libadwaita imagemagick librsvg - - name: Release Build - run: | - cargo build --release - cp target/release/lan-mouse lan-mouse-macos-aarch64 - - name: Make icns - run: scripts/makeicns.sh - - name: Install cargo bundle - run: cargo install cargo-bundle - - name: Bundle - run: | - cargo bundle --release - scripts/copy-macos-dylib.sh "target/release/bundle/osx/Lan Mouse.app/Contents/MacOS/lan-mouse" - - name: Zip bundle - run: | - cd target/release/bundle/osx - zip -r "lan-mouse-macos-aarch64.zip" "Lan Mouse.app" - - name: Upload build artifact - uses: actions/upload-artifact@v4 - with: - name: lan-mouse-macos-aarch64.zip - path: target/release/bundle/osx/lan-mouse-macos-aarch64.zip - - tagged-release: - name: "Tagged Release" - needs: [windows-release-build, linux-release-build, macos-release-build, macos-aarch64-release-build] - runs-on: "ubuntu-latest" - steps: - - name: Download build artifacts - uses: actions/download-artifact@v4 - - name: Create Release - uses: "marvinpinto/action-automatic-releases@latest" - with: - repo_token: "${{ secrets.GITHUB_TOKEN }}" - prerelease: false - files: | - lan-mouse-linux/lan-mouse - lan-mouse-macos-intel/lan-mouse-macos-intel.zip - lan-mouse-macos-aarch64/lan-mouse-macos-aarch64.zip - lan-mouse-windows/lan-mouse-windows.zip diff --git a/README.md b/README.md index 00e9674..fe6fdc3 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,9 @@ # Lan Mouse + +[![CI](https://github.com/feschber/lan-mouse/actions/workflows/rust.yml/badge.svg)](https://github.com/feschber/lan-mouse/actions/workflows/rust.yml) [![Cachix](https://github.com/feschber/lan-mouse/actions/workflows/cachix.yml/badge.svg)](https://github.com/feschber/lan-mouse/actions/workflows/cachix.yml) [![Pre-release](https://github.com/feschber/lan-mouse/actions/workflows/pre-release.yml/badge.svg)](https://github.com/feschber/lan-mouse/actions/workflows/pre-release.yml) [![Tagged release](https://github.com/feschber/lan-mouse/actions/workflows/tagged-release.yml/badge.svg)](https://github.com/feschber/lan-mouse/actions/workflows/tagged-release.yml) + +[![crates.io](https://img.shields.io/crates/v/lan-mouse.svg)](https://crates.io/crates/lan-mouse) [![license](https://img.shields.io/crates/l/lan-mouse.svg)](https://github.com/feschber/lan-mouse/blob/main/Cargo.toml) + Lan Mouse is a *cross-platform* mouse and keyboard sharing software similar to universal-control on Apple devices. It allows for using multiple PCs via a single set of mouse and keyboard. This is also known as a Software KVM switch. From 0ef8edb7b269974b918fd0c86ec4b785f36f0e8d Mon Sep 17 00:00:00 2001 From: Ferdinand Schober Date: Wed, 25 Mar 2026 13:31:20 +0100 Subject: [PATCH 05/33] update checkout and upload-artifact actions --- .github/workflows/cachix.yml | 2 +- .github/workflows/release.yml | 20 ++++++++++---------- .github/workflows/rust.yml | 4 ++-- 3 files changed, 13 insertions(+), 13 deletions(-) diff --git a/.github/workflows/cachix.yml b/.github/workflows/cachix.yml index 521f390..10d7568 100644 --- a/.github/workflows/cachix.yml +++ b/.github/workflows/cachix.yml @@ -23,7 +23,7 @@ jobs: runs-on: ${{ matrix.os }} steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v6 with: submodules: recursive diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 6c3cdcd..1b8a353 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -20,7 +20,7 @@ jobs: linux-release-build: runs-on: ubuntu-22.04 steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: install dependencies run: | sudo apt-get update @@ -31,7 +31,7 @@ jobs: cargo build --release cp target/release/lan-mouse lan-mouse-linux-x86_64 - name: Upload build artifact - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v6 with: name: lan-mouse-linux-x86_64 path: lan-mouse-linux-x86_64 @@ -39,7 +39,7 @@ jobs: linux-arm64-release-build: runs-on: ubuntu-22.04-arm steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: install dependencies run: | sudo apt-get update @@ -50,7 +50,7 @@ jobs: cargo build --release cp target/release/lan-mouse lan-mouse-linux-arm64 - name: Upload build artifact - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v6 with: name: lan-mouse-linux-arm64 path: lan-mouse-linux-arm64 @@ -92,7 +92,7 @@ jobs: gvsbuild build --msys-dir=C:\msys64 gtk4 libadwaita librsvg Move-Item "C:\Program Files\Git\usr\notbin" "C:\Program Files\Git\usr\bin" Move-Item "C:\Program Files\Git\notbin" "C:\Program Files\Git\bin" - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Release Build run: cargo build --release - name: Create Archive @@ -102,7 +102,7 @@ jobs: Copy-Item -Path "target\release\lan-mouse.exe" -Destination "lan-mouse-windows" Compress-Archive -Path "lan-mouse-windows\*" -DestinationPath lan-mouse-windows-x86_64.zip - name: Upload build artifact - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v6 with: name: lan-mouse-windows-x86_64 path: lan-mouse-windows-x86_64.zip @@ -110,7 +110,7 @@ jobs: macos-release-build: runs-on: macos-15-intel steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: install dependencies run: | brew install --cask inkscape @@ -132,7 +132,7 @@ jobs: cd target/release/bundle/osx zip -r "lan-mouse-macos-intel.zip" "Lan Mouse.app" - name: Upload build artifact - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v6 with: name: lan-mouse-macos-intel path: target/release/bundle/osx/lan-mouse-macos-intel.zip @@ -140,7 +140,7 @@ jobs: macos-arm64-release-build: runs-on: macos-15 steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: install dependencies run: | brew install --cask inkscape @@ -162,7 +162,7 @@ jobs: cd target/release/bundle/osx zip -r "lan-mouse-macos-arm64.zip" "Lan Mouse.app" - name: Upload build artifact - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v6 with: name: lan-mouse-macos-arm64 path: target/release/bundle/osx/lan-mouse-macos-arm64.zip diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index d7bfc57..e62f710 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -18,7 +18,7 @@ jobs: name: Formatting runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: cargo fmt run: cargo fmt --check @@ -39,7 +39,7 @@ jobs: - clippy - test steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - uses: Swatinem/rust-cache@v2 - name: Install Linux deps if: runner.os == 'Linux' From 4d8f7d781321ae4158661156a8c4baec92341ada Mon Sep 17 00:00:00 2001 From: Jon Stelly <967068+jonstelly@users.noreply.github.com> Date: Wed, 25 Mar 2026 07:34:17 -0500 Subject: [PATCH 06/33] chore: developer experience - pre-commit hook, ai instructions, yaml formatting (#374) * chore: developer experience - pre-commit hook, ai instructions, yaml formatting for prettier * no prettierrc, editorconfig instead * fixes from copilot suggestions --------- Co-authored-by: Ferdinand Schober --- .editorconfig | 24 ++++++++++++++++ .githooks/pre-commit | 34 +++++++++++++++++++++++ AGENTS.md | 65 ++++++++++++++++++++++++++++++++++++++++++++ README.md | 23 ++++++++++++++-- 4 files changed, 144 insertions(+), 2 deletions(-) create mode 100644 .editorconfig create mode 100755 .githooks/pre-commit create mode 100644 AGENTS.md diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..32ebb15 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,24 @@ +root = true + +[*] +end_of_line = lf +insert_final_newline = true +trim_trailing_whitespace = true +charset = utf-8 + +[*.rs] +indent_style = space +indent_size = 4 +max_line_length = 100 + +[*.{yml,yaml}] +indent_style = space +indent_size = 2 + +[*.toml] +indent_style = space +indent_size = 4 + +[*.nix] +indent_style = space +indent_size = 2 diff --git a/.githooks/pre-commit b/.githooks/pre-commit new file mode 100755 index 0000000..456b693 --- /dev/null +++ b/.githooks/pre-commit @@ -0,0 +1,34 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Ensure we're at the repo root +cd "$(git rev-parse --show-toplevel)" || exit 1 + +echo "Running cargo fmt (auto-format)..." +# Run formatter to apply fixes but do not stage them. If formatting changed, +# fail the commit so the user can review and stage the changes manually. +cargo fmt --all +if ! git diff --quiet --exit-code; then + echo "" >&2 + echo "ERROR: cargo fmt modified files. Review changes, stage them, and commit again." >&2 + git --no-pager diff --name-only + exit 1 +fi + +echo "Running cargo clippy..." +# Matches CI: deny warnings to keep code health strict +if ! cargo clippy --workspace --all-targets --all-features -- -D warnings; then + echo "" >&2 + echo "ERROR: clippy found warnings/errors. Fix them before committing." >&2 + exit 1 +fi + +echo "Running cargo test..." +if ! cargo test --workspace --all-features; then + echo "" >&2 + echo "ERROR: Some tests failed. Fix tests before committing." >&2 + exit 1 +fi + +echo "All pre-commit checks passed." +exit 0 diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..05414c4 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,65 @@ +# Lan Mouse Agent Instructions + +## Overview + +Lan Mouse is an open-source Software KVM sharing mouse/keyboard input across local networks. The Rust workspace combines a GTK frontend, CLI/daemon mode, and multi-OS capture/emulation backends for Linux, Windows, and macOS. + +## Core principles + +- **Scope discipline.** Only implement what was requested; describe follow-up work instead of absorbing it. +- **Clarify OS behavior.** Ask when requirements touch OS-specific capture/emulation (they differ significantly). +- **Docs stay current.** Update [README.md](README.md) or [DOC.md](DOC.md) when touching public APIs or platform support. +- **Rust idioms.** Use `Result`/`Option`, `thiserror` for errors, descriptive logs, and concise comments for non-obvious invariants. + +## Terminology + +- **Client:** A remote machine that can receive or send input events. Each client is either _active_ (receiving events) or _inactive_ (can send events back). This mutual exclusion prevents feedback loops. +- **Backend:** OS-specific implementation for capture or emulation (e.g., libei, layer-shell, wlroots, X11, Windows, macOS). +- **Handle:** A per-client identifier used to route events and track state (pressed keys, position). + +## Architecture + +**Pipeline:** `input-capture` → `lan-mouse-ipc` → `input-emulation` + +- **input-capture:** Reads OS events into a `Stream`. Backends tried in priority order (libei → layer-shell → X11 → fallback). Tracks `pressed_keys` to avoid stuck modifiers. `position_map` queues events when multiple clients share a screen edge. +- **input-emulation:** Replays events via the `Emulation` trait (`consume`, `create`, `destroy`, `terminate`). Maintains `pressed_keys` and releases them on disconnect. +- **lan-mouse-ipc / lan-mouse-proto:** Protocol glue and serialization. Events are UDP; connection requests are TCP on the same port. Version bumps required when serialization changes. +- **input-event:** Shared scancode enums and abstract event types—extend here, don't duplicate translations. + +## Feature & cfg discipline + +- Feature flags live in root `Cargo.toml`. Gate OS-specific modules with tight cfgs (e.g., `cfg(all(unix, feature = "layer_shell", not(target_os = "macos")))`). +- Prefer module-level gating over per-function cfgs to avoid empty stubs. +- New backends: add feature in `Cargo.toml`, create gated module, log backend selection. + +## Async patterns + +- Tokio runtime with `futures` streams and `async_trait`. Model new flows as streams or async methods. +- Avoid blocking; use `spawn_blocking` if needed. Prefer existing single-threaded stream handling. +- `InputCapture` implements `Stream` and manually pumps backends—don't short-circuit this logic. + +## Commands + +```sh +cargo build --workspace # full build +cargo build -p # single crate +cargo test --workspace # all tests +cargo fmt && cargo clippy --workspace --all-targets --all-features # lint +RUST_LOG=lan_mouse=debug cargo run # debug logging +``` + +Run from repo root—no `cd` in scripts. + +## Testing + +- Unit tests for utilities; integration tests for protocol behavior. +- OS-specific backends: test via GTK/CLI on target OS or document manual verification. +- Dummy backend exercises pipeline without real dependencies. +- Verify `terminate()` releases keys on unexpected disconnect. + +## Workflow + +1. Clarify ambiguous requirements, especially OS-specific behavior. +2. Implement minimal change; flag follow-up work. +3. Add proportional tests; run `cargo test` on affected crates. +4. Run `cargo fmt` and `cargo clippy --workspace --all-targets --all-features`. diff --git a/README.md b/README.md index fe6fdc3..f3b4b49 100644 --- a/README.md +++ b/README.md @@ -182,8 +182,27 @@ For a detailed list of available features, checkout the [Cargo.toml](./Cargo.tom +## Development -## Installing Dependencies for Development / Compiling from Source +### Git pre-commit hook + +This repository includes a local git hooks directory `.githooks/` with a `pre-commit` script that enforces formatting, lints, and tests before allowing a commit. It is optional to enable it, but it will prevent you from committing code with failing unit tests or that needs clippy/fmt fixes. To enable the hook locally: + +1. Make the hook executable: + +```sh +chmod +x .githooks/pre-commit +``` + +2. Point git to the hooks directory (one-time per clone): + +```sh +git config core.hooksPath .githooks +``` + +The `pre-commit` script runs `cargo fmt --all` (and fails if files were modified), `cargo clippy --workspace --all-targets --all-features -- -D warnings`, and `cargo test --workspace --all-features`. + +### Dependencies & Compiling from Source
MacOS @@ -456,4 +475,4 @@ The following sections detail the emulation and capture backends provided by lan - `libei`: This backend uses [libei](https://gitlab.freedesktop.org/libinput/libei) and is supported by GNOME >= 45 or KDE Plasma >= 6.1. - `windows`: Backend for input capture on Windows. - `macos`: Backend for input capture on MacOS. -- `x11`: TODO (not yet supported) +- `x11`: TODO (not yet supported) \ No newline at end of file From 2e1b5278ce789ac5388ea5548800856303a0372b Mon Sep 17 00:00:00 2001 From: Ferdinand Schober Date: Wed, 25 Mar 2026 13:36:32 +0100 Subject: [PATCH 07/33] fix: README badge --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index f3b4b49..d96607d 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Lan Mouse -[![CI](https://github.com/feschber/lan-mouse/actions/workflows/rust.yml/badge.svg)](https://github.com/feschber/lan-mouse/actions/workflows/rust.yml) [![Cachix](https://github.com/feschber/lan-mouse/actions/workflows/cachix.yml/badge.svg)](https://github.com/feschber/lan-mouse/actions/workflows/cachix.yml) [![Pre-release](https://github.com/feschber/lan-mouse/actions/workflows/pre-release.yml/badge.svg)](https://github.com/feschber/lan-mouse/actions/workflows/pre-release.yml) [![Tagged release](https://github.com/feschber/lan-mouse/actions/workflows/tagged-release.yml/badge.svg)](https://github.com/feschber/lan-mouse/actions/workflows/tagged-release.yml) +[![CI](https://github.com/feschber/lan-mouse/actions/workflows/rust.yml/badge.svg)](https://github.com/feschber/lan-mouse/actions/workflows/rust.yml) [![Cachix](https://github.com/feschber/lan-mouse/actions/workflows/cachix.yml/badge.svg)](https://github.com/feschber/lan-mouse/actions/workflows/cachix.yml) [![Release](https://github.com/feschber/lan-mouse/actions/workflows/release.yml/badge.svg)](https://github.com/feschber/lan-mouse/actions/workflows/release.yml) [![crates.io](https://img.shields.io/crates/v/lan-mouse.svg)](https://crates.io/crates/lan-mouse) [![license](https://img.shields.io/crates/l/lan-mouse.svg)](https://github.com/feschber/lan-mouse/blob/main/Cargo.toml) From 1075a90c5bf57fc59efea2f6890aafa988b54af0 Mon Sep 17 00:00:00 2001 From: Ferdinand Schober Date: Wed, 8 Apr 2026 13:00:36 +0200 Subject: [PATCH 08/33] update dependencies --- Cargo.lock | 1731 ++++++++++++++++++++++++---------------------------- 1 file changed, 795 insertions(+), 936 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index b5fcf02..1d94db9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,21 +2,6 @@ # It is not intended for manual editing. version = 4 -[[package]] -name = "addr2line" -version = "0.24.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1" -dependencies = [ - "gimli", -] - -[[package]] -name = "adler2" -version = "2.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" - [[package]] name = "aead" version = "0.5.2" @@ -54,9 +39,9 @@ dependencies = [ [[package]] name = "aho-corasick" -version = "1.1.3" +version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" dependencies = [ "memchr", ] @@ -72,9 +57,9 @@ dependencies = [ [[package]] name = "anstream" -version = "0.6.19" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "301af1932e46185686725e0fad2f8f2aa7da69dd70bf6ecc44d6b703844a3933" +checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d" dependencies = [ "anstyle", "anstyle-parse", @@ -87,44 +72,44 @@ dependencies = [ [[package]] name = "anstyle" -version = "1.0.11" +version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "862ed96ca487e809f1c8e5a8447f6ee2cf102f846893800b20cebdf541fc6bbd" +checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" [[package]] name = "anstyle-parse" -version = "0.2.7" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" +checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e" dependencies = [ "utf8parse", ] [[package]] name = "anstyle-query" -version = "1.1.3" +version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c8bdeb6047d8983be085bab0ba1472e6dc604e7041dbf6fcd5e71523014fae9" +checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" dependencies = [ - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] name = "anstyle-wincon" -version = "3.0.9" +version = "3.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "403f75924867bb1033c59fbf0797484329750cfbe3c4325cd33127941fabc882" +checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" dependencies = [ "anstyle", "once_cell_polyfill", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] name = "anyhow" -version = "1.0.98" +version = "1.0.102" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e16d2d3311acee920a9eb8d33b8cbc1787ce4a264e85f964c2404b969bdcd487" +checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" [[package]] name = "arraydeque" @@ -140,9 +125,9 @@ checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" [[package]] name = "ashpd" -version = "0.11.0" +version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6cbdf310d77fd3aaee6ea2093db7011dc2d35d2eb3481e5607f1f8d942ed99df" +checksum = "d2f3f79755c74fd155000314eb349864caa787c6592eace6c6882dad873d9c39" dependencies = [ "enumflags2", "futures-channel", @@ -231,9 +216,9 @@ dependencies = [ [[package]] name = "async-trait" -version = "0.1.88" +version = "0.1.89" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e539d3fca749fcee5236ab05e93a52867dd549cc157c8cb7f99595f3cedffdb5" +checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" dependencies = [ "proc-macro2", "quote", @@ -246,21 +231,6 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" -[[package]] -name = "backtrace" -version = "0.3.75" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6806a6321ec58106fea15becdad98371e28d92ccbc7c8f1b3b6dd724fe8f1002" -dependencies = [ - "addr2line", - "cfg-if", - "libc", - "miniz_oxide", - "object", - "rustc-demangle", - "windows-targets 0.52.6", -] - [[package]] name = "base16ct" version = "0.2.0" @@ -275,9 +245,9 @@ checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" [[package]] name = "base64ct" -version = "1.8.0" +version = "1.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55248b47b0caf0546f7988906588779981c43bb1bc9d0c44087278f80cdb44ba" +checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" [[package]] name = "bincode" @@ -296,9 +266,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.9.1" +version = "2.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967" +checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" [[package]] name = "block-buffer" @@ -320,9 +290,9 @@ dependencies = [ [[package]] name = "bumpalo" -version = "3.19.0" +version = "3.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" +checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" [[package]] name = "byteorder" @@ -332,9 +302,9 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "bytes" -version = "1.10.1" +version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" +checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" [[package]] name = "cairo-rs" @@ -342,7 +312,7 @@ version = "0.20.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "91e3bd0f4e25afa9cabc157908d14eeef9067d6448c49414d17b3fb55f0eadd0" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.11.0", "cairo-sys-rs", "glib", "libc", @@ -361,51 +331,35 @@ dependencies = [ [[package]] name = "camino" -version = "1.1.10" +version = "1.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0da45bc31171d8d6960122e222a67740df867c1dd53b4d51caa297084c185cab" +checksum = "e629a66d692cb9ff1a1c664e41771b3dcaf961985a9774c0eb0bd1b51cf60a48" dependencies = [ - "serde", + "serde_core", ] [[package]] name = "cargo-platform" -version = "0.2.0" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "84982c6c0ae343635a3a4ee6dedef965513735c8b183caa7289fa6e27399ebd4" +checksum = "87a0c0e6148f11f01f32650a2ea02d532b2ad4e81d8bd41e6e565b5adc5e6082" dependencies = [ "serde", -] - -[[package]] -name = "cargo-util-schemas" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e63d2780ac94487eb9f1fea7b0d56300abc9eb488800854ca217f102f5caccca" -dependencies = [ - "semver", - "serde", - "serde-untagged", - "serde-value", - "thiserror 1.0.69", - "toml", - "unicode-xid", - "url", + "serde_core", ] [[package]] name = "cargo_metadata" -version = "0.20.0" +version = "0.23.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f7835cfc6135093070e95eb2b53e5d9b5c403dc3a6be6040ee026270aa82502" +checksum = "ef987d17b0a113becdd19d3d0022d04d7ef41f9efe4f3fb63ac44ba61df3ade9" dependencies = [ "camino", "cargo-platform", - "cargo-util-schemas", "semver", "serde", "serde_json", - "thiserror 2.0.12", + "thiserror 2.0.18", ] [[package]] @@ -419,10 +373,11 @@ dependencies = [ [[package]] name = "cc" -version = "1.2.30" +version = "1.2.59" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "deec109607ca693028562ed836a5f1c4b8bd77755c4e132fc5ce11b0b6211ae7" +checksum = "b7a4d3ec6524d28a329fc53654bbadc9bdd7b0431f5d65f1a56ffb28a1ee5283" dependencies = [ + "find-msvc-tools", "jobserver", "libc", "shlex", @@ -442,9 +397,9 @@ dependencies = [ [[package]] name = "cfg-expr" -version = "0.20.1" +version = "0.20.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d0390889d58f934f01cd49736275b4c2da15bcfc328c78ff2349907e6cabf22" +checksum = "3c6b04e07d8080154ed4ac03546d9a2b303cc2fe1901ba0b35b301516e289368" dependencies = [ "smallvec", "target-lexicon", @@ -452,15 +407,9 @@ dependencies = [ [[package]] name = "cfg-if" -version = "1.0.1" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9555578bc9e57714c812a1f84e4fc5b4d21fcb063490c624de019f7464c91268" - -[[package]] -name = "cfg_aliases" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" [[package]] name = "cipher" @@ -474,9 +423,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.42" +version = "4.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed87a9d530bb41a67537289bafcac159cb3ee28460e0a4571123d2a778a6a882" +checksum = "b193af5b67834b676abd72466a96c1024e6a6ad978a1f484bd90b85c94041351" dependencies = [ "clap_builder", "clap_derive", @@ -484,9 +433,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.42" +version = "4.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64f4f3f3c77c94aff3c7e9aac9a2ca1974a5adf392a8bb751e827d6d127ab966" +checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f" dependencies = [ "anstream", "anstyle", @@ -496,9 +445,9 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.5.41" +version = "4.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef4f52386a59ca4c860f7393bcf8abd8dfd91ecccc0f774635ff68e92eeef491" +checksum = "1110bd8a634a1ab8cb04345d8d878267d57c3cf1b38d91b71af6686408bbca6a" dependencies = [ "heck", "proc-macro2", @@ -508,15 +457,15 @@ dependencies = [ [[package]] name = "clap_lex" -version = "0.7.5" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b94f61472cee1439c0b966b47e3aca9ae07e45d070759512cd390ea2bebc6675" +checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" [[package]] name = "colorchoice" -version = "1.0.4" +version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" +checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" [[package]] name = "concurrent-queue" @@ -535,9 +484,9 @@ checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" [[package]] name = "const_format" -version = "0.2.34" +version = "0.2.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "126f97965c8ad46d6d9163268ff28432e8f6a1196a55578867832e3049df63dd" +checksum = "7faa7469a93a566e9ccc1c73fe783b4a65c274c5ace346038dca9c39fe0030ad" dependencies = [ "const_format_proc_macros", ] @@ -575,7 +524,7 @@ version = "0.25.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "064badf302c3194842cf2c5d61f56cc88e54a759313879cdf03abdd27d0c3b97" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.11.0", "core-foundation", "core-graphics-types", "foreign-types", @@ -588,7 +537,7 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3d44a101f213f6c4cdc1853d4b78aef6db6bdfa3468798cc1d9912f4735013eb" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.11.0", "core-foundation", "libc", ] @@ -646,9 +595,9 @@ dependencies = [ [[package]] name = "crypto-common" -version = "0.1.6" +version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" dependencies = [ "generic-array", "rand_core 0.6.4", @@ -692,9 +641,9 @@ dependencies = [ [[package]] name = "data-encoding" -version = "2.9.0" +version = "2.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a2330da5de22e8a3cb63252ce2abb30116bf5265e89c0e01bc17015ce30a476" +checksum = "d7a1e2f27636f116493b8b860f5546edb47c8d8f8ea73e1d2a20be88e28d1fea" [[package]] name = "der" @@ -723,9 +672,9 @@ dependencies = [ [[package]] name = "deranged" -version = "0.4.0" +version = "0.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c9e6a11ca8224451684bc0d7d5a7adbf8f2fd6887261a1cfc3c0432f9d4068e" +checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" dependencies = [ "powerfmt", ] @@ -796,9 +745,9 @@ dependencies = [ [[package]] name = "endi" -version = "1.1.0" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3d8a32ae18130a3c84dd492d4215c3d913c3b07c6b63c2eb3eb7ff1101ab7bf" +checksum = "66b7e2430c6dff6a955451e2cfc438f09cea1965a9d6f87f7e3b90decc014099" [[package]] name = "enum-as-inner" @@ -835,9 +784,9 @@ dependencies = [ [[package]] name = "env_filter" -version = "0.1.3" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "186e05a59d4c50738528153b83b0b0194d3a29507dfec16eccd4b342903397d0" +checksum = "32e90c2accc4b07a8456ea0debdc2e7587bdd890680d71173a15d4ae604f6eef" dependencies = [ "log", "regex", @@ -845,9 +794,9 @@ dependencies = [ [[package]] name = "env_logger" -version = "0.11.8" +version = "0.11.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13c863f0904021b108aa8b2f55046443e6b1ebde8fd4a15c399893aae4fa069f" +checksum = "0621c04f2196ac3f488dd583365b9c09be011a4ab8b9f37248ffcc8f6198b56a" dependencies = [ "anstream", "anstyle", @@ -862,31 +811,21 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" -[[package]] -name = "erased-serde" -version = "0.4.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e004d887f51fcb9fef17317a2f3525c887d8aa3f4f50fed920816a688284a5b7" -dependencies = [ - "serde", - "typeid", -] - [[package]] name = "errno" -version = "0.3.13" +version = "0.3.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "778e2ac28f6c47af28e4907f13ffd1e1ddbd400980a9abd7c8df189bf578a5ad" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] name = "event-listener" -version = "5.4.0" +version = "5.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3492acde4c3fc54c845eaab3eed8bd00c7a7d881f78bfc801e43a93dec1331ae" +checksum = "e13b66accf52311f30a0db42147dadea9850cb48cd070028831ae5f5d4b856ab" dependencies = [ "concurrent-queue", "parking", @@ -905,9 +844,9 @@ dependencies = [ [[package]] name = "fastrand" -version = "2.3.0" +version = "2.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" +checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6" [[package]] name = "ff" @@ -935,6 +874,18 @@ dependencies = [ "rustc_version", ] +[[package]] +name = "find-msvc-tools" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" + +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + [[package]] name = "foreign-types" version = "0.5.0" @@ -964,18 +915,18 @@ checksum = "aa9a19cbb55df58761df49b23516a86d432839add4af60fc256da840f66ed35b" [[package]] name = "form_urlencoded" -version = "1.2.1" +version = "1.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" dependencies = [ "percent-encoding", ] [[package]] name = "futures" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" +checksum = "8b147ee9d1f6d097cef9ce628cd2ee62288d963e16fb287bd9286455b241382d" dependencies = [ "futures-channel", "futures-core", @@ -988,9 +939,9 @@ dependencies = [ [[package]] name = "futures-channel" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" +checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" dependencies = [ "futures-core", "futures-sink", @@ -998,15 +949,15 @@ dependencies = [ [[package]] name = "futures-core" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" [[package]] name = "futures-executor" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" +checksum = "baf29c38818342a3b26b5b923639e7b1f4a61fc5e76102d4b1981c6dc7a7579d" dependencies = [ "futures-core", "futures-task", @@ -1015,15 +966,15 @@ dependencies = [ [[package]] name = "futures-io" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" +checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" [[package]] name = "futures-lite" -version = "2.6.0" +version = "2.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f5edaec856126859abb19ed65f39e90fea3a9574b9707f13539acf4abf7eb532" +checksum = "f78e10609fe0e0b3f4157ffab1876319b5b0db102a2c60dc4626306dc46b44ad" dependencies = [ "fastrand", "futures-core", @@ -1034,9 +985,9 @@ dependencies = [ [[package]] name = "futures-macro" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" +checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" dependencies = [ "proc-macro2", "quote", @@ -1045,21 +996,21 @@ dependencies = [ [[package]] name = "futures-sink" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" +checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" [[package]] name = "futures-task" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" +checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" [[package]] name = "futures-util" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" +checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" dependencies = [ "futures-channel", "futures-core", @@ -1069,7 +1020,6 @@ dependencies = [ "futures-task", "memchr", "pin-project-lite", - "pin-utils", "slab", ] @@ -1130,20 +1080,6 @@ dependencies = [ "system-deps", ] -[[package]] -name = "generator" -version = "0.8.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d18470a76cb7f8ff746cf1f7470914f900252ec36bbc40b569d74b1258446827" -dependencies = [ - "cc", - "cfg-if", - "libc", - "log", - "rustversion", - "windows", -] - [[package]] name = "generic-array" version = "0.14.7" @@ -1157,25 +1093,38 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.2.16" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" dependencies = [ "cfg-if", "libc", - "wasi 0.11.1+wasi-snapshot-preview1", + "wasi", ] [[package]] name = "getrandom" -version = "0.3.3" +version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" dependencies = [ "cfg-if", "libc", - "r-efi", - "wasi 0.14.2+wasi-0.2.4", + "r-efi 5.3.0", + "wasip2", +] + +[[package]] +name = "getrandom" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" +dependencies = [ + "cfg-if", + "libc", + "r-efi 6.0.0", + "wasip2", + "wasip3", ] [[package]] @@ -1188,12 +1137,6 @@ dependencies = [ "polyval", ] -[[package]] -name = "gimli" -version = "0.31.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" - [[package]] name = "gio" version = "0.20.12" @@ -1226,11 +1169,11 @@ dependencies = [ [[package]] name = "git2" -version = "0.20.2" +version = "0.20.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2deb07a133b1520dc1a5690e9bd08950108873d7ed5de38dcc74d3b5ebffa110" +checksum = "7b88256088d75a56f8ecfa070513a775dd9107f6530ef14919dac831af9cfe2b" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.11.0", "libc", "libgit2-sys", "log", @@ -1243,7 +1186,7 @@ version = "0.20.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ffc4b6e352d4716d84d7dde562dd9aee2a7d48beb872dd9ece7f2d1515b2d683" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.11.0", "futures-channel", "futures-core", "futures-executor", @@ -1420,9 +1363,18 @@ dependencies = [ [[package]] name = "hashbrown" -version = "0.15.4" +version = "0.15.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5971ac85611da7067dbfcabef3c70ebb5606018acd9e2a3903a0da507521e0d5" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "foldhash", +] + +[[package]] +name = "hashbrown" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" [[package]] name = "heck" @@ -1454,7 +1406,7 @@ dependencies = [ "once_cell", "rand 0.9.2", "ring", - "thiserror 2.0.12", + "thiserror 2.0.18", "tinyvec", "tokio", "tracing", @@ -1477,7 +1429,7 @@ dependencies = [ "rand 0.9.2", "resolv-conf", "smallvec", - "thiserror 2.0.12", + "thiserror 2.0.18", "tokio", "tracing", ] @@ -1502,20 +1454,20 @@ dependencies = [ [[package]] name = "hostname" -version = "0.4.1" +version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a56f203cd1c76362b69e3863fd987520ac36cf70a8c92627449b2f64a8cf7d65" +checksum = "617aaa3557aef3810a6369d0a99fac8a080891b68bd9f9812a1eeda0c0730cbd" dependencies = [ "cfg-if", "libc", - "windows-link", + "windows-link 0.2.1", ] [[package]] name = "iana-time-zone" -version = "0.1.63" +version = "0.1.65" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b0c919e5debc312ad217002b8048a17b7d83f80703865bbfcfebb0458b0b27d8" +checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" dependencies = [ "android_system_properties", "core-foundation-sys", @@ -1523,7 +1475,7 @@ dependencies = [ "js-sys", "log", "wasm-bindgen", - "windows-core", + "windows-core 0.62.2", ] [[package]] @@ -1537,12 +1489,13 @@ dependencies = [ [[package]] name = "icu_collections" -version = "2.0.0" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "200072f5d0e3614556f94a9930d5dc3e0662a652823904c3a75dc3b0af7fee47" +checksum = "2984d1cd16c883d7935b9e07e44071dca8d917fd52ecc02c04d5fa0b5a3f191c" dependencies = [ "displaydoc", "potential_utf", + "utf8_iter", "yoke", "zerofrom", "zerovec", @@ -1550,9 +1503,9 @@ dependencies = [ [[package]] name = "icu_locale_core" -version = "2.0.0" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0cde2700ccaed3872079a65fb1a78f6c0a36c91570f28755dda67bc8f7d9f00a" +checksum = "92219b62b3e2b4d88ac5119f8904c10f8f61bf7e95b640d25ba3075e6cac2c29" dependencies = [ "displaydoc", "litemap", @@ -1563,11 +1516,10 @@ dependencies = [ [[package]] name = "icu_normalizer" -version = "2.0.0" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "436880e8e18df4d7bbc06d58432329d6458cc84531f7ac5f024e93deadb37979" +checksum = "c56e5ee99d6e3d33bd91c5d85458b6005a22140021cc324cea84dd0e72cff3b4" dependencies = [ - "displaydoc", "icu_collections", "icu_normalizer_data", "icu_properties", @@ -1578,42 +1530,38 @@ dependencies = [ [[package]] name = "icu_normalizer_data" -version = "2.0.0" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "00210d6893afc98edb752b664b8890f0ef174c8adbb8d0be9710fa66fbbf72d3" +checksum = "da3be0ae77ea334f4da67c12f149704f19f81d1adf7c51cf482943e84a2bad38" [[package]] name = "icu_properties" -version = "2.0.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "016c619c1eeb94efb86809b015c58f479963de65bdb6253345c1a1276f22e32b" +checksum = "bee3b67d0ea5c2cca5003417989af8996f8604e34fb9ddf96208a033901e70de" dependencies = [ - "displaydoc", "icu_collections", "icu_locale_core", "icu_properties_data", "icu_provider", - "potential_utf", "zerotrie", "zerovec", ] [[package]] name = "icu_properties_data" -version = "2.0.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "298459143998310acd25ffe6810ed544932242d3f07083eee1084d83a71bd632" +checksum = "8e2bbb201e0c04f7b4b3e14382af113e17ba4f63e2c9d2ee626b720cbce54a14" [[package]] name = "icu_provider" -version = "2.0.0" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03c80da27b5f4187909049ee2d72f276f0d9f99a42c306bd0131ecfe04d8e5af" +checksum = "139c4cf31c8b5f33d7e199446eff9c1e02decfc2f0eec2c8d71f65befa45b421" dependencies = [ "displaydoc", "icu_locale_core", - "stable_deref_trait", - "tinystr", "writeable", "yoke", "zerofrom", @@ -1622,10 +1570,16 @@ dependencies = [ ] [[package]] -name = "idna" -version = "1.0.3" +name = "id-arena" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "686f825264d630750a544639377bae737628043f20d38bbc029e8f29ea968a7e" +checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" + +[[package]] +name = "idna" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" dependencies = [ "idna_adapter", "smallvec", @@ -1644,12 +1598,14 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.10.0" +version = "2.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe4cd85333e22411419a0bcae1297d25e58c9443848b11dc6a86fefe8c78a661" +checksum = "45a8a2b9cb3e0b0c1803dbb0758ffac5de2f425b23c28f518faabd9d805342ff" dependencies = [ "equivalent", - "hashbrown", + "hashbrown 0.16.1", + "serde", + "serde_core", ] [[package]] @@ -1668,7 +1624,7 @@ version = "0.3.0" dependencies = [ "ashpd", "async-trait", - "bitflags 2.9.1", + "bitflags 2.11.0", "core-foundation", "core-foundation-sys", "core-graphics", @@ -1682,7 +1638,7 @@ dependencies = [ "once_cell", "reis", "tempfile", - "thiserror 2.0.12", + "thiserror 2.0.18", "tokio", "tokio-util", "wayland-client", @@ -1698,7 +1654,7 @@ version = "0.3.0" dependencies = [ "ashpd", "async-trait", - "bitflags 2.9.1", + "bitflags 2.11.0", "core-graphics", "futures", "input-event", @@ -1706,7 +1662,7 @@ dependencies = [ "log", "once_cell", "reis", - "thiserror 2.0.12", + "thiserror 2.0.18", "tokio", "wayland-client", "wayland-protocols", @@ -1725,37 +1681,27 @@ dependencies = [ "num_enum", "reis", "serde", - "thiserror 2.0.12", -] - -[[package]] -name = "io-uring" -version = "0.7.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d93587f37623a1a17d94ef2bc9ada592f5465fe7732084ab7beefabe5c77c0c4" -dependencies = [ - "bitflags 2.9.1", - "cfg-if", - "libc", + "thiserror 2.0.18", ] [[package]] name = "ipconfig" -version = "0.3.2" +version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b58db92f96b720de98181bbbe63c831e87005ab460c1bf306eb2622b4707997f" +checksum = "4d40460c0ce33d6ce4b0630ad68ff63d6661961c48b6dba35e5a4d81cfb48222" dependencies = [ - "socket2 0.5.10", + "socket2", "widestring", - "windows-sys 0.48.0", - "winreg", + "windows-registry", + "windows-result 0.4.1", + "windows-sys 0.61.2", ] [[package]] name = "ipnet" -version = "2.11.0" +version = "2.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" +checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2" [[package]] name = "is_debug" @@ -1765,34 +1711,34 @@ checksum = "1fe266d2e243c931d8190177f20bf7f24eed45e96f39e87dc49a27b32d12d407" [[package]] name = "is_terminal_polyfill" -version = "1.70.1" +version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" +checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" [[package]] name = "itoa" -version = "1.0.15" +version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" [[package]] name = "jiff" -version = "0.2.15" +version = "0.2.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be1f93b8b1eb69c77f24bbb0afdf66f54b632ee39af40ca21c4365a1d7347e49" +checksum = "1a3546dc96b6d42c5f24902af9e2538e82e39ad350b0c766eb3fbf2d8f3d8359" dependencies = [ "jiff-static", "log", "portable-atomic", "portable-atomic-util", - "serde", + "serde_core", ] [[package]] name = "jiff-static" -version = "0.2.15" +version = "0.2.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03343451ff899767262ec32146f6d559dd759fdadf42ff0e227c7c48f72594b4" +checksum = "2a8c8b344124222efd714b73bb41f8b5120b27a7cc1c75593a6ff768d9d05aa4" dependencies = [ "proc-macro2", "quote", @@ -1801,19 +1747,19 @@ dependencies = [ [[package]] name = "jobserver" -version = "0.1.33" +version = "0.1.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38f262f097c174adebe41eb73d66ae9c06b2844fb0da69969647bbddd9b0538a" +checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33" dependencies = [ - "getrandom 0.3.3", + "getrandom 0.3.4", "libc", ] [[package]] name = "js-sys" -version = "0.3.77" +version = "0.3.94" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f" +checksum = "2e04e2ef80ce82e13552136fabeef8a5ed1f985a96805761cbb9a2c34e7664d9" dependencies = [ "once_cell", "wasm-bindgen", @@ -1827,7 +1773,7 @@ checksum = "29541831d33940ea1c68a1b8980382c1a507c95a528a98c0e335b361b9726975" dependencies = [ "arraydeque", "arrayvec", - "bitflags 2.9.1", + "bitflags 2.11.0", "keycode_macro", ] @@ -1868,11 +1814,11 @@ dependencies = [ "sha2", "shadow-rs", "slab", - "thiserror 2.0.12", + "thiserror 2.0.18", "tokio", "tokio-util", - "toml", - "toml_edit", + "toml 0.8.23", + "toml_edit 0.22.27", "webrtc-dtls", "webrtc-util", ] @@ -1884,7 +1830,7 @@ dependencies = [ "clap", "futures", "lan-mouse-ipc", - "thiserror 2.0.12", + "thiserror 2.0.18", "tokio", ] @@ -1899,7 +1845,7 @@ dependencies = [ "lan-mouse-ipc", "libadwaita", "log", - "thiserror 2.0.12", + "thiserror 2.0.18", ] [[package]] @@ -1910,7 +1856,7 @@ dependencies = [ "log", "serde", "serde_json", - "thiserror 2.0.12", + "thiserror 2.0.18", "tokio", "tokio-stream", ] @@ -1922,7 +1868,7 @@ dependencies = [ "input-event", "num_enum", "paste", - "thiserror 2.0.12", + "thiserror 2.0.18", ] [[package]] @@ -1931,6 +1877,12 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" +[[package]] +name = "leb128fmt" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" + [[package]] name = "libadwaita" version = "0.7.2" @@ -1964,15 +1916,15 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.174" +version = "0.2.184" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1171693293099992e19cddea4e8b849964e9846f4acee11b3948bcc337be8776" +checksum = "48f5d2a454e16a5ea0f4ced81bd44e4cfc7bd3a507b61887c99fd3538b28e4af" [[package]] name = "libgit2-sys" -version = "0.18.2+1.9.1" +version = "0.18.3+1.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1c42fe03df2bd3c53a3a9c7317ad91d80c81cd1fb0caec8d7cc4cd2bfa10c222" +checksum = "c9b3acc4b91781bb0b3386669d325163746af5f6e4f73e6d2d630e09a35f3487" dependencies = [ "cc", "libc", @@ -1982,9 +1934,9 @@ dependencies = [ [[package]] name = "libz-sys" -version = "1.1.22" +version = "1.1.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b70e7a7df205e92a1a4cd9aaae7898dac0aa555503cc0a649494d0d60e7651d" +checksum = "fc3a226e576f50782b3305c5ccf458698f92798987f551c6a02efe8276721e22" dependencies = [ "cc", "libc", @@ -2000,15 +1952,15 @@ checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" [[package]] name = "linux-raw-sys" -version = "0.9.4" +version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd945864f07fe9f5371a27ad7b52a172b4b499999f1d97574c9fa68373937e12" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" [[package]] name = "litemap" -version = "0.8.0" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "241eaef5fd12c88705a01fc1066c48c4b36e0dd4377dcdc7ec3942cea7a69956" +checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0" [[package]] name = "local-channel" @@ -2029,47 +1981,24 @@ checksum = "4d873d7c67ce09b42110d801813efbc9364414e356be9935700d368351657487" [[package]] name = "lock_api" -version = "0.4.13" +version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96936507f153605bddfcda068dd804796c84324ed2510809e5b2a624c81da765" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" dependencies = [ - "autocfg", "scopeguard", ] [[package]] name = "log" -version = "0.4.27" +version = "0.4.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" - -[[package]] -name = "loom" -version = "0.7.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "419e0dc8046cb947daa77eb95ae174acfbddb7673b4151f56d1eed8e93fbfaca" -dependencies = [ - "cfg-if", - "generator", - "scoped-tls", - "tracing", - "tracing-subscriber", -] - -[[package]] -name = "matchers" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558" -dependencies = [ - "regex-automata 0.1.10", -] +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" [[package]] name = "memchr" -version = "2.7.5" +version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32a282da65faaf38286cf3be983213fcf1d2e2a58700e808f83f4ea9a4804bc0" +checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" [[package]] name = "memmap" @@ -2105,42 +2034,31 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" -[[package]] -name = "miniz_oxide" -version = "0.8.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" -dependencies = [ - "adler2", -] - [[package]] name = "mio" -version = "1.0.4" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78bed444cc8a2160f01cbcf811ef18cac863ad68ae8ca62092e8db51d51c761c" +checksum = "50b7e5b27aa02a74bac8c3f23f448f8d87ff11f92d3aac1a6ed369ee08cc56c1" dependencies = [ "libc", - "wasi 0.11.1+wasi-snapshot-preview1", - "windows-sys 0.59.0", + "wasi", + "windows-sys 0.61.2", ] [[package]] name = "moka" -version = "0.12.10" +version = "0.12.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9321642ca94a4282428e6ea4af8cc2ca4eac48ac7a6a4ea8f33f76d0ce70926" +checksum = "957228ad12042ee839f93c8f257b62b4c0ab5eaae1d4fa60de53b27c9d7c5046" dependencies = [ "crossbeam-channel", "crossbeam-epoch", "crossbeam-utils", - "loom", + "equivalent", "parking_lot", "portable-atomic", - "rustc_version", "smallvec", "tagptr", - "thiserror 1.0.69", "uuid", ] @@ -2157,19 +2075,6 @@ dependencies = [ "pin-utils", ] -[[package]] -name = "nix" -version = "0.30.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "74523f3a35e05aba87a1d978330aef40f67b0304ac79c1c00b294c9830543db6" -dependencies = [ - "bitflags 2.9.1", - "cfg-if", - "cfg_aliases", - "libc", - "memoffset 0.9.1", -] - [[package]] name = "nom" version = "7.1.3" @@ -2180,16 +2085,6 @@ dependencies = [ "minimal-lexical", ] -[[package]] -name = "nu-ansi-term" -version = "0.46.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84" -dependencies = [ - "overload", - "winapi", -] - [[package]] name = "num-bigint" version = "0.4.6" @@ -2202,9 +2097,9 @@ dependencies = [ [[package]] name = "num-conv" -version = "0.1.0" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" +checksum = "c6673768db2d862beb9b39a78fdcb1a69439615d5794a1be50caa9bc92c81967" [[package]] name = "num-integer" @@ -2226,9 +2121,9 @@ dependencies = [ [[package]] name = "num_enum" -version = "0.7.4" +version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a973b4e44ce6cad84ce69d797acf9a044532e4184c4f267913d1b546a0727b7a" +checksum = "5d0bca838442ec211fa11de3a8b0e0e8f3a4522575b5c4c06ed722e005036f26" dependencies = [ "num_enum_derive", "rustversion", @@ -2236,9 +2131,9 @@ dependencies = [ [[package]] name = "num_enum_derive" -version = "0.7.4" +version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77e878c846a8abae00dd069496dbe8751b16ac1c3d6bd2a7283a938e8228f90d" +checksum = "680998035259dcfcafe653688bf2aa6d3e2dc05e98be6ab46afb089dc84f1df8" dependencies = [ "proc-macro-crate", "proc-macro2", @@ -2255,15 +2150,6 @@ dependencies = [ "libc", ] -[[package]] -name = "object" -version = "0.36.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87" -dependencies = [ - "memchr", -] - [[package]] name = "oid-registry" version = "0.7.1" @@ -2275,9 +2161,9 @@ dependencies = [ [[package]] name = "once_cell" -version = "1.21.3" +version = "1.21.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" dependencies = [ "critical-section", "portable-atomic", @@ -2285,9 +2171,9 @@ dependencies = [ [[package]] name = "once_cell_polyfill" -version = "1.70.1" +version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4895175b425cb1f87721b59f0f286c2092bd4af812243672510e1ac53e2e0ad" +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" [[package]] name = "opaque-debug" @@ -2295,15 +2181,6 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" -[[package]] -name = "ordered-float" -version = "2.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68f19d67e5a2795c94e73e0bb1cc1a7edeb2e28efd39e2e1c9b7a40c1108b11c" -dependencies = [ - "num-traits", -] - [[package]] name = "ordered-stream" version = "0.2.0" @@ -2314,12 +2191,6 @@ dependencies = [ "pin-project-lite", ] -[[package]] -name = "overload" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" - [[package]] name = "p256" version = "0.13.2" @@ -2376,9 +2247,9 @@ checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" [[package]] name = "parking_lot" -version = "0.12.4" +version = "0.12.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70d58bf43669b5795d1576d0641cfb6fbb2057bf629506267a92807158584a13" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" dependencies = [ "lock_api", "parking_lot_core", @@ -2386,15 +2257,15 @@ dependencies = [ [[package]] name = "parking_lot_core" -version = "0.9.11" +version = "0.9.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc838d2a56b5b1a6c25f55575dfc605fabb63bb2365f6c2353ef9159aa69e4a5" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" dependencies = [ "cfg-if", "libc", "redox_syscall", "smallvec", - "windows-targets 0.52.6", + "windows-link 0.2.1", ] [[package]] @@ -2405,12 +2276,12 @@ checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" [[package]] name = "pem" -version = "3.0.5" +version = "3.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38af38e8470ac9dee3ce1bae1af9c1671fffc44ddfd8bd1d0a3445bf349a8ef3" +checksum = "1d30c53c26bc5b31a98cd02d20f25a7c8567146caf63ed593a9d87b2775291be" dependencies = [ "base64", - "serde", + "serde_core", ] [[package]] @@ -2424,15 +2295,15 @@ dependencies = [ [[package]] name = "percent-encoding" -version = "2.3.1" +version = "2.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" [[package]] name = "pin-project-lite" -version = "0.2.16" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" [[package]] name = "pin-utils" @@ -2470,24 +2341,24 @@ dependencies = [ [[package]] name = "portable-atomic" -version = "1.11.1" +version = "1.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f84267b20a16ea918e43c6a88433c2d54fa145c92a811b5b047ccbe153674483" +checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49" [[package]] name = "portable-atomic-util" -version = "0.2.4" +version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8a2f0d8d040d7848a709caf78912debcc3f33ee4b3cac47d73d1e1069e83507" +checksum = "091397be61a01d4be58e7841595bd4bfedb15f1cd54977d79b8271e94ed799a3" dependencies = [ "portable-atomic", ] [[package]] name = "potential_utf" -version = "0.1.2" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5a7c30837279ca13e7c867e9e40053bc68740f988cb07f7ca6df43cc734b585" +checksum = "0103b1cef7ec0cf76490e969665504990193874ea05c85ff9bab8b911d0a0564" dependencies = [ "zerovec", ] @@ -2507,6 +2378,16 @@ dependencies = [ "zerocopy", ] +[[package]] +name = "prettyplease" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +dependencies = [ + "proc-macro2", + "syn", +] + [[package]] name = "primeorder" version = "0.13.6" @@ -2518,36 +2399,36 @@ dependencies = [ [[package]] name = "proc-macro-crate" -version = "3.3.0" +version = "3.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "edce586971a4dfaa28950c6f18ed55e0406c1ab88bbce2c6f6293a7aaba73d35" +checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f" dependencies = [ - "toml_edit", + "toml_edit 0.25.11+spec-1.1.0", ] [[package]] name = "proc-macro2" -version = "1.0.95" +version = "1.0.106" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" dependencies = [ "unicode-ident", ] [[package]] name = "quick-xml" -version = "0.37.5" +version = "0.39.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "331e97a1af0bf59823e6eadffe373d7b27f485be8748f71471c662c1f269b7fb" +checksum = "958f21e8e7ceb5a1aa7fa87fab28e7c75976e0bfe7e23ff069e0a260f894067d" dependencies = [ "memchr", ] [[package]] name = "quote" -version = "1.0.40" +version = "1.0.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" dependencies = [ "proc-macro2", ] @@ -2558,6 +2439,12 @@ version = "5.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + [[package]] name = "rand" version = "0.8.5" @@ -2576,7 +2463,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" dependencies = [ "rand_chacha 0.9.0", - "rand_core 0.9.3", + "rand_core 0.9.5", ] [[package]] @@ -2596,7 +2483,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" dependencies = [ "ppv-lite86", - "rand_core 0.9.3", + "rand_core 0.9.5", ] [[package]] @@ -2605,16 +2492,16 @@ version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" dependencies = [ - "getrandom 0.2.16", + "getrandom 0.2.17", ] [[package]] name = "rand_core" -version = "0.9.3" +version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" +checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" dependencies = [ - "getrandom 0.3.3", + "getrandom 0.3.4", ] [[package]] @@ -2632,56 +2519,41 @@ dependencies = [ [[package]] name = "redox_syscall" -version = "0.5.17" +version = "0.5.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5407465600fb0548f1442edf71dd20683c6ed326200ace4b1ef0763521bb3b77" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.11.0", ] [[package]] name = "regex" -version = "1.11.1" +version = "1.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" +checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" dependencies = [ "aho-corasick", "memchr", - "regex-automata 0.4.9", - "regex-syntax 0.8.5", + "regex-automata", + "regex-syntax", ] [[package]] name = "regex-automata" -version = "0.1.10" +version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" -dependencies = [ - "regex-syntax 0.6.29", -] - -[[package]] -name = "regex-automata" -version = "0.4.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" +checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" dependencies = [ "aho-corasick", "memchr", - "regex-syntax 0.8.5", + "regex-syntax", ] [[package]] name = "regex-syntax" -version = "0.6.29" +version = "0.8.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" - -[[package]] -name = "regex-syntax" -version = "0.8.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" +checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" [[package]] name = "reis" @@ -2696,9 +2568,9 @@ dependencies = [ [[package]] name = "resolv-conf" -version = "0.7.4" +version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95325155c684b1c89f7765e30bc1c42e4a6da51ca513615660cb8a62ef9a88e3" +checksum = "1e061d1b48cb8d38042de4ae0a7a6401009d6143dc80d2e2d6f31f0bdd6470c7" [[package]] name = "rfc6979" @@ -2718,18 +2590,12 @@ checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" dependencies = [ "cc", "cfg-if", - "getrandom 0.2.16", + "getrandom 0.2.17", "libc", "untrusted", "windows-sys 0.52.0", ] -[[package]] -name = "rustc-demangle" -version = "0.1.26" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56f7d92ca342cea22a06f2121d944b4fd82af56988c270852495420f961d4ace" - [[package]] name = "rustc_version" version = "0.4.1" @@ -2754,7 +2620,7 @@ version = "0.38.44" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.11.0", "errno", "libc", "linux-raw-sys 0.4.15", @@ -2763,22 +2629,22 @@ dependencies = [ [[package]] name = "rustix" -version = "1.0.8" +version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11181fbabf243db407ef8df94a6ce0b2f9a733bd8be4ad02b4eda9602296cac8" +checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.11.0", "errno", "libc", - "linux-raw-sys 0.9.4", - "windows-sys 0.60.2", + "linux-raw-sys 0.12.1", + "windows-sys 0.61.2", ] [[package]] name = "rustls" -version = "0.23.31" +version = "0.23.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0ebcbd2f03de0fc1122ad9bb24b127a5a6cd51d72604a3f3c50ac459762b6cc" +checksum = "758025cb5fccfd3bc2fd74708fd4682be41d99e5dff73c377c0646c6012c73a4" dependencies = [ "once_cell", "ring", @@ -2790,18 +2656,18 @@ dependencies = [ [[package]] name = "rustls-pki-types" -version = "1.12.0" +version = "1.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "229a4a4c221013e7e1f1a043678c5cc39fe5171437c88fb47151a21e6f5b5c79" +checksum = "be040f8b0a225e40375822a563fa9524378b9d63112f53e19ffff34df5d33fdd" dependencies = [ "zeroize", ] [[package]] name = "rustls-webpki" -version = "0.103.4" +version = "0.103.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0a17884ae0c1b773f1ccd2bd4a8c72f16da897310a98b0e84bf349ad5ead92fc" +checksum = "df33b2b81ac578cabaf06b89b0631153a3f416b0a886e8a7a1707fb51abbd1ef" dependencies = [ "ring", "rustls-pki-types", @@ -2810,21 +2676,9 @@ dependencies = [ [[package]] name = "rustversion" -version = "1.0.21" +version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a0d197bd2c9dc6e53b84da9556a69ba4cdfab8619eb41a8bd1cc2027a0f6b1d" - -[[package]] -name = "ryu" -version = "1.0.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" - -[[package]] -name = "scoped-tls" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" [[package]] name = "scopeguard" @@ -2848,48 +2702,38 @@ dependencies = [ [[package]] name = "semver" -version = "1.0.26" +version = "1.0.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56e6fa9c48d24d85fb3de5ad847117517440f6beceb7798af16b4a87d616b8d0" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" dependencies = [ "serde", + "serde_core", ] [[package]] name = "serde" -version = "1.0.219" +version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" dependencies = [ "serde_derive", ] -[[package]] -name = "serde-untagged" -version = "0.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "299d9c19d7d466db4ab10addd5703e4c615dec2a5a16dbbafe191045e87ee66e" -dependencies = [ - "erased-serde", - "serde", - "typeid", -] - -[[package]] -name = "serde-value" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3a1a3341211875ef120e117ea7fd5228530ae7e7036a779fdc9117be6b3282c" -dependencies = [ - "ordered-float", - "serde", -] - [[package]] name = "serde_derive" -version = "1.0.219" +version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2", "quote", @@ -2898,14 +2742,15 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.141" +version = "1.0.149" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30b9eff21ebe718216c6ec64e1d9ac57087aad11efc64e32002bce4a0d4c03d3" +checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" dependencies = [ "itoa", "memchr", - "ryu", "serde", + "serde_core", + "zmij", ] [[package]] @@ -2928,6 +2773,15 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_spanned" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6662b5879511e06e8999a8a235d848113e942c9124f211511b16466ee2995f26" +dependencies = [ + "serde_core", +] + [[package]] name = "sha1" version = "0.10.6" @@ -2952,9 +2806,9 @@ dependencies = [ [[package]] name = "shadow-rs" -version = "1.2.0" +version = "1.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f6fd27df794ced2ef39872879c93a9f87c012607318af8621cd56d2c3a8b3a2" +checksum = "3c798acfc78a69c7b038adde44084d8df875555b091da42c90ae46257cdcc41a" dependencies = [ "cargo_metadata", "const_format", @@ -2965,15 +2819,6 @@ dependencies = [ "tzdb", ] -[[package]] -name = "sharded-slab" -version = "0.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" -dependencies = [ - "lazy_static", -] - [[package]] name = "shlex" version = "1.3.0" @@ -2982,10 +2827,11 @@ checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" [[package]] name = "signal-hook-registry" -version = "1.4.5" +version = "1.4.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9203b8055f63a2a00e2f593bb0510367fe707d7ff1e5c872de2f537b339e5410" +checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" dependencies = [ + "errno", "libc", ] @@ -3001,9 +2847,9 @@ dependencies = [ [[package]] name = "slab" -version = "0.4.10" +version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04dc19736151f35336d325007ac991178d504a119863a2fcb3758cdb5e52c50d" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" [[package]] name = "smallvec" @@ -3013,22 +2859,12 @@ checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" [[package]] name = "socket2" -version = "0.5.10" +version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e22376abed350d73dd1cd119b57ffccad95b4e585a7cda43e286245ce23c0678" +checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e" dependencies = [ "libc", - "windows-sys 0.52.0", -] - -[[package]] -name = "socket2" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "233504af464074f9d066d7b5416c5f9b894a5862a6506e306f7b816cdd6f1807" -dependencies = [ - "libc", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -3043,15 +2879,9 @@ dependencies = [ [[package]] name = "stable_deref_trait" -version = "1.2.0" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" - -[[package]] -name = "static_assertions" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" [[package]] name = "strsim" @@ -3067,9 +2897,9 @@ checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" [[package]] name = "syn" -version = "2.0.104" +version = "2.0.117" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17b6f705963418cdb9927482fa304bc562ece2fdd4f616084c50b7023b435a40" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" dependencies = [ "proc-macro2", "quote", @@ -3089,14 +2919,14 @@ dependencies = [ [[package]] name = "system-deps" -version = "7.0.5" +version = "7.0.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e4be53aa0cba896d2dc615bd42bbc130acdcffa239e0a2d965ea5b3b2a86ffdb" +checksum = "396a35feb67335377e0251fcbc1092fc85c484bd4e3a7a54319399da127796e7" dependencies = [ "cfg-expr", "heck", "pkg-config", - "toml", + "toml 1.1.2+spec-1.1.0", "version-compare", ] @@ -3108,21 +2938,21 @@ checksum = "7b2093cf4c8eb1e67749a6762251bc9cd836b6fc171623bd0a9d324d37af2417" [[package]] name = "target-lexicon" -version = "0.13.2" +version = "0.13.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e502f78cdbb8ba4718f566c418c52bc729126ffd16baee5baa718cf25dd5a69a" +checksum = "df7f62577c25e07834649fc3b39fafdc597c0a3527dc1c60129201ccfcbaa50c" [[package]] name = "tempfile" -version = "3.20.0" +version = "3.27.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8a64e3985349f2441a1a9ef0b853f869006c3855f2cda6862a94d26ebb9d6a1" +checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" dependencies = [ "fastrand", - "getrandom 0.3.3", + "getrandom 0.4.2", "once_cell", - "rustix 1.0.8", - "windows-sys 0.59.0", + "rustix 1.1.4", + "windows-sys 0.61.2", ] [[package]] @@ -3136,11 +2966,11 @@ dependencies = [ [[package]] name = "thiserror" -version = "2.0.12" +version = "2.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" dependencies = [ - "thiserror-impl 2.0.12", + "thiserror-impl 2.0.18", ] [[package]] @@ -3156,29 +2986,20 @@ dependencies = [ [[package]] name = "thiserror-impl" -version = "2.0.12" +version = "2.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" dependencies = [ "proc-macro2", "quote", "syn", ] -[[package]] -name = "thread_local" -version = "1.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185" -dependencies = [ - "cfg-if", -] - [[package]] name = "time" -version = "0.3.41" +version = "0.3.47" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a7619e19bc266e0f9c5e6686659d394bc57973859340060a69221e57dbc0c40" +checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c" dependencies = [ "deranged", "itoa", @@ -3186,22 +3007,22 @@ dependencies = [ "num-conv", "num_threads", "powerfmt", - "serde", + "serde_core", "time-core", "time-macros", ] [[package]] name = "time-core" -version = "0.1.4" +version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c9e9a38711f559d9e3ce1cdb06dd7c5b8ea546bc90052da6d06bb76da74bb07c" +checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca" [[package]] name = "time-macros" -version = "0.2.22" +version = "0.2.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3526739392ec93fd8b359c8e98514cb3e8e021beb4e5f597b00a0221f8ed8a49" +checksum = "2e70e4c5a0e0a8a4823ad65dfe1a6930e4f4d756dcd9dd7939022b5e8c501215" dependencies = [ "num-conv", "time-core", @@ -3209,9 +3030,9 @@ dependencies = [ [[package]] name = "tinystr" -version = "0.8.1" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d4f6d1145dcb577acf783d4e601bc1d76a13337bb54e6233add580b07344c8b" +checksum = "c8323304221c2a851516f22236c5722a72eaa19749016521d6dff0824447d96d" dependencies = [ "displaydoc", "zerovec", @@ -3219,9 +3040,9 @@ dependencies = [ [[package]] name = "tinyvec" -version = "1.9.0" +version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09b3661f17e86524eccd4371ab0429194e0d7c008abb45f7a7495b1719463c71" +checksum = "3e61e67053d25a4e82c844e8424039d9745781b3fc4f32b8d55ed50f5f667ef3" dependencies = [ "tinyvec_macros", ] @@ -3234,30 +3055,27 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.47.0" +version = "1.51.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43864ed400b6043a4757a25c7a64a8efde741aed79a056a2fb348a406701bb35" +checksum = "f66bf9585cda4b724d3e78ab34b73fb2bbaba9011b9bfdf69dc836382ea13b8c" dependencies = [ - "backtrace", "bytes", - "io-uring", "libc", "mio", "parking_lot", "pin-project-lite", "signal-hook-registry", - "slab", - "socket2 0.6.0", + "socket2", "tokio-macros", "tracing", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] name = "tokio-macros" -version = "2.5.0" +version = "2.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8" +checksum = "385a6cb71ab9ab790c5fe8d67f1645e6c450a7ce006a33de03daa956cf70a496" dependencies = [ "proc-macro2", "quote", @@ -3266,9 +3084,9 @@ dependencies = [ [[package]] name = "tokio-stream" -version = "0.1.17" +version = "0.1.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eca58d7bba4a75707817a2c44174253f9236b2d5fbd055602e9d5c07c139a047" +checksum = "32da49809aab5c3bc678af03902d4ccddea2a87d028d86392a4b1560c6906c70" dependencies = [ "futures-core", "pin-project-lite", @@ -3277,9 +3095,9 @@ dependencies = [ [[package]] name = "tokio-util" -version = "0.7.15" +version = "0.7.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "66a539a9ad6d5d281510d5bd368c973d636c02dbf8a67300bfb6b950696ad7df" +checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098" dependencies = [ "bytes", "futures-core", @@ -3295,9 +3113,24 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362" dependencies = [ "serde", - "serde_spanned", - "toml_datetime", - "toml_edit", + "serde_spanned 0.6.9", + "toml_datetime 0.6.11", + "toml_edit 0.22.27", +] + +[[package]] +name = "toml" +version = "1.1.2+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81f3d15e84cbcd896376e6730314d59fb5a87f31e4b038454184435cd57defee" +dependencies = [ + "indexmap", + "serde_core", + "serde_spanned 1.1.1", + "toml_datetime 1.1.1+spec-1.1.0", + "toml_parser", + "toml_writer", + "winnow 1.0.1", ] [[package]] @@ -3309,6 +3142,15 @@ dependencies = [ "serde", ] +[[package]] +name = "toml_datetime" +version = "1.1.1+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7" +dependencies = [ + "serde_core", +] + [[package]] name = "toml_edit" version = "0.22.27" @@ -3317,10 +3159,31 @@ checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" dependencies = [ "indexmap", "serde", - "serde_spanned", - "toml_datetime", + "serde_spanned 0.6.9", + "toml_datetime 0.6.11", "toml_write", - "winnow", + "winnow 0.7.15", +] + +[[package]] +name = "toml_edit" +version = "0.25.11+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b59c4d22ed448339746c59b905d24568fcbb3ab65a500494f7b8c3e97739f2b" +dependencies = [ + "indexmap", + "toml_datetime 1.1.1+spec-1.1.0", + "toml_parser", + "winnow 1.0.1", +] + +[[package]] +name = "toml_parser" +version = "1.1.2+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526" +dependencies = [ + "winnow 1.0.1", ] [[package]] @@ -3330,10 +3193,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801" [[package]] -name = "tracing" -version = "0.1.41" +name = "toml_writer" +version = "1.1.1+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" +checksum = "756daf9b1013ebe47a8776667b466417e2d4c5679d441c26230efd9ef78692db" + +[[package]] +name = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" dependencies = [ "pin-project-lite", "tracing-attributes", @@ -3342,9 +3211,9 @@ dependencies = [ [[package]] name = "tracing-attributes" -version = "0.1.30" +version = "0.1.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81383ab64e72a7a8b8e13130c49e3dab29def6d0c7d76a03087b3cf71c5c6903" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" dependencies = [ "proc-macro2", "quote", @@ -3353,66 +3222,30 @@ dependencies = [ [[package]] name = "tracing-core" -version = "0.1.34" +version = "0.1.36" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9d12581f227e93f094d3af2ae690a574abb8a2b9b7a96e7cfe9647b2b617678" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" dependencies = [ "once_cell", - "valuable", ] -[[package]] -name = "tracing-log" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" -dependencies = [ - "log", - "once_cell", - "tracing-core", -] - -[[package]] -name = "tracing-subscriber" -version = "0.3.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8189decb5ac0fa7bc8b96b7cb9b2701d60d48805aca84a238004d665fcc4008" -dependencies = [ - "matchers", - "nu-ansi-term", - "once_cell", - "regex", - "sharded-slab", - "smallvec", - "thread_local", - "tracing", - "tracing-core", - "tracing-log", -] - -[[package]] -name = "typeid" -version = "1.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc7d623258602320d5c55d1bc22793b57daff0ec7efc270ea7d55ce1d5f5471c" - [[package]] name = "typenum" -version = "1.18.0" +version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1dccffe3ce07af9386bfd29e80c0ab1a8205a2fc34e4bcd40364df902cfa8f3f" +checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" [[package]] name = "tz-rs" -version = "0.7.0" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1450bf2b99397e72070e7935c89facaa80092ac812502200375f1f7d33c71a1" +checksum = "4fc6c929ffa10fb34f4a3c7e9a73620a83ef2e85e47f9ec3381b8289e6762f42" [[package]] name = "tzdb" -version = "0.7.2" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0be2ea5956f295449f47c0b825c5e109022ff1a6a53bb4f77682a87c2341fbf5" +checksum = "56d4e985b6dda743ae7fd4140c28105316ffd75bc58258ee6cc12934e3eb7a0c" dependencies = [ "iana-time-zone", "tz-rs", @@ -3421,29 +3254,29 @@ dependencies = [ [[package]] name = "tzdb_data" -version = "0.2.2" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c4c81d75033770e40fbd3643ce7472a1a9fd301f90b7139038228daf8af03ec" +checksum = "125a0a63c4bd75c73f61863463cb400db4b1aa5039b203b0ee1d628a7e3dabb2" dependencies = [ "tz-rs", ] [[package]] name = "uds_windows" -version = "1.1.0" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89daebc3e6fd160ac4aa9fc8b3bf71e1f74fbf92367ae71fb83a037e8bf164b9" +checksum = "f2f6fb2847f6742cd76af783a2a2c49e9375d0a111c7bef6f71cd9e738c72d6e" dependencies = [ "memoffset 0.9.1", "tempfile", - "winapi", + "windows-sys 0.61.2", ] [[package]] name = "unicode-ident" -version = "1.0.18" +version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" [[package]] name = "unicode-xid" @@ -3469,14 +3302,15 @@ checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" [[package]] name = "url" -version = "2.5.4" +version = "2.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32f8b686cadd1473f4bd0117a5d28d36b1ade384ea9b5069a1c40aefed7fda60" +checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" dependencies = [ "form_urlencoded", "idna", "percent-encoding", "serde", + "serde_derive", ] [[package]] @@ -3493,21 +3327,16 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "uuid" -version = "1.17.0" +version = "1.23.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3cf4199d1e5d15ddd86a694e4d0dffa9c323ce759fea589f00fef9d81cc1931d" +checksum = "5ac8b6f42ead25368cf5b098aeb3dc8a1a2c05a3eee8a9a1a68c640edbfc79d9" dependencies = [ - "getrandom 0.3.3", + "getrandom 0.4.2", "js-sys", + "serde_core", "wasm-bindgen", ] -[[package]] -name = "valuable" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" - [[package]] name = "vcpkg" version = "0.2.15" @@ -3516,9 +3345,9 @@ checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" [[package]] name = "version-compare" -version = "0.2.0" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "852e951cb7832cb45cb1169900d19760cfa39b82bc0ea9c0e5a14ae88411c98b" +checksum = "03c2856837ef78f57382f06b2b8563a2f512f7185d732608fd9176cb3b8edf0e" [[package]] name = "version_check" @@ -3533,45 +3362,41 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" [[package]] -name = "wasi" -version = "0.14.2+wasi-0.2.4" +name = "wasip2" +version = "1.0.2+wasi-0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3" +checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5" dependencies = [ - "wit-bindgen-rt", + "wit-bindgen", +] + +[[package]] +name = "wasip3" +version = "0.4.0+wasi-0.3.0-rc-2026-01-06" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" +dependencies = [ + "wit-bindgen", ] [[package]] name = "wasm-bindgen" -version = "0.2.100" +version = "0.2.117" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5" +checksum = "0551fc1bb415591e3372d0bc4780db7e587d84e2a7e79da121051c5c4b89d0b0" dependencies = [ "cfg-if", "once_cell", "rustversion", "wasm-bindgen-macro", -] - -[[package]] -name = "wasm-bindgen-backend" -version = "0.2.100" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6" -dependencies = [ - "bumpalo", - "log", - "proc-macro2", - "quote", - "syn", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-macro" -version = "0.2.100" +version = "0.2.117" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407" +checksum = "7fbdf9a35adf44786aecd5ff89b4563a90325f9da0923236f6104e603c7e86be" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -3579,58 +3404,92 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.100" +version = "0.2.117" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" +checksum = "dca9693ef2bab6d4e6707234500350d8dad079eb508dca05530c85dc3a529ff2" dependencies = [ + "bumpalo", "proc-macro2", "quote", "syn", - "wasm-bindgen-backend", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.100" +version = "0.2.117" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d" +checksum = "39129a682a6d2d841b6c429d0c51e5cb0ed1a03829d8b3d1e69a011e62cb3d3b" dependencies = [ "unicode-ident", ] [[package]] -name = "wayland-backend" -version = "0.3.11" +name = "wasm-encoder" +version = "0.244.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "673a33c33048a5ade91a6b139580fa174e19fb0d23f396dca9fa15f2e1e49b35" +checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" +dependencies = [ + "leb128fmt", + "wasmparser", +] + +[[package]] +name = "wasm-metadata" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" +dependencies = [ + "anyhow", + "indexmap", + "wasm-encoder", + "wasmparser", +] + +[[package]] +name = "wasmparser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" +dependencies = [ + "bitflags 2.11.0", + "hashbrown 0.15.5", + "indexmap", + "semver", +] + +[[package]] +name = "wayland-backend" +version = "0.3.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2857dd20b54e916ec7253b3d6b4d5c4d7d4ca2c33c2e11c6c76a99bd8744755d" dependencies = [ "cc", "downcast-rs", - "rustix 1.0.8", + "rustix 1.1.4", "smallvec", "wayland-sys", ] [[package]] name = "wayland-client" -version = "0.31.11" +version = "0.31.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c66a47e840dc20793f2264eb4b3e4ecb4b75d91c0dd4af04b456128e0bdd449d" +checksum = "645c7c96bb74690c3189b5c9cb4ca1627062bb23693a4fad9d8c3de958260144" dependencies = [ - "bitflags 2.9.1", - "rustix 1.0.8", + "bitflags 2.11.0", + "rustix 1.1.4", "wayland-backend", "wayland-scanner", ] [[package]] name = "wayland-protocols" -version = "0.32.9" +version = "0.32.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "efa790ed75fbfd71283bd2521a1cfdc022aabcc28bdcff00851f9e4ae88d9901" +checksum = "563a85523cade2429938e790815fd7319062103b9f4a2dc806e9b53b95982d8f" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.11.0", "wayland-backend", "wayland-client", "wayland-scanner", @@ -3638,11 +3497,11 @@ dependencies = [ [[package]] name = "wayland-protocols-misc" -version = "0.3.9" +version = "0.3.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2dfe33d551eb8bffd03ff067a8b44bb963919157841a99957151299a6307d19c" +checksum = "6e9567599ef23e09b8dad6e429e5738d4509dfc46b3b21f32841a304d16b29c8" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.11.0", "wayland-backend", "wayland-client", "wayland-protocols", @@ -3651,11 +3510,11 @@ dependencies = [ [[package]] name = "wayland-protocols-wlr" -version = "0.3.9" +version = "0.3.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "efd94963ed43cf9938a090ca4f7da58eb55325ec8200c3848963e98dc25b78ec" +checksum = "eb04e52f7836d7c7976c78ca0250d61e33873c34156a2a1fc9474828ec268234" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.11.0", "wayland-backend", "wayland-client", "wayland-protocols", @@ -3664,9 +3523,9 @@ dependencies = [ [[package]] name = "wayland-scanner" -version = "0.31.7" +version = "0.31.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "54cb1e9dc49da91950bdfd8b848c49330536d9d1fb03d4bfec8cae50caa50ae3" +checksum = "9c324a910fd86ebdc364a3e61ec1f11737d3b1d6c273c0239ee8ff4bc0d24b4a" dependencies = [ "proc-macro2", "quick-xml", @@ -3675,9 +3534,9 @@ dependencies = [ [[package]] name = "wayland-sys" -version = "0.31.7" +version = "0.31.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34949b42822155826b41db8e5d0c1be3a2bd296c747577a43a3e6daefc296142" +checksum = "d8eab23fefc9e41f8e841df4a9c707e8a8c4ed26e944ef69297184de2785e3be" dependencies = [ "pkg-config", ] @@ -3733,7 +3592,7 @@ dependencies = [ "lazy_static", "libc", "log", - "nix 0.26.4", + "nix", "portable-atomic", "rand 0.8.5", "thiserror 1.0.69", @@ -3743,9 +3602,9 @@ dependencies = [ [[package]] name = "widestring" -version = "1.2.0" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd7cf3379ca1aac9eea11fba24fd7e315d621f8dfe35c8d7d2be8b793726e07d" +checksum = "72069c3113ab32ab29e5584db3c6ec55d416895e60715417b5b883a357c3e471" [[package]] name = "winapi" @@ -3776,9 +3635,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9babd3a767a4c1aef6900409f85f5d53ce2544ccdfaa86dad48c91782c6d6893" dependencies = [ "windows-collections", - "windows-core", + "windows-core 0.61.2", "windows-future", - "windows-link", + "windows-link 0.1.3", "windows-numerics", ] @@ -3788,7 +3647,7 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3beeceb5e5cfd9eb1d76b381630e82c4241ccd0d27f1a39ed41b2760b255c5e8" dependencies = [ - "windows-core", + "windows-core 0.61.2", ] [[package]] @@ -3799,9 +3658,22 @@ checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3" dependencies = [ "windows-implement", "windows-interface", - "windows-link", - "windows-result", - "windows-strings", + "windows-link 0.1.3", + "windows-result 0.3.4", + "windows-strings 0.4.2", +] + +[[package]] +name = "windows-core" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link 0.2.1", + "windows-result 0.4.1", + "windows-strings 0.5.1", ] [[package]] @@ -3810,16 +3682,16 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc6a41e98427b19fe4b73c550f060b59fa592d7d686537eebf9385621bfbad8e" dependencies = [ - "windows-core", - "windows-link", + "windows-core 0.61.2", + "windows-link 0.1.3", "windows-threading", ] [[package]] name = "windows-implement" -version = "0.60.0" +version = "0.60.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a47fddd13af08290e67f4acabf4b459f647552718f683a7b415d290ac744a836" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" dependencies = [ "proc-macro2", "quote", @@ -3828,9 +3700,9 @@ dependencies = [ [[package]] name = "windows-interface" -version = "0.59.1" +version = "0.59.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd9211b69f8dcdfa817bfd14bf1c97c9188afa36f4750130fcdf3f400eca9fa8" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" dependencies = [ "proc-macro2", "quote", @@ -3843,14 +3715,31 @@ version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + [[package]] name = "windows-numerics" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9150af68066c4c5c07ddc0ce30421554771e528bde427614c61038bc2c92c2b1" dependencies = [ - "windows-core", - "windows-link", + "windows-core 0.61.2", + "windows-link 0.1.3", +] + +[[package]] +name = "windows-registry" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02752bf7fbdcce7f2a27a742f798510f3e5ad88dbe84871e5168e2120c3d5720" +dependencies = [ + "windows-link 0.2.1", + "windows-result 0.4.1", + "windows-strings 0.5.1", ] [[package]] @@ -3859,7 +3748,16 @@ version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6" dependencies = [ - "windows-link", + "windows-link 0.1.3", +] + +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link 0.2.1", ] [[package]] @@ -3868,16 +3766,16 @@ version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57" dependencies = [ - "windows-link", + "windows-link 0.1.3", ] [[package]] -name = "windows-sys" -version = "0.48.0" +name = "windows-strings" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" dependencies = [ - "windows-targets 0.48.5", + "windows-link 0.2.1", ] [[package]] @@ -3886,7 +3784,7 @@ version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" dependencies = [ - "windows-targets 0.52.6", + "windows-targets", ] [[package]] @@ -3895,31 +3793,16 @@ version = "0.59.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" dependencies = [ - "windows-targets 0.52.6", + "windows-targets", ] [[package]] name = "windows-sys" -version = "0.60.2" +version = "0.61.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" dependencies = [ - "windows-targets 0.53.3", -] - -[[package]] -name = "windows-targets" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" -dependencies = [ - "windows_aarch64_gnullvm 0.48.5", - "windows_aarch64_msvc 0.48.5", - "windows_i686_gnu 0.48.5", - "windows_i686_msvc 0.48.5", - "windows_x86_64_gnu 0.48.5", - "windows_x86_64_gnullvm 0.48.5", - "windows_x86_64_msvc 0.48.5", + "windows-link 0.2.1", ] [[package]] @@ -3928,31 +3811,14 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" dependencies = [ - "windows_aarch64_gnullvm 0.52.6", - "windows_aarch64_msvc 0.52.6", - "windows_i686_gnu 0.52.6", - "windows_i686_gnullvm 0.52.6", - "windows_i686_msvc 0.52.6", - "windows_x86_64_gnu 0.52.6", - "windows_x86_64_gnullvm 0.52.6", - "windows_x86_64_msvc 0.52.6", -] - -[[package]] -name = "windows-targets" -version = "0.53.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d5fe6031c4041849d7c496a8ded650796e7b6ecc19df1a431c1a363342e5dc91" -dependencies = [ - "windows-link", - "windows_aarch64_gnullvm 0.53.0", - "windows_aarch64_msvc 0.53.0", - "windows_i686_gnu 0.53.0", - "windows_i686_gnullvm 0.53.0", - "windows_i686_msvc 0.53.0", - "windows_x86_64_gnu 0.53.0", - "windows_x86_64_gnullvm 0.53.0", - "windows_x86_64_msvc 0.53.0", + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", ] [[package]] @@ -3961,180 +3827,168 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b66463ad2e0ea3bbf808b7f1d371311c80e115c0b71d60efc142cafbcfb057a6" dependencies = [ - "windows-link", + "windows-link 0.1.3", ] -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" - [[package]] name = "windows_aarch64_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.53.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "86b8d5f90ddd19cb4a147a5fa63ca848db3df085e25fee3cc10b39b6eebae764" - -[[package]] -name = "windows_aarch64_msvc" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" - [[package]] name = "windows_aarch64_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" -[[package]] -name = "windows_aarch64_msvc" -version = "0.53.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7651a1f62a11b8cbd5e0d42526e55f2c99886c77e007179efff86c2b137e66c" - -[[package]] -name = "windows_i686_gnu" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" - [[package]] name = "windows_i686_gnu" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" -[[package]] -name = "windows_i686_gnu" -version = "0.53.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1dc67659d35f387f5f6c479dc4e28f1d4bb90ddd1a5d3da2e5d97b42d6272c3" - [[package]] name = "windows_i686_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" -[[package]] -name = "windows_i686_gnullvm" -version = "0.53.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ce6ccbdedbf6d6354471319e781c0dfef054c81fbc7cf83f338a4296c0cae11" - -[[package]] -name = "windows_i686_msvc" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" - [[package]] name = "windows_i686_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" -[[package]] -name = "windows_i686_msvc" -version = "0.53.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "581fee95406bb13382d2f65cd4a908ca7b1e4c2f1917f143ba16efe98a589b5d" - -[[package]] -name = "windows_x86_64_gnu" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" - [[package]] name = "windows_x86_64_gnu" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" -[[package]] -name = "windows_x86_64_gnu" -version = "0.53.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e55b5ac9ea33f2fc1716d1742db15574fd6fc8dadc51caab1c16a3d3b4190ba" - -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" - [[package]] name = "windows_x86_64_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.53.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0a6e035dd0599267ce1ee132e51c27dd29437f63325753051e71dd9e42406c57" - -[[package]] -name = "windows_x86_64_msvc" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" - [[package]] name = "windows_x86_64_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" -[[package]] -name = "windows_x86_64_msvc" -version = "0.53.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486" - [[package]] name = "winnow" -version = "0.7.12" +version = "0.7.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3edebf492c8125044983378ecb5766203ad3b4c2f7a922bd7dd207f6d443e95" +checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945" dependencies = [ "memchr", ] [[package]] -name = "winreg" -version = "0.50.0" +name = "winnow" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "524e57b2c537c0f9b1e69f1965311ec12182b4122e45035b1508cd24d2adadb1" +checksum = "09dac053f1cd375980747450bfc7250c264eaae0583872e845c0c7cd578872b5" dependencies = [ - "cfg-if", - "windows-sys 0.48.0", + "memchr", ] [[package]] -name = "wit-bindgen-rt" -version = "0.39.0" +name = "wit-bindgen" +version = "0.51.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1" +checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" dependencies = [ - "bitflags 2.9.1", + "wit-bindgen-rust-macro", +] + +[[package]] +name = "wit-bindgen-core" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" +dependencies = [ + "anyhow", + "heck", + "wit-parser", +] + +[[package]] +name = "wit-bindgen-rust" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" +dependencies = [ + "anyhow", + "heck", + "indexmap", + "prettyplease", + "syn", + "wasm-metadata", + "wit-bindgen-core", + "wit-component", +] + +[[package]] +name = "wit-bindgen-rust-macro" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a" +dependencies = [ + "anyhow", + "prettyplease", + "proc-macro2", + "quote", + "syn", + "wit-bindgen-core", + "wit-bindgen-rust", +] + +[[package]] +name = "wit-component" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" +dependencies = [ + "anyhow", + "bitflags 2.11.0", + "indexmap", + "log", + "serde", + "serde_derive", + "serde_json", + "wasm-encoder", + "wasm-metadata", + "wasmparser", + "wit-parser", +] + +[[package]] +name = "wit-parser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" +dependencies = [ + "anyhow", + "id-arena", + "indexmap", + "log", + "semver", + "serde", + "serde_derive", + "serde_json", + "unicode-xid", + "wasmparser", ] [[package]] name = "writeable" -version = "0.6.1" +version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea2f10b9bb0928dfb1b42b65e1f9e36f7f54dbdf08457afefb38afcdec4fa2bb" +checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4" [[package]] name = "x11" @@ -4186,11 +4040,10 @@ dependencies = [ [[package]] name = "yoke" -version = "0.8.0" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f41bb01b8226ef4bfd589436a297c53d118f65921786300e427be8d487695cc" +checksum = "abe8c5fda708d9ca3df187cae8bfb9ceda00dd96231bed36e445a1a48e66f9ca" dependencies = [ - "serde", "stable_deref_trait", "yoke-derive", "zerofrom", @@ -4198,9 +4051,9 @@ dependencies = [ [[package]] name = "yoke-derive" -version = "0.8.0" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38da3c9736e16c5d3c8c597a9aaa5d1fa565d0532ae05e27c24aa62fb32c0ab6" +checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e" dependencies = [ "proc-macro2", "quote", @@ -4210,9 +4063,9 @@ dependencies = [ [[package]] name = "zbus" -version = "5.9.0" +version = "5.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4bb4f9a464286d42851d18a605f7193b8febaf5b0919d71c6399b7b26e5b0aad" +checksum = "ca82f95dbd3943a40a53cfded6c2d0a2ca26192011846a1810c4256ef92c60bc" dependencies = [ "async-broadcast", "async-recursion", @@ -4222,15 +4075,17 @@ dependencies = [ "futures-core", "futures-lite", "hex", - "nix 0.30.1", + "libc", "ordered-stream", + "rustix 1.1.4", "serde", "serde_repr", "tokio", "tracing", "uds_windows", - "windows-sys 0.59.0", - "winnow", + "uuid", + "windows-sys 0.61.2", + "winnow 0.7.15", "zbus_macros", "zbus_names", "zvariant", @@ -4238,9 +4093,9 @@ dependencies = [ [[package]] name = "zbus_macros" -version = "5.9.0" +version = "5.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef9859f68ee0c4ee2e8cde84737c78e3f4c54f946f2a38645d0d4c7a95327659" +checksum = "897e79616e84aac4b2c46e9132a4f63b93105d54fe8c0e8f6bffc21fa8d49222" dependencies = [ "proc-macro-crate", "proc-macro2", @@ -4253,30 +4108,29 @@ dependencies = [ [[package]] name = "zbus_names" -version = "4.2.0" +version = "4.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7be68e64bf6ce8db94f63e72f0c7eb9a60d733f7e0499e628dfab0f84d6bcb97" +checksum = "ffd8af6d5b78619bab301ff3c560a5bd22426150253db278f164d6cf3b72c50f" dependencies = [ "serde", - "static_assertions", - "winnow", + "winnow 0.7.15", "zvariant", ] [[package]] name = "zerocopy" -version = "0.8.26" +version = "0.8.48" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1039dd0d3c310cf05de012d8a39ff557cb0d23087fd44cad61df08fc31907a2f" +checksum = "eed437bf9d6692032087e337407a86f04cd8d6a16a37199ed57949d415bd68e9" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.26" +version = "0.8.48" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ecf5b4cc5364572d7f4c329661bcc82724222973f2cab6f050a4e5c22f75181" +checksum = "70e3cd084b1788766f53af483dd21f93881ff30d7320490ec3ef7526d203bad4" dependencies = [ "proc-macro2", "quote", @@ -4285,18 +4139,18 @@ dependencies = [ [[package]] name = "zerofrom" -version = "0.1.6" +version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" +checksum = "69faa1f2a1ea75661980b013019ed6687ed0e83d069bc1114e2cc74c6c04c4df" dependencies = [ "zerofrom-derive", ] [[package]] name = "zerofrom-derive" -version = "0.1.6" +version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" +checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1" dependencies = [ "proc-macro2", "quote", @@ -4306,18 +4160,18 @@ dependencies = [ [[package]] name = "zeroize" -version = "1.8.1" +version = "1.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" +checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" dependencies = [ "zeroize_derive", ] [[package]] name = "zeroize_derive" -version = "1.4.2" +version = "1.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" +checksum = "85a5b4158499876c763cb03bc4e49185d3cccbabb15b33c627f7884f43db852e" dependencies = [ "proc-macro2", "quote", @@ -4326,9 +4180,9 @@ dependencies = [ [[package]] name = "zerotrie" -version = "0.2.2" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "36f0bbd478583f79edad978b407914f61b2972f5af6fa089686016be8f9af595" +checksum = "0f9152d31db0792fa83f70fb2f83148effb5c1f5b8c7686c3459e361d9bc20bf" dependencies = [ "displaydoc", "yoke", @@ -4337,9 +4191,9 @@ dependencies = [ [[package]] name = "zerovec" -version = "0.11.2" +version = "0.11.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a05eb080e015ba39cc9e23bbe5e7fb04d5fb040350f99f34e338d5fdd294428" +checksum = "90f911cbc359ab6af17377d242225f4d75119aec87ea711a880987b18cd7b239" dependencies = [ "yoke", "zerofrom", @@ -4348,9 +4202,9 @@ dependencies = [ [[package]] name = "zerovec-derive" -version = "0.11.1" +version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b96237efa0c878c64bd89c436f661be4e46b2f3eff1ebb976f7ef2321d2f58f" +checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555" dependencies = [ "proc-macro2", "quote", @@ -4358,25 +4212,31 @@ dependencies = [ ] [[package]] -name = "zvariant" -version = "5.6.0" +name = "zmij" +version = "1.0.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d91b3680bb339216abd84714172b5138a4edac677e641ef17e1d8cb1b3ca6e6f" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" + +[[package]] +name = "zvariant" +version = "5.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5708299b21903bbe348e94729f22c49c55d04720a004aa350f1f9c122fd2540b" dependencies = [ "endi", "enumflags2", "serde", "url", - "winnow", + "winnow 0.7.15", "zvariant_derive", "zvariant_utils", ] [[package]] name = "zvariant_derive" -version = "5.6.0" +version = "5.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a8c68501be459a8dbfffbe5d792acdd23b4959940fc87785fb013b32edbc208" +checksum = "5b59b012ebe9c46656f9cc08d8da8b4c726510aef12559da3e5f1bf72780752c" dependencies = [ "proc-macro-crate", "proc-macro2", @@ -4387,14 +4247,13 @@ dependencies = [ [[package]] name = "zvariant_utils" -version = "3.2.0" +version = "3.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e16edfee43e5d7b553b77872d99bc36afdda75c223ca7ad5e3fbecd82ca5fc34" +checksum = "f75c23a64ef8f40f13a6989991e643554d9bef1d682a281160cf0c1bc389c5e9" dependencies = [ "proc-macro2", "quote", "serde", - "static_assertions", "syn", - "winnow", + "winnow 0.7.15", ] From 38920917cdf51ff37d50882bdf99d87160ac2392 Mon Sep 17 00:00:00 2001 From: Ferdinand Schober Date: Wed, 8 Apr 2026 13:11:18 +0200 Subject: [PATCH 09/33] update ashpd --- Cargo.lock | 10 +-- input-capture/Cargo.toml | 5 +- input-capture/src/libei.rs | 83 +++++++++++++---------- input-emulation/Cargo.toml | 4 +- input-emulation/src/libei.rs | 41 +++++------ input-emulation/src/xdg_desktop_portal.rs | 68 ++++++++++++------- 6 files changed, 122 insertions(+), 89 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 1d94db9..33a721e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -125,18 +125,16 @@ checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" [[package]] name = "ashpd" -version = "0.11.1" +version = "0.13.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2f3f79755c74fd155000314eb349864caa787c6592eace6c6882dad873d9c39" +checksum = "13bdf0fd848239dcd5e64eeeee35dbc00378ebcc6f3aa4ead0a305eec83d0cfb" dependencies = [ "enumflags2", - "futures-channel", "futures-util", - "rand 0.9.2", + "getrandom 0.4.2", "serde", "serde_repr", "tokio", - "url", "zbus", ] @@ -3310,7 +3308,6 @@ dependencies = [ "idna", "percent-encoding", "serde", - "serde_derive", ] [[package]] @@ -4226,7 +4223,6 @@ dependencies = [ "endi", "enumflags2", "serde", - "url", "winnow 0.7.15", "zvariant_derive", "zvariant_utils", diff --git a/input-capture/Cargo.toml b/input-capture/Cargo.toml index 0f77eb2..9b8d598 100644 --- a/input-capture/Cargo.toml +++ b/input-capture/Cargo.toml @@ -12,7 +12,7 @@ futures-core = "0.3.30" log = "0.4.22" input-event = { path = "../input-event", version = "0.3.0" } memmap = "0.7" -tempfile = "3.8" +tempfile = "3.25.0" thiserror = "2.0.0" tokio = { version = "1.32.0", features = [ "io-util", @@ -41,7 +41,8 @@ wayland-protocols-wlr = { version = "0.3.1", features = [ "client", ], optional = true } x11 = { version = "2.21.0", features = ["xlib", "xtest"], optional = true } -ashpd = { version = "0.11.0", default-features = false, features = [ +ashpd = { version = "0.13.9", default-features = false, features = [ + "input_capture", "tokio", ], optional = true } reis = { version = "0.5.0", features = ["tokio"], optional = true } diff --git a/input-capture/src/libei.rs b/input-capture/src/libei.rs index 697b782..fa16895 100644 --- a/input-capture/src/libei.rs +++ b/input-capture/src/libei.rs @@ -2,8 +2,8 @@ use ashpd::{ desktop::{ Session, input_capture::{ - Activated, ActivatedBarrier, Barrier, BarrierID, Capabilities, InputCapture, Region, - Zones, + Activated, ActivatedBarrier, Barrier, BarrierID, Capabilities, CreateSessionOptions, + InputCapture, Region, ReleaseOptions, Zones, }, }, enumflags2::BitFlags, @@ -58,8 +58,8 @@ enum LibeiNotifyEvent { } #[allow(dead_code)] -pub struct LibeiInputCapture<'a> { - input_capture: Pin>>, +pub struct LibeiInputCapture { + input_capture: Pin>, capture_task: JoinHandle>, event_rx: Receiver<(Position, CaptureEvent)>, notify_capture: Sender, @@ -130,12 +130,15 @@ fn select_barriers( } async fn update_barriers( - input_capture: &InputCapture<'_>, - session: &Session<'_, InputCapture<'_>>, + input_capture: &InputCapture, + session: &Session, active_clients: &[Position], next_barrier_id: &mut NonZeroU32, ) -> Result<(Vec, HashMap), ashpd::Error> { - let zones = input_capture.zones(session).await?.response()?; + let zones = input_capture + .zones(session, Default::default()) + .await? + .response()?; log::debug!("zones: {zones:?}"); let (barriers, id_map) = select_barriers(&zones, active_clients, next_barrier_id); @@ -144,31 +147,38 @@ async fn update_barriers( let ashpd_barriers: Vec = barriers.iter().copied().map(|b| b.into()).collect(); let response = input_capture - .set_pointer_barriers(session, &ashpd_barriers, zones.zone_set()) + .set_pointer_barriers( + session, + &ashpd_barriers, + zones.zone_set(), + Default::default(), + ) .await?; let response = response.response()?; log::debug!("{response:?}"); Ok((barriers, id_map)) } -async fn create_session<'a>( - input_capture: &'a InputCapture<'a>, -) -> std::result::Result<(Session<'a, InputCapture<'a>>, BitFlags), ashpd::Error> { +async fn create_session( + input_capture: &InputCapture, +) -> std::result::Result<(Session, BitFlags), ashpd::Error> { log::debug!("creating input capture session"); + let create_session_options = CreateSessionOptions::default().set_capabilities( + Capabilities::Keyboard | Capabilities::Pointer | Capabilities::Touchscreen, + ); input_capture - .create_session( - None, - Capabilities::Keyboard | Capabilities::Pointer | Capabilities::Touchscreen, - ) + .create_session(None, create_session_options) .await } async fn connect_to_eis( - input_capture: &InputCapture<'_>, - session: &Session<'_, InputCapture<'_>>, + input_capture: &InputCapture, + session: &Session, ) -> Result<(ei::Context, Connection, EiConvertEventStream), CaptureError> { log::debug!("connect_to_eis"); - let fd = input_capture.connect_to_eis(session).await?; + let fd = input_capture + .connect_to_eis(session, Default::default()) + .await?; // create unix stream from fd let stream = UnixStream::from(fd); @@ -201,10 +211,10 @@ async fn libei_event_handler( } } -impl LibeiInputCapture<'_> { +impl LibeiInputCapture { pub async fn new() -> std::result::Result { let input_capture = Box::pin(InputCapture::new().await?); - let input_capture_ptr = input_capture.as_ref().get_ref() as *const InputCapture<'static>; + let input_capture_ptr = input_capture.as_ref().get_ref() as *const InputCapture; let first_session = Some(create_session(unsafe { &*input_capture_ptr }).await?); let (event_tx, event_rx) = mpsc::channel(1); @@ -238,10 +248,10 @@ impl LibeiInputCapture<'_> { } async fn do_capture( - input_capture: *const InputCapture<'static>, + input_capture: *const InputCapture, mut capture_event: Receiver, notify_release: Arc, - session: Option<(Session<'_, InputCapture<'_>>, BitFlags)>, + session: Option<(Session, BitFlags)>, event_tx: Sender<(Position, CaptureEvent)>, cancellation_token: CancellationToken, ) -> Result<(), CaptureError> { @@ -307,7 +317,7 @@ async fn do_capture( // disable capture log::debug!("disabling input capture"); - if let Err(e) = input_capture.disable(&session).await { + if let Err(e) = input_capture.disable(&session, Default::default()).await { log::warn!("input_capture.disable(&session) {e}"); } if let Err(e) = session.close().await { @@ -336,8 +346,8 @@ async fn do_capture( } async fn do_capture_session( - input_capture: &InputCapture<'_>, - session: &mut Session<'_, InputCapture<'_>>, + input_capture: &InputCapture, + session: &mut Session, event_tx: &Sender<(Position, CaptureEvent)>, active_clients: &[Position], next_barrier_id: &mut NonZeroU32, @@ -356,7 +366,7 @@ async fn do_capture_session( update_barriers(input_capture, session, active_clients, next_barrier_id).await?; log::debug!("enabling session"); - input_capture.enable(session).await?; + input_capture.enable(session, Default::default()).await?; // cancellation token to release session let release_session = Arc::new(Notify::new()); @@ -462,9 +472,9 @@ async fn do_capture_session( Ok(()) } -async fn release_capture<'a>( - input_capture: &InputCapture<'a>, - session: &Session<'a, InputCapture<'a>>, +async fn release_capture( + input_capture: &InputCapture, + session: &Session, activated: Activated, current_pos: Position, ) -> Result<(), CaptureError> { @@ -484,9 +494,10 @@ async fn release_capture<'a>( }; // release 1px to the right of the entered zone let cursor_position = (x as f64 + dx, y as f64 + dy); - input_capture - .release(session, activated.activation_id(), Some(cursor_position)) - .await?; + let release_options = ReleaseOptions::default() + .set_activation_id(activated.activation_id()) + .set_cursor_position(Some(cursor_position)); + input_capture.release(session, release_options).await?; Ok(()) } @@ -561,7 +572,7 @@ async fn handle_ei_event( } #[async_trait] -impl LanMouseInputCapture for LibeiInputCapture<'_> { +impl LanMouseInputCapture for LibeiInputCapture { async fn create(&mut self, pos: Position) -> Result<(), CaptureError> { let _ = self .notify_capture @@ -598,7 +609,7 @@ impl LanMouseInputCapture for LibeiInputCapture<'_> { } } -impl Drop for LibeiInputCapture<'_> { +impl Drop for LibeiInputCapture { fn drop(&mut self) { if !self.terminated { /* this workaround is needed until async drop is stabilized */ @@ -607,10 +618,10 @@ impl Drop for LibeiInputCapture<'_> { } } -impl Stream for LibeiInputCapture<'_> { +impl Stream for LibeiInputCapture { type Item = Result<(Position, CaptureEvent), CaptureError>; - fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { + fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context) -> Poll> { match self.capture_task.poll_unpin(cx) { Poll::Ready(r) => match r.expect("failed to join") { Ok(()) => Poll::Ready(None), diff --git a/input-emulation/Cargo.toml b/input-emulation/Cargo.toml index e2c98b1..28c5ecd 100644 --- a/input-emulation/Cargo.toml +++ b/input-emulation/Cargo.toml @@ -40,7 +40,9 @@ wayland-protocols-misc = { version = "0.3.1", features = [ "client", ], optional = true } x11 = { version = "2.21.0", features = ["xlib", "xtest"], optional = true } -ashpd = { version = "0.11.0", default-features = false, features = [ +ashpd = { version = "0.13.9", default-features = false, features = [ + "remote_desktop", + "screencast", "tokio", ], optional = true } reis = { version = "0.5.0", features = ["tokio"], optional = true } diff --git a/input-emulation/src/libei.rs b/input-emulation/src/libei.rs index 4e60c98..3ac6e89 100644 --- a/input-emulation/src/libei.rs +++ b/input-emulation/src/libei.rs @@ -13,7 +13,7 @@ use tokio::task::JoinHandle; use ashpd::desktop::{ PersistMode, Session, - remote_desktop::{DeviceType, RemoteDesktop}, + remote_desktop::{DeviceType, RemoteDesktop, SelectDevicesOptions}, }; use async_trait::async_trait; @@ -40,15 +40,15 @@ struct Devices { keyboard: Arc>>, } -pub(crate) struct LibeiEmulation<'a> { +pub(crate) struct LibeiEmulation { context: ei::Context, conn: event::Connection, devices: Devices, ei_task: JoinHandle<()>, error: Arc>>, libei_error: Arc, - _remote_desktop: RemoteDesktop<'a>, - session: Session<'a, RemoteDesktop<'a>>, + _remote_desktop: RemoteDesktop, + session: Session, } /// Get the path to the RemoteDesktop token file @@ -84,27 +84,26 @@ fn write_token(token: &str) -> io::Result<()> { Ok(()) } -async fn get_ei_fd<'a>() --> Result<(RemoteDesktop<'a>, Session<'a, RemoteDesktop<'a>>, OwnedFd), ashpd::Error> { +async fn get_ei_fd() -> Result<(RemoteDesktop, Session, OwnedFd), ashpd::Error> { let remote_desktop = RemoteDesktop::new().await?; let restore_token = read_token(); log::debug!("creating session ..."); - let session = remote_desktop.create_session().await?; + let session = remote_desktop.create_session(Default::default()).await?; log::debug!("selecting devices ..."); - remote_desktop - .select_devices( - &session, - DeviceType::Keyboard | DeviceType::Pointer, - restore_token.as_deref(), - PersistMode::ExplicitlyRevoked, - ) - .await?; + let options = SelectDevicesOptions::default() + .set_devices(DeviceType::Keyboard | DeviceType::Pointer) + .set_persist_mode(PersistMode::ExplicitlyRevoked) + .set_restore_token(restore_token.as_deref()); + remote_desktop.select_devices(&session, options).await?; log::info!("requesting permission for input emulation"); - let start_response = remote_desktop.start(&session, None).await?.response()?; + let start_response = remote_desktop + .start(&session, None, Default::default()) + .await? + .response()?; // The restore token is only valid once, we need to re-save it each time if let Some(token_str) = start_response.restore_token() { @@ -113,11 +112,13 @@ async fn get_ei_fd<'a>() } } - let fd = remote_desktop.connect_to_eis(&session).await?; + let fd = remote_desktop + .connect_to_eis(&session, Default::default()) + .await?; Ok((remote_desktop, session, fd)) } -impl LibeiEmulation<'_> { +impl LibeiEmulation { pub(crate) async fn new() -> Result { let (_remote_desktop, session, eifd) = get_ei_fd().await?; let stream = UnixStream::from(eifd); @@ -152,14 +153,14 @@ impl LibeiEmulation<'_> { } } -impl Drop for LibeiEmulation<'_> { +impl Drop for LibeiEmulation { fn drop(&mut self) { self.ei_task.abort(); } } #[async_trait] -impl Emulation for LibeiEmulation<'_> { +impl Emulation for LibeiEmulation { async fn consume( &mut self, event: Event, diff --git a/input-emulation/src/xdg_desktop_portal.rs b/input-emulation/src/xdg_desktop_portal.rs index 37d2974..7c2d133 100644 --- a/input-emulation/src/xdg_desktop_portal.rs +++ b/input-emulation/src/xdg_desktop_portal.rs @@ -1,7 +1,10 @@ use ashpd::{ desktop::{ PersistMode, Session, - remote_desktop::{Axis, DeviceType, KeyState, RemoteDesktop}, + remote_desktop::{ + Axis, DeviceType, KeyState, NotifyPointerAxisOptions, RemoteDesktop, + SelectDevicesOptions, + }, }, zbus::AsyncDrop, }; @@ -17,32 +20,31 @@ use crate::error::EmulationError; use super::{Emulation, EmulationHandle, error::XdpEmulationCreationError}; -pub(crate) struct DesktopPortalEmulation<'a> { - proxy: RemoteDesktop<'a>, - session: Session<'a, RemoteDesktop<'a>>, +pub(crate) struct DesktopPortalEmulation { + proxy: RemoteDesktop, + session: Session, } -impl<'a> DesktopPortalEmulation<'a> { - pub(crate) async fn new() -> Result, XdpEmulationCreationError> { +impl DesktopPortalEmulation { + pub(crate) async fn new() -> Result { log::debug!("connecting to org.freedesktop.portal.RemoteDesktop portal ..."); let proxy = RemoteDesktop::new().await?; // retry when user presses the cancel button log::debug!("creating session ..."); - let session = proxy.create_session().await?; + let session = proxy.create_session(Default::default()).await?; log::debug!("selecting devices ..."); - proxy - .select_devices( - &session, - DeviceType::Keyboard | DeviceType::Pointer, - None, - PersistMode::ExplicitlyRevoked, - ) - .await?; + let options = SelectDevicesOptions::default() + .set_devices(DeviceType::Keyboard | DeviceType::Pointer) + .set_persist_mode(PersistMode::ExplicitlyRevoked); + proxy.select_devices(&session, options).await?; log::info!("requesting permission for input emulation"); - let _devices = proxy.start(&session, None).await?.response()?; + let _devices = proxy + .start(&session, None, Default::default()) + .await? + .response()?; log::debug!("started session"); let session = session; @@ -52,7 +54,7 @@ impl<'a> DesktopPortalEmulation<'a> { } #[async_trait] -impl Emulation for DesktopPortalEmulation<'_> { +impl Emulation for DesktopPortalEmulation { async fn consume( &mut self, event: input_event::Event, @@ -62,7 +64,7 @@ impl Emulation for DesktopPortalEmulation<'_> { Pointer(p) => match p { PointerEvent::Motion { time: _, dx, dy } => { self.proxy - .notify_pointer_motion(&self.session, dx, dy) + .notify_pointer_motion(&self.session, dx, dy, Default::default()) .await?; } PointerEvent::Button { @@ -75,7 +77,12 @@ impl Emulation for DesktopPortalEmulation<'_> { _ => KeyState::Pressed, }; self.proxy - .notify_pointer_button(&self.session, button as i32, state) + .notify_pointer_button( + &self.session, + button as i32, + state, + Default::default(), + ) .await?; } PointerEvent::AxisDiscrete120 { axis, value } => { @@ -84,7 +91,12 @@ impl Emulation for DesktopPortalEmulation<'_> { _ => Axis::Horizontal, }; self.proxy - .notify_pointer_axis_discrete(&self.session, axis, value / 120) + .notify_pointer_axis_discrete( + &self.session, + axis, + value / 120, + Default::default(), + ) .await?; } PointerEvent::Axis { @@ -101,7 +113,12 @@ impl Emulation for DesktopPortalEmulation<'_> { Axis::Horizontal => (value, 0.), }; self.proxy - .notify_pointer_axis(&self.session, dx, dy, true) + .notify_pointer_axis( + &self.session, + dx, + dy, + NotifyPointerAxisOptions::default().set_finish(true), + ) .await?; } }, @@ -117,7 +134,12 @@ impl Emulation for DesktopPortalEmulation<'_> { _ => KeyState::Pressed, }; self.proxy - .notify_keyboard_keycode(&self.session, key as i32, state) + .notify_keyboard_keycode( + &self.session, + key as i32, + state, + Default::default(), + ) .await?; } KeyboardEvent::Modifiers { .. } => { @@ -141,7 +163,7 @@ impl Emulation for DesktopPortalEmulation<'_> { } } -impl AsyncDrop for DesktopPortalEmulation<'_> { +impl AsyncDrop for DesktopPortalEmulation { #[doc = r" Perform the async cleanup."] #[allow(clippy::type_complexity, clippy::type_repetition_in_bounds)] fn async_drop<'async_trait>( From aef05f386f5dd4d7e5c4fd0fb4e25477cef0613a Mon Sep 17 00:00:00 2001 From: Ferdinand Schober Date: Wed, 8 Apr 2026 17:43:25 +0200 Subject: [PATCH 10/33] fix: config initialization in authorize --- src/config.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/config.rs b/src/config.rs index 78750f3..a8f74a7 100644 --- a/src/config.rs +++ b/src/config.rs @@ -432,7 +432,7 @@ impl Config { return; } if self.config_toml.is_none() { - self.config_toml = Default::default(); + self.config_toml = Some(Default::default()); } self.config_toml .as_mut() From a878c985f0633a12b00a363883fd56385c2368e7 Mon Sep 17 00:00:00 2001 From: Ferdinand Schober Date: Thu, 9 Apr 2026 12:04:21 +0200 Subject: [PATCH 11/33] automatically update config when changed (#402) --- Cargo.lock | 200 ++++++++++++++++++++++++++++++++++++++++++++++--- Cargo.toml | 1 + src/capture.rs | 14 +++- src/client.rs | 30 ++++++++ src/config.rs | 107 +++++++++++++++++++++----- src/service.rs | 47 +++++++----- 6 files changed, 351 insertions(+), 48 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 33a721e..876cf16 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -920,6 +920,15 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "fsevent-sys" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76ee7a02da4d231650c7cea31349b889be2f45ddb3ef3032d2ec8185f6313fd2" +dependencies = [ + "libc", +] + [[package]] name = "futures" version = "0.3.32" @@ -1606,6 +1615,26 @@ dependencies = [ "serde_core", ] +[[package]] +name = "inotify" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd5b3eaf1a28b758ac0faa5a4254e8ab2705605496f1b1f3fbbc3988ad73d199" +dependencies = [ + "bitflags 2.11.0", + "inotify-sys", + "libc", +] + +[[package]] +name = "inotify-sys" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e05c02b5e89bff3b946cedeca278abc628fe811e604f027c45a8aa3cf793d0eb" +dependencies = [ + "libc", +] + [[package]] name = "inout" version = "0.1.4" @@ -1787,6 +1816,26 @@ dependencies = [ "quote", ] +[[package]] +name = "kqueue" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eac30106d7dce88daf4a3fcb4879ea939476d5074a9b7ddd0fb97fa4bed5596a" +dependencies = [ + "kqueue-sys", + "libc", +] + +[[package]] +name = "kqueue-sys" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed9625ffda8729b85e45cf04090035ac368927b8cebc34898e7c120f52e4838b" +dependencies = [ + "bitflags 1.3.2", + "libc", +] + [[package]] name = "lan-mouse" version = "0.10.0" @@ -1805,6 +1854,7 @@ dependencies = [ "libc", "local-channel", "log", + "notify", "rcgen", "rustls", "serde", @@ -2039,6 +2089,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "50b7e5b27aa02a74bac8c3f23f448f8d87ff11f92d3aac1a6ed369ee08cc56c1" dependencies = [ "libc", + "log", "wasi", "windows-sys 0.61.2", ] @@ -2083,6 +2134,33 @@ dependencies = [ "minimal-lexical", ] +[[package]] +name = "notify" +version = "8.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d3d07927151ff8575b7087f245456e549fea62edf0ec4e565a5ee50c8402bc3" +dependencies = [ + "bitflags 2.11.0", + "fsevent-sys", + "inotify", + "kqueue", + "libc", + "log", + "mio", + "notify-types", + "walkdir", + "windows-sys 0.60.2", +] + +[[package]] +name = "notify-types" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42b8cfee0e339a0337359f3c88165702ac6e600dc01c0cc9579a92d62b08477a" +dependencies = [ + "bitflags 2.11.0", +] + [[package]] name = "num-bigint" version = "0.4.6" @@ -2678,6 +2756,15 @@ version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + [[package]] name = "scopeguard" version = "1.2.0" @@ -3352,6 +3439,16 @@ version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + [[package]] name = "wasi" version = "0.11.1+wasi-snapshot-preview1" @@ -3619,6 +3716,15 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" +[[package]] +name = "winapi-util" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys 0.61.2", +] + [[package]] name = "winapi-x86_64-pc-windows-gnu" version = "0.4.0" @@ -3781,7 +3887,7 @@ version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" dependencies = [ - "windows-targets", + "windows-targets 0.52.6", ] [[package]] @@ -3790,7 +3896,16 @@ version = "0.59.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" dependencies = [ - "windows-targets", + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +dependencies = [ + "windows-targets 0.53.5", ] [[package]] @@ -3808,14 +3923,31 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" dependencies = [ - "windows_aarch64_gnullvm", - "windows_aarch64_msvc", - "windows_i686_gnu", - "windows_i686_gnullvm", - "windows_i686_msvc", - "windows_x86_64_gnu", - "windows_x86_64_gnullvm", - "windows_x86_64_msvc", + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm 0.52.6", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + +[[package]] +name = "windows-targets" +version = "0.53.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" +dependencies = [ + "windows-link 0.2.1", + "windows_aarch64_gnullvm 0.53.1", + "windows_aarch64_msvc 0.53.1", + "windows_i686_gnu 0.53.1", + "windows_i686_gnullvm 0.53.1", + "windows_i686_msvc 0.53.1", + "windows_x86_64_gnu 0.53.1", + "windows_x86_64_gnullvm 0.53.1", + "windows_x86_64_msvc 0.53.1", ] [[package]] @@ -3833,48 +3965,96 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" + [[package]] name = "windows_aarch64_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" + [[package]] name = "windows_i686_gnu" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" +[[package]] +name = "windows_i686_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" + [[package]] name = "windows_i686_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" +[[package]] +name = "windows_i686_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" + [[package]] name = "windows_i686_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" +[[package]] +name = "windows_i686_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" + [[package]] name = "windows_x86_64_gnu" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" +[[package]] +name = "windows_x86_64_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" + [[package]] name = "windows_x86_64_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" + [[package]] name = "windows_x86_64_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" +[[package]] +name = "windows_x86_64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" + [[package]] name = "winnow" version = "0.7.15" diff --git a/Cargo.toml b/Cargo.toml index 29c0097..b482e5e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -67,6 +67,7 @@ rustls = { version = "0.23.12", default-features = false, features = [ ] } rcgen = "0.13.1" sha2 = "0.10.8" +notify = "8.2.0" [target.'cfg(unix)'.dependencies] libc = "0.2.148" diff --git a/src/capture.rs b/src/capture.rs index e4d9c8a..4300dea 100644 --- a/src/capture.rs +++ b/src/capture.rs @@ -49,7 +49,7 @@ pub(crate) enum CaptureType { EnterOnly, } -#[derive(Clone, Copy, Debug)] +#[derive(Clone, Debug)] enum CaptureRequest { /// capture must release the mouse Release, @@ -59,6 +59,8 @@ enum CaptureRequest { Destroy(CaptureHandle), /// reenable input capture Reenable, + /// set release bind + SetReleaseBind(Vec), } impl Capture { @@ -131,6 +133,10 @@ impl Capture { pub(crate) async fn event(&mut self) -> ICaptureEvent { self.event_rx.recv().await.expect("channel closed") } + + pub(crate) fn set_release_bind(&mut self, bind: Vec) { + let _ = self.request_tx.send(CaptureRequest::SetReleaseBind(bind)); + } } /// debounce a statement `$st`, i.e. the statement is executed only if the @@ -205,6 +211,9 @@ impl CaptureTask { CaptureRequest::Create(h, p, t) => self.add_capture(h, p, t), CaptureRequest::Destroy(h) => self.remove_capture(h), CaptureRequest::Release => { /* nothing to do */ } + CaptureRequest::SetReleaseBind(bind) => { + self.release_bind.borrow_mut().clone_from(&bind); + } }, _ = self.cancellation_token.cancelled() => return, } @@ -295,6 +304,9 @@ impl CaptureTask { self.remove_capture(h); capture.destroy(h).await?; } + CaptureRequest::SetReleaseBind(bind) => { + self.release_bind.borrow_mut().clone_from(&bind); + } }, _ = self.cancellation_token.cancelled() => break, } diff --git a/src/client.rs b/src/client.rs index b67f787..3229c8c 100644 --- a/src/client.rs +++ b/src/client.rs @@ -9,6 +9,8 @@ use slab::Slab; use lan_mouse_ipc::{ClientConfig, ClientHandle, ClientState, Position}; +use crate::config::ConfigClient; + #[derive(Clone, Default)] pub struct ClientManager { clients: Rc>>, @@ -24,6 +26,25 @@ impl ClientManager { .collect::>() } + pub fn add_with_config(&self, config_client: ConfigClient) -> ClientHandle { + let config = ClientConfig { + hostname: config_client.hostname, + fix_ips: config_client.ips.into_iter().collect(), + port: config_client.port, + pos: config_client.pos, + cmd: config_client.enter_hook, + }; + let state = ClientState { + active: config_client.active, + ips: HashSet::from_iter(config.fix_ips.iter().cloned()), + ..Default::default() + }; + let handle = self.add_client(); + self.set_config(handle, config); + self.set_state(handle, state); + handle + } + /// add a new client to this manager pub fn add_client(&self) -> ClientHandle { self.clients.borrow_mut().insert(Default::default()) as ClientHandle @@ -230,6 +251,15 @@ impl ClientManager { .and_then(|(c, _)| c.cmd.clone()) } + /// returns all clients that are currently registered + pub(crate) fn registered_clients(&self) -> Vec { + self.clients + .borrow() + .iter() + .map(|(h, _)| h as ClientHandle) + .collect() + } + /// returns all clients that are currently active pub(crate) fn active_clients(&self) -> Vec { self.clients diff --git a/src/config.rs b/src/config.rs index a8f74a7..6300aa5 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,6 +1,7 @@ use crate::capture_test::TestCaptureArgs; use crate::emulation_test::TestEmulationArgs; use clap::{Parser, Subcommand, ValueEnum}; +use notify::{EventKind, RecommendedWatcher, Watcher}; use serde::{Deserialize, Serialize}; use std::collections::HashMap; use std::env::{self, VarError}; @@ -46,7 +47,7 @@ fn default_path() -> Result { Ok(PathBuf::from(default_path)) } -#[derive(Serialize, Deserialize, Clone, Debug, Default)] +#[derive(Serialize, Deserialize, Clone, Debug, Default, PartialEq)] struct ConfigToml { capture_backend: Option, emulation_backend: Option, @@ -244,8 +245,14 @@ pub struct Config { cert_path: PathBuf, /// path to the config file used config_path: PathBuf, + /// path to config directory (parent of above) + config_dir: PathBuf, /// the (optional) toml config and it's path config_toml: Option, + // filesystem watcher + watcher: notify::RecommendedWatcher, + // channel for filesystem events + watch_rx: tokio::sync::mpsc::Receiver>, } pub struct ConfigClient { @@ -311,6 +318,8 @@ pub enum ConfigError { Io(#[from] io::Error), #[error(transparent)] Var(#[from] VarError), + #[error(transparent)] + Watcher(#[from] notify::Error), } const DEFAULT_RELEASE_KEYS: [scancode::Linux; 4] = @@ -342,12 +351,55 @@ impl Config { .or(config_toml.as_ref().and_then(|c| c.cert_path.clone())) .unwrap_or(default_path()?.join(CERT_FILE_NAME)); - Ok(Config { + let (tx, watch_rx) = tokio::sync::mpsc::channel(16); + let watcher = RecommendedWatcher::new( + move |res| { + let _ = tx.blocking_send(res); + }, + notify::Config::default(), + )?; + let config_dir = config_path + .parent() + .expect("config directory") + .to_path_buf(); + let mut config = Config { args, cert_path, config_path, + config_dir, config_toml, - }) + watcher, + watch_rx, + }; + config.watch()?; + Ok(config) + } + + fn watch(&mut self) -> Result<(), notify::Error> { + self.watcher + .watch(&self.config_dir, notify::RecursiveMode::NonRecursive)?; + Ok(()) + } + + fn unwatch(&mut self) -> Result<(), notify::Error> { + self.watcher.unwatch(&self.config_dir)?; + Ok(()) + } + + pub async fn changed(&mut self) -> Result<(), notify::Error> { + loop { + let event = self.watch_rx.recv().await.expect("channel closed"); + let event = event.expect("filesystem event"); + if event.paths.contains(&self.config_path) + && matches!( + event.kind, + EventKind::Create(_) | EventKind::Modify(_) | EventKind::Remove(_) + ) + && self.read_from_disk()? + { + return Ok(()); + } + } } /// the command to run @@ -428,9 +480,6 @@ impl Config { /// set authorized keys pub fn set_authorized_keys(&mut self, fingerprints: HashMap) { - if fingerprints.is_empty() { - return; - } if self.config_toml.is_none() { self.config_toml = Some(Default::default()); } @@ -440,38 +489,58 @@ impl Config { .authorized_fingerprints = Some(fingerprints); } - pub fn write_back(&self) -> Result<(), io::Error> { - log::info!("writing config to {:?}", &self.config_path); - /* load the current configuration file */ - let current_config = match fs::read_to_string(&self.config_path) { - Ok(c) => c.parse::().unwrap_or_default(), + pub fn read_from_disk(&mut self) -> Result { + log::info!("reading config from {:?}", &self.config_path); + + let current_config = fs::read_to_string(&self.config_path)?; + let current_config = match current_config.parse::() { + Ok(c) => c, Err(e) => { - log::info!("{:?} {e} => creating new config", self.config_path()); - Default::default() + log::warn!("{:?} {e}", self.config_path()); + return Ok(false); } }; - let _current_config = - toml_edit::de::from_document::(current_config).unwrap_or_default(); + let mut changed = false; + match toml_edit::de::from_document::(current_config) { + Ok(current_config) => { + changed = self + .config_toml + .as_ref() + .is_none_or(|c| c != ¤t_config); + self.config_toml.replace(current_config); + } + Err(e) => log::warn!("{:?} {e}", self.config_path()), + }; + Ok(changed) + } + pub fn write_back(&mut self) -> Result<(), io::Error> { + log::info!("writing config to {:?}", &self.config_path); /* the new config */ let new_config = self.config_toml.clone().unwrap_or_default(); - // let new_config = toml_edit::ser::to_document::(&new_config).expect("fixme"); let new_config = toml_edit::ser::to_string_pretty(&new_config).expect("config"); /* - * TODO merge documents => eventually we might want to split this up into clients configured + * TODO merge with current config file to preserve comments + * => eventually we might want to split this up into clients configured * via the config file and clients managed through the GUI / frontend. * The latter should be saved to $XDG_DATA_HOME instead of $XDG_CONFIG_HOME, * and clients configured through .config could be made permanent. * For now we just override the config file. */ + let _ = self.unwatch(); /* write new config to file */ if let Some(p) = self.config_path().parent() { fs::create_dir_all(p)?; } - let mut f = File::create(self.config_path())?; - f.write_all(new_config.as_bytes())?; + { + let mut f = File::create(self.config_path())?; + f.write_all(new_config.as_bytes())?; + f.sync_all()?; + } + + let _ = self.watch(); Ok(()) } diff --git a/src/service.rs b/src/service.rs index ff7c22f..d0772f6 100644 --- a/src/service.rs +++ b/src/service.rs @@ -11,8 +11,8 @@ use crate::{ use futures::StreamExt; use hickory_resolver::ResolveError; use lan_mouse_ipc::{ - AsyncFrontendListener, ClientConfig, ClientHandle, ClientState, FrontendEvent, FrontendRequest, - IpcError, IpcListenerCreationError, Position, Status, + AsyncFrontendListener, ClientHandle, FrontendEvent, FrontendRequest, IpcError, + IpcListenerCreationError, Position, Status, }; use log; use std::{ @@ -83,21 +83,7 @@ impl Service { pub async fn new(config: Config) -> Result { let client_manager = ClientManager::default(); for client in config.clients() { - let config = ClientConfig { - hostname: client.hostname, - fix_ips: client.ips.into_iter().collect(), - port: client.port, - pos: client.pos, - cmd: client.enter_hook, - }; - let state = ClientState { - active: client.active, - ips: HashSet::from_iter(config.fix_ips.iter().cloned()), - ..Default::default() - }; - let handle = client_manager.add_client(); - client_manager.set_config(handle, config); - client_manager.set_state(handle, state); + client_manager.add_with_config(client); } // load certificate @@ -164,6 +150,7 @@ impl Service { event = self.emulation.event() => self.handle_emulation_event(event), event = self.capture.event() => self.handle_capture_event(event), event = self.resolver.event() => self.handle_resolver_event(event), + _ = self.config.changed() => self.handle_config_change(), r = signal::ctrl_c() => break r.expect("failed to wait for CTRL+C"), } } @@ -255,6 +242,30 @@ impl Service { } } + fn handle_config_change(&mut self) { + for h in self.client_manager.registered_clients() { + self.remove_client(h); + } + for c in self.config.clients() { + let handle = self.client_manager.add_with_config(c); + log::info!("added client {handle}"); + let (c, s) = self.client_manager.get_state(handle).unwrap(); + if s.active { + self.client_manager.deactivate_client(handle); + self.activate_client(handle); + } + self.notify_frontend(FrontendEvent::Created(handle, c, s)); + } + let release_bind = self.config.release_bind(); + self.capture.set_release_bind(release_bind); + let authorized_keys = self.config.authorized_fingerprints(); + self.authorized_keys + .write() + .unwrap() + .clone_from(&authorized_keys); + self.sync_frontend(); + } + async fn handle_frontend_pending(&mut self) { while let Some(event) = self.pending_frontend_events.pop_front() { self.frontend_listener.broadcast(event).await; @@ -477,7 +488,7 @@ impl Service { } fn activate_client(&mut self, handle: ClientHandle) { - log::debug!("activating client"); + log::debug!("activating client {handle}"); /* resolve dns on activate */ self.resolve(handle); From e6cd1630b20fd985dc9ad0d6101f543bab434caa Mon Sep 17 00:00:00 2001 From: Jon Kinney Date: Thu, 23 Apr 2026 13:55:06 -0500 Subject: [PATCH 12/33] bundle Adwaita symbolic icons in gresource On macOS the Adwaita icon theme is not installed by default, so symbolic icons (edit-copy, auth-fingerprint, network-wired, dialog-warning, etc.) render as the "image-missing" placeholder. Bundle the symbolic SVGs used by the GTK frontend into the embedded gresource so the app is self-contained and doesn't depend on any system-installed icon theme. The existing `IconTheme::add_resource_path("/de/feschber/LanMouse/icons")` call already tells GTK to search this prefix, so no code changes are needed. Icons are sourced from Adwaita and placed under the standard `scalable/{actions,devices,places,status}/` hicolor layout. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../scalable/actions/edit-copy-symbolic.svg | 4 ++++ .../scalable/actions/edit-delete-symbolic.svg | 4 ++++ .../scalable/actions/emblem-ok-symbolic.svg | 4 ++++ .../scalable/actions/list-add-symbolic.svg | 4 ++++ .../actions/object-rotate-right-symbolic.svg | 4 ++++ .../actions/object-select-symbolic.svg | 4 ++++ .../scalable/actions/open-menu-symbolic.svg | 8 ++++++++ .../scalable/actions/process-stop-symbolic.svg | 4 ++++ .../devices/auth-fingerprint-symbolic.svg | 4 ++++ .../devices/network-wired-symbolic.svg | 4 ++++ .../scalable/places/user-trash-symbolic.svg | 8 ++++++++ .../status/dialog-warning-symbolic.svg | 4 ++++ .../network-wired-disconnected-symbolic.svg | 7 +++++++ .../resources/resources.gresource.xml | 18 ++++++++++++++++++ 14 files changed, 81 insertions(+) create mode 100644 lan-mouse-gtk/resources/icons/scalable/actions/edit-copy-symbolic.svg create mode 100644 lan-mouse-gtk/resources/icons/scalable/actions/edit-delete-symbolic.svg create mode 100644 lan-mouse-gtk/resources/icons/scalable/actions/emblem-ok-symbolic.svg create mode 100644 lan-mouse-gtk/resources/icons/scalable/actions/list-add-symbolic.svg create mode 100644 lan-mouse-gtk/resources/icons/scalable/actions/object-rotate-right-symbolic.svg create mode 100644 lan-mouse-gtk/resources/icons/scalable/actions/object-select-symbolic.svg create mode 100644 lan-mouse-gtk/resources/icons/scalable/actions/open-menu-symbolic.svg create mode 100644 lan-mouse-gtk/resources/icons/scalable/actions/process-stop-symbolic.svg create mode 100644 lan-mouse-gtk/resources/icons/scalable/devices/auth-fingerprint-symbolic.svg create mode 100644 lan-mouse-gtk/resources/icons/scalable/devices/network-wired-symbolic.svg create mode 100644 lan-mouse-gtk/resources/icons/scalable/places/user-trash-symbolic.svg create mode 100644 lan-mouse-gtk/resources/icons/scalable/status/dialog-warning-symbolic.svg create mode 100644 lan-mouse-gtk/resources/icons/scalable/status/network-wired-disconnected-symbolic.svg diff --git a/lan-mouse-gtk/resources/icons/scalable/actions/edit-copy-symbolic.svg b/lan-mouse-gtk/resources/icons/scalable/actions/edit-copy-symbolic.svg new file mode 100644 index 0000000..5964403 --- /dev/null +++ b/lan-mouse-gtk/resources/icons/scalable/actions/edit-copy-symbolic.svg @@ -0,0 +1,4 @@ + + + + diff --git a/lan-mouse-gtk/resources/icons/scalable/actions/edit-delete-symbolic.svg b/lan-mouse-gtk/resources/icons/scalable/actions/edit-delete-symbolic.svg new file mode 100644 index 0000000..4131277 --- /dev/null +++ b/lan-mouse-gtk/resources/icons/scalable/actions/edit-delete-symbolic.svg @@ -0,0 +1,4 @@ + + + + diff --git a/lan-mouse-gtk/resources/icons/scalable/actions/emblem-ok-symbolic.svg b/lan-mouse-gtk/resources/icons/scalable/actions/emblem-ok-symbolic.svg new file mode 100644 index 0000000..7a9551f --- /dev/null +++ b/lan-mouse-gtk/resources/icons/scalable/actions/emblem-ok-symbolic.svg @@ -0,0 +1,4 @@ + + + + diff --git a/lan-mouse-gtk/resources/icons/scalable/actions/list-add-symbolic.svg b/lan-mouse-gtk/resources/icons/scalable/actions/list-add-symbolic.svg new file mode 100644 index 0000000..cf68622 --- /dev/null +++ b/lan-mouse-gtk/resources/icons/scalable/actions/list-add-symbolic.svg @@ -0,0 +1,4 @@ + + + + diff --git a/lan-mouse-gtk/resources/icons/scalable/actions/object-rotate-right-symbolic.svg b/lan-mouse-gtk/resources/icons/scalable/actions/object-rotate-right-symbolic.svg new file mode 100644 index 0000000..2794d53 --- /dev/null +++ b/lan-mouse-gtk/resources/icons/scalable/actions/object-rotate-right-symbolic.svg @@ -0,0 +1,4 @@ + + + + diff --git a/lan-mouse-gtk/resources/icons/scalable/actions/object-select-symbolic.svg b/lan-mouse-gtk/resources/icons/scalable/actions/object-select-symbolic.svg new file mode 100644 index 0000000..7a9551f --- /dev/null +++ b/lan-mouse-gtk/resources/icons/scalable/actions/object-select-symbolic.svg @@ -0,0 +1,4 @@ + + + + diff --git a/lan-mouse-gtk/resources/icons/scalable/actions/open-menu-symbolic.svg b/lan-mouse-gtk/resources/icons/scalable/actions/open-menu-symbolic.svg new file mode 100644 index 0000000..7f44743 --- /dev/null +++ b/lan-mouse-gtk/resources/icons/scalable/actions/open-menu-symbolic.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/lan-mouse-gtk/resources/icons/scalable/actions/process-stop-symbolic.svg b/lan-mouse-gtk/resources/icons/scalable/actions/process-stop-symbolic.svg new file mode 100644 index 0000000..19b9537 --- /dev/null +++ b/lan-mouse-gtk/resources/icons/scalable/actions/process-stop-symbolic.svg @@ -0,0 +1,4 @@ + + + + diff --git a/lan-mouse-gtk/resources/icons/scalable/devices/auth-fingerprint-symbolic.svg b/lan-mouse-gtk/resources/icons/scalable/devices/auth-fingerprint-symbolic.svg new file mode 100644 index 0000000..f64af0a --- /dev/null +++ b/lan-mouse-gtk/resources/icons/scalable/devices/auth-fingerprint-symbolic.svg @@ -0,0 +1,4 @@ + + + + diff --git a/lan-mouse-gtk/resources/icons/scalable/devices/network-wired-symbolic.svg b/lan-mouse-gtk/resources/icons/scalable/devices/network-wired-symbolic.svg new file mode 100644 index 0000000..166b48f --- /dev/null +++ b/lan-mouse-gtk/resources/icons/scalable/devices/network-wired-symbolic.svg @@ -0,0 +1,4 @@ + + + + diff --git a/lan-mouse-gtk/resources/icons/scalable/places/user-trash-symbolic.svg b/lan-mouse-gtk/resources/icons/scalable/places/user-trash-symbolic.svg new file mode 100644 index 0000000..2e20f9c --- /dev/null +++ b/lan-mouse-gtk/resources/icons/scalable/places/user-trash-symbolic.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/lan-mouse-gtk/resources/icons/scalable/status/dialog-warning-symbolic.svg b/lan-mouse-gtk/resources/icons/scalable/status/dialog-warning-symbolic.svg new file mode 100644 index 0000000..0b8cbe5 --- /dev/null +++ b/lan-mouse-gtk/resources/icons/scalable/status/dialog-warning-symbolic.svg @@ -0,0 +1,4 @@ + + + + diff --git a/lan-mouse-gtk/resources/icons/scalable/status/network-wired-disconnected-symbolic.svg b/lan-mouse-gtk/resources/icons/scalable/status/network-wired-disconnected-symbolic.svg new file mode 100644 index 0000000..df1f039 --- /dev/null +++ b/lan-mouse-gtk/resources/icons/scalable/status/network-wired-disconnected-symbolic.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/lan-mouse-gtk/resources/resources.gresource.xml b/lan-mouse-gtk/resources/resources.gresource.xml index c94be6b..d382074 100644 --- a/lan-mouse-gtk/resources/resources.gresource.xml +++ b/lan-mouse-gtk/resources/resources.gresource.xml @@ -9,5 +9,23 @@ de.feschber.LanMouse.svg + + icons/scalable/actions/edit-copy-symbolic.svg + icons/scalable/actions/edit-delete-symbolic.svg + icons/scalable/actions/emblem-ok-symbolic.svg + icons/scalable/actions/list-add-symbolic.svg + icons/scalable/actions/object-rotate-right-symbolic.svg + icons/scalable/actions/object-select-symbolic.svg + icons/scalable/actions/open-menu-symbolic.svg + icons/scalable/actions/process-stop-symbolic.svg + icons/scalable/devices/auth-fingerprint-symbolic.svg + icons/scalable/devices/network-wired-symbolic.svg + icons/scalable/places/user-trash-symbolic.svg + icons/scalable/status/dialog-warning-symbolic.svg + icons/scalable/status/network-wired-disconnected-symbolic.svg From f858a7de00836f9fad61aefc5c504ac3ea8c86ae Mon Sep 17 00:00:00 2001 From: Jon Kinney Date: Thu, 23 Apr 2026 13:55:14 -0500 Subject: [PATCH 13/33] makeicns.sh: produce Big Sur+ style macOS app icon The previous script generated a 1024x1024 icon from the SVG with no squircle background and no transparent padding, which caused macOS to render the Dock/Finder icon noticeably larger than first-party apps and without the rounded-square shape users expect. Rewrite the script to follow Apple's Big Sur+ icon template: - 1024x1024 canvas - 824x824 white squircle, centered (100px transparent padding outside) - Artwork rendered at 560x560, centered inside the squircle - Squircle corner radius ~22.5% of the squircle size Use rsvg-convert to rasterize the SVG (ImageMagick crops this particular SVG when rendering directly), then composite onto the squircle background in two steps for reliability. Co-Authored-By: Claude Opus 4.7 (1M context) --- scripts/makeicns.sh | 73 +++++++++++++++++++++++++++++++++++++-------- 1 file changed, 60 insertions(+), 13 deletions(-) diff --git a/scripts/makeicns.sh b/scripts/makeicns.sh index babce97..d9cfbbc 100755 --- a/scripts/makeicns.sh +++ b/scripts/makeicns.sh @@ -3,8 +3,15 @@ set -e usage() { cat < Date: Fri, 24 Apr 2026 02:08:59 -0500 Subject: [PATCH 14/33] fix: create ~/.config/lan-mouse/ on first launch notify::Watcher fails on macOS if the config directory doesn't exist. Create it with create_dir_all before calling config.watch(). Co-Authored-By: Claude Opus 4.7 (1M context) --- src/config.rs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/config.rs b/src/config.rs index 6300aa5..5b02728 100644 --- a/src/config.rs +++ b/src/config.rs @@ -362,6 +362,11 @@ impl Config { .parent() .expect("config directory") .to_path_buf(); + // notify::Watcher requires the directory to exist on macOS (FSEvents) + // and some Linux backends. Create it eagerly so a first launch on a + // fresh machine — where ~/.config/lan-mouse/ has never been touched — + // doesn't surface as a notify::Error out of Config::new(). + fs::create_dir_all(&config_dir)?; let mut config = Config { args, cert_path, From 903b0504e020afaca534a5be9299763da58775a2 Mon Sep 17 00:00:00 2001 From: Jon Kinney Date: Fri, 24 Apr 2026 02:09:12 -0500 Subject: [PATCH 15/33] macos: run as LSUIElement menubar app with NSStatusItem Ship Lan Mouse on macOS as an accessory app (no Dock icon, no main window on launch) with a status-bar item for show/quit. Closing the window hides it instead of quitting so the menu bar stays the primary surface. - build-aux/macos-lsui-element.plist: LSUIElement=true plus NSInputMonitoringUsageDescription and NSAppleEventsUsageDescription (merged into Info.plist via cargo-bundle's osx_info_plist_exts). - lan-mouse-gtk/src/macos_status_item.rs: NSStatusItem setup via raw objc_msgSend FFI. Loads a bundled 22pt PNG as a template image so it auto-tints for light/dark menu bars. - scripts/makeicns.sh: emit Contents/Resources/menubar-template.png from the existing SVG. - scripts/copy-macos-dylib.sh: flatten cargo-bundle's preserved target/ subdir under Resources so NSBundle pathForResource: finds the template image. - lan-mouse-gtk/src/lib.rs: register the new modules, set up a Cmd+Q-wired quit action, configure bundle env vars (schemas, XDG_DATA_DIRS, GTK_DATA_PREFIX) when running from inside the .app, and filter the known upstream Gtk theme-parser warning spam. Co-Authored-By: Claude Opus 4.7 (1M context) --- Cargo.lock | 2 + Cargo.toml | 2 + README.md | 3 +- build-aux/macos-lsui-element.plist | 6 + lan-mouse-gtk/src/lib.rs | 79 +++++++ lan-mouse-gtk/src/macos_status_item.rs | 283 +++++++++++++++++++++++++ scripts/copy-macos-dylib.sh | 43 ++++ scripts/makeicns.sh | 54 ++++- 8 files changed, 468 insertions(+), 4 deletions(-) create mode 100644 build-aux/macos-lsui-element.plist create mode 100644 lan-mouse-gtk/src/macos_status_item.rs diff --git a/Cargo.lock b/Cargo.lock index 876cf16..b241e01 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1682,6 +1682,8 @@ dependencies = [ "ashpd", "async-trait", "bitflags 2.11.0", + "core-foundation", + "core-foundation-sys", "core-graphics", "futures", "input-event", diff --git a/Cargo.toml b/Cargo.toml index b482e5e..dc673fa 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -96,3 +96,5 @@ rdp_emulation = ["input-emulation/remote_desktop_portal"] name = "Lan Mouse" icon = ["target/icon.icns"] identifier = "de.feschber.LanMouse" +osx_info_plist_exts = ["build-aux/macos-lsui-element.plist"] +resources = ["target/menubar-template.png"] diff --git a/README.md b/README.md index d96607d..78834c3 100644 --- a/README.md +++ b/README.md @@ -105,6 +105,7 @@ dnf install lan-mouse - Unzip it - Remove the quarantine with `xattr -rd com.apple.quarantine "Lan Mouse.app"` - Launch the app +- Use the menu bar item to open the settings window or quit Lan Mouse. Bundled macOS builds run as a menu bar app and do not keep a Dock icon visible. - Grant accessibility permissions in System Preferences
@@ -475,4 +476,4 @@ The following sections detail the emulation and capture backends provided by lan - `libei`: This backend uses [libei](https://gitlab.freedesktop.org/libinput/libei) and is supported by GNOME >= 45 or KDE Plasma >= 6.1. - `windows`: Backend for input capture on Windows. - `macos`: Backend for input capture on MacOS. -- `x11`: TODO (not yet supported) \ No newline at end of file +- `x11`: TODO (not yet supported) diff --git a/build-aux/macos-lsui-element.plist b/build-aux/macos-lsui-element.plist new file mode 100644 index 0000000..4a60be7 --- /dev/null +++ b/build-aux/macos-lsui-element.plist @@ -0,0 +1,6 @@ + LSUIElement + + NSInputMonitoringUsageDescription + Lan Mouse needs Input Monitoring access to capture keyboard and mouse input and forward it to remote machines on your network. + NSAppleEventsUsageDescription + Lan Mouse uses Apple Events to deliver synthesized keyboard and mouse events to the system. diff --git a/lan-mouse-gtk/src/lib.rs b/lan-mouse-gtk/src/lib.rs index 9908e05..6204879 100644 --- a/lan-mouse-gtk/src/lib.rs +++ b/lan-mouse-gtk/src/lib.rs @@ -4,6 +4,10 @@ mod client_row; mod fingerprint_window; mod key_object; mod key_row; +#[cfg(target_os = "macos")] +mod macos_privacy; +#[cfg(target_os = "macos")] +mod macos_status_item; mod window; use std::{env, process, str}; @@ -47,6 +51,12 @@ pub fn run() -> Result<(), GtkError> { } fn gtk_main() -> glib::ExitCode { + #[cfg(target_os = "macos")] + { + configure_macos_bundle_environment(); + install_macos_gtk_log_filter(); + } + gio::resources_register_include!("lan-mouse.gresource").expect("Failed to register resources."); let app = Application::builder() @@ -64,6 +74,64 @@ fn gtk_main() -> glib::ExitCode { app.run_with_args(&args) } +#[cfg(target_os = "macos")] +fn install_macos_gtk_log_filter() { + glib::log_set_writer_func(|level, fields| { + if level == glib::LogLevel::Warning && is_gtk_theme_parser_warning(fields) { + return glib::LogWriterOutput::Handled; + } + + glib::log_writer_default(level, fields) + }); +} + +#[cfg(target_os = "macos")] +fn is_gtk_theme_parser_warning(fields: &[glib::LogField<'_>]) -> bool { + let mut domain = None; + let mut message = None; + + for field in fields { + match field.key() { + "GLIB_DOMAIN" => domain = field.value_str(), + "MESSAGE" => message = field.value_str(), + _ => {} + } + } + + domain == Some("Gtk") + && message.is_some_and(|message| message.starts_with("Theme parser warning: gtk.css:")) +} + +#[cfg(target_os = "macos")] +fn configure_macos_bundle_environment() { + let Ok(exe) = env::current_exe() else { + return; + }; + let Some(contents) = exe + .parent() + .and_then(|dir| dir.parent()) + .map(std::path::Path::to_owned) + else { + return; + }; + + let share = contents.join("Resources").join("share"); + if !share.exists() { + return; + } + + let schemas = share.join("glib-2.0").join("schemas"); + if schemas.exists() { + env::set_var("GSETTINGS_SCHEMA_DIR", schemas); + } + + env::set_var("XDG_DATA_DIRS", &share); + env::set_var( + "GTK_DATA_PREFIX", + contents.join("Resources").to_string_lossy().as_ref(), + ); +} + fn load_icons() { let display = &Display::default().expect("Could not connect to a display."); let icon_theme = IconTheme::for_display(display); @@ -123,6 +191,16 @@ fn build_ui(app: &Application) { }); let window = Window::new(app, frontend_tx); + #[cfg(target_os = "macos")] + { + window.connect_close_request(|window| { + window.set_visible(false); + glib::Propagation::Stop + }); + macos_status_item::setup(app, &window); + // First-launch TCC prompts. No-op when already granted. + macos_privacy::fire_initial_prompts(); + } glib::spawn_future_local(clone!( #[weak] @@ -171,5 +249,6 @@ fn build_ui(app: &Application) { } )); + #[cfg(not(target_os = "macos"))] window.present(); } diff --git a/lan-mouse-gtk/src/macos_status_item.rs b/lan-mouse-gtk/src/macos_status_item.rs new file mode 100644 index 0000000..d8aed2e --- /dev/null +++ b/lan-mouse-gtk/src/macos_status_item.rs @@ -0,0 +1,283 @@ +#![allow(clashing_extern_declarations)] + +use std::{ + cell::RefCell, + ffi::{CStr, CString, c_char, c_double, c_void}, + sync::OnceLock, +}; + +use adw::prelude::*; +use gtk::{gio, glib}; + +use crate::window::Window; + +type Id = *mut c_void; +type Class = *mut c_void; +type Sel = *mut c_void; +type Bool = i8; + +struct StatusItem { + app: glib::WeakRef, + window: glib::WeakRef, + _hold: gio::ApplicationHoldGuard, + _delegate: Id, + _status_item: Id, +} + +thread_local! { + static STATUS_ITEM: RefCell> = const { RefCell::new(None) }; +} + +pub fn setup(app: &adw::Application, window: &Window) { + log::debug!("macos_status_item::setup entered"); + STATUS_ITEM.with(|item| { + let already_initialized = item.borrow().is_some(); + if already_initialized { + let mut cell = item.borrow_mut(); + if let Some(existing) = cell.as_mut() { + existing.app.set(Some(app)); + existing.window.set(Some(window)); + } + return; + } + + unsafe { + let hold = app.hold(); + + let ns_app = msg_send_id(class(c"NSApplication"), sel(c"sharedApplication")); + assert!(!ns_app.is_null(), "NSApplication sharedApplication returned null"); + msg_send_bool_usize(ns_app, sel(c"setActivationPolicy:"), 1); + + let delegate = new_delegate(); + let menu = menu(&[ + menu_item(c"Open Lan Mouse", c"showLanMouse:"), + separator_item(), + menu_item(c"Quit Lan Mouse", c"quitLanMouse:"), + ]); + + let status_bar = msg_send_id(class(c"NSStatusBar"), sel(c"systemStatusBar")); + assert!(!status_bar.is_null(), "NSStatusBar systemStatusBar returned null"); + let status_item = msg_send_id_f64(status_bar, sel(c"statusItemWithLength:"), -1.0); + assert!(!status_item.is_null(), "statusItemWithLength returned null"); + // Retain so the status item survives autorelease pool drain. + let status_item = msg_send_id(status_item, sel(c"retain")); + + let button = msg_send_id(status_item, sel(c"button")); + assert!(!button.is_null(), "NSStatusItem.button was null"); + set_button_image(button); + msg_send_void_id(button, sel(c"setToolTip:"), nsstring(c"Lan Mouse")); + msg_send_void_id(status_item, sel(c"setMenu:"), menu); + + for item in menu_items(menu) { + msg_send_void_id(item, sel(c"setTarget:"), delegate); + } + + log::debug!("macos_status_item ready at {:p}", status_item); + + item.replace(Some(StatusItem { + app: app.downgrade(), + window: window.downgrade(), + _hold: hold, + _delegate: delegate, + _status_item: status_item, + })); + } + }); +} + +// Prefer a pre-rendered template PNG (black silhouette with alpha) so macOS +// auto-tints the glyph to match the menu bar in light and dark modes. +// Falls back to the full-color icns, then to "LM" text. +unsafe fn set_button_image(button: Id) { + if let Some(image) = load_menubar_template() { + msg_send_void_bool(image, sel(c"setTemplate:"), 1); + msg_send_void_id(button, sel(c"setImage:"), image); + return; + } + if let Some(image) = load_app_icon() { + msg_send_void_id(button, sel(c"setImage:"), image); + return; + } + log::warn!("no menu bar image available; falling back to text title"); + msg_send_void_id(button, sel(c"setTitle:"), nsstring(c"LM")); +} + +unsafe fn load_menubar_template() -> Option { + load_resource_image(c"menubar-template", c"png", MENUBAR_ICON_SIZE) +} + +unsafe fn load_app_icon() -> Option { + load_resource_image(c"icon", c"icns", MENUBAR_ICON_SIZE) +} + +unsafe fn load_resource_image(name: &CStr, ext: &CStr, size_pt: c_double) -> Option { + let bundle = msg_send_id(class(c"NSBundle"), sel(c"mainBundle")); + if bundle.is_null() { + return None; + } + let path = msg_send_id_id_id( + bundle, + sel(c"pathForResource:ofType:"), + nsstring(name), + nsstring(ext), + ); + if path.is_null() { + return None; + } + let image = msg_send_id_id( + msg_send_id(class(c"NSImage"), sel(c"alloc")), + sel(c"initWithContentsOfFile:"), + path, + ); + if image.is_null() { + return None; + } + // Render at menu bar height; 22pt is the full status bar icon height. + msg_send_void_size(image, sel(c"setSize:"), size_pt, size_pt); + Some(image) +} + +const MENUBAR_ICON_SIZE: c_double = 22.0; + +unsafe fn menu(items: &[Id]) -> Id { + let menu = msg_send_id(msg_send_id(class(c"NSMenu"), sel(c"alloc")), sel(c"init")); + for item in items { + msg_send_void_id(menu, sel(c"addItem:"), *item); + } + menu +} + +unsafe fn menu_item(title: &CStr, action: &CStr) -> Id { + msg_send_id_id_sel_id( + msg_send_id(class(c"NSMenuItem"), sel(c"alloc")), + sel(c"initWithTitle:action:keyEquivalent:"), + nsstring(title), + sel(action), + nsstring(c""), + ) +} + +unsafe fn separator_item() -> Id { + msg_send_id(class(c"NSMenuItem"), sel(c"separatorItem")) +} + +unsafe fn menu_items(menu: Id) -> Vec { + let count = msg_send_usize(menu, sel(c"numberOfItems")); + (0..count) + .map(|idx| msg_send_id_usize(menu, sel(c"itemAtIndex:"), idx)) + .collect() +} + +unsafe fn new_delegate() -> Id { + let class = delegate_class(); + msg_send_id(msg_send_id(class, sel(c"alloc")), sel(c"init")) +} + +fn delegate_class() -> Class { + static CLASS: OnceLock = OnceLock::new(); + + *CLASS.get_or_init(|| unsafe { + let superclass = class(c"NSObject"); + let class_name = CString::new("LanMouseStatusItemDelegate").unwrap(); + let class = objc_allocateClassPair(superclass, class_name.as_ptr(), 0); + assert!(!class.is_null(), "failed to allocate status item delegate"); + + class_addMethod( + class, + sel(c"showLanMouse:"), + show_lan_mouse as *const c_void, + c"v@:@".as_ptr(), + ); + class_addMethod( + class, + sel(c"quitLanMouse:"), + quit_lan_mouse as *const c_void, + c"v@:@".as_ptr(), + ); + objc_registerClassPair(class); + class as usize + }) as Class +} + +extern "C" fn show_lan_mouse(_this: Id, _cmd: Sel, _sender: Id) { + STATUS_ITEM.with(|item| { + let item = item.borrow(); + let Some(item) = item.as_ref() else { + return; + }; + if let Some(window) = item.window.upgrade() { + window.present(); + } + + unsafe { + let ns_app = msg_send_id(class(c"NSApplication"), sel(c"sharedApplication")); + msg_send_void_bool(ns_app, sel(c"activateIgnoringOtherApps:"), 1); + } + }); +} + +extern "C" fn quit_lan_mouse(_this: Id, _cmd: Sel, _sender: Id) { + STATUS_ITEM.with(|item| { + if let Some(app) = item.borrow().as_ref().and_then(|item| item.app.upgrade()) { + app.quit(); + } + }); +} + +unsafe fn class(name: &CStr) -> Class { + let class = objc_getClass(name.as_ptr()); + assert!(!class.is_null(), "missing Objective-C class {name:?}"); + class +} + +unsafe fn sel(name: &CStr) -> Sel { + sel_registerName(name.as_ptr()) +} + +unsafe fn nsstring(value: &CStr) -> Id { + msg_send_id_ptr( + class(c"NSString"), + sel(c"stringWithUTF8String:"), + value.as_ptr(), + ) +} + +#[link(name = "objc")] +extern "C" { + fn objc_allocateClassPair(superclass: Class, name: *const c_char, extra_bytes: usize) -> Class; + fn objc_getClass(name: *const c_char) -> Class; + fn objc_registerClassPair(class: Class); + fn sel_registerName(name: *const c_char) -> Sel; + fn class_addMethod(class: Class, name: Sel, imp: *const c_void, types: *const c_char) -> Bool; +} + +#[link(name = "AppKit", kind = "framework")] +extern "C" {} + +#[link(name = "objc")] +extern "C" { + #[link_name = "objc_msgSend"] + fn msg_send_id(receiver: Id, selector: Sel) -> Id; + #[link_name = "objc_msgSend"] + fn msg_send_id_f64(receiver: Id, selector: Sel, value: c_double) -> Id; + #[link_name = "objc_msgSend"] + fn msg_send_id_id_sel_id(receiver: Id, selector: Sel, a: Id, b: Sel, c: Id) -> Id; + #[link_name = "objc_msgSend"] + fn msg_send_id_id_id(receiver: Id, selector: Sel, a: Id, b: Id) -> Id; + #[link_name = "objc_msgSend"] + fn msg_send_id_id(receiver: Id, selector: Sel, a: Id) -> Id; + #[link_name = "objc_msgSend"] + fn msg_send_void_size(receiver: Id, selector: Sel, width: c_double, height: c_double); + #[link_name = "objc_msgSend"] + fn msg_send_id_ptr(receiver: Id, selector: Sel, value: *const c_char) -> Id; + #[link_name = "objc_msgSend"] + fn msg_send_id_usize(receiver: Id, selector: Sel, value: usize) -> Id; + #[link_name = "objc_msgSend"] + fn msg_send_usize(receiver: Id, selector: Sel) -> usize; + #[link_name = "objc_msgSend"] + fn msg_send_void_bool(receiver: Id, selector: Sel, value: Bool); + #[link_name = "objc_msgSend"] + fn msg_send_void_id(receiver: Id, selector: Sel, value: Id); + #[link_name = "objc_msgSend"] + fn msg_send_bool_usize(receiver: Id, selector: Sel, value: usize) -> Bool; +} diff --git a/scripts/copy-macos-dylib.sh b/scripts/copy-macos-dylib.sh index f2ba501..84fb4d9 100755 --- a/scripts/copy-macos-dylib.sh +++ b/scripts/copy-macos-dylib.sh @@ -43,6 +43,9 @@ bundle_path=$(dirname "$(dirname "$(dirname "$exec_path")")") # Path to the Frameworks directory fwks_path="$bundle_path/Contents/Frameworks" mkdir -p "$fwks_path" +# Path to bundled GTK/GSettings data +resources_path="$bundle_path/Contents/Resources" +share_path="$resources_path/share" # Copy and fix references for a binary (executable or dylib) # @@ -58,6 +61,10 @@ fix_references() { libs=$(otool -L "$bin" | awk -v homebrew="$homebrew_path" '$0 ~ homebrew {print $1}') echo "$libs" | while IFS= read -r old_path; do + if [ -z "$old_path" ]; then + continue + fi + local base_name="$(basename "$old_path")" local dest="$fwks_path/$base_name" @@ -81,6 +88,42 @@ fix_references() { fix_references "$exec_path" +copy_runtime_data() { + mkdir -p "$share_path" + + if [ -d "$homebrew_path/share/glib-2.0/schemas" ]; then + mkdir -p "$share_path/glib-2.0" + rm -rf "$share_path/glib-2.0/schemas" + cp -RL "$homebrew_path/share/glib-2.0/schemas" "$share_path/glib-2.0/schemas" + if command -v glib-compile-schemas >/dev/null 2>&1; then + glib-compile-schemas "$share_path/glib-2.0/schemas" + elif [ -x "$homebrew_path/bin/glib-compile-schemas" ]; then + "$homebrew_path/bin/glib-compile-schemas" "$share_path/glib-2.0/schemas" + fi + fi + + if [ -d "$homebrew_path/share/gtk-4.0" ]; then + rm -rf "$share_path/gtk-4.0" + cp -RL "$homebrew_path/share/gtk-4.0" "$share_path/gtk-4.0" + fi + + if [ -d "$homebrew_path/share/icons/Adwaita" ]; then + mkdir -p "$share_path/icons" + rm -rf "$share_path/icons/Adwaita" + cp -RL "$homebrew_path/share/icons/Adwaita" "$share_path/icons/Adwaita" + fi +} + +copy_runtime_data + +# cargo-bundle preserves the source path under Contents/Resources (so +# `target/menubar-template.png` lands at `Resources/target/...`). Flatten it +# so NSBundle pathForResource: finds the file at the Resources root. +if [ -f "$resources_path/target/menubar-template.png" ]; then + mv "$resources_path/target/menubar-template.png" "$resources_path/menubar-template.png" + rmdir "$resources_path/target" 2>/dev/null || true +fi + # Ensure the main executable has our Frameworks path in its RPATH if ! otool -l "$exec_path" | grep -q "@executable_path/../Frameworks"; then echo "Adding RPATH to $exec_path" diff --git a/scripts/makeicns.sh b/scripts/makeicns.sh index d9cfbbc..a820898 100755 --- a/scripts/makeicns.sh +++ b/scripts/makeicns.sh @@ -67,12 +67,13 @@ magick -size ${CANVAS}x${CANVAS} xc:none \ # 3) Composite the artwork onto the background, centered inside the content area. magick "$workdir/background.png" \ "$workdir/content.png" -geometry +${CONTENT_OFFSET}+${CONTENT_OFFSET} -composite \ - "$workdir/icon-1024.png" + -colorspace sRGB -type TrueColorAlpha PNG32:"$workdir/icon-1024.png" # 4) Generate each iconset size from the master so all sizes share the same # squircle proportions and look consistent at every resolution. for size in 1024 512 256 128 64 32 16; do - magick "$workdir/icon-1024.png" -resize ${size}x${size} "$workdir/${size}.png" + magick "$workdir/icon-1024.png" -resize ${size}x${size} \ + -colorspace sRGB -type TrueColorAlpha PNG32:"$workdir/${size}.png" done cp "$workdir/1024.png" "$iconset"/icon_512x512@2x.png @@ -86,4 +87,51 @@ cp "$workdir/32.png" "$iconset"/icon_32x32.png cp "$workdir/32.png" "$iconset"/icon_16x16@2x.png cp "$workdir/16.png" "$iconset"/icon_16x16.png -iconutil -c icns "$iconset" -o "$icns" +mkdir -p "$(dirname "$icns")" + +# Menu bar template icon: flatten all RGB channels to 0 (black) while keeping +# alpha so the artwork reads as a clean silhouette. NSStatusBarButton tints +# template images to match the menu bar appearance in light and dark modes. +menubar_template="$(dirname "$icns")/menubar-template.png" +rsvg-convert -w 44 -h 44 "$svg" -o "$workdir/menubar-44.png" +magick "$workdir/menubar-44.png" -channel RGB -evaluate set 0 +channel \ + "$menubar_template" + +if ! iconutil -c icns "$iconset" -o "$icns"; then + if ! command -v perl >/dev/null 2>&1; then + echo "iconutil failed and perl is not available for the fallback icns writer" >&2 + exit 1 + fi + + echo "iconutil rejected the iconset; writing icns directly" >&2 + perl - "$icns" "$iconset" <<'PERL' +use strict; +use warnings; + +my ($icns, $iconset) = @ARGV; +my @icons = ( + [ 'icp4', "$iconset/icon_16x16.png" ], + [ 'ic11', "$iconset/icon_16x16\@2x.png" ], + [ 'icp5', "$iconset/icon_32x32.png" ], + [ 'ic12', "$iconset/icon_32x32\@2x.png" ], + [ 'ic07', "$iconset/icon_128x128.png" ], + [ 'ic13', "$iconset/icon_128x128\@2x.png" ], + [ 'ic08', "$iconset/icon_256x256.png" ], + [ 'ic14', "$iconset/icon_256x256\@2x.png" ], + [ 'ic09', "$iconset/icon_512x512.png" ], + [ 'ic10', "$iconset/icon_512x512\@2x.png" ], +); + +my $body = ''; +for my $icon (@icons) { + my ($type, $path) = @$icon; + open my $fh, '<:raw', $path or die "$path: $!"; + local $/; + my $png = <$fh>; + $body .= $type . pack('N', length($png) + 8) . $png; +} + +open my $out, '>:raw', $icns or die "$icns: $!"; +print {$out} 'icns' . pack('N', length($body) + 8) . $body; +PERL +fi From 5e79743bd05a4bcfa48f9e9262896985e767a91b Mon Sep 17 00:00:00 2001 From: Jon Kinney Date: Fri, 24 Apr 2026 02:09:32 -0500 Subject: [PATCH 16/33] macos: per-pane TCC navigation and Sequoia-tolerant permission flow MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit On macOS the three TCC grants (Accessibility, Input Monitoring, Post Event) live in separate Privacy panes. Before this change the "Reenable" row sent the user to Accessibility regardless of which grant was actually missing, and the daemon's own permission checks re-fired the Accessibility prompt on every retry. - lan-mouse-gtk/src/macos_privacy.rs: new module that exposes silent preflight checks (AXIsProcessTrusted, CGPreflightListenEventAccess, CGPreflightPostEventAccess), per-pane URL-scheme navigation, and a Once-guarded fire_initial_prompts() called from build_ui. The initial-prompt path only fires the Accessibility prompt if AX is missing and then returns; secondary registrations run only after AX is granted, which prevents a double Accessibility alert on Sequoia where Post Event is nested under Accessibility. - Input Monitoring registration attempts CGEventTapCreate at kCGSessionEventTap (not kCGHIDEventTap) so a failure surfaces as an Input Monitoring signal rather than triggering an Accessibility prompt as a side effect. - lan-mouse-gtk/src/window/imp.rs: handle_capture / handle_emulation switch on the missing-pane enum and navigate to the specific pane via x-apple.systempreferences:... URLs before re-requesting. - lan-mouse-gtk/resources/window.ui: pill class on the Reenable buttons so the hover padding matches the rest of libadwaita. - input-capture/src/macos.rs, input-emulation/src/macos.rs: make request_*_permission() a silent preflight (AXIsProcessTrusted / CGPreflightListenEventAccess / CGPreflightPostEventAccess), so the daemon no longer fires TCC prompts on retry — all prompting is owned by the GUI. - input-capture/src/error.rs, input-emulation/src/error.rs: new error variants so the GUI can distinguish missing-AX from missing-IM / missing-PostEvent for pane routing. Verified on macOS 15.5: first launch fires a single AX prompt; second launch (AX granted) registers under Input Monitoring via the session-tap attempt and requests Post Event. Sequoia auto-grants the listen-only path via AX so the IM list may stay empty, which is the intended OS behavior and no longer blocks capture. Co-Authored-By: Claude Opus 4.7 (1M context) --- input-capture/src/error.rs | 4 + input-capture/src/macos.rs | 40 +++++ input-emulation/Cargo.toml | 2 + input-emulation/src/error.rs | 4 + input-emulation/src/macos.rs | 38 +++++ lan-mouse-gtk/resources/window.ui | 4 +- lan-mouse-gtk/src/macos_privacy.rs | 245 +++++++++++++++++++++++++++++ lan-mouse-gtk/src/window/imp.rs | 34 ++++ 8 files changed, 369 insertions(+), 2 deletions(-) create mode 100644 lan-mouse-gtk/src/macos_privacy.rs diff --git a/input-capture/src/error.rs b/input-capture/src/error.rs index 469234a..7324156 100644 --- a/input-capture/src/error.rs +++ b/input-capture/src/error.rs @@ -149,6 +149,10 @@ pub enum MacosCaptureCreationError { #[cfg(target_os = "macos")] #[error("event tap creation failed")] EventTapCreation, + #[error("accessibility permission is required")] + AccessibilityPermission, + #[error("input monitoring permission is required")] + InputMonitoringPermission, #[error("failed to set CG Cursor property")] CGCursorProperty, #[cfg(target_os = "macos")] diff --git a/input-capture/src/macos.rs b/input-capture/src/macos.rs index 2abb15e..a99bde6 100644 --- a/input-capture/src/macos.rs +++ b/input-capture/src/macos.rs @@ -527,6 +527,8 @@ pub struct MacOSInputCapture { impl MacOSInputCapture { pub async fn new() -> Result { + request_macos_capture_permissions()?; + let state = Arc::new(Mutex::new(InputCaptureState::new()?)); let (event_tx, event_rx) = mpsc::channel(32); let (notify_tx, mut notify_rx) = mpsc::channel(32); @@ -580,6 +582,38 @@ impl MacOSInputCapture { } } +fn request_macos_capture_permissions() -> Result<(), MacosCaptureCreationError> { + // Call both request functions unconditionally so macOS surfaces both + // TCC prompts on the very first launch. TCC always returns `false` the + // first time a permission is requested (the grant only becomes visible + // on the next process launch), so returning early on the first failure + // would skip the second prompt and force the user through an extra + // relaunch just to see it. + let accessibility = request_accessibility_permission(); + let input_monitoring = request_input_monitoring_permission(); + + if !accessibility { + return Err(MacosCaptureCreationError::AccessibilityPermission); + } + if !input_monitoring { + return Err(MacosCaptureCreationError::InputMonitoringPermission); + } + Ok(()) +} + +fn request_accessibility_permission() -> bool { + // Silent check. The GUI owns the one-time user-visible prompt at + // startup (see lan_mouse_gtk::macos_privacy) so retries triggered by + // clicking the "Reenable" button don't pop a fresh Accessibility + // alert every time. + unsafe { AXIsProcessTrusted() } +} + +fn request_input_monitoring_permission() -> bool { + // Silent check, same reasoning as above. + unsafe { CGPreflightListenEventAccess() } +} + impl Drop for MacOSInputCapture { fn drop(&mut self) { self.run_loop.stop(); @@ -651,6 +685,12 @@ extern "C" { event_source: CGEventSource, seconds: CFTimeInterval, ); + fn CGPreflightListenEventAccess() -> bool; +} + +#[link(name = "ApplicationServices", kind = "framework")] +extern "C" { + fn AXIsProcessTrusted() -> bool; } unsafe fn configure_cf_settings() -> Result<(), MacosCaptureCreationError> { diff --git a/input-emulation/Cargo.toml b/input-emulation/Cargo.toml index 28c5ecd..3839122 100644 --- a/input-emulation/Cargo.toml +++ b/input-emulation/Cargo.toml @@ -49,6 +49,8 @@ reis = { version = "0.5.0", features = ["tokio"], optional = true } [target.'cfg(target_os="macos")'.dependencies] bitflags = "2.6.0" +core-foundation = "0.10.0" +core-foundation-sys = "0.8.6" core-graphics = { version = "0.25.0", features = ["highsierra"] } keycode = "1.0.0" diff --git a/input-emulation/src/error.rs b/input-emulation/src/error.rs index 078e851..9ba9f0e 100644 --- a/input-emulation/src/error.rs +++ b/input-emulation/src/error.rs @@ -154,6 +154,10 @@ pub enum X11EmulationCreationError { pub enum MacOSEmulationCreationError { #[error("could not create event source")] EventSourceCreation, + #[error("accessibility permission is required")] + AccessibilityPermission, + #[error("input control permission is required")] + InputControlPermission, } #[cfg(windows)] diff --git a/input-emulation/src/macos.rs b/input-emulation/src/macos.rs index ae5f307..881fc22 100644 --- a/input-emulation/src/macos.rs +++ b/input-emulation/src/macos.rs @@ -61,6 +61,8 @@ unsafe impl Send for MacOSEmulation {} impl MacOSEmulation { pub(crate) fn new() -> Result { + request_macos_emulation_permissions()?; + let event_source = CGEventSource::new(CGEventSourceStateID::CombinedSessionState) .map_err(|_| MacOSEmulationCreationError::EventSourceCreation)?; Ok(Self { @@ -119,6 +121,42 @@ impl MacOSEmulation { } } +fn request_macos_emulation_permissions() -> Result<(), MacOSEmulationCreationError> { + // Request both permissions up front so the user sees both TCC prompts + // on the first launch. See the matching comment in input-capture/src/ + // macos.rs::request_macos_capture_permissions for the rationale. + let accessibility = request_accessibility_permission(); + let input_control = request_input_control_permission(); + + if !accessibility { + return Err(MacOSEmulationCreationError::AccessibilityPermission); + } + if !input_control { + return Err(MacOSEmulationCreationError::InputControlPermission); + } + Ok(()) +} + +fn request_accessibility_permission() -> bool { + // Silent check. The GUI owns the one-time user-visible prompt at + // startup (see lan_mouse_gtk::macos_privacy). + unsafe { AXIsProcessTrusted() } +} + +fn request_input_control_permission() -> bool { + unsafe { CGPreflightPostEventAccess() } +} + +#[link(name = "CoreGraphics", kind = "framework")] +extern "C" { + fn CGPreflightPostEventAccess() -> bool; +} + +#[link(name = "ApplicationServices", kind = "framework")] +extern "C" { + fn AXIsProcessTrusted() -> bool; +} + fn key_event(event_source: CGEventSource, key: u16, state: u8, modifiers: XMods) { let event = match CGEvent::new_keyboard_event(event_source, key, state != 0) { Ok(e) => e, diff --git a/lan-mouse-gtk/resources/window.ui b/lan-mouse-gtk/resources/window.ui index a3d1c89..609ea4e 100644 --- a/lan-mouse-gtk/resources/window.ui +++ b/lan-mouse-gtk/resources/window.ui @@ -63,7 +63,7 @@ center @@ -89,7 +89,7 @@ center diff --git a/lan-mouse-gtk/src/macos_privacy.rs b/lan-mouse-gtk/src/macos_privacy.rs new file mode 100644 index 0000000..22583b0 --- /dev/null +++ b/lan-mouse-gtk/src/macos_privacy.rs @@ -0,0 +1,245 @@ +#![cfg(target_os = "macos")] + +//! Tiny macOS Privacy-pane helpers used by the GUI. +//! +//! Clicking "Reenable" on the capture/emulation warning row should take the +//! user to whichever Privacy pane is actually missing a grant — opening the +//! Accessibility pane when the user has already granted Accessibility (and +//! only needs Input Monitoring) is confusing and hides the real request. + +use std::ffi::{c_uchar, c_void}; +use std::process::Command; +use std::sync::Once; + +// Apple declares `AXIsProcessTrusted` as returning `Boolean` (`unsigned char`), +// NOT C's `bool`. Rust's `bool` has a strict bit pattern (0 or 1) so binding +// a `Boolean`-returning function as `-> bool` is technically UB if Apple ever +// returns a non-canonical true value. Keep these as `c_uchar` and normalize. +#[link(name = "ApplicationServices", kind = "framework")] +extern "C" { + fn AXIsProcessTrusted() -> c_uchar; + fn AXIsProcessTrustedWithOptions(options: *const c_void) -> c_uchar; +} + +#[link(name = "CoreFoundation", kind = "framework")] +extern "C" { + static kCFAllocatorDefault: *const c_void; + static kCFTypeDictionaryKeyCallBacks: *const c_void; + static kCFTypeDictionaryValueCallBacks: *const c_void; + static kCFBooleanTrue: *const c_void; + fn CFDictionaryCreate( + allocator: *const c_void, + keys: *const *const c_void, + values: *const *const c_void, + num: isize, + key_callbacks: *const c_void, + value_callbacks: *const c_void, + ) -> *const c_void; + fn CFRelease(cf: *const c_void); +} + +// kAXTrustedCheckOptionPrompt is a CFStringRef exported from ApplicationServices. +#[link(name = "ApplicationServices", kind = "framework")] +extern "C" { + static kAXTrustedCheckOptionPrompt: *const c_void; +} + +#[link(name = "CoreGraphics", kind = "framework")] +extern "C" { + fn CGPreflightListenEventAccess() -> c_uchar; + fn CGRequestListenEventAccess() -> c_uchar; + fn CGPreflightPostEventAccess() -> c_uchar; + fn CGRequestPostEventAccess() -> c_uchar; + + // CFMachPortRef CGEventTapCreate( + // CGEventTapLocation tap, CGEventTapPlacement place, + // CGEventTapOptions options, CGEventMask eventsOfInterest, + // CGEventTapCallBack callback, void *userInfo); + fn CGEventTapCreate( + tap: u32, + place: u32, + options: u32, + events_of_interest: u64, + callback: *const c_void, + user_info: *const c_void, + ) -> *const c_void; +} + +pub fn accessibility_granted() -> bool { + let raw = unsafe { AXIsProcessTrusted() }; + log::debug!("AXIsProcessTrusted() = {raw}"); + raw != 0 +} + +pub fn input_monitoring_granted() -> bool { + let raw = unsafe { CGPreflightListenEventAccess() }; + log::debug!("CGPreflightListenEventAccess() = {raw}"); + raw != 0 +} + +pub fn post_event_granted() -> bool { + let raw = unsafe { CGPreflightPostEventAccess() }; + log::debug!("CGPreflightPostEventAccess() = {raw}"); + raw != 0 +} + +pub enum CapturePane { + Accessibility, + InputMonitoring, + /// Everything is already granted; the caller should just retry. + None, +} + +pub enum EmulationPane { + Accessibility, + PostEvent, + None, +} + +pub fn missing_capture_pane() -> CapturePane { + if !accessibility_granted() { + CapturePane::Accessibility + } else if !input_monitoring_granted() { + CapturePane::InputMonitoring + } else { + CapturePane::None + } +} + +pub fn missing_emulation_pane() -> EmulationPane { + if !accessibility_granted() { + EmulationPane::Accessibility + } else if !post_event_granted() { + EmulationPane::PostEvent + } else { + EmulationPane::None + } +} + +pub fn open_accessibility_settings() { + open_url("x-apple.systempreferences:com.apple.preference.security?Privacy_Accessibility"); +} + +pub fn open_input_monitoring_settings() { + unsafe { + ensure_listed_in_input_monitoring(); + } + open_url("x-apple.systempreferences:com.apple.preference.security?Privacy_ListenEvent"); +} + +pub fn open_post_event_settings() { + unsafe { + CGRequestPostEventAccess(); + } + open_url("x-apple.systempreferences:com.apple.preference.security?Privacy_PostEvent"); +} + +/// Make sure the app appears in System Settings → Privacy → Input Monitoring. +/// +/// `CGRequestListenEventAccess()` is *supposed* to register the app in the +/// list (and prompt) on first call, but in practice — particularly after a +/// `tccutil reset ListenEvent ` — it often silently no-ops and the +/// app never gets added. The reliable way to force registration is to +/// attempt a protected action: create a `CGEventTap`. If permission is +/// missing the call returns null, but the attempt itself causes TCC to add +/// the bundle to the Input Monitoring pane so the user can toggle it on. +/// If permission already exists the tap is created successfully, and we +/// tear it down immediately so it doesn't intercept events. +unsafe fn ensure_listed_in_input_monitoring() { + let req = CGRequestListenEventAccess(); + log::debug!("CGRequestListenEventAccess() = {req}"); + let cb = input_monitoring_noop_tap_callback as *const c_void; + // Use kCGSessionEventTap (1), NOT kCGHIDEventTap (0). The HID tap sits + // below window-server input and requires Accessibility in addition to + // Input Monitoring, so attempting it when Accessibility isn't granted + // surfaces an Accessibility prompt as a side effect — which is confusing + // on top of the real Accessibility prompt we already fire explicitly. + // The session tap requires only Input Monitoring, so its failure is a + // clean "Input Monitoring missing" signal that TCC uses to list the + // bundle under the Input Monitoring pane. + // kCGHeadInsertEventTap = 0, kCGEventTapOptionListenOnly = 1, + // mask kCGEventKeyDown = 1 << 10. + let tap = CGEventTapCreate(1, 0, 1, 1 << 10, cb, std::ptr::null()); + log::debug!("CGEventTapCreate(kCGSessionEventTap) -> {tap:?}"); + if !tap.is_null() { + CFRelease(tap); + } +} + +extern "C" fn input_monitoring_noop_tap_callback( + _proxy: *const c_void, + _ty: u32, + event: *const c_void, + _refcon: *const c_void, +) -> *const c_void { + // Pass through unchanged. This tap is never added to a run loop, so + // in practice the callback never fires — it exists only so the tap + // can be created (and the attempt is what forces TCC registration). + event +} + +fn open_url(url: &str) { + if let Err(e) = Command::new("open").arg(url).spawn() { + log::warn!("failed to open {url}: {e}"); + } +} + +/// One-shot, at GUI startup: if a permission is missing, fire the system +/// prompt. This is where the familiar first-launch "Lan Mouse.app would +/// like to control this computer" alert comes from. Subsequent clicks on +/// the Reenable button use URL-scheme navigation instead, so we never +/// double up alerts on retries. +/// +/// Guarded with a `Once` because GApplication::activate can fire more +/// than once in a process (reactivation, window presentation) and we +/// must not re-pop the TCC alert on each activation — that looks like a +/// bug to the user. +pub fn fire_initial_prompts() { + static FIRED: Once = Once::new(); + FIRED.call_once(fire_initial_prompts_inner); +} + +fn fire_initial_prompts_inner() { + if !accessibility_granted() { + // When Accessibility isn't granted yet, ONLY fire the Accessibility + // prompt. Do NOT also try to register Input Monitoring or Post Event + // — those paths have been observed to surface a second Accessibility + // dialog on top of the one we fire explicitly (Post Event is part of + // the Accessibility category on modern macOS, and CGEventTap attempts + // can bail on Accessibility before they reach the Input Monitoring + // check). Once the user grants Accessibility and relaunches, this + // branch is skipped and we register the other grants cleanly below. + log::info!("firing first-launch Accessibility prompt"); + unsafe { + let key = kAXTrustedCheckOptionPrompt; + let value = kCFBooleanTrue; + let options = CFDictionaryCreate( + kCFAllocatorDefault, + &key as *const _, + &value as *const _, + 1, + kCFTypeDictionaryKeyCallBacks, + kCFTypeDictionaryValueCallBacks, + ); + AXIsProcessTrustedWithOptions(options); + CFRelease(options); + } + return; + } + // Accessibility is granted. Attempt Input Monitoring registration + // unconditionally — even if preflight returns true — so the bundle gets + // listed in System Settings under its own identity (otherwise launches + // from a parent process that already has Input Monitoring, e.g. Terminal, + // inherit the grant but the bundle is never listed for the user to + // toggle persistently). + log::info!("ensuring Lan Mouse is listed under Input Monitoring"); + unsafe { + ensure_listed_in_input_monitoring(); + } + // Same for Post Event: now that Accessibility is present, this call is + // safe — it won't surface the generic Accessibility prompt. + log::info!("ensuring Lan Mouse is listed under Accessibility > Post Event"); + unsafe { + CGRequestPostEventAccess(); + } +} diff --git a/lan-mouse-gtk/src/window/imp.rs b/lan-mouse-gtk/src/window/imp.rs index f37647e..25c753c 100644 --- a/lan-mouse-gtk/src/window/imp.rs +++ b/lan-mouse-gtk/src/window/imp.rs @@ -142,11 +142,45 @@ impl Window { #[template_callback] fn handle_emulation(&self) { + #[cfg(target_os = "macos")] + { + use crate::macos_privacy::{self, EmulationPane}; + match macos_privacy::missing_emulation_pane() { + EmulationPane::Accessibility => { + log::info!("Reenable emulation: opening Accessibility pane"); + macos_privacy::open_accessibility_settings(); + } + EmulationPane::PostEvent => { + log::info!("Reenable emulation: opening Post Event pane"); + macos_privacy::open_post_event_settings(); + } + EmulationPane::None => { + log::info!("Reenable emulation: both grants present, retry only"); + } + } + } self.obj().request_emulation(); } #[template_callback] fn handle_capture(&self) { + #[cfg(target_os = "macos")] + { + use crate::macos_privacy::{self, CapturePane}; + match macos_privacy::missing_capture_pane() { + CapturePane::Accessibility => { + log::info!("Reenable capture: opening Accessibility pane"); + macos_privacy::open_accessibility_settings(); + } + CapturePane::InputMonitoring => { + log::info!("Reenable capture: opening Input Monitoring pane"); + macos_privacy::open_input_monitoring_settings(); + } + CapturePane::None => { + log::info!("Reenable capture: both grants present, retry only"); + } + } + } self.obj().request_capture(); } From cbdb86ce0568eec65c480345c3098dd7c65cbd1d Mon Sep 17 00:00:00 2001 From: Jon Kinney Date: Fri, 24 Apr 2026 02:22:42 -0500 Subject: [PATCH 17/33] fix: write a default config.toml on every Config::new() MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The previous fix created the config directory but left config.toml absent on a first launch, which surfaced as two "No such file or directory (os error 2)" warnings (one from the main process, one from the spawned daemon child) and left the app starting up with config_toml=None until the GUI persisted something. Write ConfigToml::default() to the path if it doesn't exist, so every entry point — GUI main, spawned daemon, CLI, test commands — gets a concrete file to read, and first-launch logs stay clean. Also reorders Config::new() so both the directory creation and the file bootstrap run before the first read attempt, eliminating the warning at the source rather than hiding it. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/config.rs | 26 +++++++++++++++++--------- 1 file changed, 17 insertions(+), 9 deletions(-) diff --git a/src/config.rs b/src/config.rs index 5b02728..3faa59b 100644 --- a/src/config.rs +++ b/src/config.rs @@ -334,6 +334,23 @@ impl Config { .config .clone() .unwrap_or(default_path()?.join(CONFIG_FILE_NAME)); + let config_dir = config_path + .parent() + .expect("config directory") + .to_path_buf(); + + // Ensure the config directory exists and write a default config file + // if none is present. Runs on every Config::new(), regardless of which + // entry path (GUI main, spawned daemon, CLI, test commands) we're on, + // so a fresh Mac never hits "No such file or directory" on config.toml + // and notify::Watcher (which requires the dir to exist on macOS + // FSEvents and some Linux backends) has a concrete path to watch. + fs::create_dir_all(&config_dir)?; + if !config_path.exists() { + let default_toml = toml::to_string_pretty(&ConfigToml::default()) + .expect("default ConfigToml serialization cannot fail"); + fs::write(&config_path, default_toml)?; + } let config_toml = match ConfigToml::new(&config_path) { Err(e) => { @@ -358,15 +375,6 @@ impl Config { }, notify::Config::default(), )?; - let config_dir = config_path - .parent() - .expect("config directory") - .to_path_buf(); - // notify::Watcher requires the directory to exist on macOS (FSEvents) - // and some Linux backends. Create it eagerly so a first launch on a - // fresh machine — where ~/.config/lan-mouse/ has never been touched — - // doesn't surface as a notify::Error out of Config::new(). - fs::create_dir_all(&config_dir)?; let mut config = Config { args, cert_path, From b3cade9bac5fd16c579fd1f79221e228a9afbdc1 Mon Sep 17 00:00:00 2001 From: Jon Kinney Date: Fri, 24 Apr 2026 08:27:34 -0500 Subject: [PATCH 18/33] macos: prompt to relaunch after live Accessibility grant MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The daemon subprocess initializes at startup and bails immediately if Accessibility is missing ("accessibility permission is required"). If the user then grants AX mid-session via the system prompt, the daemon has no way to retry from its bailed state — capture and emulation stay dead until the next restart. Make the GUI watch for the AX transition and surface a toast with a "Relaunch" button that quits the app and spawns a fresh instance via Launch Services. While here: - Route capture/emulation "missing pane" fallback to the Accessibility pane instead of the Input Monitoring / Post Event panes when AX is already granted. On macOS 13+ those separate grants auto-confer via Accessibility and the bundle typically isn't listed in the IM pane at all, so the old navigation was a dead end. - Reword the status-row subtitles so the action is clearer: the user now sees "click Reenable to grant permission" instead of a generic "required for outgoing connections". - Bump libadwaita feature flag to v1_2 for AdwToast button signals. Co-Authored-By: Claude Opus 4.7 (1M context) --- lan-mouse-gtk/Cargo.toml | 2 +- lan-mouse-gtk/resources/window.ui | 4 +- lan-mouse-gtk/src/lib.rs | 71 ++++++++++++++++++++++++++++++ lan-mouse-gtk/src/macos_privacy.rs | 49 ++++++++++++++++++++- lan-mouse-gtk/src/window.rs | 4 ++ 5 files changed, 125 insertions(+), 5 deletions(-) diff --git a/lan-mouse-gtk/Cargo.toml b/lan-mouse-gtk/Cargo.toml index 4a1a202..71c4083 100644 --- a/lan-mouse-gtk/Cargo.toml +++ b/lan-mouse-gtk/Cargo.toml @@ -8,7 +8,7 @@ repository = "https://github.com/feschber/lan-mouse" [dependencies] gtk = { package = "gtk4", version = "0.9.0", features = ["v4_2"] } -adw = { package = "libadwaita", version = "0.7.0", features = ["v1_1"] } +adw = { package = "libadwaita", version = "0.7.0", features = ["v1_2"] } async-channel = { version = "2.1.1" } hostname = "0.4.0" log = "0.4.20" diff --git a/lan-mouse-gtk/resources/window.ui b/lan-mouse-gtk/resources/window.ui index 609ea4e..caa969f 100644 --- a/lan-mouse-gtk/resources/window.ui +++ b/lan-mouse-gtk/resources/window.ui @@ -50,7 +50,7 @@ input capture is disabled - required for outgoing and incoming connections + required for outgoing connections — click Reenable to grant permission dialog-warning-symbolic @@ -76,7 +76,7 @@ input emulation is disabled - required for incoming connections + required for incoming connections — click Reenable to grant permission dialog-warning-symbolic diff --git a/lan-mouse-gtk/src/lib.rs b/lan-mouse-gtk/src/lib.rs index 6204879..f4001d8 100644 --- a/lan-mouse-gtk/src/lib.rs +++ b/lan-mouse-gtk/src/lib.rs @@ -200,6 +200,20 @@ fn build_ui(app: &Application) { macos_status_item::setup(app, &window); // First-launch TCC prompts. No-op when already granted. macos_privacy::fire_initial_prompts(); + // If Accessibility wasn't granted at startup, watch for the grant + // and prompt the user to relaunch when it lands. The daemon + // subprocess initialized without AX (bailed with "accessibility + // permission is required") and can't recover without a restart, + // so a live AX toggle without a relaunch leaves the app in a + // broken state otherwise. + let app_weak = app.downgrade(); + let window_weak = window.downgrade(); + macos_privacy::watch_for_accessibility_grant(move || { + if let (Some(app), Some(window)) = (app_weak.upgrade(), window_weak.upgrade()) + { + show_macos_relaunch_dialog(&app, &window); + } + }); } glib::spawn_future_local(clone!( @@ -252,3 +266,60 @@ fn build_ui(app: &Application) { #[cfg(not(target_os = "macos"))] window.present(); } + +#[cfg(target_os = "macos")] +fn show_macos_relaunch_dialog(app: &Application, window: &Window) { + // Present the window so the toast is visible — on macOS the main + // window starts hidden (LSUIElement accessory app), so a toast + // otherwise fires into a surface the user can't see. + window.present(); + + let toast = adw::Toast::builder() + .title( + "Accessibility granted. Relaunch Lan Mouse so capture and \ + emulation can initialize.", + ) + .button_label("Relaunch") + .priority(adw::ToastPriority::High) + // 0 => never auto-dismiss. Relaunch is mandatory for things to + // work, so don't let the user miss the action. + .timeout(0) + .build(); + + let app = app.clone(); + toast.connect_button_clicked(move |_| { + relaunch_macos_bundle(); + app.quit(); + }); + + window.add_toast(toast); +} + +#[cfg(target_os = "macos")] +fn relaunch_macos_bundle() { + // Resolve the .app bundle path from the current executable: it lives + // at /Contents/MacOS/lan-mouse, so three parents up is the + // bundle root we hand to `open`. + let Ok(exe) = std::env::current_exe() else { + return; + }; + let Some(bundle) = exe + .parent() + .and_then(std::path::Path::parent) + .and_then(std::path::Path::parent) + else { + return; + }; + + // Fire `sleep 1 && open ` in a detached shell so the new + // instance starts *after* we've quit — otherwise Launch Services + // reactivates the existing process instead of launching a fresh one, + // and the stale IPC socket would block the new daemon subprocess. + // The trailing `&` backgrounds the command, and we don't wait on the + // spawn, so the shell is adopted by launchd after we exit. + let cmd = format!("(sleep 1 && open {bundle:?}) &", bundle = bundle); + let _ = std::process::Command::new("sh") + .arg("-c") + .arg(cmd) + .spawn(); +} diff --git a/lan-mouse-gtk/src/macos_privacy.rs b/lan-mouse-gtk/src/macos_privacy.rs index 22583b0..5f13e1c 100644 --- a/lan-mouse-gtk/src/macos_privacy.rs +++ b/lan-mouse-gtk/src/macos_privacy.rs @@ -11,6 +11,8 @@ use std::ffi::{c_uchar, c_void}; use std::process::Command; use std::sync::Once; +use gtk::glib; + // Apple declares `AXIsProcessTrusted` as returning `Boolean` (`unsigned char`), // NOT C's `bool`. Rust's `bool` has a strict bit pattern (0 or 1) so binding // a `Boolean`-returning function as `-> bool` is technically UB if Apple ever @@ -83,6 +85,13 @@ pub fn post_event_granted() -> bool { raw != 0 } +// Variants `InputMonitoring` and `PostEvent` are currently never returned +// by `missing_capture_pane` / `missing_emulation_pane` — on macOS 13+ those +// categories auto-grant via Accessibility and the bundle typically isn't +// listed in those separate panes, so routing users there is a dead end. +// Kept in the enum so older-macOS behavior can be restored without a +// structural change. +#[allow(dead_code)] pub enum CapturePane { Accessibility, InputMonitoring, @@ -90,6 +99,7 @@ pub enum CapturePane { None, } +#[allow(dead_code)] pub enum EmulationPane { Accessibility, PostEvent, @@ -100,7 +110,13 @@ pub fn missing_capture_pane() -> CapturePane { if !accessibility_granted() { CapturePane::Accessibility } else if !input_monitoring_granted() { - CapturePane::InputMonitoring + // On macOS 13+, Accessibility trust confers the listen-only + // event-tap privilege that Input Monitoring gates, and on Sequoia + // the bundle typically isn't listed in the Input Monitoring pane + // at all. The actionable fix when IM preflight is still 0 is to + // re-toggle Accessibility, so send the user there rather than to + // an empty IM list. + CapturePane::Accessibility } else { CapturePane::None } @@ -110,12 +126,41 @@ pub fn missing_emulation_pane() -> EmulationPane { if !accessibility_granted() { EmulationPane::Accessibility } else if !post_event_granted() { - EmulationPane::PostEvent + // Post Event is nested under Accessibility on modern macOS and + // auto-grants alongside it. Point the user back to Accessibility + // for the same reason as the capture case above. + EmulationPane::Accessibility } else { EmulationPane::None } } +/// Poll for an Accessibility grant transition. Starts a 1-second GLib +/// timer that fires `on_granted` once, the first time +/// `AXIsProcessTrusted()` returns true. A no-op if AX is already granted. +/// +/// We rely on polling rather than AXObserver because the AX notification +/// API requires a trusted process to subscribe — the precondition we're +/// waiting for. This runs on the GTK main thread (via timeout_add_seconds_local). +pub fn watch_for_accessibility_grant(mut on_granted: F) +where + F: FnMut() + 'static, +{ + if accessibility_granted() { + return; + } + log::info!("watching for Accessibility grant"); + glib::timeout_add_seconds_local(1, move || { + if accessibility_granted() { + log::info!("Accessibility granted; firing relaunch prompt"); + on_granted(); + glib::ControlFlow::Break + } else { + glib::ControlFlow::Continue + } + }); +} + pub fn open_accessibility_settings() { open_url("x-apple.systempreferences:com.apple.preference.security?Privacy_Accessibility"); } diff --git a/lan-mouse-gtk/src/window.rs b/lan-mouse-gtk/src/window.rs index 47db926..3b87730 100644 --- a/lan-mouse-gtk/src/window.rs +++ b/lan-mouse-gtk/src/window.rs @@ -432,6 +432,10 @@ impl Window { pub(super) fn show_toast(&self, msg: &str) { let toast = adw::Toast::new(msg); + self.add_toast(toast); + } + + pub(super) fn add_toast(&self, toast: adw::Toast) { let toast_overlay = &self.imp().toast_overlay; toast_overlay.add_toast(toast); } From 8a444f98ddf9b8e4af309c63c1d97c03e56d4832 Mon Sep 17 00:00:00 2001 From: Jon Kinney Date: Fri, 24 Apr 2026 08:35:10 -0500 Subject: [PATCH 19/33] macos: drop the CapturePane / EmulationPane enums Now that we always route Reenable clicks to the Accessibility pane on macOS 13+ (AX transitively covers Input Monitoring listen-only and Post Event, and the bundle isn't listed in the separate panes anyway), the CapturePane / EmulationPane enums and their non-Accessibility variants are dead weight. Remove them along with: - `missing_capture_pane` / `missing_emulation_pane` - `open_input_monitoring_settings` / `open_post_event_settings` - `input_monitoring_granted` / `post_event_granted` preflight wrappers - the `CGPreflightListenEventAccess` / `CGPreflightPostEventAccess` FFI declarations in lan-mouse-gtk (the daemon crates keep their own) `handle_capture` / `handle_emulation` collapse to a single helper that opens the Accessibility pane if AX is missing, otherwise just retries. `ensure_listed_in_input_monitoring` is kept because it still has a side effect on macOS 13/14, where Input Monitoring is a separately- granted category. Co-Authored-By: Claude Opus 4.7 (1M context) --- lan-mouse-gtk/src/macos_privacy.rs | 87 +++--------------------------- lan-mouse-gtk/src/window/imp.rs | 44 +++++---------- 2 files changed, 18 insertions(+), 113 deletions(-) diff --git a/lan-mouse-gtk/src/macos_privacy.rs b/lan-mouse-gtk/src/macos_privacy.rs index 5f13e1c..41aad57 100644 --- a/lan-mouse-gtk/src/macos_privacy.rs +++ b/lan-mouse-gtk/src/macos_privacy.rs @@ -2,10 +2,12 @@ //! Tiny macOS Privacy-pane helpers used by the GUI. //! -//! Clicking "Reenable" on the capture/emulation warning row should take the -//! user to whichever Privacy pane is actually missing a grant — opening the -//! Accessibility pane when the user has already granted Accessibility (and -//! only needs Input Monitoring) is confusing and hides the real request. +//! On macOS 13+, the Accessibility grant transitively confers the +//! listen-only event-tap privilege that Input Monitoring gates and the +//! synthesize-event privilege that Post Event gates, and the bundle +//! typically isn't even listed in those separate panes. So the single +//! user-facing action for any missing-capture or missing-emulation +//! scenario is "re-toggle Accessibility" — we don't route elsewhere. use std::ffi::{c_uchar, c_void}; use std::process::Command; @@ -48,9 +50,7 @@ extern "C" { #[link(name = "CoreGraphics", kind = "framework")] extern "C" { - fn CGPreflightListenEventAccess() -> c_uchar; fn CGRequestListenEventAccess() -> c_uchar; - fn CGPreflightPostEventAccess() -> c_uchar; fn CGRequestPostEventAccess() -> c_uchar; // CFMachPortRef CGEventTapCreate( @@ -73,67 +73,6 @@ pub fn accessibility_granted() -> bool { raw != 0 } -pub fn input_monitoring_granted() -> bool { - let raw = unsafe { CGPreflightListenEventAccess() }; - log::debug!("CGPreflightListenEventAccess() = {raw}"); - raw != 0 -} - -pub fn post_event_granted() -> bool { - let raw = unsafe { CGPreflightPostEventAccess() }; - log::debug!("CGPreflightPostEventAccess() = {raw}"); - raw != 0 -} - -// Variants `InputMonitoring` and `PostEvent` are currently never returned -// by `missing_capture_pane` / `missing_emulation_pane` — on macOS 13+ those -// categories auto-grant via Accessibility and the bundle typically isn't -// listed in those separate panes, so routing users there is a dead end. -// Kept in the enum so older-macOS behavior can be restored without a -// structural change. -#[allow(dead_code)] -pub enum CapturePane { - Accessibility, - InputMonitoring, - /// Everything is already granted; the caller should just retry. - None, -} - -#[allow(dead_code)] -pub enum EmulationPane { - Accessibility, - PostEvent, - None, -} - -pub fn missing_capture_pane() -> CapturePane { - if !accessibility_granted() { - CapturePane::Accessibility - } else if !input_monitoring_granted() { - // On macOS 13+, Accessibility trust confers the listen-only - // event-tap privilege that Input Monitoring gates, and on Sequoia - // the bundle typically isn't listed in the Input Monitoring pane - // at all. The actionable fix when IM preflight is still 0 is to - // re-toggle Accessibility, so send the user there rather than to - // an empty IM list. - CapturePane::Accessibility - } else { - CapturePane::None - } -} - -pub fn missing_emulation_pane() -> EmulationPane { - if !accessibility_granted() { - EmulationPane::Accessibility - } else if !post_event_granted() { - // Post Event is nested under Accessibility on modern macOS and - // auto-grants alongside it. Point the user back to Accessibility - // for the same reason as the capture case above. - EmulationPane::Accessibility - } else { - EmulationPane::None - } -} /// Poll for an Accessibility grant transition. Starts a 1-second GLib /// timer that fires `on_granted` once, the first time @@ -165,20 +104,6 @@ pub fn open_accessibility_settings() { open_url("x-apple.systempreferences:com.apple.preference.security?Privacy_Accessibility"); } -pub fn open_input_monitoring_settings() { - unsafe { - ensure_listed_in_input_monitoring(); - } - open_url("x-apple.systempreferences:com.apple.preference.security?Privacy_ListenEvent"); -} - -pub fn open_post_event_settings() { - unsafe { - CGRequestPostEventAccess(); - } - open_url("x-apple.systempreferences:com.apple.preference.security?Privacy_PostEvent"); -} - /// Make sure the app appears in System Settings → Privacy → Input Monitoring. /// /// `CGRequestListenEventAccess()` is *supposed* to register the app in the diff --git a/lan-mouse-gtk/src/window/imp.rs b/lan-mouse-gtk/src/window/imp.rs index 25c753c..8d11928 100644 --- a/lan-mouse-gtk/src/window/imp.rs +++ b/lan-mouse-gtk/src/window/imp.rs @@ -10,6 +10,16 @@ use lan_mouse_ipc::{DEFAULT_PORT, FrontendRequestWriter}; use crate::authorization_window::AuthorizationWindow; +#[cfg(target_os = "macos")] +fn open_accessibility_if_missing(ctx: &str) { + if crate::macos_privacy::accessibility_granted() { + log::info!("{ctx}: Accessibility already granted, retry only"); + } else { + log::info!("{ctx}: opening Accessibility pane"); + crate::macos_privacy::open_accessibility_settings(); + } +} + #[derive(CompositeTemplate, Default)] #[template(resource = "/de/feschber/LanMouse/window.ui")] pub struct Window { @@ -143,44 +153,14 @@ impl Window { #[template_callback] fn handle_emulation(&self) { #[cfg(target_os = "macos")] - { - use crate::macos_privacy::{self, EmulationPane}; - match macos_privacy::missing_emulation_pane() { - EmulationPane::Accessibility => { - log::info!("Reenable emulation: opening Accessibility pane"); - macos_privacy::open_accessibility_settings(); - } - EmulationPane::PostEvent => { - log::info!("Reenable emulation: opening Post Event pane"); - macos_privacy::open_post_event_settings(); - } - EmulationPane::None => { - log::info!("Reenable emulation: both grants present, retry only"); - } - } - } + open_accessibility_if_missing("Reenable emulation"); self.obj().request_emulation(); } #[template_callback] fn handle_capture(&self) { #[cfg(target_os = "macos")] - { - use crate::macos_privacy::{self, CapturePane}; - match macos_privacy::missing_capture_pane() { - CapturePane::Accessibility => { - log::info!("Reenable capture: opening Accessibility pane"); - macos_privacy::open_accessibility_settings(); - } - CapturePane::InputMonitoring => { - log::info!("Reenable capture: opening Input Monitoring pane"); - macos_privacy::open_input_monitoring_settings(); - } - CapturePane::None => { - log::info!("Reenable capture: both grants present, retry only"); - } - } - } + open_accessibility_if_missing("Reenable capture"); self.obj().request_capture(); } From 2dc9ebb6cdeaac05be200f96bcaf9d2e38b40f5d Mon Sep 17 00:00:00 2001 From: Jon Kinney Date: Fri, 24 Apr 2026 08:54:23 -0500 Subject: [PATCH 20/33] macos: hide Reenable warning row once Accessibility is granted The yellow "input capture is disabled" / "input emulation is disabled" rows were showing simultaneously with the Relaunch toast after a live AX grant, double-prompting the user for the same action. Gate the warning row visibility on Accessibility actually being missing: when AX is granted but capture/emulation remain inactive, we're in the pending-relaunch state and the Relaunch toast is the authoritative prompt. Trigger a status refresh from the AX watcher so the rows hide the instant AX flips to granted, not when the daemon next reports status. On non-macOS platforms the visibility logic is unchanged. Co-Authored-By: Claude Opus 4.7 (1M context) --- lan-mouse-gtk/src/lib.rs | 6 ++++++ lan-mouse-gtk/src/window.rs | 25 ++++++++++++++++++++++--- 2 files changed, 28 insertions(+), 3 deletions(-) diff --git a/lan-mouse-gtk/src/lib.rs b/lan-mouse-gtk/src/lib.rs index f4001d8..d4d57c6 100644 --- a/lan-mouse-gtk/src/lib.rs +++ b/lan-mouse-gtk/src/lib.rs @@ -274,6 +274,12 @@ fn show_macos_relaunch_dialog(app: &Application, window: &Window) { // otherwise fires into a surface the user can't see. window.present(); + // Refresh the capture/emulation status rows so the yellow + // "Reenable" warning disappears. It was showing because the daemon + // reports capture/emulation inactive; now that AX is granted the + // Relaunch toast is the right prompt and the warning is redundant. + window.refresh_capture_emulation_status(); + let toast = adw::Toast::builder() .title( "Accessibility granted. Relaunch Lan Mouse so capture and \ diff --git a/lan-mouse-gtk/src/window.rs b/lan-mouse-gtk/src/window.rs index 3b87730..0396233 100644 --- a/lan-mouse-gtk/src/window.rs +++ b/lan-mouse-gtk/src/window.rs @@ -450,14 +450,33 @@ impl Window { self.update_capture_emulation_status(); } + #[cfg(target_os = "macos")] + pub(super) fn refresh_capture_emulation_status(&self) { + self.update_capture_emulation_status(); + } + fn update_capture_emulation_status(&self) { let capture = self.imp().capture_active.get(); let emulation = self.imp().emulation_active.get(); - self.imp().capture_status_row.set_visible(!capture); - self.imp().emulation_status_row.set_visible(!emulation); + + // On macOS the yellow "Reenable" row only makes sense when + // Accessibility is actually missing. When AX is granted but + // capture/emulation are still off, the daemon simply hasn't been + // restarted yet — the Relaunch toast covers that state, and a + // yellow "grant permission" warning on top of it would be + // redundant and confusing. + #[cfg(target_os = "macos")] + let show_warning = !crate::macos_privacy::accessibility_granted(); + #[cfg(not(target_os = "macos"))] + let show_warning = true; + + let show_capture_row = !capture && show_warning; + let show_emulation_row = !emulation && show_warning; + self.imp().capture_status_row.set_visible(show_capture_row); + self.imp().emulation_status_row.set_visible(show_emulation_row); self.imp() .capture_emulation_group - .set_visible(!capture || !emulation); + .set_visible(show_capture_row || show_emulation_row); } pub(super) fn set_authorized_keys(&self, fingerprints: HashMap) { From 5d7d14fbf779faf37141a475a6b9d0735740f37e Mon Sep 17 00:00:00 2001 From: Jon Kinney Date: Fri, 24 Apr 2026 09:15:40 -0500 Subject: [PATCH 21/33] macos: fold relaunch prompt into the warning row instead of a toast MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The cut-off toast UX ("Accessibility granted. Relaunch Lan Mouse so capture and emulat…") was unreadable in a compact window and split the "grant" and "relaunch" flows into two disconnected surfaces. Fold everything into the existing warning row with state-dependent content: - AX missing: title = "input capture is disabled" subtitle = "grant Accessibility permission to enable" button = "Grant" → opens System Settings → Accessibility - AX granted, daemon still bailed: title = "relaunch required" subtitle = "Accessibility granted — restart to activate capture and emulation" button = "Relaunch" → spawns a fresh bundle via `open` after a 1s delay, then quits. - Both active: row hidden. The emulation_status_row is kept hidden on macOS because capture and emulation share the same TCC gate — a single row is sufficient and two identical-looking warnings were noisy. `handle_emulation` still exists for the non-macOS platforms where the rows are distinct. Side effects: - `relaunch_bundle` moved from lib.rs to macos_privacy so imp.rs can call it from the row button handler. - AX watcher callback shrinks to `window.present()` + `refresh_capture_emulation_status()`; the toast-based dialog is gone along with its helper. Co-Authored-By: Claude Opus 4.7 (1M context) --- lan-mouse-gtk/resources/window.ui | 4 +- lan-mouse-gtk/src/lib.rs | 79 +++--------------------------- lan-mouse-gtk/src/macos_privacy.rs | 27 ++++++++++ lan-mouse-gtk/src/window.rs | 71 +++++++++++++++++++++------ lan-mouse-gtk/src/window/imp.rs | 33 ++++++++----- 5 files changed, 112 insertions(+), 102 deletions(-) diff --git a/lan-mouse-gtk/resources/window.ui b/lan-mouse-gtk/resources/window.ui index caa969f..609ea4e 100644 --- a/lan-mouse-gtk/resources/window.ui +++ b/lan-mouse-gtk/resources/window.ui @@ -50,7 +50,7 @@ input capture is disabled - required for outgoing connections — click Reenable to grant permission + required for outgoing and incoming connections dialog-warning-symbolic @@ -76,7 +76,7 @@ input emulation is disabled - required for incoming connections — click Reenable to grant permission + required for incoming connections dialog-warning-symbolic diff --git a/lan-mouse-gtk/src/lib.rs b/lan-mouse-gtk/src/lib.rs index d4d57c6..3c976ef 100644 --- a/lan-mouse-gtk/src/lib.rs +++ b/lan-mouse-gtk/src/lib.rs @@ -201,17 +201,16 @@ fn build_ui(app: &Application) { // First-launch TCC prompts. No-op when already granted. macos_privacy::fire_initial_prompts(); // If Accessibility wasn't granted at startup, watch for the grant - // and prompt the user to relaunch when it lands. The daemon - // subprocess initialized without AX (bailed with "accessibility - // permission is required") and can't recover without a restart, - // so a live AX toggle without a relaunch leaves the app in a - // broken state otherwise. - let app_weak = app.downgrade(); + // and switch the status row into its "relaunch required" state + // when it lands. The daemon subprocess initialized without AX + // (bailed with "accessibility permission is required") and can't + // recover without a restart, so a live AX toggle without a + // relaunch leaves the app in a broken state otherwise. let window_weak = window.downgrade(); macos_privacy::watch_for_accessibility_grant(move || { - if let (Some(app), Some(window)) = (app_weak.upgrade(), window_weak.upgrade()) - { - show_macos_relaunch_dialog(&app, &window); + if let Some(window) = window_weak.upgrade() { + window.present(); + window.refresh_capture_emulation_status(); } }); } @@ -267,65 +266,3 @@ fn build_ui(app: &Application) { window.present(); } -#[cfg(target_os = "macos")] -fn show_macos_relaunch_dialog(app: &Application, window: &Window) { - // Present the window so the toast is visible — on macOS the main - // window starts hidden (LSUIElement accessory app), so a toast - // otherwise fires into a surface the user can't see. - window.present(); - - // Refresh the capture/emulation status rows so the yellow - // "Reenable" warning disappears. It was showing because the daemon - // reports capture/emulation inactive; now that AX is granted the - // Relaunch toast is the right prompt and the warning is redundant. - window.refresh_capture_emulation_status(); - - let toast = adw::Toast::builder() - .title( - "Accessibility granted. Relaunch Lan Mouse so capture and \ - emulation can initialize.", - ) - .button_label("Relaunch") - .priority(adw::ToastPriority::High) - // 0 => never auto-dismiss. Relaunch is mandatory for things to - // work, so don't let the user miss the action. - .timeout(0) - .build(); - - let app = app.clone(); - toast.connect_button_clicked(move |_| { - relaunch_macos_bundle(); - app.quit(); - }); - - window.add_toast(toast); -} - -#[cfg(target_os = "macos")] -fn relaunch_macos_bundle() { - // Resolve the .app bundle path from the current executable: it lives - // at /Contents/MacOS/lan-mouse, so three parents up is the - // bundle root we hand to `open`. - let Ok(exe) = std::env::current_exe() else { - return; - }; - let Some(bundle) = exe - .parent() - .and_then(std::path::Path::parent) - .and_then(std::path::Path::parent) - else { - return; - }; - - // Fire `sleep 1 && open ` in a detached shell so the new - // instance starts *after* we've quit — otherwise Launch Services - // reactivates the existing process instead of launching a fresh one, - // and the stale IPC socket would block the new daemon subprocess. - // The trailing `&` backgrounds the command, and we don't wait on the - // spawn, so the shell is adopted by launchd after we exit. - let cmd = format!("(sleep 1 && open {bundle:?}) &", bundle = bundle); - let _ = std::process::Command::new("sh") - .arg("-c") - .arg(cmd) - .spawn(); -} diff --git a/lan-mouse-gtk/src/macos_privacy.rs b/lan-mouse-gtk/src/macos_privacy.rs index 41aad57..31fedfa 100644 --- a/lan-mouse-gtk/src/macos_privacy.rs +++ b/lan-mouse-gtk/src/macos_privacy.rs @@ -104,6 +104,33 @@ pub fn open_accessibility_settings() { open_url("x-apple.systempreferences:com.apple.preference.security?Privacy_Accessibility"); } +/// Spawn a fresh instance of the current `.app` bundle via Launch Services +/// after a 1-second delay, so the new instance starts *after* the current +/// process has exited — otherwise Launch Services reactivates the existing +/// process instead of launching a fresh one, and the stale IPC socket +/// would block the new daemon subprocess. The caller is responsible for +/// quitting the current process (e.g. `Application::quit()`) after this. +pub fn relaunch_bundle() { + // Resolve the .app bundle path from the current executable: it lives + // at /Contents/MacOS/lan-mouse, so three parents up is the + // bundle root we hand to `open`. + let Ok(exe) = std::env::current_exe() else { + return; + }; + let Some(bundle) = exe + .parent() + .and_then(std::path::Path::parent) + .and_then(std::path::Path::parent) + else { + return; + }; + + // Trailing `&` backgrounds the sleep+open so our shell call returns + // immediately; the spawned shell is adopted by launchd once we exit. + let cmd = format!("(sleep 1 && open {bundle:?}) &"); + let _ = Command::new("sh").arg("-c").arg(cmd).spawn(); +} + /// Make sure the app appears in System Settings → Privacy → Input Monitoring. /// /// `CGRequestListenEventAccess()` is *supposed* to register the app in the diff --git a/lan-mouse-gtk/src/window.rs b/lan-mouse-gtk/src/window.rs index 0396233..f650157 100644 --- a/lan-mouse-gtk/src/window.rs +++ b/lan-mouse-gtk/src/window.rs @@ -22,6 +22,17 @@ use crate::{ use super::{client_object::ClientObject, client_row::ClientRow}; +#[cfg(target_os = "macos")] +fn set_button_content_label(button: >k::Button, label: &str) { + // The Reenable/Grant/Relaunch button wraps its icon+label in an + // AdwButtonContent (see window.ui). Walk into it and swap the label + // rather than GtkButton::set_label, which would replace the content + // widget and drop the icon. + if let Some(content) = button.child().and_downcast::() { + content.set_label(label); + } +} + glib::wrapper! { pub struct Window(ObjectSubclass) @extends adw::ApplicationWindow, gtk::Window, gtk::Widget, @@ -459,24 +470,52 @@ impl Window { let capture = self.imp().capture_active.get(); let emulation = self.imp().emulation_active.get(); - // On macOS the yellow "Reenable" row only makes sense when - // Accessibility is actually missing. When AX is granted but - // capture/emulation are still off, the daemon simply hasn't been - // restarted yet — the Relaunch toast covers that state, and a - // yellow "grant permission" warning on top of it would be - // redundant and confusing. #[cfg(target_os = "macos")] - let show_warning = !crate::macos_privacy::accessibility_granted(); - #[cfg(not(target_os = "macos"))] - let show_warning = true; + { + // On macOS, capture and emulation share the same TCC gate + // (Accessibility). Collapse to a single warning row — + // emulation_status_row stays hidden and capture_status_row + // doubles as the shared status indicator. Its text and + // button mutate based on whether we're waiting for AX or + // waiting for the user to relaunch the app. + let anything_off = !capture || !emulation; + self.imp().emulation_status_row.set_visible(false); + self.imp().capture_status_row.set_visible(anything_off); + self.imp().capture_emulation_group.set_visible(anything_off); - let show_capture_row = !capture && show_warning; - let show_emulation_row = !emulation && show_warning; - self.imp().capture_status_row.set_visible(show_capture_row); - self.imp().emulation_status_row.set_visible(show_emulation_row); - self.imp() - .capture_emulation_group - .set_visible(show_capture_row || show_emulation_row); + if anything_off { + self.update_macos_warning_row_text(); + } + } + + #[cfg(not(target_os = "macos"))] + { + self.imp().capture_status_row.set_visible(!capture); + self.imp().emulation_status_row.set_visible(!emulation); + self.imp() + .capture_emulation_group + .set_visible(!capture || !emulation); + } + } + + #[cfg(target_os = "macos")] + fn update_macos_warning_row_text(&self) { + let row = &self.imp().capture_status_row; + let button = &self.imp().input_capture_button; + + if crate::macos_privacy::accessibility_granted() { + // AX granted but capture/emulation still off → the daemon + // subprocess bailed at startup and needs a fresh process to + // re-initialize with the new grant in place. + row.set_title("relaunch required"); + row.set_subtitle("Accessibility granted — restart to activate capture and emulation"); + set_button_content_label(button, "Relaunch"); + } else { + // AX missing → send the user to System Settings. + row.set_title("input capture is disabled"); + row.set_subtitle("grant Accessibility permission to enable"); + set_button_content_label(button, "Grant"); + } } pub(super) fn set_authorized_keys(&self, fingerprints: HashMap) { diff --git a/lan-mouse-gtk/src/window/imp.rs b/lan-mouse-gtk/src/window/imp.rs index 8d11928..bb7d48c 100644 --- a/lan-mouse-gtk/src/window/imp.rs +++ b/lan-mouse-gtk/src/window/imp.rs @@ -10,16 +10,6 @@ use lan_mouse_ipc::{DEFAULT_PORT, FrontendRequestWriter}; use crate::authorization_window::AuthorizationWindow; -#[cfg(target_os = "macos")] -fn open_accessibility_if_missing(ctx: &str) { - if crate::macos_privacy::accessibility_granted() { - log::info!("{ctx}: Accessibility already granted, retry only"); - } else { - log::info!("{ctx}: opening Accessibility pane"); - crate::macos_privacy::open_accessibility_settings(); - } -} - #[derive(CompositeTemplate, Default)] #[template(resource = "/de/feschber/LanMouse/window.ui")] pub struct Window { @@ -152,15 +142,32 @@ impl Window { #[template_callback] fn handle_emulation(&self) { - #[cfg(target_os = "macos")] - open_accessibility_if_missing("Reenable emulation"); + // On macOS the emulation_status_row is hidden — capture_status_row + // acts as the shared warning (see update_capture_emulation_status). + // This handler still fires for the non-macOS platforms where the + // emulation row is distinct. self.obj().request_emulation(); } #[template_callback] fn handle_capture(&self) { #[cfg(target_os = "macos")] - open_accessibility_if_missing("Reenable capture"); + { + use crate::macos_privacy; + if macos_privacy::accessibility_granted() { + // AX granted but the row is still visible => the daemon + // subprocess bailed before AX was in place and needs a + // fresh process. Quit + relaunch via Launch Services. + log::info!("capture row clicked in relaunch-required state"); + macos_privacy::relaunch_bundle(); + if let Some(app) = self.obj().application() { + app.quit(); + } + return; + } + log::info!("capture row clicked in AX-missing state, opening pane"); + macos_privacy::open_accessibility_settings(); + } self.obj().request_capture(); } From 07cc40f6ba4610d1125350e46e0b481cfb1b292d Mon Sep 17 00:00:00 2001 From: Jon Kinney Date: Fri, 24 Apr 2026 09:36:58 -0500 Subject: [PATCH 22/33] fix(input-capture): don't drop events after TCC Accessibility revocation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When the user revokes Accessibility in System Settings while Lan Mouse is in a captured session (cursor on a remote client), the session-level event tap receives `TapDisabledByUserInput`. The previous flow was: 1. Callback sends `ProducerEvent::EventTapDisabled` to notify_tx. 2. Callback falls through the `current_pos.is_some()` branch and returns `CallbackResult::Drop` — *this very event*, plus any racing callback still in flight, get `set_type(Null)`'d and consumed. 3. Outer task calls `handle_producer_event(..).unwrap_or_else(|e| log::error!(..))` — the `EventTapDisabled` error is just logged, the loop keeps running, `current_pos` stays `Some`, cursor stays hidden. Net effect for the user: mouse motion keeps working (pass-through when the tap is fully dead), but clicks and keypresses in the brief disable window silently disappear, and the cursor is still hidden where the captured session left it. Input looks broken until the app is force-quit. Fix: - In the callback, when `TapDisabled*` fires, clear `current_pos` and `CGDisplay::show_cursor` synchronously, then return `CallbackResult::Keep` so this event (and any subsequent racing one) can't hit the drop branch. - Mirror the cleanup in `handle_producer_event`'s `EventTapDisabled` arm so even if the outer task only logs the error, state is still released. Co-Authored-By: Claude Opus 4.7 (1M context) --- input-capture/src/macos.rs | 29 +++++++++++++++++++++++++++-- 1 file changed, 27 insertions(+), 2 deletions(-) diff --git a/input-capture/src/macos.rs b/input-capture/src/macos.rs index a99bde6..517127d 100644 --- a/input-capture/src/macos.rs +++ b/input-capture/src/macos.rs @@ -176,7 +176,17 @@ impl InputCaptureState { } self.active_clients.remove(&p); } - ProducerEvent::EventTapDisabled => return Err(CaptureError::EventTapDisabled), + ProducerEvent::EventTapDisabled => { + // Tap death can happen mid-capture (TCC Accessibility + // revoked, tap-timeout, etc). Release state so we + // don't leave the cursor hidden even if the outer + // task only logs this error rather than propagating. + if self.current_pos.is_some() { + self.show_cursor()?; + self.current_pos = None; + } + return Err(CaptureError::EventTapDisabled); + } }; Ok(()) } @@ -413,12 +423,27 @@ fn create_event_tap<'a>( event_type, CGEventType::TapDisabledByTimeout | CGEventType::TapDisabledByUserInput ) { - log::error!("CGEventTap disabled"); + // When the tap is disabled (including the case where TCC + // Accessibility is revoked mid-session), we MUST drop + // captured state synchronously and return Keep on this + // event. Otherwise the `current_pos.is_some()` branch + // below would drop this event (and any racing callback + // still in flight) back into `CallbackResult::Drop`, + // silently eating the user's clicks and keypresses while + // the tap winds down. Clear state + show the cursor + // here, then notify the producer loop so the service + // can tear down cleanly. + log::error!("CGEventTap disabled, releasing capture state"); + if state.current_pos.is_some() { + let _ = CGDisplay::show_cursor(&CGDisplay::main()); + state.current_pos = None; + } notify_tx .blocking_send(ProducerEvent::EventTapDisabled) .unwrap_or_else(|e| { log::error!("Failed to send notification: {e}"); }); + return CallbackResult::Keep; } // Are we in a client? From 94e9301e9c71d382c89e9dd80e6eeeb39f666a72 Mon Sep 17 00:00:00 2001 From: Jon Kinney Date: Fri, 24 Apr 2026 09:58:23 -0500 Subject: [PATCH 23/33] macos: quit immediately when Accessibility is revoked mid-session MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Even with the earlier event-tap-callback cleanup fix, revoking Accessibility in System Settings while Lan Mouse was running with an active capture tap could still leave system input wedged — clicks and keypresses silently dropped until the app was force-quit. The reliable fix is to not rely on in-process tap teardown at all. When AX is revoked: - The kernel guarantees an active CGEventTap is dismantled when the owning process exits. - SIGINT+wait on the daemon child (main.rs already does this on GUI exit) drops the daemon's tap and restores the cursor. So: continuously poll AX state (1-second GLib timer, replacing the one-shot grant watcher), and on a revoke transition call `app.quit()`. Input is restored within ~1-2 seconds regardless of capture state — no force-quit required, no stuck cursor, no silently consumed events beyond the brief window until the poller fires. The grant-transition case is preserved: on a 0→1 flip the warning row swaps to its "relaunch required" state, same as before. Co-Authored-By: Claude Opus 4.7 (1M context) --- lan-mouse-gtk/src/lib.rs | 34 ++++++++++++++------ lan-mouse-gtk/src/macos_privacy.rs | 51 ++++++++++++++++++++---------- 2 files changed, 58 insertions(+), 27 deletions(-) diff --git a/lan-mouse-gtk/src/lib.rs b/lan-mouse-gtk/src/lib.rs index 3c976ef..112aae6 100644 --- a/lan-mouse-gtk/src/lib.rs +++ b/lan-mouse-gtk/src/lib.rs @@ -200,17 +200,31 @@ fn build_ui(app: &Application) { macos_status_item::setup(app, &window); // First-launch TCC prompts. No-op when already granted. macos_privacy::fire_initial_prompts(); - // If Accessibility wasn't granted at startup, watch for the grant - // and switch the status row into its "relaunch required" state - // when it lands. The daemon subprocess initialized without AX - // (bailed with "accessibility permission is required") and can't - // recover without a restart, so a live AX toggle without a - // relaunch leaves the app in a broken state otherwise. + // Watch the Accessibility grant continuously for the lifetime + // of the process. On a grant, swap the warning row into its + // "relaunch required" state (the daemon subprocess already + // bailed and can't recover without a restart). On a REVOKE, + // quit immediately — an active CGEventTap at + // HeadInsertEventTap can wedge system input if the process + // lingers after losing AX, and forcing the process to exit is + // the only bulletproof way to guarantee the kernel tears the + // tap down. let window_weak = window.downgrade(); - macos_privacy::watch_for_accessibility_grant(move || { - if let Some(window) = window_weak.upgrade() { - window.present(); - window.refresh_capture_emulation_status(); + let app_weak = app.downgrade(); + macos_privacy::watch_accessibility_state(move |change| match change { + macos_privacy::AccessibilityChange::Granted => { + if let Some(window) = window_weak.upgrade() { + window.present(); + window.refresh_capture_emulation_status(); + } + } + macos_privacy::AccessibilityChange::Revoked => { + log::warn!( + "Accessibility revoked — quitting to avoid wedging system input" + ); + if let Some(app) = app_weak.upgrade() { + app.quit(); + } } }); } diff --git a/lan-mouse-gtk/src/macos_privacy.rs b/lan-mouse-gtk/src/macos_privacy.rs index 31fedfa..9183a96 100644 --- a/lan-mouse-gtk/src/macos_privacy.rs +++ b/lan-mouse-gtk/src/macos_privacy.rs @@ -74,29 +74,46 @@ pub fn accessibility_granted() -> bool { } -/// Poll for an Accessibility grant transition. Starts a 1-second GLib -/// timer that fires `on_granted` once, the first time -/// `AXIsProcessTrusted()` returns true. A no-op if AX is already granted. +pub enum AccessibilityChange { + /// AX was missing at startup and the user has now granted it. + /// Capture/emulation still need a relaunch to take effect, since + /// the daemon subprocess already bailed. + Granted, + /// AX was granted and the user has now revoked it. Quit immediately + /// — leaving the process alive with an active CGEventTap at + /// HeadInsertEventTap can wedge system input (clicks/keys silently + /// consumed) until the process dies. See + /// macos-cgeventtap-drop-fallthrough-tcc-revoke skill for the + /// underlying event-tap-disable footgun. + Revoked, +} + +/// Poll for Accessibility grant/revoke transitions. Starts a 1-second +/// GLib timer that fires `on_change` every time `AXIsProcessTrusted()` +/// flips, and keeps running for the lifetime of the process. /// /// We rely on polling rather than AXObserver because the AX notification -/// API requires a trusted process to subscribe — the precondition we're -/// waiting for. This runs on the GTK main thread (via timeout_add_seconds_local). -pub fn watch_for_accessibility_grant(mut on_granted: F) +/// API requires a trusted process to subscribe — the precondition we +/// can't assume. This runs on the GTK main thread (via +/// `timeout_add_seconds_local`). +pub fn watch_accessibility_state(mut on_change: F) where - F: FnMut() + 'static, + F: FnMut(AccessibilityChange) + 'static, { - if accessibility_granted() { - return; - } - log::info!("watching for Accessibility grant"); + let mut last = accessibility_granted(); + log::info!("watching Accessibility state (initial = {last})"); glib::timeout_add_seconds_local(1, move || { - if accessibility_granted() { - log::info!("Accessibility granted; firing relaunch prompt"); - on_granted(); - glib::ControlFlow::Break - } else { - glib::ControlFlow::Continue + let current = accessibility_granted(); + if current != last { + log::info!("Accessibility state flip: {last} -> {current}"); + on_change(if current { + AccessibilityChange::Granted + } else { + AccessibilityChange::Revoked + }); + last = current; } + glib::ControlFlow::Continue }); } From 99344a310465966bb037e82537050f77dd2d1e0e Mon Sep 17 00:00:00 2001 From: Jon Kinney Date: Fri, 24 Apr 2026 10:43:23 -0500 Subject: [PATCH 24/33] macos: present the window on the post-grant relaunch MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit After the user clicks Relaunch on the warning row, the new instance started hidden in the menu bar — no visible confirmation that the relaunch actually worked. Present the window on that specific launch so the user sees the app come up healthy. Mechanism: relaunch_bundle() sets LAN_MOUSE_RELAUNCHED=1 via `open --env` when spawning the new instance. build_ui reads the env var and calls window.present() only when it's set. Normal fresh launches (from Finder / Dock / Launchpad / any other Launch Services path) continue to start hidden in the menu bar. Co-Authored-By: Claude Opus 4.7 (1M context) --- lan-mouse-gtk/src/lib.rs | 10 ++++++++++ lan-mouse-gtk/src/macos_privacy.rs | 6 +++++- 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/lan-mouse-gtk/src/lib.rs b/lan-mouse-gtk/src/lib.rs index 112aae6..c1acbd4 100644 --- a/lan-mouse-gtk/src/lib.rs +++ b/lan-mouse-gtk/src/lib.rs @@ -278,5 +278,15 @@ fn build_ui(app: &Application) { #[cfg(not(target_os = "macos"))] window.present(); + + // On macOS, surface the window on the specific launch that follows + // the user clicking "Relaunch" after granting Accessibility, so + // they see the app come up in its working state rather than just + // a menu-bar icon and wonder whether anything happened. + // relaunch_bundle() sets LAN_MOUSE_RELAUNCHED=1 via `open --env`. + #[cfg(target_os = "macos")] + if env::var_os("LAN_MOUSE_RELAUNCHED").is_some() { + window.present(); + } } diff --git a/lan-mouse-gtk/src/macos_privacy.rs b/lan-mouse-gtk/src/macos_privacy.rs index 9183a96..3d93667 100644 --- a/lan-mouse-gtk/src/macos_privacy.rs +++ b/lan-mouse-gtk/src/macos_privacy.rs @@ -144,7 +144,11 @@ pub fn relaunch_bundle() { // Trailing `&` backgrounds the sleep+open so our shell call returns // immediately; the spawned shell is adopted by launchd once we exit. - let cmd = format!("(sleep 1 && open {bundle:?}) &"); + // `--env LAN_MOUSE_RELAUNCHED=1` sets an env var on the new process + // so `build_ui` can present the main window on this specific launch + // (confirming to the user that the grant + relaunch worked) while + // still starting hidden in the menu bar on normal fresh launches. + let cmd = format!("(sleep 1 && open --env LAN_MOUSE_RELAUNCHED=1 {bundle:?}) &"); let _ = Command::new("sh").arg("-c").arg(cmd).spawn(); } From 10fd7288044e2ab27d8af6805edfd374526690cf Mon Sep 17 00:00:00 2001 From: Jon Kinney Date: Fri, 24 Apr 2026 10:45:24 -0500 Subject: [PATCH 25/33] macos: default to showing the window on every launch Replaces the narrow LAN_MOUSE_RELAUNCHED signal with an inverted opt-out: present the main window on every macOS launch unless LAN_MOUSE_HIDDEN=1 is in the environment. User feedback: on any manual launch (Dock, Finder, `open`, post- grant relaunch) the window should come up so the user has a visible confirmation the app is alive and can see its current state. A hidden-to-menu-bar-only launch should be opt-in for a LaunchAgent / login-item configuration, not the default. LaunchAgent plists can set the env via `EnvironmentVariables` and `RunAtLoad=true` for a quiet boot launch. Co-Authored-By: Claude Opus 4.7 (1M context) --- lan-mouse-gtk/src/lib.rs | 14 ++++++++------ lan-mouse-gtk/src/macos_privacy.rs | 6 +----- 2 files changed, 9 insertions(+), 11 deletions(-) diff --git a/lan-mouse-gtk/src/lib.rs b/lan-mouse-gtk/src/lib.rs index c1acbd4..112feba 100644 --- a/lan-mouse-gtk/src/lib.rs +++ b/lan-mouse-gtk/src/lib.rs @@ -279,13 +279,15 @@ fn build_ui(app: &Application) { #[cfg(not(target_os = "macos"))] window.present(); - // On macOS, surface the window on the specific launch that follows - // the user clicking "Relaunch" after granting Accessibility, so - // they see the app come up in its working state rather than just - // a menu-bar icon and wonder whether anything happened. - // relaunch_bundle() sets LAN_MOUSE_RELAUNCHED=1 via `open --env`. + // On macOS, default to presenting the main window on every launch + // so the user gets a visible confirmation that the app is running + // — including the post-grant relaunch and normal Dock/Finder/`open` + // launches. Opt out by setting `LAN_MOUSE_HIDDEN=1` in the + // environment (useful for a LaunchAgent / login-item configuration + // where the user wants the app to come up quietly into the menu + // bar only, with no window on boot). #[cfg(target_os = "macos")] - if env::var_os("LAN_MOUSE_RELAUNCHED").is_some() { + if env::var_os("LAN_MOUSE_HIDDEN").is_none() { window.present(); } } diff --git a/lan-mouse-gtk/src/macos_privacy.rs b/lan-mouse-gtk/src/macos_privacy.rs index 3d93667..9183a96 100644 --- a/lan-mouse-gtk/src/macos_privacy.rs +++ b/lan-mouse-gtk/src/macos_privacy.rs @@ -144,11 +144,7 @@ pub fn relaunch_bundle() { // Trailing `&` backgrounds the sleep+open so our shell call returns // immediately; the spawned shell is adopted by launchd once we exit. - // `--env LAN_MOUSE_RELAUNCHED=1` sets an env var on the new process - // so `build_ui` can present the main window on this specific launch - // (confirming to the user that the grant + relaunch worked) while - // still starting hidden in the menu bar on normal fresh launches. - let cmd = format!("(sleep 1 && open --env LAN_MOUSE_RELAUNCHED=1 {bundle:?}) &"); + let cmd = format!("(sleep 1 && open {bundle:?}) &"); let _ = Command::new("sh").arg("-c").arg(cmd).spawn(); } From 373e38215281692f15f8d3fc36a0adccff80df62 Mon Sep 17 00:00:00 2001 From: Jon Kinney Date: Mon, 27 Apr 2026 12:59:33 -0500 Subject: [PATCH 26/33] fix(capture): release peer keys on release-bind MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When the user pressed the release-bind chord (typically all four modifiers) the down events for the chord were forwarded to the peer while capture was active, but the matching up events arrived after the local tap flipped to passthrough and were never forwarded. The peer was left with phantom held modifiers until either its watchdog ran (1+ s) or the Leave message was processed — and Leave is sent over UDP/DTLS and can be lost. Drain the capture's pressed_keys set in release_capture and emit a KeyboardEvent::Key{state: 0} for every still-held key, plus a zeroed KeyboardEvent::Modifiers, before sending Leave. The receiver already maintains pressed_keys per handle and processes these key-up events through its normal path, so no protocol change is required and an unmodified peer picks up the fix automatically. The receiver-side release_keys safety net stays in place for the genuine packet-loss / disconnect-without-Leave cases. Co-Authored-By: Claude Opus 4.7 (1M context) --- input-capture/src/lib.rs | 13 +++++++++++++ src/capture.rs | 37 ++++++++++++++++++++++++++++++++++++- 2 files changed, 49 insertions(+), 1 deletion(-) diff --git a/input-capture/src/lib.rs b/input-capture/src/lib.rs index 4767503..b1ef6c0 100644 --- a/input-capture/src/lib.rs +++ b/input-capture/src/lib.rs @@ -171,6 +171,19 @@ impl InputCapture { self.capture.release().await } + /// Drain and return every key the capture has forwarded as + /// down-but-not-up. The caller is expected to synthesize key-up + /// events to the remote peer for each — otherwise the peer + /// retains phantom-held keys after capture is released. The + /// canonical case is the release-bind chord + /// (Ctrl+Shift+Alt+Meta): the down events were sent while + /// capture was active, but the matching up events arrive after + /// the local tap has flipped to passthrough and never reach + /// the peer. + pub fn take_pressed_keys(&mut self) -> HashSet { + std::mem::take(&mut self.pressed_keys) + } + /// destroy the input capture pub async fn terminate(&mut self) -> Result<(), CaptureError> { self.capture.terminate().await diff --git a/src/capture.rs b/src/capture.rs index 4300dea..8f739bd 100644 --- a/src/capture.rs +++ b/src/capture.rs @@ -8,7 +8,7 @@ use futures::StreamExt; use input_capture::{ CaptureError, CaptureEvent, CaptureHandle, InputCapture, InputCaptureError, Position, }; -use input_event::scancode; +use input_event::{Event, KeyboardEvent, scancode}; use lan_mouse_proto::ProtoEvent; use local_channel::mpsc::{Receiver, Sender, channel}; use tokio::task::{JoinHandle, spawn_local}; @@ -376,6 +376,41 @@ impl CaptureTask { async fn release_capture(&mut self, capture: &mut InputCapture) -> Result<(), CaptureError> { // If we have an active client, notify them we're leaving if let Some(handle) = self.active_client.take() { + // Synthesize key-up events for every key still held in the + // capture's pressed_keys set BEFORE sending Leave. Without + // this, pressing the release-bind chord (typically all four + // modifiers) leaves the peer with phantom held modifiers: + // the down events were forwarded while capture was active, + // but the matching up events arrive after the local tap + // flips to passthrough and never reach the peer. The peer + // then runs every subsequent keystroke through those held + // mods until its watchdog times out (1+ s) or our Leave + // arrives — and Leave can be lost over UDP/DTLS. + for key in capture.take_pressed_keys() { + let key_up = ProtoEvent::Input(Event::Keyboard(KeyboardEvent::Key { + time: 0, + key: key as u32, + state: 0, + })); + if let Err(e) = self.conn.send(key_up, handle).await { + log::warn!("failed to send key-up to client {handle}: {e}"); + } + } + // Reset the modifier mask too. The peer's input-emulation + // layer keeps a separate XKB-style modifier state that's + // updated by KeyboardEvent::Modifiers, distinct from the + // pressed_keys set drained above. Without this, an + // already-locked CapsLock would survive the release. + let mods_zero = ProtoEvent::Input(Event::Keyboard(KeyboardEvent::Modifiers { + depressed: 0, + latched: 0, + locked: 0, + group: 0, + })); + if let Err(e) = self.conn.send(mods_zero, handle).await { + log::warn!("failed to reset modifiers on client {handle}: {e}"); + } + log::info!("sending Leave event to client {handle}"); if let Err(e) = self.conn.send(ProtoEvent::Leave(0), handle).await { log::warn!("failed to send Leave to client {handle}: {e}"); From 3b4b3a51aaf219dbb0b522c835d65393c6e24513 Mon Sep 17 00:00:00 2001 From: Jon Kinney Date: Mon, 27 Apr 2026 12:59:45 -0500 Subject: [PATCH 27/33] macos: re-enable CGEventTap on tap timeout MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The kernel disables a session-level CGEventTap when its callback runs longer than ~1 s on a single event — typical causes are heavy load, scheduler contention, or the process being briefly suspended (App Nap on a long idle, debugger pause). It is not a fatal condition: Apple's documented recovery is to call CGEventTapEnable and resume processing. Before this change the tap stayed dead until the user manually clicked Re-enable from the menubar. Stash the tap's mach port pointer in an Arc> set immediately after CGEventTap::new returns, and on TapDisabledByTimeout call CGEventTapEnable from the callback to revive the tap while preserving capture state — the user doesn't see the cursor pop back to the local screen mid-session for a transient slow callback. TapDisabledByUserInput keeps the existing teardown path: those causes (TCC Accessibility revoked mid-session, secure-input mode, explicit kill) are not safely recoverable from inside the callback, and the existing fallthrough-fix from 59d9e45 / d1e963e still applies there. Co-Authored-By: Claude Opus 4.7 (1M context) --- input-capture/src/macos.rs | 77 +++++++++++++++++++++++++++++--------- 1 file changed, 60 insertions(+), 17 deletions(-) diff --git a/input-capture/src/macos.rs b/input-capture/src/macos.rs index 517127d..17ff730 100644 --- a/input-capture/src/macos.rs +++ b/input-capture/src/macos.rs @@ -2,7 +2,7 @@ use super::{Capture, CaptureError, CaptureEvent, Position, error::MacosCaptureCr use async_trait::async_trait; use bitflags::bitflags; use core_foundation::{ - base::{CFRelease, kCFAllocatorDefault}, + base::{CFRelease, TCFType, kCFAllocatorDefault}, date::CFTimeInterval, number::{CFBooleanRef, kCFBooleanTrue}, runloop::{CFRunLoop, CFRunLoopSource, kCFRunLoopCommonModes}, @@ -28,7 +28,7 @@ use std::{ collections::HashSet, ffi::{CString, c_char}, pin::Pin, - sync::Arc, + sync::{Arc, OnceLock}, task::{Context, Poll, ready}, thread::{self}, }; @@ -395,6 +395,14 @@ fn create_event_tap<'a>( notify_tx: Sender, event_tx: Sender<(Position, CaptureEvent)>, ) -> Result, MacosCaptureCreationError> { + // Shared slot for the tap's mach port pointer. Stored as `usize` + // because raw pointers aren't `Send`, but the integer + // representation is — and CGEventTapEnable is documented as + // thread-safe. Set immediately after CGEventTap::new returns; + // read by the callback to recover from a TapDisabledByTimeout. + let tap_mach_port: Arc> = Arc::new(OnceLock::new()); + let tap_mach_port_cb = Arc::clone(&tap_mach_port); + let cg_events_of_interest: Vec = vec![ CGEventType::LeftMouseDown, CGEventType::LeftMouseUp, @@ -419,21 +427,43 @@ fn create_event_tap<'a>( let mut capture_position = None; let mut res_events = vec![]; - if matches!( - event_type, - CGEventType::TapDisabledByTimeout | CGEventType::TapDisabledByUserInput - ) { - // When the tap is disabled (including the case where TCC - // Accessibility is revoked mid-session), we MUST drop - // captured state synchronously and return Keep on this - // event. Otherwise the `current_pos.is_some()` branch - // below would drop this event (and any racing callback - // still in flight) back into `CallbackResult::Drop`, - // silently eating the user's clicks and keypresses while - // the tap winds down. Clear state + show the cursor - // here, then notify the producer loop so the service - // can tear down cleanly. - log::error!("CGEventTap disabled, releasing capture state"); + if matches!(event_type, CGEventType::TapDisabledByTimeout) { + // The kernel disables the tap when our callback runs + // longer than ~1s on a single event — typical causes + // are heavy load, scheduler contention, or this + // process being briefly suspended (e.g. App Nap on a + // long idle). It is NOT a fatal condition: Apple's + // documented recovery is to call CGEventTapEnable + // and resume processing. Re-enable in place and KEEP + // existing capture state so the user doesn't see the + // cursor pop back to the local screen mid-session. + if let Some(&port) = tap_mach_port_cb.get() { + log::warn!("CGEventTap disabled by timeout — re-enabling"); + unsafe { + CGEventTapEnable(port as *mut c_void, true); + } + } else { + log::error!( + "CGEventTap disabled by timeout, but mach port not yet stored — cannot re-enable" + ); + } + return CallbackResult::Keep; + } + + if matches!(event_type, CGEventType::TapDisabledByUserInput) { + // Deliberate kill — secure-input mode (e.g. password + // field), TCC Accessibility revoked mid-session, or + // the user disabling event-monitoring. We can't + // recover from this; drop captured state synchronously + // and return Keep on this event. Otherwise the + // `current_pos.is_some()` branch below would drop this + // event (and any racing callback still in flight) back + // into `CallbackResult::Drop`, silently eating the + // user's clicks and keypresses while the tap winds + // down. Clear state + show the cursor here, then + // notify the producer loop so the service can tear + // down cleanly. + log::error!("CGEventTap disabled by user input, releasing capture state"); if state.current_pos.is_some() { let _ = CGDisplay::show_cursor(&CGDisplay::main()); state.current_pos = None; @@ -507,6 +537,13 @@ fn create_event_tap<'a>( ) .map_err(|_| MacosCaptureCreationError::EventTapCreation)?; + // Hand the mach port pointer to the callback so it can re-enable + // the tap on TapDisabledByTimeout. The pointer is valid for the + // lifetime of `tap` (which lives on the event-tap thread until + // the run loop exits). + let port_ptr = tap.mach_port().as_concrete_TypeRef() as usize; + let _ = tap_mach_port.set(port_ptr); + let tap_source: CFRunLoopSource = tap .mach_port() .create_runloop_source(0) @@ -711,6 +748,12 @@ extern "C" { seconds: CFTimeInterval, ); fn CGPreflightListenEventAccess() -> bool; + /// Re-enable an event tap that was disabled by a + /// `kCGEventTapDisabledByTimeout` event. The Apple-documented + /// recovery path: see Quartz Event Services Reference. The `tap` + /// argument is a `CFMachPortRef`; we pass the raw pointer so we + /// can store it as `usize` for cross-thread sharing. + fn CGEventTapEnable(tap: *mut c_void, enable: bool); } #[link(name = "ApplicationServices", kind = "framework")] From bb1cc805c1c7798c618183016d9eff0b3a32e483 Mon Sep 17 00:00:00 2001 From: Jon Kinney Date: Mon, 27 Apr 2026 12:59:53 -0500 Subject: [PATCH 28/33] macos: opt out of App Nap via NSAppSleepDisabled MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit LSUIElement processes are prime App Nap candidates when idle, and once App-Napped macOS throttles user-space timers. The ping_pong keepalive loop in src/connect.rs uses tokio sleeps with a 2 s ping cycle; if those sleeps slip past the peer's 1 s grace the peer's watchdog tears down the DTLS connection and the daemon reconnects from a fresh ephemeral port on the next event — the "silence-then-new-source-port" pattern visible in the peer's logs after long idle. Setting NSAppSleepDisabled keeps the timers running on schedule across long idle windows. No code-side changes; the key is honored by macOS for the lifetime of the process. Co-Authored-By: Claude Opus 4.7 (1M context) --- build-aux/macos-lsui-element.plist | 2 ++ 1 file changed, 2 insertions(+) diff --git a/build-aux/macos-lsui-element.plist b/build-aux/macos-lsui-element.plist index 4a60be7..5ac2193 100644 --- a/build-aux/macos-lsui-element.plist +++ b/build-aux/macos-lsui-element.plist @@ -1,5 +1,7 @@ LSUIElement + NSAppSleepDisabled + NSInputMonitoringUsageDescription Lan Mouse needs Input Monitoring access to capture keyboard and mouse input and forward it to remote machines on your network. NSAppleEventsUsageDescription From e863cdb80146d2d7ea8444e235d93c2be9be6208 Mon Sep 17 00:00:00 2001 From: Jon Kinney Date: Mon, 27 Apr 2026 14:12:05 -0500 Subject: [PATCH 29/33] macos: refresh display bounds on reconfiguration MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit InputCaptureState fetches the active-display bounds once in new() via CGDisplay::active_displays(), and crossed() / start_capture() both consult those frozen bounds for every barrier check and cursor warp. Plug in a monitor, change resolution, or rearrange displays in System Settings and the bounds go stale immediately: the cursor either stops crossing at the new edge or warps to the old edge coordinates. Register a Quartz CGDisplayRegisterReconfigurationCallback on the event-tap thread's CFRunLoop and route a new ProducerEvent::DisplayReconfigured into the existing producer channel. The producer task re-runs update_bounds() on the live state, so subsequent barrier checks use the current geometry. The callback fires twice per change — once with the kCGDisplayBeginConfigurationFlag (BEFORE the bounds update) and once after — we filter the begin-phase out and only refresh on the post-change notification. The callback registration is removed and the leaked sender Box is reclaimed when the run loop exits, so create/destroy cycles don't leak channel senders. Co-Authored-By: Claude Opus 4.7 (1M context) --- input-capture/src/macos.rs | 87 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 87 insertions(+) diff --git a/input-capture/src/macos.rs b/input-capture/src/macos.rs index 17ff730..a5a4439 100644 --- a/input-capture/src/macos.rs +++ b/input-capture/src/macos.rs @@ -67,6 +67,7 @@ enum ProducerEvent { Destroy(Position), Grab(Position), EventTapDisabled, + DisplayReconfigured, } impl InputCaptureState { @@ -187,6 +188,20 @@ impl InputCaptureState { } return Err(CaptureError::EventTapDisabled); } + ProducerEvent::DisplayReconfigured => { + // The macOS display configuration changed — a monitor + // was plugged in/out, the resolution changed, the + // arrangement was rearranged, etc. Re-fetch the + // active-display bounds so barrier crossings and the + // cursor-warp on capture-start use the current + // geometry instead of whatever was true at process + // start. + if let Err(e) = self.update_bounds() { + log::warn!("failed to refresh display bounds: {e}"); + } else { + log::info!("display reconfigured: {:?}", self.bounds); + } + } }; Ok(()) } @@ -563,6 +578,9 @@ fn event_tap_thread( ready: std::sync::mpsc::Sender>, exit: oneshot::Sender<()>, ) { + // Clone now: create_event_tap consumes notify_tx into its closure. + let display_notify_tx = notify_tx.clone(); + let _tap = match create_event_tap(client_state, notify_tx, event_tx) { Err(e) => { ready.send(Err(e)).expect("channel closed"); @@ -574,13 +592,70 @@ fn event_tap_thread( tap } }; + + // Register a Quartz display-reconfiguration callback so the + // capture state's bounds get refreshed when the user plugs in a + // monitor, changes resolution, or rearranges displays. The + // callback runs on this thread's CFRunLoop. Box-leak the sender + // so the C side has a stable user_info pointer; reclaim it after + // the run loop exits. + let display_user_info = + Box::into_raw(Box::new(display_notify_tx)) as *mut c_void; + unsafe { + CGDisplayRegisterReconfigurationCallback( + display_reconfiguration_callback, + display_user_info, + ); + } + log::debug!("running CFRunLoop..."); CFRunLoop::run_current(); log::debug!("event tap thread exiting!..."); + unsafe { + CGDisplayRemoveReconfigurationCallback( + display_reconfiguration_callback, + display_user_info, + ); + // Reclaim the leaked sender Box so we don't leak a tokio + // channel sender on every capture create/destroy cycle. + drop(Box::from_raw( + display_user_info as *mut Sender, + )); + } + let _ = exit.send(()); } +/// Quartz display-reconfiguration callback. Fires twice per change: +/// once with `kCGDisplayBeginConfigurationFlag` set (BEFORE the +/// change is applied — the bounds are still stale at this point), +/// then again afterwards with the actual change flags (Add, Remove, +/// Mode, DesktopShapeChanged, etc.). Skip the begin phase; on the +/// real notification, kick the producer task to refresh bounds. +extern "C" fn display_reconfiguration_callback( + _display: u32, + flags: u32, + user_info: *mut c_void, +) { + const K_CG_DISPLAY_BEGIN_CONFIGURATION_FLAG: u32 = 1 << 0; + if flags & K_CG_DISPLAY_BEGIN_CONFIGURATION_FLAG != 0 { + return; + } + if user_info.is_null() { + return; + } + // SAFETY: user_info is a Box::into_raw of Sender + // owned by `event_tap_thread`. It's valid for the lifetime of + // that thread; the registration is removed before the box is + // freed. The callback only fires while the run loop is running + // on that thread, so we know the box is live here. + let sender = unsafe { &*(user_info as *const Sender) }; + if let Err(e) = sender.blocking_send(ProducerEvent::DisplayReconfigured) { + log::warn!("failed to notify display reconfiguration: {e}"); + } +} + pub struct MacOSInputCapture { event_rx: Receiver<(Position, CaptureEvent)>, notify_tx: Sender, @@ -754,6 +829,18 @@ extern "C" { /// argument is a `CFMachPortRef`; we pass the raw pointer so we /// can store it as `usize` for cross-thread sharing. fn CGEventTapEnable(tap: *mut c_void, enable: bool); + + /// Register a callback invoked when the display configuration + /// changes (monitor add/remove, resolution change, mirror, + /// rearrange, etc). See Quartz Display Services Reference. + fn CGDisplayRegisterReconfigurationCallback( + callback: extern "C" fn(u32, u32, *mut c_void), + user_info: *mut c_void, + ) -> CGError; + fn CGDisplayRemoveReconfigurationCallback( + callback: extern "C" fn(u32, u32, *mut c_void), + user_info: *mut c_void, + ) -> CGError; } #[link(name = "ApplicationServices", kind = "framework")] From e5862e10e3d8dc9ca1bdcd2fff6456563c4d3183 Mon Sep 17 00:00:00 2001 From: Jon Kinney Date: Tue, 28 Apr 2026 17:34:24 -0500 Subject: [PATCH 30/33] style: apply cargo fmt No behavior changes. Brings three files back in line with the project's `style_edition = "2024"` rustfmt config so subsequent edits don't carry unrelated formatting in their diffs. --- input-capture/src/macos.rs | 221 ++++++++++++++--------------- lan-mouse-gtk/src/lib.rs | 5 +- lan-mouse-gtk/src/macos_privacy.rs | 1 - 3 files changed, 108 insertions(+), 119 deletions(-) diff --git a/input-capture/src/macos.rs b/input-capture/src/macos.rs index a5a4439..dc941b2 100644 --- a/input-capture/src/macos.rs +++ b/input-capture/src/macos.rs @@ -435,113 +435,114 @@ fn create_event_tap<'a>( CGEventType::FlagsChanged, ]; - let event_tap_callback = - move |_proxy: CGEventTapProxy, event_type: CGEventType, cg_ev: &CGEvent| { - log::trace!("Got event from tap: {event_type:?}"); - let mut state = client_state.blocking_lock(); - let mut capture_position = None; - let mut res_events = vec![]; + let event_tap_callback = move |_proxy: CGEventTapProxy, + event_type: CGEventType, + cg_ev: &CGEvent| { + log::trace!("Got event from tap: {event_type:?}"); + let mut state = client_state.blocking_lock(); + let mut capture_position = None; + let mut res_events = vec![]; - if matches!(event_type, CGEventType::TapDisabledByTimeout) { - // The kernel disables the tap when our callback runs - // longer than ~1s on a single event — typical causes - // are heavy load, scheduler contention, or this - // process being briefly suspended (e.g. App Nap on a - // long idle). It is NOT a fatal condition: Apple's - // documented recovery is to call CGEventTapEnable - // and resume processing. Re-enable in place and KEEP - // existing capture state so the user doesn't see the - // cursor pop back to the local screen mid-session. - if let Some(&port) = tap_mach_port_cb.get() { - log::warn!("CGEventTap disabled by timeout — re-enabling"); - unsafe { - CGEventTapEnable(port as *mut c_void, true); - } - } else { - log::error!( - "CGEventTap disabled by timeout, but mach port not yet stored — cannot re-enable" - ); + if matches!(event_type, CGEventType::TapDisabledByTimeout) { + // The kernel disables the tap when our callback runs + // longer than ~1s on a single event — typical causes + // are heavy load, scheduler contention, or this + // process being briefly suspended (e.g. App Nap on a + // long idle). It is NOT a fatal condition: Apple's + // documented recovery is to call CGEventTapEnable + // and resume processing. Re-enable in place and KEEP + // existing capture state so the user doesn't see the + // cursor pop back to the local screen mid-session. + if let Some(&port) = tap_mach_port_cb.get() { + log::warn!("CGEventTap disabled by timeout — re-enabling"); + unsafe { + CGEventTapEnable(port as *mut c_void, true); } - return CallbackResult::Keep; - } - - if matches!(event_type, CGEventType::TapDisabledByUserInput) { - // Deliberate kill — secure-input mode (e.g. password - // field), TCC Accessibility revoked mid-session, or - // the user disabling event-monitoring. We can't - // recover from this; drop captured state synchronously - // and return Keep on this event. Otherwise the - // `current_pos.is_some()` branch below would drop this - // event (and any racing callback still in flight) back - // into `CallbackResult::Drop`, silently eating the - // user's clicks and keypresses while the tap winds - // down. Clear state + show the cursor here, then - // notify the producer loop so the service can tear - // down cleanly. - log::error!("CGEventTap disabled by user input, releasing capture state"); - if state.current_pos.is_some() { - let _ = CGDisplay::show_cursor(&CGDisplay::main()); - state.current_pos = None; - } - notify_tx - .blocking_send(ProducerEvent::EventTapDisabled) - .unwrap_or_else(|e| { - log::error!("Failed to send notification: {e}"); - }); - return CallbackResult::Keep; - } - - // Are we in a client? - if let Some(current_pos) = state.current_pos { - capture_position = Some(current_pos); - get_events( - &event_type, - cg_ev, - &mut res_events, - &mut state.modifier_state, - ) - .unwrap_or_else(|e| { - log::error!("Failed to get events: {e}"); - }); - - // Keep (hidden) cursor at the edge of the screen - if matches!( - event_type, - CGEventType::MouseMoved - | CGEventType::LeftMouseDragged - | CGEventType::RightMouseDragged - | CGEventType::OtherMouseDragged - ) { - state.reset_cursor().unwrap_or_else(|e| log::warn!("{e}")); - } - } else if matches!(event_type, CGEventType::MouseMoved) { - // Did we cross a barrier? - if let Some(new_pos) = state.crossed(cg_ev) { - capture_position = Some(new_pos); - state - .start_capture(cg_ev, new_pos) - .unwrap_or_else(|e| log::warn!("{e}")); - res_events.push(CaptureEvent::Begin); - notify_tx - .blocking_send(ProducerEvent::Grab(new_pos)) - .expect("Failed to send notification"); - } - } - - if let Some(pos) = capture_position { - res_events.iter().for_each(|e| { - // error must be ignored, since the event channel - // may already be closed when the InputCapture instance is dropped. - let _ = event_tx.blocking_send((pos, *e)); - }); - // Returning Drop should stop the event from being processed - // but core fundation still returns the event - cg_ev.set_type(CGEventType::Null); - CallbackResult::Drop } else { - CallbackResult::Keep + log::error!( + "CGEventTap disabled by timeout, but mach port not yet stored — cannot re-enable" + ); } - }; + return CallbackResult::Keep; + } + + if matches!(event_type, CGEventType::TapDisabledByUserInput) { + // Deliberate kill — secure-input mode (e.g. password + // field), TCC Accessibility revoked mid-session, or + // the user disabling event-monitoring. We can't + // recover from this; drop captured state synchronously + // and return Keep on this event. Otherwise the + // `current_pos.is_some()` branch below would drop this + // event (and any racing callback still in flight) back + // into `CallbackResult::Drop`, silently eating the + // user's clicks and keypresses while the tap winds + // down. Clear state + show the cursor here, then + // notify the producer loop so the service can tear + // down cleanly. + log::error!("CGEventTap disabled by user input, releasing capture state"); + if state.current_pos.is_some() { + let _ = CGDisplay::show_cursor(&CGDisplay::main()); + state.current_pos = None; + } + notify_tx + .blocking_send(ProducerEvent::EventTapDisabled) + .unwrap_or_else(|e| { + log::error!("Failed to send notification: {e}"); + }); + return CallbackResult::Keep; + } + + // Are we in a client? + if let Some(current_pos) = state.current_pos { + capture_position = Some(current_pos); + get_events( + &event_type, + cg_ev, + &mut res_events, + &mut state.modifier_state, + ) + .unwrap_or_else(|e| { + log::error!("Failed to get events: {e}"); + }); + + // Keep (hidden) cursor at the edge of the screen + if matches!( + event_type, + CGEventType::MouseMoved + | CGEventType::LeftMouseDragged + | CGEventType::RightMouseDragged + | CGEventType::OtherMouseDragged + ) { + state.reset_cursor().unwrap_or_else(|e| log::warn!("{e}")); + } + } else if matches!(event_type, CGEventType::MouseMoved) { + // Did we cross a barrier? + if let Some(new_pos) = state.crossed(cg_ev) { + capture_position = Some(new_pos); + state + .start_capture(cg_ev, new_pos) + .unwrap_or_else(|e| log::warn!("{e}")); + res_events.push(CaptureEvent::Begin); + notify_tx + .blocking_send(ProducerEvent::Grab(new_pos)) + .expect("Failed to send notification"); + } + } + + if let Some(pos) = capture_position { + res_events.iter().for_each(|e| { + // error must be ignored, since the event channel + // may already be closed when the InputCapture instance is dropped. + let _ = event_tx.blocking_send((pos, *e)); + }); + // Returning Drop should stop the event from being processed + // but core fundation still returns the event + cg_ev.set_type(CGEventType::Null); + CallbackResult::Drop + } else { + CallbackResult::Keep + } + }; let tap = CGEventTap::new( CGEventTapLocation::Session, @@ -599,8 +600,7 @@ fn event_tap_thread( // callback runs on this thread's CFRunLoop. Box-leak the sender // so the C side has a stable user_info pointer; reclaim it after // the run loop exits. - let display_user_info = - Box::into_raw(Box::new(display_notify_tx)) as *mut c_void; + let display_user_info = Box::into_raw(Box::new(display_notify_tx)) as *mut c_void; unsafe { CGDisplayRegisterReconfigurationCallback( display_reconfiguration_callback, @@ -613,10 +613,7 @@ fn event_tap_thread( log::debug!("event tap thread exiting!..."); unsafe { - CGDisplayRemoveReconfigurationCallback( - display_reconfiguration_callback, - display_user_info, - ); + CGDisplayRemoveReconfigurationCallback(display_reconfiguration_callback, display_user_info); // Reclaim the leaked sender Box so we don't leak a tokio // channel sender on every capture create/destroy cycle. drop(Box::from_raw( @@ -633,11 +630,7 @@ fn event_tap_thread( /// then again afterwards with the actual change flags (Add, Remove, /// Mode, DesktopShapeChanged, etc.). Skip the begin phase; on the /// real notification, kick the producer task to refresh bounds. -extern "C" fn display_reconfiguration_callback( - _display: u32, - flags: u32, - user_info: *mut c_void, -) { +extern "C" fn display_reconfiguration_callback(_display: u32, flags: u32, user_info: *mut c_void) { const K_CG_DISPLAY_BEGIN_CONFIGURATION_FLAG: u32 = 1 << 0; if flags & K_CG_DISPLAY_BEGIN_CONFIGURATION_FLAG != 0 { return; diff --git a/lan-mouse-gtk/src/lib.rs b/lan-mouse-gtk/src/lib.rs index 112feba..ecdd708 100644 --- a/lan-mouse-gtk/src/lib.rs +++ b/lan-mouse-gtk/src/lib.rs @@ -219,9 +219,7 @@ fn build_ui(app: &Application) { } } macos_privacy::AccessibilityChange::Revoked => { - log::warn!( - "Accessibility revoked — quitting to avoid wedging system input" - ); + log::warn!("Accessibility revoked — quitting to avoid wedging system input"); if let Some(app) = app_weak.upgrade() { app.quit(); } @@ -291,4 +289,3 @@ fn build_ui(app: &Application) { window.present(); } } - diff --git a/lan-mouse-gtk/src/macos_privacy.rs b/lan-mouse-gtk/src/macos_privacy.rs index 9183a96..7bf83e3 100644 --- a/lan-mouse-gtk/src/macos_privacy.rs +++ b/lan-mouse-gtk/src/macos_privacy.rs @@ -73,7 +73,6 @@ pub fn accessibility_granted() -> bool { raw != 0 } - pub enum AccessibilityChange { /// AX was missing at startup and the user has now granted it. /// Capture/emulation still need a relaunch to take effect, since From 53c668b355983479c97a8da753f95f65bf83d69d Mon Sep 17 00:00:00 2001 From: Jon Kinney Date: Tue, 28 Apr 2026 17:34:45 -0500 Subject: [PATCH 31/33] macos: re-present window on app re-launch MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closing the menubar window hides it; re-launching Lan Mouse.app via Finder, `open`, or the Dock should bring the window back — but it didn't, because the kAEReopenApplication Apple Event was silently dropped. NSApp's default 'aevt'/'rapp' handler funnels the event into applicationShouldHandleReopen:hasVisibleWindows:, and GtkApplication owns NSApp's delegate without implementing that selector. Register the existing status-item delegate as a direct handler for 'aevt'/'rapp' via NSAppleEventManager. setEventHandler: replaces NSApplication's default, so we receive the event in our code and re-present the window plus call activateIgnoringOtherApps:. Extract a present_window() helper so the menubar's "Open Lan Mouse" item and the new re-open handler share one code path. Co-Authored-By: Claude Opus 4.7 (1M context) --- lan-mouse-gtk/src/macos_status_item.rs | 69 ++++++++++++++++++++++++-- 1 file changed, 66 insertions(+), 3 deletions(-) diff --git a/lan-mouse-gtk/src/macos_status_item.rs b/lan-mouse-gtk/src/macos_status_item.rs index d8aed2e..ef1a345 100644 --- a/lan-mouse-gtk/src/macos_status_item.rs +++ b/lan-mouse-gtk/src/macos_status_item.rs @@ -2,7 +2,7 @@ use std::{ cell::RefCell, - ffi::{CStr, CString, c_char, c_double, c_void}, + ffi::{CStr, CString, c_char, c_double, c_uint, c_void}, sync::OnceLock, }; @@ -45,7 +45,10 @@ pub fn setup(app: &adw::Application, window: &Window) { let hold = app.hold(); let ns_app = msg_send_id(class(c"NSApplication"), sel(c"sharedApplication")); - assert!(!ns_app.is_null(), "NSApplication sharedApplication returned null"); + assert!( + !ns_app.is_null(), + "NSApplication sharedApplication returned null" + ); msg_send_bool_usize(ns_app, sel(c"setActivationPolicy:"), 1); let delegate = new_delegate(); @@ -56,7 +59,10 @@ pub fn setup(app: &adw::Application, window: &Window) { ]); let status_bar = msg_send_id(class(c"NSStatusBar"), sel(c"systemStatusBar")); - assert!(!status_bar.is_null(), "NSStatusBar systemStatusBar returned null"); + assert!( + !status_bar.is_null(), + "NSStatusBar systemStatusBar returned null" + ); let status_item = msg_send_id_f64(status_bar, sel(c"statusItemWithLength:"), -1.0); assert!(!status_item.is_null(), "statusItemWithLength returned null"); // Retain so the status item survives autorelease pool drain. @@ -72,6 +78,8 @@ pub fn setup(app: &adw::Application, window: &Window) { msg_send_void_id(item, sel(c"setTarget:"), delegate); } + install_reopen_handler(delegate); + log::debug!("macos_status_item ready at {:p}", status_item); item.replace(Some(StatusItem { @@ -194,12 +202,29 @@ fn delegate_class() -> Class { quit_lan_mouse as *const c_void, c"v@:@".as_ptr(), ); + // kAEReopenApplication handler — fires when the user re-launches + // the .app while it's already running (Finder, `open`, Dock). + class_addMethod( + class, + sel(c"handleReopenEvent:withReplyEvent:"), + handle_reopen_event as *const c_void, + c"v@:@@".as_ptr(), + ); objc_registerClassPair(class); class as usize }) as Class } extern "C" fn show_lan_mouse(_this: Id, _cmd: Sel, _sender: Id) { + present_window(); +} + +extern "C" fn handle_reopen_event(_this: Id, _cmd: Sel, _event: Id, _reply: Id) { + log::debug!("kAEReopenApplication received — presenting main window"); + present_window(); +} + +fn present_window() { STATUS_ITEM.with(|item| { let item = item.borrow(); let Some(item) = item.as_ref() else { @@ -216,6 +241,35 @@ extern "C" fn show_lan_mouse(_this: Id, _cmd: Sel, _sender: Id) { }); } +// Register the status-item delegate as the handler for the +// kAEReopenApplication Apple Event ('aevt'/'rapp'). NSApplication +// installs a default handler at -finishLaunching that just delegates to +// applicationShouldHandleReopen:hasVisibleWindows: — which is a no-op +// here because GApplication owns NSApp's delegate. Replacing it lets us +// re-present the window when the user double-clicks the .app while +// we're already running. +unsafe fn install_reopen_handler(delegate: Id) { + const K_CORE_EVENT_CLASS: c_uint = 0x6165_7674; // 'aevt' + const K_AE_REOPEN_APPLICATION: c_uint = 0x7261_7070; // 'rapp' + + let manager = msg_send_id( + class(c"NSAppleEventManager"), + sel(c"sharedAppleEventManager"), + ); + if manager.is_null() { + log::warn!("NSAppleEventManager unavailable; re-launch will not re-open window"); + return; + } + msg_send_void_id_sel_u32_u32( + manager, + sel(c"setEventHandler:andSelector:forEventClass:andEventID:"), + delegate, + sel(c"handleReopenEvent:withReplyEvent:"), + K_CORE_EVENT_CLASS, + K_AE_REOPEN_APPLICATION, + ); +} + extern "C" fn quit_lan_mouse(_this: Id, _cmd: Sel, _sender: Id) { STATUS_ITEM.with(|item| { if let Some(app) = item.borrow().as_ref().and_then(|item| item.app.upgrade()) { @@ -280,4 +334,13 @@ extern "C" { fn msg_send_void_id(receiver: Id, selector: Sel, value: Id); #[link_name = "objc_msgSend"] fn msg_send_bool_usize(receiver: Id, selector: Sel, value: usize) -> Bool; + #[link_name = "objc_msgSend"] + fn msg_send_void_id_sel_u32_u32( + receiver: Id, + selector: Sel, + a: Id, + b: Sel, + c: c_uint, + d: c_uint, + ); } From f252567ef9b2f083377876ae4b027697610e5b57 Mon Sep 17 00:00:00 2001 From: Jon Kinney Date: Tue, 28 Apr 2026 17:36:20 -0500 Subject: [PATCH 32/33] chore: silence two pre-existing clippy lints - macos_privacy.rs: drop the redundant inner #![cfg(target_os = "macos")]; lib.rs already gates the mod declaration with #[cfg(target_os = "macos")] (clippy::duplicated_attributes). - macos_status_item.rs: inline status_item into the format string (clippy::uninlined_format_args). No behavior change. Brings `cargo clippy --workspace --all-targets --all-features -- -D warnings` back to clean. --- lan-mouse-gtk/src/macos_privacy.rs | 2 -- lan-mouse-gtk/src/macos_status_item.rs | 2 +- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/lan-mouse-gtk/src/macos_privacy.rs b/lan-mouse-gtk/src/macos_privacy.rs index 7bf83e3..f9fbd9c 100644 --- a/lan-mouse-gtk/src/macos_privacy.rs +++ b/lan-mouse-gtk/src/macos_privacy.rs @@ -1,5 +1,3 @@ -#![cfg(target_os = "macos")] - //! Tiny macOS Privacy-pane helpers used by the GUI. //! //! On macOS 13+, the Accessibility grant transitively confers the diff --git a/lan-mouse-gtk/src/macos_status_item.rs b/lan-mouse-gtk/src/macos_status_item.rs index ef1a345..663ec2e 100644 --- a/lan-mouse-gtk/src/macos_status_item.rs +++ b/lan-mouse-gtk/src/macos_status_item.rs @@ -80,7 +80,7 @@ pub fn setup(app: &adw::Application, window: &Window) { install_reopen_handler(delegate); - log::debug!("macos_status_item ready at {:p}", status_item); + log::debug!("macos_status_item ready at {status_item:p}"); item.replace(Some(StatusItem { app: app.downgrade(), From 3e7b04c1848afb2d77f802c7ddf1f5f3720c1b47 Mon Sep 17 00:00:00 2001 From: Jon Kinney Date: Wed, 29 Apr 2026 10:27:44 -0500 Subject: [PATCH 33/33] deps: downgrade libadwaita feature flag to v1_1 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Per maintainer review feedback. The bump to v1_2 in commit a45e00e was unnecessary — none of the macOS menubar work uses a libadwaita 1.2-only API (no AdwMessageDialog, AdwAboutWindow, AdwBanner, etc.). Drop the floor back to v1.1 so distros and flake users on slightly older libadwaita aren't pushed forward just to run the GTK frontend. Verified with cargo clean -p libadwaita followed by cargo check -p lan-mouse-gtk: libadwaita rebuilds with v1_1 features only and the workspace compiles, clippy-clean, tests passing. Co-Authored-By: Claude Opus 4.7 (1M context) --- lan-mouse-gtk/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lan-mouse-gtk/Cargo.toml b/lan-mouse-gtk/Cargo.toml index 71c4083..4a1a202 100644 --- a/lan-mouse-gtk/Cargo.toml +++ b/lan-mouse-gtk/Cargo.toml @@ -8,7 +8,7 @@ repository = "https://github.com/feschber/lan-mouse" [dependencies] gtk = { package = "gtk4", version = "0.9.0", features = ["v4_2"] } -adw = { package = "libadwaita", version = "0.7.0", features = ["v1_2"] } +adw = { package = "libadwaita", version = "0.7.0", features = ["v1_1"] } async-channel = { version = "2.1.1" } hostname = "0.4.0" log = "0.4.20"