├── .envrc ├── rust-toolchain.toml ├── .gitignore ├── CONTRIBUTING.md ├── src ├── gestures │ ├── hold.rs │ ├── mod.rs │ ├── pinch.rs │ └── swipe.rs ├── ipc_client.rs ├── tests │ └── mod.rs ├── config.rs ├── utils.rs ├── ipc.rs ├── mouse_handler.rs ├── main.rs └── event_handler.rs ├── shell.nix ├── default.nix ├── .github ├── ISSUE_TEMPLATE │ ├── feature_request.md │ └── bug_report.md ├── workflows │ ├── nix.yml │ ├── ci.yml │ └── release.yml └── scripts │ └── build-linux.sh ├── Cargo.toml ├── LICENSE ├── flake.nix ├── flake.lock ├── config.md ├── README.md ├── CLAUDE.md └── Cargo.lock /.envrc: -------------------------------------------------------------------------------- 1 | use flake 2 | -------------------------------------------------------------------------------- /rust-toolchain.toml: -------------------------------------------------------------------------------- 1 | [toolchain] 2 | channel = "stable" 3 | components = [ "rustfmt", "clippy", "rust-src", "rust-analyzer" ] 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /result 3 | /.direnv 4 | .idea 5 | 6 | # Local test scripts with personal configuration 7 | test-build-local.sh 8 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | Feel free to open an issue if you find a bug, and if you have a solution, a PR would be great! 3 | If you have a feature request, prefer to use discussions rather than an issue. 4 | -------------------------------------------------------------------------------- /src/gestures/hold.rs: -------------------------------------------------------------------------------- 1 | use knuffel::Decode; 2 | 3 | #[derive(Decode, Debug, Clone, PartialEq, Eq)] 4 | pub struct Hold { 5 | #[knuffel(property)] 6 | pub fingers: i32, 7 | #[knuffel(property)] 8 | pub action: Option, 9 | } 10 | -------------------------------------------------------------------------------- /src/gestures/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod hold; 2 | pub mod pinch; 3 | pub mod swipe; 4 | 5 | use knuffel::Decode; 6 | 7 | use hold::Hold; 8 | use pinch::Pinch; 9 | use swipe::Swipe; 10 | 11 | #[derive(Decode, Debug, Clone, PartialEq)] 12 | pub enum Gesture { 13 | Swipe(Swipe), 14 | Pinch(Pinch), 15 | Hold(Hold), 16 | None, 17 | } 18 | -------------------------------------------------------------------------------- /shell.nix: -------------------------------------------------------------------------------- 1 | (import 2 | ( 3 | let 4 | lock = builtins.fromJSON (builtins.readFile ./flake.lock); 5 | in 6 | fetchTarball { 7 | url = "https://github.com/edolstra/flake-compat/archive/${lock.nodes.flake-compat.locked.rev}.tar.gz"; 8 | sha256 = lock.nodes.flake-compat.locked.narHash; 9 | } 10 | ) 11 | { 12 | src = ./.; 13 | }).shellNix 14 | -------------------------------------------------------------------------------- /default.nix: -------------------------------------------------------------------------------- 1 | (import 2 | ( 3 | let 4 | lock = builtins.fromJSON (builtins.readFile ./flake.lock); 5 | in 6 | fetchTarball { 7 | url = "https://github.com/edolstra/flake-compat/archive/${lock.nodes.flake-compat.locked.rev}.tar.gz"; 8 | sha256 = lock.nodes.flake-compat.locked.narHash; 9 | } 10 | ) 11 | { 12 | src = ./.; 13 | }).defaultNix 14 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest a feature for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "gestures" 3 | version = "0.8.1" 4 | edition = "2021" 5 | license = "MIT" 6 | description = "Libinput based touchpad gestures program" 7 | repository = "https://github.com/ferstar/gestures" 8 | keywords = ["touchpad", "gestures", "libinput", "multitouch", "linux"] 9 | 10 | [dependencies] 11 | chrono = "0.4" 12 | clap = { version = "4.5", features = ["derive"] } 13 | ctrlc = "3.5" 14 | env_logger = { version = "0.11", features = ["auto-color"] } 15 | input = "0.9" 16 | knuffel = "3.2" 17 | libxdo = "0.6" 18 | log = "0.4" 19 | miette = { version = "7.6", features = ["fancy"] } 20 | nix = { version = "0.30", features = ["poll", "fs"] } 21 | once_cell = "1.21" 22 | parking_lot = "0.12" 23 | regex = "1.12" 24 | signal-hook = "0.3" 25 | threadpool = "1.8" 26 | timer = "0.2" 27 | -------------------------------------------------------------------------------- /src/ipc_client.rs: -------------------------------------------------------------------------------- 1 | use std::env; 2 | use std::io::Write; 3 | use std::os::unix::net::UnixStream; 4 | 5 | use crate::Commands; 6 | 7 | pub fn handle_command(cmd: Commands) { 8 | let socket_dir = env::var("XDG_RUNTIME_DIR").unwrap_or("/tmp".to_string()); 9 | let mut stream = match UnixStream::connect(format!("{socket_dir}/gestures.sock")) { 10 | Ok(s) => s, 11 | Err(e) => panic!("Got this while trying to connect to ipc: {e} \nPerhaps the main program is not running"), 12 | }; 13 | #[allow(clippy::single_match)] 14 | match cmd { 15 | Commands::Reload => { 16 | stream 17 | .write_all(b"reload") 18 | .map_err(|e| panic!("Failed to write to socket: {e}")) 19 | .unwrap(); 20 | } 21 | _ => (), 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Help improve this project by reporting bugs 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. ... 16 | 2. ... 17 | 18 | **Expected behavior** 19 | A clear and concise description of what you expected to happen. 20 | 21 | **Screenshots** 22 | If applicable, add screenshots to help explain your problem. 23 | 24 | **Configuration** 25 | Relevant part of your config file, if applicable 26 | 27 | **System info (please complete the following information):** 28 | - OS: [e.g. Arch Linux, Ubuntu] 29 | - Version [e.g. 22] 30 | 31 | **Additional context** 32 | Add any other context about the problem here. 33 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Riley Martin 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/gestures/pinch.rs: -------------------------------------------------------------------------------- 1 | use knuffel::{Decode, DecodeScalar}; 2 | 3 | #[derive(Decode, Debug, Clone, PartialEq, Eq)] 4 | pub struct Pinch { 5 | #[knuffel(property)] 6 | pub fingers: i32, 7 | #[knuffel(property)] 8 | pub direction: PinchDir, 9 | #[knuffel(property)] 10 | pub update: Option, 11 | #[knuffel(property)] 12 | pub start: Option, 13 | #[knuffel(property)] 14 | pub end: Option, 15 | } 16 | 17 | /// Direction of pinch gestures 18 | #[derive(DecodeScalar, Debug, Clone, PartialEq, Eq)] 19 | pub enum PinchDir { 20 | In, 21 | Out, 22 | Clockwise, 23 | CounterClockwise, 24 | Any, 25 | } 26 | 27 | impl PinchDir { 28 | pub fn dir(scale: f64, delta_angle: f64) -> Self { 29 | // We have some rotation and very little scale 30 | if scale > 0.95 && scale < 1.05 && delta_angle.abs() > 0.03 { 31 | if delta_angle > 0.0 { 32 | Self::Clockwise 33 | } else { 34 | Self::CounterClockwise 35 | } 36 | // Otherwise we have a normal pinch 37 | } else if scale > 1.0 { 38 | Self::Out 39 | } else { 40 | Self::In 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/tests/mod.rs: -------------------------------------------------------------------------------- 1 | use crate::config::Config; 2 | use crate::gestures::swipe::SwipeDir; 3 | use crate::utils::exec_command_from_string; 4 | 5 | #[test] 6 | fn test_zombie_process() { 7 | for _ in 0..100 { 8 | let _ = exec_command_from_string("echo", 0.0, 0.0, 0.0, 0.0); 9 | } 10 | } 11 | 12 | #[test] 13 | fn test_config_default() { 14 | let c = Config::default(); 15 | assert_eq!( 16 | c, 17 | Config { 18 | // // device: None, 19 | gestures: vec![], 20 | } 21 | ); 22 | } 23 | 24 | #[test] 25 | fn test_dir() { 26 | let test_cases = vec![ 27 | (0.0, 0.0, SwipeDir::Any), 28 | (1.0, 0.0, SwipeDir::E), 29 | (-1.0, 0.0, SwipeDir::W), 30 | (0.0, 1.0, SwipeDir::S), 31 | (0.0, -1.0, SwipeDir::N), 32 | (1.0, 1.0, SwipeDir::SE), 33 | (-1.0, 1.0, SwipeDir::SW), 34 | (1.0, -1.0, SwipeDir::NE), 35 | (-1.0, -1.0, SwipeDir::NW), 36 | (2.0, 1.0, SwipeDir::SE), 37 | (-2.0, 1.0, SwipeDir::SW), 38 | (2.0, -1.0, SwipeDir::NE), 39 | (-2.0, -1.0, SwipeDir::NW), 40 | ]; 41 | 42 | for (x, y, expected) in test_cases { 43 | assert_eq!(SwipeDir::dir(x, y), expected); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/config.rs: -------------------------------------------------------------------------------- 1 | use std::{env, fs, path::Path}; 2 | 3 | use miette::{bail, IntoDiagnostic, Result}; 4 | // use serde::{Deserialize, Serialize}; 5 | use knuffel::{parse, Decode}; 6 | 7 | use crate::gestures::Gesture; 8 | 9 | #[derive(Decode, PartialEq, Debug, Default)] 10 | pub struct Config { 11 | // pub device: Option, 12 | #[knuffel(children)] 13 | pub gestures: Vec, 14 | } 15 | 16 | impl Config { 17 | pub fn read_from_file(file: &Path) -> Result { 18 | log::debug!("{:?}", &file); 19 | match fs::read_to_string(file) { 20 | Ok(s) => Ok(parse::(file.to_str().unwrap(), &s).into_diagnostic()?), 21 | _ => bail!("Could not read config file"), 22 | } 23 | } 24 | 25 | pub fn read_default_config() -> Result { 26 | let config_home = env::var("XDG_CONFIG_HOME") 27 | .unwrap_or_else(|_| format!("{}/.config", env::var("HOME").unwrap())); 28 | 29 | log::debug!("{:?}", &config_home); 30 | 31 | for path in ["gestures.kdl", "gestures/gestures.kdl"] { 32 | match Self::read_from_file(Path::new(&format!("{config_home}/{path}"))) { 33 | Ok(s) => return Ok(s), 34 | Err(e) => log::warn!("{}", e), 35 | } 36 | } 37 | 38 | bail!("Could not find config file") 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /.github/workflows/nix.yml: -------------------------------------------------------------------------------- 1 | name: Test nix build 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | paths: 8 | - 'src/**' 9 | - 'Cargo.toml' 10 | - '.github/workflows/nix.yml' 11 | - 'flake.nix' 12 | - 'flake.lock' 13 | pull_request: 14 | branches: 15 | - main 16 | paths: 17 | - 'src/**' 18 | - 'Cargo.toml' 19 | - '.github/workflows/nix.yml' 20 | - 'flake.nix' 21 | - 'flake.lock' 22 | 23 | concurrency: 24 | group: ${{ github.workflow }}-${{ github.ref }} 25 | cancel-in-progress: true 26 | 27 | jobs: 28 | check_nix: 29 | name: Test Nix 30 | runs-on: ubuntu-22.04 31 | timeout-minutes: 20 32 | steps: 33 | - name: git checkout 34 | uses: actions/checkout@v3 35 | - name: Set up cache 36 | uses: actions/cache@v3 37 | with: 38 | path: | 39 | ~/.cargo/bin/ 40 | ~/.cargo/registry/index/ 41 | ~/.cargo/registry/cache/ 42 | ~/.cargo/git/db/ 43 | target/ 44 | /nix/store 45 | key: ${{ runner.os }}-nix-${{ hashFiles('**/*.lock') }} 46 | - name: Install Nix 47 | uses: DeterminateSystems/nix-installer-action@main 48 | 49 | - name: Check flake 50 | run: nix flake check 51 | - name: Build with nix 52 | run: nix build 53 | -------------------------------------------------------------------------------- /src/utils.rs: -------------------------------------------------------------------------------- 1 | use miette::Result; 2 | use once_cell::sync::Lazy; 3 | use regex::Regex; 4 | use std::process::Command; 5 | use threadpool::ThreadPool; 6 | 7 | static REGEX_DELTA_X: Lazy = Lazy::new(|| Regex::new(r"\$delta_x").unwrap()); 8 | static REGEX_DELTA_Y: Lazy = Lazy::new(|| Regex::new(r"\$delta_y").unwrap()); 9 | static REGEX_SCALE: Lazy = Lazy::new(|| Regex::new(r"\$scale").unwrap()); 10 | static REGEX_DELTA_ANGLE: Lazy = Lazy::new(|| Regex::new(r"\$delta_angle").unwrap()); 11 | 12 | // Thread pool with 4 workers to handle command execution 13 | static THREAD_POOL: Lazy = Lazy::new(|| ThreadPool::new(4)); 14 | 15 | pub fn exec_command_from_string(args: &str, dx: f64, dy: f64, da: f64, scale: f64) -> Result<()> { 16 | if !args.is_empty() { 17 | let args = REGEX_DELTA_Y.replace_all(args, format!("{:.2}", dy)); 18 | let args = REGEX_DELTA_X.replace_all(&args, format!("{:.2}", dx)); 19 | let args = REGEX_SCALE.replace_all(&args, format!("{:.2}", scale)); 20 | let args = REGEX_DELTA_ANGLE.replace_all(&args, format!("{:.2}", da)); 21 | let args = args.to_string(); 22 | 23 | THREAD_POOL.execute(move || { 24 | log::debug!("{:?}", &args); 25 | let status = Command::new("sh").arg("-c").arg(&args).status(); 26 | if let Err(e) = status { 27 | log::error!("Failed to execute command '{}': {}", &args, e); 28 | } 29 | }); 30 | } 31 | Ok(()) 32 | } 33 | -------------------------------------------------------------------------------- /src/gestures/swipe.rs: -------------------------------------------------------------------------------- 1 | use knuffel::{Decode, DecodeScalar}; 2 | 3 | #[derive(Decode, Debug, Clone, PartialEq, Eq)] 4 | pub struct Swipe { 5 | #[knuffel(property)] 6 | pub direction: SwipeDir, 7 | #[knuffel(property)] 8 | pub fingers: i32, 9 | #[knuffel(property)] 10 | pub update: Option, 11 | #[knuffel(property)] 12 | pub start: Option, 13 | #[knuffel(property)] 14 | pub end: Option, 15 | #[knuffel(property)] 16 | pub acceleration: Option, 17 | #[knuffel(property)] 18 | pub mouse_up_delay: Option, 19 | } 20 | 21 | /// Direction of swipe gestures 22 | /// 23 | /// NW N NE 24 | /// W C E 25 | /// SW S SE 26 | #[derive(DecodeScalar, Debug, Clone, PartialEq, Eq)] 27 | pub enum SwipeDir { 28 | Any, 29 | N, 30 | S, 31 | E, 32 | W, 33 | NE, 34 | NW, 35 | SE, 36 | SW, 37 | } 38 | 39 | impl SwipeDir { 40 | pub fn dir(x: f64, y: f64) -> SwipeDir { 41 | use std::f64::consts::FRAC_PI_8; 42 | 43 | if x == 0.0 && y == 0.0 { 44 | return SwipeDir::Any; 45 | } 46 | 47 | let angle = y.atan2(x); // Range: -π to π 48 | 49 | match angle { 50 | a if a < -7.0 * FRAC_PI_8 => SwipeDir::W, // -π to -7π/8 51 | a if a < -5.0 * FRAC_PI_8 => SwipeDir::NW, // -7π/8 to -5π/8 52 | a if a < -3.0 * FRAC_PI_8 => SwipeDir::N, // -5π/8 to -3π/8 53 | a if a < -FRAC_PI_8 => SwipeDir::NE, // -3π/8 to -π/8 54 | a if a < FRAC_PI_8 => SwipeDir::E, // -π/8 to π/8 55 | a if a < 3.0 * FRAC_PI_8 => SwipeDir::SE, // π/8 to 3π/8 56 | a if a < 5.0 * FRAC_PI_8 => SwipeDir::S, // 3π/8 to 5π/8 57 | a if a < 7.0 * FRAC_PI_8 => SwipeDir::SW, // 5π/8 to 7π/8 58 | _ => SwipeDir::W, // 7π/8 to π 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /flake.nix: -------------------------------------------------------------------------------- 1 | { 2 | description = "A fast libinput-based touchpad gestures program"; 3 | 4 | inputs = { 5 | nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable"; 6 | flake-compat = { 7 | url = "github:edolstra/flake-compat"; 8 | flake = false; 9 | }; 10 | utils.url = "github:numtide/flake-utils"; 11 | fenix.url = "github:nix-community/fenix"; 12 | crane = { 13 | url = "github:ipetkov/crane"; 14 | }; 15 | }; 16 | 17 | outputs = { nixpkgs, utils, fenix, crane, ... }: 18 | let 19 | name = "gestures"; 20 | in utils.lib.eachSystem 21 | [ 22 | utils.lib.system.x86_64-linux 23 | ] 24 | (system: 25 | let 26 | toolchain = fenix.packages.${system}.fromToolchainFile { 27 | file = ./rust-toolchain.toml; 28 | sha256 = "sha256-SJwZ8g0zF2WrKDVmHrVG3pD2RGoQeo24MEXnNx5FyuI="; 29 | }; 30 | 31 | pkgs = import nixpkgs { 32 | inherit system; 33 | }; 34 | 35 | craneLib = (crane.mkLib pkgs).overrideToolchain toolchain; 36 | 37 | buildInputs = with pkgs; [ libinput udev xdotool ]; 38 | nativeBuildInputs = with pkgs; [ pkg-config makeWrapper ]; 39 | in 40 | rec { 41 | packages = { 42 | ${name} = craneLib.buildPackage { 43 | pname = name; 44 | src = craneLib.cleanCargoSource ./.; 45 | 46 | inherit buildInputs nativeBuildInputs; 47 | 48 | # Set runtime library paths 49 | runtimeDependencies = buildInputs; 50 | 51 | # Ensure dynamic libraries can be found at runtime 52 | postInstall = '' 53 | wrapProgram $out/bin/${name} \ 54 | --prefix LD_LIBRARY_PATH : ${pkgs.lib.makeLibraryPath buildInputs} 55 | ''; 56 | }; 57 | default = packages.${name}; 58 | }; 59 | 60 | apps = { 61 | ${name} = utils.lib.mkApp { 62 | inherit name; 63 | drv = packages.${name}; 64 | }; 65 | default = apps.${name}; 66 | }; 67 | 68 | devShells.default = craneLib.devShell { 69 | inherit buildInputs; 70 | nativeBuildInputs = nativeBuildInputs ++ [ toolchain pkgs.nixpkgs-fmt ]; 71 | RUST_SRC_PATH = "${pkgs.rust.packages.stable.rustPlatform.rustLibSrc}"; 72 | }; 73 | } 74 | ); 75 | } 76 | -------------------------------------------------------------------------------- /.github/scripts/build-linux.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -euo pipefail 3 | 4 | # Build script for maximum Linux compatibility using manylinux 5 | # This script runs inside a manylinux Docker container 6 | 7 | echo "==> Building gestures for maximum Linux compatibility" 8 | echo "Target: ${TARGET}" 9 | 10 | # Show proxy configuration if set 11 | if [ -n "${http_proxy:-}" ] || [ -n "${https_proxy:-}" ]; then 12 | echo "Proxy configuration:" 13 | [ -n "${http_proxy:-}" ] && echo " http_proxy: ${http_proxy}" 14 | [ -n "${https_proxy:-}" ] && echo " https_proxy: ${https_proxy}" 15 | fi 16 | 17 | # Set up Cargo home 18 | export CARGO_HOME="${CARGO_HOME:-/rust/cargo}" 19 | export RUSTUP_HOME="${RUSTUP_HOME:-/rust/rustup}" 20 | 21 | # Install Rust if not present 22 | if ! command -v rustc &> /dev/null; then 23 | echo "==> Installing Rust toolchain" 24 | curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --default-toolchain stable --profile minimal 25 | source "${CARGO_HOME}/env" 26 | else 27 | echo "==> Using cached Rust toolchain" 28 | source "${CARGO_HOME}/env" 29 | fi 30 | 31 | # Add target if needed 32 | rustup target add "${TARGET}" || echo "Target already added" 33 | 34 | # Install system dependencies 35 | echo "==> Installing system dependencies" 36 | yum install -y \ 37 | systemd-devel \ 38 | libinput-devel \ 39 | libxdo-devel \ 40 | libevdev-devel \ 41 | gcc \ 42 | gcc-c++ \ 43 | make \ 44 | pkgconfig 45 | 46 | # Build the project 47 | echo "==> Building release binary" 48 | cargo build --release --target "${TARGET}" --verbose 49 | 50 | # Strip the binary 51 | echo "==> Stripping binary" 52 | strip "target/${TARGET}/release/gestures" 53 | 54 | # Verify glibc compatibility 55 | echo "==> Checking glibc version requirement" 56 | echo "glibc requirement:" 57 | objdump -T "target/${TARGET}/release/gestures" | grep GLIBC | sed 's/.*GLIBC_\([.0-9]*\).*/\1/g' | sort -Vu | tail -1 58 | 59 | echo "==> Build complete" 60 | ls -lh "target/${TARGET}/release/gestures" 61 | 62 | # Create tarball if ASSET_NAME is provided 63 | if [ -n "${ASSET_NAME:-}" ]; then 64 | echo "==> Creating tarball" 65 | cd "target/${TARGET}/release" 66 | tar czf "${ASSET_NAME}.tar.gz" gestures 67 | mv "${ASSET_NAME}.tar.gz" ../../.. 68 | cd ../../.. 69 | echo "Tarball created: ${ASSET_NAME}.tar.gz" 70 | ls -lh "${ASSET_NAME}.tar.gz" 71 | fi 72 | 73 | # Fix permissions for non-root users 74 | echo "==> Fixing file permissions" 75 | chmod -R a+rw target/ || true 76 | [ -f "${ASSET_NAME}.tar.gz" ] && chmod a+rw "${ASSET_NAME}.tar.gz" || true 77 | -------------------------------------------------------------------------------- /src/ipc.rs: -------------------------------------------------------------------------------- 1 | use parking_lot::RwLock; 2 | use std::env; 3 | use std::io::{BufRead, BufReader}; 4 | use std::os::unix::net::{UnixListener, UnixStream}; 5 | use std::sync::Arc; 6 | use std::thread; 7 | use std::time::Duration; 8 | 9 | use crate::config::Config; 10 | 11 | pub fn create_socket(config: Arc>) { 12 | let socket_dir = env::var("XDG_RUNTIME_DIR").unwrap_or("/tmp".to_string()); 13 | let socket_path = format!("{}/gestures.sock", socket_dir); 14 | 15 | if std::path::Path::new(&socket_path).exists() { 16 | std::fs::remove_file(&socket_path).expect("Could not remove existing socket file"); 17 | } 18 | 19 | let listener = UnixListener::bind(&socket_path).unwrap(); 20 | 21 | // Set non-blocking mode 22 | listener 23 | .set_nonblocking(true) 24 | .expect("Cannot set non-blocking"); 25 | 26 | // Cleanup socket on shutdown 27 | let socket_path_clone = socket_path.clone(); 28 | let cleanup = move || { 29 | let _ = std::fs::remove_file(&socket_path_clone); 30 | }; 31 | 32 | // Register cleanup handler 33 | let socket_path_for_handler = socket_path.clone(); 34 | ctrlc::set_handler(move || { 35 | let _ = std::fs::remove_file(&socket_path_for_handler); 36 | std::process::exit(0); 37 | }) 38 | .unwrap(); 39 | 40 | loop { 41 | // Check shutdown flag 42 | if crate::SHUTDOWN.load(std::sync::atomic::Ordering::Relaxed) { 43 | log::info!("IPC listener shutting down"); 44 | cleanup(); 45 | break; 46 | } 47 | 48 | match listener.accept() { 49 | Ok((stream, _)) => { 50 | let config = config.clone(); 51 | thread::spawn(|| handle_connection(stream, config)); 52 | } 53 | Err(ref e) if e.kind() == std::io::ErrorKind::WouldBlock => { 54 | // No incoming connection, sleep briefly and check again 55 | thread::sleep(Duration::from_millis(100)); 56 | } 57 | Err(e) => { 58 | eprintln!("Got error while handling IPC connection: {e}"); 59 | break; 60 | } 61 | } 62 | } 63 | } 64 | 65 | fn handle_connection(stream: UnixStream, config: Arc>) { 66 | let stream = BufReader::new(stream); 67 | 68 | for line in stream.lines() { 69 | if line.unwrap().contains("reload") { 70 | let mut c = config.write(); 71 | *c = Config::read_default_config().unwrap_or_else(|_| { 72 | log::error!("Could not read configuration file, using empty config!"); 73 | Config::default() 74 | }); 75 | } 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: ["main"] 6 | paths: 7 | - "src/**" 8 | - "Cargo.toml" 9 | - "Cargo.lock" 10 | - ".github/workflows/ci.yml" 11 | pull_request: 12 | branches: ["main"] 13 | paths: 14 | - "src/**" 15 | - "Cargo.toml" 16 | - "Cargo.lock" 17 | - ".github/workflows/ci.yml" 18 | workflow_dispatch: 19 | 20 | env: 21 | CARGO_TERM_COLOR: always 22 | RUST_BACKTRACE: 1 23 | 24 | concurrency: 25 | group: ${{ github.workflow }}-${{ github.ref }} 26 | cancel-in-progress: true 27 | 28 | jobs: 29 | lint: 30 | name: Lint 31 | runs-on: ubuntu-latest 32 | timeout-minutes: 10 33 | 34 | steps: 35 | - name: Checkout code 36 | uses: actions/checkout@v4 37 | 38 | - name: Install Rust toolchain 39 | uses: dtolnay/rust-toolchain@stable 40 | with: 41 | components: rustfmt, clippy 42 | 43 | - name: Setup Rust cache 44 | uses: Swatinem/rust-cache@v2 45 | with: 46 | shared-key: "ci-ubuntu" 47 | save-if: ${{ github.ref == 'refs/heads/main' }} 48 | 49 | - name: Install system dependencies 50 | run: | 51 | sudo apt-get update 52 | sudo apt-get install -y libudev-dev libinput-dev libxdo-dev 53 | 54 | - name: Check formatting 55 | run: cargo fmt --all -- --check 56 | 57 | - name: Run clippy 58 | run: cargo clippy --all-targets --all-features -- -D warnings 59 | 60 | test: 61 | name: Test 62 | runs-on: ubuntu-latest 63 | needs: lint 64 | timeout-minutes: 15 65 | 66 | steps: 67 | - name: Checkout code 68 | uses: actions/checkout@v4 69 | 70 | - name: Install Rust toolchain 71 | uses: dtolnay/rust-toolchain@stable 72 | 73 | - name: Setup Rust cache 74 | uses: Swatinem/rust-cache@v2 75 | with: 76 | shared-key: "ci-ubuntu" 77 | save-if: ${{ github.ref == 'refs/heads/main' }} 78 | 79 | - name: Install system dependencies 80 | run: | 81 | sudo apt-get update 82 | sudo apt-get install -y libudev-dev libinput-dev libxdo-dev 83 | 84 | - name: Build 85 | run: cargo build --verbose 86 | 87 | - name: Run tests 88 | run: cargo test --verbose 89 | 90 | build-release: 91 | name: Build Release 92 | runs-on: ubuntu-latest 93 | needs: [test, lint] 94 | timeout-minutes: 15 95 | 96 | steps: 97 | - name: Checkout code 98 | uses: actions/checkout@v4 99 | 100 | - name: Install Rust toolchain 101 | uses: dtolnay/rust-toolchain@stable 102 | 103 | - name: Setup Rust cache 104 | uses: Swatinem/rust-cache@v2 105 | with: 106 | shared-key: "ci-ubuntu" 107 | save-if: ${{ github.ref == 'refs/heads/main' }} 108 | 109 | - name: Install system dependencies 110 | run: | 111 | sudo apt-get update 112 | sudo apt-get install -y libudev-dev libinput-dev libxdo-dev 113 | 114 | - name: Build release 115 | run: cargo build --release --verbose 116 | 117 | - name: Upload build artifacts 118 | uses: actions/upload-artifact@v4 119 | with: 120 | name: gestures-linux-x86_64 121 | path: target/release/gestures 122 | if-no-files-found: error 123 | -------------------------------------------------------------------------------- /flake.lock: -------------------------------------------------------------------------------- 1 | { 2 | "nodes": { 3 | "crane": { 4 | "locked": { 5 | "lastModified": 1759893430, 6 | "narHash": "sha256-yAy4otLYm9iZ+NtQwTMEbqHwswSFUbhn7x826RR6djw=", 7 | "owner": "ipetkov", 8 | "repo": "crane", 9 | "rev": "1979a2524cb8c801520bd94c38bb3d5692419d93", 10 | "type": "github" 11 | }, 12 | "original": { 13 | "owner": "ipetkov", 14 | "repo": "crane", 15 | "type": "github" 16 | } 17 | }, 18 | "fenix": { 19 | "inputs": { 20 | "nixpkgs": "nixpkgs", 21 | "rust-analyzer-src": "rust-analyzer-src" 22 | }, 23 | "locked": { 24 | "lastModified": 1735626869, 25 | "narHash": "sha256-hWGkpAWB59YWAOtBC6AE3DDnhMrBaqtiOaw1g+/mdLU=", 26 | "owner": "nix-community", 27 | "repo": "fenix", 28 | "rev": "120e688c881f2233f053dca5a5ddb8945d8ca5d7", 29 | "type": "github" 30 | }, 31 | "original": { 32 | "owner": "nix-community", 33 | "repo": "fenix", 34 | "type": "github" 35 | } 36 | }, 37 | "flake-compat": { 38 | "flake": false, 39 | "locked": { 40 | "lastModified": 1733328505, 41 | "narHash": "sha256-NeCCThCEP3eCl2l/+27kNNK7QrwZB1IJCrXfrbv5oqU=", 42 | "owner": "edolstra", 43 | "repo": "flake-compat", 44 | "rev": "ff81ac966bb2cae68946d5ed5fc4994f96d0ffec", 45 | "type": "github" 46 | }, 47 | "original": { 48 | "owner": "edolstra", 49 | "repo": "flake-compat", 50 | "type": "github" 51 | } 52 | }, 53 | "nixpkgs": { 54 | "locked": { 55 | "lastModified": 1735471104, 56 | "narHash": "sha256-0q9NGQySwDQc7RhAV2ukfnu7Gxa5/ybJ2ANT8DQrQrs=", 57 | "owner": "nixos", 58 | "repo": "nixpkgs", 59 | "rev": "88195a94f390381c6afcdaa933c2f6ff93959cb4", 60 | "type": "github" 61 | }, 62 | "original": { 63 | "owner": "nixos", 64 | "ref": "nixos-unstable", 65 | "repo": "nixpkgs", 66 | "type": "github" 67 | } 68 | }, 69 | "nixpkgs_2": { 70 | "locked": { 71 | "lastModified": 1735471104, 72 | "narHash": "sha256-0q9NGQySwDQc7RhAV2ukfnu7Gxa5/ybJ2ANT8DQrQrs=", 73 | "owner": "nixos", 74 | "repo": "nixpkgs", 75 | "rev": "88195a94f390381c6afcdaa933c2f6ff93959cb4", 76 | "type": "github" 77 | }, 78 | "original": { 79 | "owner": "nixos", 80 | "ref": "nixos-unstable", 81 | "repo": "nixpkgs", 82 | "type": "github" 83 | } 84 | }, 85 | "root": { 86 | "inputs": { 87 | "crane": "crane", 88 | "fenix": "fenix", 89 | "flake-compat": "flake-compat", 90 | "nixpkgs": "nixpkgs_2", 91 | "utils": "utils" 92 | } 93 | }, 94 | "rust-analyzer-src": { 95 | "flake": false, 96 | "locked": { 97 | "lastModified": 1735570005, 98 | "narHash": "sha256-ekN1mLeHM9upiAXykoNm646ctsm0qcS8+G2SjGtXp5k=", 99 | "owner": "rust-lang", 100 | "repo": "rust-analyzer", 101 | "rev": "1c6b83852b0d3bc129a3558386663373f126337e", 102 | "type": "github" 103 | }, 104 | "original": { 105 | "owner": "rust-lang", 106 | "ref": "nightly", 107 | "repo": "rust-analyzer", 108 | "type": "github" 109 | } 110 | }, 111 | "systems": { 112 | "locked": { 113 | "lastModified": 1681028828, 114 | "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", 115 | "owner": "nix-systems", 116 | "repo": "default", 117 | "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", 118 | "type": "github" 119 | }, 120 | "original": { 121 | "owner": "nix-systems", 122 | "repo": "default", 123 | "type": "github" 124 | } 125 | }, 126 | "utils": { 127 | "inputs": { 128 | "systems": "systems" 129 | }, 130 | "locked": { 131 | "lastModified": 1731533236, 132 | "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=", 133 | "owner": "numtide", 134 | "repo": "flake-utils", 135 | "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b", 136 | "type": "github" 137 | }, 138 | "original": { 139 | "owner": "numtide", 140 | "repo": "flake-utils", 141 | "type": "github" 142 | } 143 | } 144 | }, 145 | "root": "root", 146 | "version": 7 147 | } 148 | -------------------------------------------------------------------------------- /config.md: -------------------------------------------------------------------------------- 1 | # Gestures Configuration 2 | 3 | ## Location 4 | Configuration file is searched in order: 5 | 1. `$XDG_CONFIG_HOME/gestures.kdl` 6 | 2. `$XDG_CONFIG_HOME/gestures/gestures.kdl` 7 | 3. `~/.config/gestures.kdl` (if XDG_CONFIG_HOME is unset) 8 | 9 | ## Format 10 | Uses [KDL](https://kdl.dev) configuration language (since v0.5.0). 11 | 12 | ## Swipe Gestures 13 | 14 | ### Basic Syntax 15 | ```kdl 16 | swipe direction="" fingers= [start=""] [update=""] [end=""] 17 | ``` 18 | 19 | **Parameters:** 20 | - `direction`: `n`, `s`, `e`, `w`, `ne`, `nw`, `se`, `sw`, or `any` 21 | - `fingers`: Number of fingers (typically 3 or 4) 22 | - `start`: Command executed when gesture begins (optional) 23 | - `update`: Command executed on each movement update (optional) 24 | - `end`: Command executed when gesture ends (optional) 25 | 26 | **Variable Substitution:** 27 | In commands, these variables are replaced with actual values: 28 | - `$delta_x`: Horizontal movement delta 29 | - `$delta_y`: Vertical movement delta 30 | - `$scale`: Pinch scale (for pinch gestures) 31 | - `$delta_angle`: Rotation angle (for pinch gestures) 32 | 33 | ### 3-Finger Drag (macOS-like) 34 | 35 | **Works on both X11 and Wayland:** 36 | ```kdl 37 | swipe direction="any" fingers=3 mouse-up-delay=500 acceleration=20 38 | ``` 39 | 40 | **Parameters:** 41 | - `mouse-up-delay`: Delay in milliseconds before releasing mouse button (allows finger to leave trackpad temporarily) 42 | - `acceleration`: Mouse speed multiplier (20 = 2x speed, 10 = 1x speed) 43 | 44 | **Requirements:** 45 | - X11: Install `xdotool` 46 | - Wayland: Install `ydotool` and run `ydotoold` daemon 47 | 48 | **How it works:** 49 | - X11: Uses libxdo API directly (minimal latency) 50 | - Wayland: Uses timer-scheduled ydotool commands (optimized with 60 FPS throttling) 51 | 52 | ### Manual Wayland Control 53 | If you prefer full control over Wayland commands: 54 | ```kdl 55 | swipe direction="any" fingers=3 \ 56 | start="ydotool click -- 0x40" \ 57 | update="ydotool mousemove -x $delta_x -y $delta_y" \ 58 | end="ydotool click -- 0x80" 59 | ``` 60 | 61 | ### Workspace Switching Examples 62 | 63 | **Hyprland:** 64 | ```kdl 65 | swipe direction="w" fingers=4 end="hyprctl dispatch workspace e-1" 66 | swipe direction="e" fingers=4 end="hyprctl dispatch workspace e+1" 67 | swipe direction="n" fingers=4 end="hyprctl dispatch fullscreen" 68 | swipe direction="s" fingers=4 end="hyprctl dispatch killactive" 69 | ``` 70 | 71 | **i3/Sway:** 72 | ```kdl 73 | swipe direction="w" fingers=4 end="i3-msg workspace prev" 74 | swipe direction="e" fingers=4 end="i3-msg workspace next" 75 | ``` 76 | 77 | **GNOME:** 78 | ```kdl 79 | swipe direction="n" fingers=4 end="gdbus call --session --dest org.gnome.Shell --object-path /org/gnome/Shell --method org.gnome.Shell.Eval global.workspace_manager.get_active_workspace().get_neighbor(Meta.MotionDirection.UP).activate(global.get_current_time())" 80 | ``` 81 | 82 | ## Pinch Gestures 83 | 84 | ### Syntax 85 | ```kdl 86 | pinch direction="" fingers= [start=""] [update=""] [end=""] 87 | ``` 88 | 89 | ### Examples 90 | ```kdl 91 | // Zoom in browser 92 | pinch direction="out" fingers=2 end="xdotool key ctrl+plus" 93 | pinch direction="in" fingers=2 end="xdotool key ctrl+minus" 94 | 95 | // With continuous updates 96 | pinch direction="out" fingers=2 \ 97 | update="notify-send 'Scaling: $scale'" 98 | ``` 99 | 100 | ## Hold Gestures 101 | 102 | ### Syntax 103 | ```kdl 104 | hold fingers= action="" 105 | ``` 106 | 107 | ### Examples 108 | ```kdl 109 | // Show launcher 110 | hold fingers=4 action="rofi -show drun" 111 | 112 | // Screenshot 113 | hold fingers=3 action="flameshot gui" 114 | ``` 115 | 116 | ## Complete Example Configuration 117 | 118 | ```kdl 119 | // 3-finger drag (X11 + Wayland) 120 | swipe direction="any" fingers=3 mouse-up-delay=500 acceleration=20 121 | 122 | // Workspace navigation 123 | swipe direction="w" fingers=4 end="hyprctl dispatch workspace e-1" 124 | swipe direction="e" fingers=4 end="hyprctl dispatch workspace e+1" 125 | 126 | // Application launcher 127 | swipe direction="n" fingers=4 end="rofi -show drun" 128 | 129 | // Close window 130 | swipe direction="s" fingers=4 end="hyprctl dispatch killactive" 131 | 132 | // Browser zoom 133 | pinch direction="in" fingers=2 end="xdotool key ctrl+minus" 134 | pinch direction="out" fingers=2 end="xdotool key ctrl+plus" 135 | 136 | // App launcher on hold 137 | hold fingers=4 action="rofi -show drun" 138 | ``` 139 | 140 | ## Tips 141 | 142 | 1. **Test commands first**: Run commands manually before adding to config 143 | 2. **Reload config**: `gestures reload` (no restart needed) 144 | 3. **Wayland ydotool**: Ensure `ydotoold` daemon is running 145 | 4. **Disable DE gestures**: Prevent conflicts with built-in gestures 146 | 5. **Check logs**: Run `journalctl --user -u gestures -f` for debugging 147 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Gestures 2 | 3 | > This fork focuses on high-performance three-finger dragging with optimizations for both X11 and Wayland. 4 | > 5 | > For technical details, see: https://github.com/riley-martin/gestures/discussions/6 6 | > 7 | > Pre-compiled binaries: https://github.com/ferstar/gestures/releases 8 | > 9 | > Install via cargo: `cargo install --git https://github.com/ferstar/gestures.git` 10 | 11 | ## About 12 | A libinput-based touchpad gesture handler that executes commands based on gestures. 13 | Unlike alternatives, it uses the libinput API directly for better performance and reliability. 14 | 15 | ## Features 16 | - **Platform Support**: Both X11 and Wayland 17 | - **High Performance**: 18 | - X11: Direct libxdo API for minimal latency 19 | - Wayland: Optimized ydotool integration with 60 FPS throttling 20 | - Thread pool for command execution (4 workers, prevents PID exhaustion) 21 | - **Gesture Types**: Swipe (8 directions + any), Pinch, Hold 22 | - **Advanced Features**: 23 | - Mouse acceleration and delay for smooth 3-finger dragging 24 | - Real-time config reload via IPC 25 | - Graceful shutdown (SIGTERM/SIGINT) 26 | 27 | ## Configuration 28 | See [config.md](./config.md) for detailed configuration instructions. 29 | 30 | ### Quick Setup 31 | ```bash 32 | # Generate default config file 33 | gestures generate-config 34 | 35 | # Preview config without installing 36 | gestures generate-config --print 37 | 38 | # Force overwrite existing config 39 | gestures generate-config --force 40 | ``` 41 | 42 | ### Quick Example 43 | ```kdl 44 | // 3-finger drag (works on both X11 and Wayland) 45 | swipe direction="any" fingers=3 mouse-up-delay=500 acceleration=20 46 | 47 | // 4-finger workspace switching 48 | swipe direction="w" fingers=4 end="hyprctl dispatch workspace e-1" 49 | swipe direction="e" fingers=4 end="hyprctl dispatch workspace e+1" 50 | ``` 51 | 52 | ## Installation 53 | 54 | ### Prerequisites 55 | **System packages:** 56 | - `libudev-dev` / `libudev-devel` 57 | - `libinput-dev` / `libinput-devel` 58 | - `libxdo-dev` / `libxdo-devel` 59 | 60 | **Runtime dependencies:** 61 | - X11: `xdotool` (for 3-finger drag) 62 | - Wayland: `ydotool` + `ydotoold` daemon (for 3-finger drag) 63 | - If your distribution package has issues, try the official [ydotool binaries from GitHub releases](https://github.com/ReimuNotMoe/ydotool/releases) 64 | 65 | ### With Cargo 66 | ```bash 67 | cargo install --git https://github.com/ferstar/gestures.git 68 | ``` 69 | 70 | ### Manual Build 71 | ```bash 72 | git clone https://github.com/ferstar/gestures 73 | cd gestures 74 | cargo build --release 75 | sudo cp target/release/gestures /usr/local/bin/ 76 | ``` 77 | 78 | ### Nix Flakes 79 | ```nix 80 | # flake.nix 81 | { 82 | inputs.gestures.url = "github:ferstar/gestures"; 83 | 84 | # Then add to packages: 85 | # inputs.gestures.packages.${system}.gestures 86 | } 87 | ``` 88 | 89 | ## Running 90 | 91 | ### Systemd (Recommended) 92 | ```bash 93 | # 1. Generate config file (first time only) 94 | gestures generate-config 95 | 96 | # 2. Install service file 97 | gestures install-service 98 | 99 | # 3. Enable and start the service 100 | systemctl --user enable --now gestures.service 101 | ``` 102 | 103 | ### Manual 104 | ```bash 105 | # Auto-detect display server (X11 or Wayland) 106 | gestures start 107 | 108 | # Force Wayland mode (if needed) 109 | gestures --wayland start 110 | 111 | # Force X11 mode (if needed) 112 | gestures --x11 start 113 | 114 | # Reload config 115 | gestures reload 116 | 117 | # Preview service file (without installing) 118 | gestures install-service --print 119 | ``` 120 | 121 | **Note**: The display server (X11/Wayland) is automatically detected via `WAYLAND_DISPLAY` and `XDG_SESSION_TYPE` environment variables. Manual override is rarely needed. 122 | 123 | ## Performance Optimizations 124 | 125 | This fork includes several performance improvements: 126 | 127 | 1. **Regex Caching**: One-time compilation using `once_cell::Lazy` 128 | 2. **Thread Pool**: 4-worker pool prevents PID exhaustion during fast gestures 129 | 3. **FPS Throttling**: 60 FPS limit for Wayland (considering ydotool ~100ms latency) 130 | 4. **Timer-based Delays**: Non-blocking mouse-up delays for smooth dragging 131 | 5. **Event Caching**: 1-second cache for gesture configuration lookups 132 | 133 | ## Troubleshooting 134 | 135 | ### High CPU on Wayland 136 | - Default 60 FPS throttle should keep CPU <5% 137 | - Adjust in `src/event_handler.rs` line 89 if needed 138 | 139 | ### 3-Finger Drag Not Working 140 | **X11:** 141 | - Ensure `xdotool` is installed: `which xdotool` 142 | 143 | **Wayland:** 144 | - If your distribution package has issues, try the official [ydotool binaries from GitHub releases](https://github.com/ReimuNotMoe/ydotool/releases) 145 | - Ensure `ydotoold` daemon is running: `systemctl --user status ydotoold` 146 | - Configure uinput permissions (see [issue #4](https://github.com/ferstar/gestures/issues/4)) 147 | 148 | ### Conflicts with DE Gestures 149 | Disable built-in gestures in your desktop environment (GNOME, KDE, etc.) 150 | 151 | ## Alternatives 152 | - [libinput-gestures](https://github.com/bulletmark/libinput-gestures) - Parses debug output 153 | - [gebaar](https://github.com/Coffee2CodeNL/gebaar-libinput) - Swipe only 154 | - [fusuma](https://github.com/iberianpig/fusuma) - Ruby-based 155 | -------------------------------------------------------------------------------- /src/mouse_handler.rs: -------------------------------------------------------------------------------- 1 | use chrono::Duration; 2 | use libxdo::XDo; 3 | use std::env; 4 | use std::path::Path; 5 | use std::process::Command; 6 | use std::sync::mpsc; 7 | use std::thread; 8 | use timer::Timer; 9 | 10 | #[derive(Copy, Clone)] 11 | pub enum MouseCommand { 12 | MouseUp, 13 | MouseDown, 14 | MoveMouseRelative, 15 | } 16 | 17 | pub struct MouseHandler { 18 | tx: Option>, 19 | timer: Timer, 20 | guard: Option, 21 | handler_mouse_down: bool, 22 | } 23 | 24 | /// Try to setup X11 environment variables by detecting XAUTHORITY file 25 | fn setup_x11_env() { 26 | // Ensure DISPLAY is set 27 | if env::var("DISPLAY").is_err() { 28 | env::set_var("DISPLAY", ":0"); 29 | log::info!("DISPLAY not set, using default :0"); 30 | } 31 | 32 | // Check if XAUTHORITY is already set and valid 33 | if let Ok(xauth) = env::var("XAUTHORITY") { 34 | if Path::new(&xauth).exists() { 35 | log::debug!("XAUTHORITY already set to: {}", xauth); 36 | return; 37 | } 38 | } 39 | 40 | // Try to find XAUTHORITY in common locations 41 | let home = env::var("HOME").unwrap_or_default(); 42 | let possible_paths = vec![ 43 | format!("{}/.Xauthority", home), 44 | "/tmp/.Xauthority".to_string(), 45 | ]; 46 | 47 | // Also check /tmp for dynamic xauth files (pattern: /tmp/xauth_*) 48 | if let Ok(entries) = std::fs::read_dir("/tmp") { 49 | for entry in entries.flatten() { 50 | let path = entry.path(); 51 | if let Some(name) = path.file_name() { 52 | if name.to_string_lossy().starts_with("xauth_") { 53 | if let Some(path_str) = path.to_str() { 54 | env::set_var("XAUTHORITY", path_str); 55 | log::info!("Set XAUTHORITY to: {}", path_str); 56 | return; 57 | } 58 | } 59 | } 60 | } 61 | } 62 | 63 | // Try the standard locations 64 | for path in possible_paths { 65 | if Path::new(&path).exists() { 66 | env::set_var("XAUTHORITY", &path); 67 | log::info!("Set XAUTHORITY to: {}", path); 68 | return; 69 | } 70 | } 71 | 72 | log::warn!("Could not find XAUTHORITY file, X11 initialization may fail"); 73 | } 74 | 75 | pub fn start_handler(is_xorg: bool) -> MouseHandler { 76 | let tx = if is_xorg { 77 | // Setup X11 environment before initializing XDo 78 | setup_x11_env(); 79 | 80 | let (tx, rx) = mpsc::channel(); 81 | thread::spawn(move || { 82 | // Try to initialize XDo, fallback to None if it fails 83 | let xdo_result = XDo::new(None); 84 | 85 | match xdo_result { 86 | Ok(xdo) => { 87 | log::info!("Successfully initialized libxdo for X11"); 88 | while let Ok((command, param1, param2)) = rx.recv() { 89 | let _ = match command { 90 | MouseCommand::MouseDown => xdo.mouse_down(param1), 91 | MouseCommand::MouseUp => xdo.mouse_up(param1), 92 | MouseCommand::MoveMouseRelative => { 93 | xdo.move_mouse_relative(param1, param2) 94 | } 95 | }; 96 | } 97 | } 98 | Err(e) => { 99 | log::error!("Failed to initialize libxdo: {:?}", e); 100 | log::warn!("X11 mouse control will not work. Consider:"); 101 | log::warn!(" 1. Running with --wayland flag to use ydotool instead"); 102 | log::warn!(" 2. Checking DISPLAY and XAUTHORITY environment variables"); 103 | log::warn!(" 3. Installing ydotool for Wayland support"); 104 | // Don't panic, just exit the thread - will fallback to ydotool mode 105 | } 106 | } 107 | }); 108 | Some(tx) 109 | } else { 110 | None 111 | }; 112 | 113 | MouseHandler { 114 | tx, 115 | timer: Timer::new(), 116 | guard: None, 117 | handler_mouse_down: false, 118 | } 119 | } 120 | 121 | impl MouseHandler { 122 | pub fn mouse_down(&mut self, button: i32) { 123 | self.cancel_timer_if_present(); 124 | if let Some(ref tx) = self.tx { 125 | let _ = tx.send((MouseCommand::MouseDown, button, 255)); 126 | } else { 127 | let _ = Command::new("ydotool") 128 | .args(["click", "--", "0x40"]) 129 | .spawn(); 130 | } 131 | self.handler_mouse_down = true; 132 | } 133 | 134 | pub fn mouse_up_delay(&mut self, button: i32, delay_ms: i64) { 135 | if let Some(ref tx) = self.tx { 136 | let tx_clone = tx.clone(); 137 | self.guard = Some(self.timer.schedule_with_delay( 138 | Duration::milliseconds(delay_ms), 139 | move || { 140 | let _ = tx_clone.send((MouseCommand::MouseUp, button, 255)); 141 | }, 142 | )); 143 | } else { 144 | self.guard = Some(self.timer.schedule_with_delay( 145 | Duration::milliseconds(delay_ms), 146 | move || { 147 | let _ = Command::new("ydotool") 148 | .args(["click", "--", "0x80"]) 149 | .spawn(); 150 | }, 151 | )); 152 | } 153 | self.handler_mouse_down = false; 154 | } 155 | 156 | pub fn move_mouse_relative(&mut self, x_val: i32, y_val: i32) { 157 | self.cancel_timer_if_present(); 158 | if let Some(ref tx) = self.tx { 159 | let _ = tx.send((MouseCommand::MoveMouseRelative, x_val, y_val)); 160 | } else { 161 | let _ = Command::new("ydotool") 162 | .args([ 163 | "mousemove", 164 | "-x", 165 | &x_val.to_string(), 166 | "-y", 167 | &y_val.to_string(), 168 | ]) 169 | .spawn(); 170 | } 171 | } 172 | 173 | fn cancel_timer_if_present(&mut self) { 174 | if self.guard.is_some() { 175 | self.guard = None; 176 | self.handler_mouse_down = true; 177 | } 178 | } 179 | } 180 | -------------------------------------------------------------------------------- /CLAUDE.md: -------------------------------------------------------------------------------- 1 | # CLAUDE.md 2 | 3 | This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. 4 | 5 | ## Project Overview 6 | 7 | A high-performance libinput-based touchpad gesture handler focused on optimizing three-finger dragging. Supports both X11 and Wayland display servers. 8 | 9 | **Core Features:** 10 | - Direct libinput API usage (no debug output parsing) 11 | - X11: libxdo API for minimal latency 12 | - Wayland: ydotool with 60 FPS throttling optimization 13 | - Three gesture types: Swipe, Pinch, Hold 14 | - Thread pool for command execution (4 workers, prevents PID exhaustion) 15 | - Real-time config reload via IPC 16 | 17 | ## Build and Test 18 | 19 | ### Development Commands 20 | 21 | ```bash 22 | # Build project (release version) 23 | cargo build --release 24 | 25 | # Run tests 26 | cargo test 27 | 28 | # Run a specific test 29 | cargo test 30 | 31 | # Run with verbose logging 32 | cargo run -- -vv start 33 | 34 | # Lint and format checking 35 | cargo fmt --all -- --check # Check code formatting 36 | cargo fmt --all # Auto-format code 37 | cargo clippy --all-targets --all-features -- -D warnings # Run clippy with warnings as errors 38 | 39 | # Using Nix (if available) 40 | nix build 41 | 42 | # Development environment (Nix) 43 | nix develop 44 | ``` 45 | 46 | ### System Dependencies 47 | 48 | **Build-time dependencies:** 49 | - libudev-dev / libudev-devel 50 | - libinput-dev / libinput-devel 51 | - libxdo-dev / libxdo-devel 52 | 53 | **Runtime dependencies:** 54 | - X11 mode: xdotool (for 3-finger drag) 55 | - Wayland mode: ydotool + ydotoold daemon (for 3-finger drag) 56 | 57 | ## Code Architecture 58 | 59 | ### Module Structure 60 | 61 | ``` 62 | src/ 63 | ├── main.rs # Entry point: CLI parsing, signal handling, display server detection 64 | ├── event_handler.rs # Core event handler: libinput event loop, gesture recognition 65 | ├── mouse_handler.rs # Mouse control abstraction: X11 (libxdo) vs Wayland (ydotool) 66 | ├── config.rs # Configuration parsing (KDL format) 67 | ├── ipc.rs # IPC server (Unix socket) for config reload 68 | ├── ipc_client.rs # IPC client 69 | ├── utils.rs # Command execution, variable substitution utilities 70 | └── gestures/ 71 | ├── mod.rs # Gesture type definitions 72 | ├── swipe.rs # Swipe gestures (8 directions + any) 73 | ├── pinch.rs # Pinch gestures (in/out) 74 | └── hold.rs # Hold gestures 75 | ``` 76 | 77 | ### Key Design Patterns 78 | 79 | **1. Display Server Auto-detection (main.rs:32-45)** 80 | - Checks `WAYLAND_DISPLAY` environment variable (most reliable) 81 | - Falls back to `XDG_SESSION_TYPE` 82 | - Defaults to X11 if unable to detect 83 | - Can be forced via `--wayland` or `--x11` flags 84 | 85 | **2. MouseHandler Abstraction (mouse_handler.rs)** 86 | - X11 mode: Creates dedicated thread running libxdo, communicates via mpsc channel 87 | - Wayland mode: Directly invokes ydotool commands 88 | - X11 initialization failure logs error but doesn't panic (allows fallback to Wayland mode) 89 | - Uses Timer for non-blocking mouse-up delays (for 3-finger drag) 90 | 91 | **3. Performance Optimizations (event_handler.rs)** 92 | - **Gesture Cache** (GestureCache): Groups gesture configs by finger count, refreshes every second 93 | - **FPS Throttling** (ThrottleState): 60 FPS limit for Wayland updates (accounting for ydotool ~100ms latency) 94 | - **Regex Caching**: One-time compilation using `once_cell::Lazy` (utils.rs) 95 | - **Thread Pool**: 4 worker threads for command execution (prevents PID exhaustion during fast gestures) 96 | 97 | **4. IPC Config Reload (ipc.rs)** 98 | - Creates Unix socket at `$XDG_RUNTIME_DIR/gestures.sock` 99 | - Non-blocking mode, periodically checks SHUTDOWN flag 100 | - Updates shared config using RwLock when "reload" command received 101 | 102 | **5. Direct Mouse Control Detection (event_handler.rs:344-350)** 103 | ```rust 104 | fn is_direct_mouse_gesture(gesture: &Gesture) -> bool { 105 | if let Gesture::Swipe(j) = gesture { 106 | j.acceleration.is_some() && j.mouse_up_delay.is_some() && j.direction == SwipeDir::Any 107 | } else { 108 | false 109 | } 110 | } 111 | ``` 112 | This function identifies 3-finger drag gestures (direction="any" + mouse-up-delay + acceleration) to use direct mouse control instead of command execution. 113 | 114 | ### Configuration System 115 | 116 | - Uses KDL format (via knuffel crate) 117 | - Config search order: 118 | 1. `$XDG_CONFIG_HOME/gestures.kdl` 119 | 2. `$XDG_CONFIG_HOME/gestures/gestures.kdl` 120 | 3. `~/.config/gestures.kdl` 121 | - Supports variable substitution: `$delta_x`, `$delta_y`, `$scale`, `$delta_angle` 122 | 123 | ## Common Development Tasks 124 | 125 | ### Modifying Gesture Handling Logic 126 | 127 | Main handler functions in `event_handler.rs`: 128 | - `handle_swipe_event()` - Swipe gestures 129 | - `handle_pinch_event()` - Pinch gestures 130 | - `handle_hold_event()` - Hold gestures 131 | 132 | ### Adding New Gesture Types 133 | 134 | 1. Add new variant to `Gesture` enum in `src/gestures/mod.rs` 135 | 2. Create new module file in `src/gestures/` 136 | 3. Add handling branch in `handle_event()` in `event_handler.rs` 137 | 4. Update KDL parsing in `config.rs` (via Decode trait) 138 | 139 | ### Adjusting Performance Parameters 140 | 141 | - **FPS Throttling**: Modify `ThrottleState::new(60)` in `event_handler.rs:89` 142 | - **Cache Refresh Interval**: Modify `Duration::from_secs(1)` in `event_handler.rs:330` 143 | - **Thread Pool Size**: Modify thread pool configuration in `utils.rs` 144 | 145 | ### Debugging 146 | 147 | ```bash 148 | # View verbose logs 149 | RUST_LOG=debug gestures start 150 | 151 | # Or use command-line flags 152 | gestures -vv start # Very verbose 153 | gestures -v start # Info level 154 | gestures -d start # Debug level 155 | 156 | # With systemd 157 | journalctl --user -u gestures -f 158 | ``` 159 | 160 | ## Important Constraints 161 | 162 | 1. **X11 Environment Detection** (mouse_handler.rs:24-73): 163 | - Automatically attempts to set `DISPLAY` and `XAUTHORITY` 164 | - Searches common locations: `~/.Xauthority`, `/tmp/xauth_*` 165 | - Gracefully degrades if libxdo initialization fails (logs warning but doesn't panic) 166 | 167 | 2. **Graceful Shutdown**: 168 | - Uses global `SHUTDOWN` atomic boolean flag 169 | - Registers SIGTERM and SIGINT signal handlers 170 | - Both event loop and IPC listener check shutdown flag 171 | 172 | 3. **3-Finger Drag Requirements**: 173 | - Must set both `mouse-up-delay` and `acceleration` 174 | - `direction` must be "any" 175 | - X11: Requires successful libxdo initialization 176 | - Wayland: Requires ydotoold daemon running 177 | 178 | 4. **Thread Safety**: 179 | - Config shared between threads using `Arc>` 180 | - MouseHandler communicates with dedicated thread via mpsc channel 181 | - Uses `parking_lot::RwLock` (faster than std) 182 | 183 | ## Testing Strategy 184 | 185 | - Unit tests located in `src/tests/mod.rs` 186 | - Integration tests require touchpad device, typically manual testing 187 | - Recommended manual testing workflow after modifying gesture logic: 188 | 1. Generate config: `gestures generate-config` 189 | 2. Start service: `gestures start` 190 | 3. Test various gestures 191 | 4. Modify config: Edit `~/.config/gestures.kdl` 192 | 5. Reload: `gestures reload` 193 | 194 | ## CI/CD 195 | 196 | - GitHub Actions workflows in `.github/workflows/` 197 | - **CI Pipeline** (`ci.yml`): 198 | 1. Lint: Runs `cargo fmt --check` and `cargo clippy` (warnings treated as errors) 199 | 2. Test: Runs `cargo test` after lint passes 200 | 3. Build Release: Builds release binary after tests pass 201 | - Supports Nix builds (flake.nix) 202 | - Automatically builds binaries and uploads to GitHub Releases on release 203 | 204 | ### Pre-commit Checks 205 | 206 | Before committing code, ensure: 207 | ```bash 208 | cargo fmt --all 209 | cargo clippy --all-targets --all-features -- -D warnings 210 | cargo test 211 | ``` 212 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | tags: 6 | - "v*" 7 | workflow_dispatch: 8 | inputs: 9 | version: 10 | description: "Version to release (e.g., 0.8.0, leave empty to use Cargo.toml version)" 11 | required: false 12 | type: string 13 | skip_tests: 14 | description: "Skip tests before building" 15 | required: false 16 | default: false 17 | type: boolean 18 | create_tag: 19 | description: "Create and push git tag" 20 | required: false 21 | default: true 22 | type: boolean 23 | create_github_release: 24 | description: "Create GitHub Release" 25 | required: false 26 | default: true 27 | type: boolean 28 | 29 | permissions: 30 | contents: write 31 | 32 | env: 33 | CARGO_TERM_COLOR: always 34 | RUST_BACKTRACE: 1 35 | 36 | jobs: 37 | lint: 38 | name: Lint 39 | runs-on: ubuntu-latest 40 | timeout-minutes: 10 41 | if: github.event_name == 'push' || (github.event_name == 'workflow_dispatch' && !inputs.skip_tests) 42 | 43 | steps: 44 | - name: Checkout code 45 | uses: actions/checkout@v4 46 | 47 | - name: Install Rust toolchain 48 | uses: dtolnay/rust-toolchain@stable 49 | with: 50 | components: rustfmt, clippy 51 | 52 | - name: Setup Rust cache 53 | uses: Swatinem/rust-cache@v2 54 | with: 55 | shared-key: "release-ubuntu" 56 | 57 | - name: Install system dependencies 58 | run: | 59 | sudo apt-get update 60 | sudo apt-get install -y libudev-dev libinput-dev libxdo-dev 61 | 62 | - name: Check formatting 63 | run: cargo fmt --all -- --check 64 | 65 | - name: Run clippy 66 | run: cargo clippy --all-targets --all-features -- -D warnings 67 | 68 | test: 69 | name: Test before release 70 | runs-on: ubuntu-latest 71 | needs: lint 72 | timeout-minutes: 15 73 | if: github.event_name == 'push' || (github.event_name == 'workflow_dispatch' && !inputs.skip_tests) 74 | 75 | steps: 76 | - name: Checkout code 77 | uses: actions/checkout@v4 78 | 79 | - name: Install Rust toolchain 80 | uses: dtolnay/rust-toolchain@stable 81 | with: 82 | components: rustfmt, clippy 83 | 84 | - name: Setup Rust cache 85 | uses: Swatinem/rust-cache@v2 86 | with: 87 | shared-key: "release-ubuntu" 88 | 89 | - name: Install system dependencies 90 | run: | 91 | sudo apt-get update 92 | sudo apt-get install -y libudev-dev libinput-dev libxdo-dev 93 | 94 | - name: Run tests 95 | run: cargo test --verbose 96 | 97 | build: 98 | name: Build ${{ matrix.target }} 99 | runs-on: ${{ matrix.os }} 100 | needs: [lint, test] 101 | if: always() && (needs.lint.result == 'success' || needs.lint.result == 'skipped') && (needs.test.result == 'success' || needs.test.result == 'skipped') 102 | timeout-minutes: 30 103 | strategy: 104 | fail-fast: false 105 | matrix: 106 | include: 107 | - os: ubuntu-latest 108 | target: x86_64-unknown-linux-gnu 109 | artifact_name: gestures 110 | asset_name: gestures-linux-x86_64 111 | container: quay.io/pypa/manylinux2014_x86_64 112 | - os: ubuntu-24.04-arm 113 | target: aarch64-unknown-linux-gnu 114 | artifact_name: gestures 115 | asset_name: gestures-linux-aarch64 116 | container: '' 117 | 118 | steps: 119 | - name: Checkout code 120 | uses: actions/checkout@v4 121 | with: 122 | fetch-depth: 0 123 | 124 | - name: Update version if specified 125 | if: github.event.inputs.version != '' 126 | run: | 127 | VERSION="${{ github.event.inputs.version }}" 128 | sed -i "s/^version = \".*\"/version = \"$VERSION\"/" Cargo.toml 129 | echo "Updated version to $VERSION" 130 | 131 | - name: Make build script executable 132 | if: matrix.container != '' 133 | run: chmod +x .github/scripts/build-linux.sh 134 | 135 | - name: Set up Docker cache directories 136 | if: matrix.container != '' 137 | run: | 138 | mkdir -p ~/.cargo-cache 139 | mkdir -p ~/.rustup-cache 140 | 141 | - name: Cache Rust toolchain and dependencies (Docker) 142 | if: matrix.container != '' 143 | uses: actions/cache@v4 144 | with: 145 | path: | 146 | ~/.cargo-cache 147 | ~/.rustup-cache 148 | key: ${{ runner.os }}-manylinux-rust-${{ matrix.target }}-${{ hashFiles('**/Cargo.lock') }} 149 | restore-keys: | 150 | ${{ runner.os }}-manylinux-rust-${{ matrix.target }}- 151 | ${{ runner.os }}-manylinux-rust- 152 | 153 | - name: Build in manylinux container 154 | if: matrix.container != '' 155 | run: | 156 | docker run --rm \ 157 | -v "$(pwd)":/workspace \ 158 | -v "$HOME/.cargo-cache":/rust/cargo \ 159 | -v "$HOME/.rustup-cache":/rust/rustup \ 160 | -w /workspace \ 161 | -e TARGET=${{ matrix.target }} \ 162 | -e ASSET_NAME=${{ matrix.asset_name }} \ 163 | -e CARGO_HOME=/rust/cargo \ 164 | -e RUSTUP_HOME=/rust/rustup \ 165 | ${{ matrix.container }} \ 166 | /workspace/.github/scripts/build-linux.sh 167 | 168 | # Native ARM64 build (no container) 169 | - name: Install Rust toolchain (native) 170 | if: matrix.container == '' 171 | uses: dtolnay/rust-toolchain@stable 172 | 173 | - name: Setup Rust cache (native) 174 | if: matrix.container == '' 175 | uses: Swatinem/rust-cache@v2 176 | with: 177 | shared-key: "release-${{ matrix.target }}" 178 | 179 | - name: Install system dependencies (native) 180 | if: matrix.container == '' 181 | run: | 182 | sudo apt-get update 183 | sudo apt-get install -y libudev-dev libinput-dev libxdo-dev 184 | 185 | - name: Build release (native) 186 | if: matrix.container == '' 187 | run: | 188 | cargo build --release --target ${{ matrix.target }} --verbose 189 | strip target/${{ matrix.target }}/release/gestures 190 | cd target/${{ matrix.target }}/release 191 | tar czf ${{ matrix.asset_name }}.tar.gz gestures 192 | mv ${{ matrix.asset_name }}.tar.gz ../../.. 193 | ls -lh ../../../${{ matrix.asset_name }}.tar.gz 194 | 195 | - name: Upload build artifacts 196 | uses: actions/upload-artifact@v4 197 | with: 198 | name: ${{ matrix.asset_name }} 199 | path: ${{ matrix.asset_name }}.tar.gz 200 | if-no-files-found: error 201 | 202 | create-tag: 203 | name: Create Git Tag 204 | runs-on: ubuntu-latest 205 | needs: build 206 | if: github.event_name == 'workflow_dispatch' && github.event.inputs.create_tag == 'true' && github.event.inputs.version != '' 207 | steps: 208 | - name: Checkout code 209 | uses: actions/checkout@v4 210 | 211 | - name: Create and push tag 212 | run: | 213 | VERSION="${{ github.event.inputs.version }}" 214 | git config user.name "github-actions[bot]" 215 | git config user.email "github-actions[bot]@users.noreply.github.com" 216 | git tag -a "v${VERSION}" -m "Release v${VERSION}" 217 | git push origin "v${VERSION}" 218 | echo "Created and pushed tag v${VERSION}" 219 | 220 | github-release: 221 | name: Create GitHub Release 222 | runs-on: ubuntu-latest 223 | needs: build 224 | if: always() && (github.event_name == 'push' || (github.event_name == 'workflow_dispatch' && github.event.inputs.create_github_release == 'true')) && needs.build.result == 'success' 225 | 226 | steps: 227 | - name: Checkout code 228 | uses: actions/checkout@v4 229 | with: 230 | fetch-depth: 0 231 | 232 | - name: Download all artifacts 233 | uses: actions/download-artifact@v4 234 | with: 235 | path: artifacts/ 236 | 237 | - name: Extract version 238 | id: get_version 239 | run: | 240 | if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then 241 | if [ -n "${{ github.event.inputs.version }}" ]; then 242 | VERSION="${{ github.event.inputs.version }}" 243 | else 244 | VERSION=$(grep '^version =' Cargo.toml | head -1 | sed 's/version = "\(.*\)"/\1/') 245 | fi 246 | TAG="v${VERSION}" 247 | else 248 | VERSION=${GITHUB_REF#refs/tags/v} 249 | TAG=${GITHUB_REF#refs/tags/} 250 | fi 251 | echo "version=$VERSION" >> $GITHUB_OUTPUT 252 | echo "tag=$TAG" >> $GITHUB_OUTPUT 253 | echo "Version: $VERSION, Tag: $TAG" 254 | 255 | - name: Ensure tag exists 256 | run: | 257 | TAG="${{ steps.get_version.outputs.tag }}" 258 | if ! git rev-parse "$TAG" >/dev/null 2>&1; then 259 | echo "Tag $TAG does not exist, creating it..." 260 | git config user.name "github-actions[bot]" 261 | git config user.email "github-actions[bot]@users.noreply.github.com" 262 | git tag -a "$TAG" -m "Release $TAG" 263 | git push origin "$TAG" 264 | echo "Created and pushed tag $TAG" 265 | else 266 | echo "Tag $TAG already exists" 267 | fi 268 | 269 | - name: Generate changelog 270 | id: changelog 271 | run: | 272 | TAG="${{ steps.get_version.outputs.tag }}" 273 | PREV_TAG=$(git describe --tags --abbrev=0 "$TAG^" 2>/dev/null || echo "") 274 | 275 | if [ -z "$PREV_TAG" ]; then 276 | CHANGELOG=$(git log --pretty=format:"- %s (%h)" --no-merges) 277 | else 278 | CHANGELOG=$(git log ${PREV_TAG}..${TAG} --pretty=format:"- %s (%h)" --no-merges) 279 | fi 280 | 281 | echo "$CHANGELOG" > changelog.txt 282 | 283 | { 284 | echo 'changelog<> $GITHUB_OUTPUT 288 | 289 | - name: Prepare release assets 290 | run: | 291 | mkdir -p release-assets 292 | find artifacts -type f -name "*.tar.gz" -exec cp {} release-assets/ \; 293 | ls -lh release-assets/ 294 | 295 | - name: Create GitHub Release 296 | uses: softprops/action-gh-release@v2 297 | with: 298 | tag_name: ${{ steps.get_version.outputs.tag }} 299 | name: Release ${{ steps.get_version.outputs.tag }} 300 | body_path: changelog.txt 301 | files: release-assets/* 302 | draft: false 303 | prerelease: false 304 | generate_release_notes: true 305 | env: 306 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 307 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | mod config; 2 | mod event_handler; 3 | mod gestures; 4 | mod ipc; 5 | mod ipc_client; 6 | mod mouse_handler; 7 | mod utils; 8 | 9 | #[cfg(test)] 10 | mod tests; 11 | 12 | use parking_lot::RwLock; 13 | use std::{ 14 | env, fs, 15 | io::Write, 16 | path::PathBuf, 17 | sync::{atomic::AtomicBool, Arc, LazyLock}, 18 | thread::{self, JoinHandle}, 19 | }; 20 | 21 | use clap::{Parser, Subcommand}; 22 | use env_logger::Builder; 23 | use log::LevelFilter; 24 | use miette::Result; 25 | 26 | use crate::config::*; 27 | use crate::mouse_handler::start_handler; 28 | 29 | pub static SHUTDOWN: LazyLock> = LazyLock::new(|| Arc::new(AtomicBool::new(false))); 30 | 31 | /// Detect if running under Wayland by checking environment variables 32 | fn detect_wayland() -> bool { 33 | // Check WAYLAND_DISPLAY first (most reliable indicator) 34 | if std::env::var("WAYLAND_DISPLAY").is_ok() { 35 | return true; 36 | } 37 | 38 | // Check XDG_SESSION_TYPE as fallback 39 | if let Ok(session_type) = std::env::var("XDG_SESSION_TYPE") { 40 | return session_type.to_lowercase() == "wayland"; 41 | } 42 | 43 | // Default to X11 if unable to detect 44 | false 45 | } 46 | 47 | /// Generate systemd user service file content 48 | fn generate_service_file() -> Result { 49 | let exe_path = env::current_exe() 50 | .map_err(|e| miette::miette!("Failed to get current executable path: {}", e))?; 51 | 52 | let exe_path_str = exe_path 53 | .to_str() 54 | .ok_or_else(|| miette::miette!("Executable path contains invalid UTF-8"))?; 55 | 56 | let service_content = format!( 57 | r#"[Unit] 58 | Description=Touchpad Gestures (with 3-finger drag performance improvements) 59 | Documentation=https://github.com/ferstar/gestures 60 | 61 | [Service] 62 | Environment=PATH=/usr/local/bin:/usr/local/sbin:/usr/bin:/bin 63 | Type=simple 64 | ExecStart={} start 65 | ExecReload={} reload 66 | Restart=no 67 | 68 | [Install] 69 | WantedBy=default.target 70 | "#, 71 | exe_path_str, exe_path_str 72 | ); 73 | 74 | Ok(service_content) 75 | } 76 | 77 | /// Install or print systemd user service file 78 | fn install_service(print_only: bool) -> Result<()> { 79 | let service_content = generate_service_file()?; 80 | 81 | if print_only { 82 | print!("{}", service_content); 83 | return Ok(()); 84 | } 85 | 86 | // Get user's systemd directory 87 | let home = 88 | env::var("HOME").map_err(|_| miette::miette!("HOME environment variable not set"))?; 89 | 90 | let systemd_dir = PathBuf::from(home).join(".config/systemd/user"); 91 | let service_path = systemd_dir.join("gestures.service"); 92 | 93 | // Create directory if it doesn't exist 94 | fs::create_dir_all(&systemd_dir).map_err(|e| { 95 | miette::miette!( 96 | "Failed to create directory {}: {}", 97 | systemd_dir.display(), 98 | e 99 | ) 100 | })?; 101 | 102 | // Write service file 103 | let mut file = fs::File::create(&service_path).map_err(|e| { 104 | miette::miette!( 105 | "Failed to create service file {}: {}", 106 | service_path.display(), 107 | e 108 | ) 109 | })?; 110 | 111 | file.write_all(service_content.as_bytes()) 112 | .map_err(|e| miette::miette!("Failed to write service file: {}", e))?; 113 | 114 | println!("✓ Service file installed to: {}", service_path.display()); 115 | println!("\nTo enable and start the service, run:"); 116 | println!(" systemctl --user enable --now gestures.service"); 117 | println!("\nTo view service status:"); 118 | println!(" systemctl --user status gestures.service"); 119 | 120 | Ok(()) 121 | } 122 | 123 | /// Generate default configuration content 124 | fn get_default_config() -> &'static str { 125 | r#"// Gestures Configuration 126 | // See https://github.com/ferstar/gestures for full documentation 127 | 128 | // ==================== 129 | // 3-Finger Drag (macOS-like) 130 | // ==================== 131 | // Works on both X11 and Wayland 132 | // - X11: Uses libxdo API directly (minimal latency) 133 | // - Wayland: Uses ydotool (ensure ydotoold daemon is running) 134 | swipe direction="any" fingers=3 mouse-up-delay=500 acceleration=20 135 | 136 | // ==================== 137 | // 4-Finger Workspace Switching 138 | // ==================== 139 | // Uncomment and adjust for your desktop environment: 140 | 141 | // Hyprland: 142 | // swipe direction="w" fingers=4 end="hyprctl dispatch workspace e-1" 143 | // swipe direction="e" fingers=4 end="hyprctl dispatch workspace e+1" 144 | // swipe direction="n" fingers=4 end="hyprctl dispatch fullscreen" 145 | // swipe direction="s" fingers=4 end="hyprctl dispatch killactive" 146 | 147 | // i3/Sway: 148 | // swipe direction="w" fingers=4 end="i3-msg workspace prev" 149 | // swipe direction="e" fingers=4 end="i3-msg workspace next" 150 | 151 | // GNOME (requires gdbus): 152 | // swipe direction="n" fingers=4 end="gdbus call --session --dest org.gnome.Shell --object-path /org/gnome/Shell --method org.gnome.Shell.Eval global.workspace_manager.get_active_workspace().get_neighbor(Meta.MotionDirection.UP).activate(global.get_current_time())" 153 | 154 | // ==================== 155 | // Pinch Gestures 156 | // ==================== 157 | // Browser zoom: 158 | // pinch direction="out" fingers=2 end="xdotool key ctrl+plus" 159 | // pinch direction="in" fingers=2 end="xdotool key ctrl+minus" 160 | 161 | // ==================== 162 | // Hold Gestures 163 | // ==================== 164 | // Application launcher: 165 | // hold fingers=4 action="rofi -show drun" 166 | 167 | // Screenshot: 168 | // hold fingers=3 action="flameshot gui" 169 | "# 170 | } 171 | 172 | /// Generate or print default configuration file 173 | fn generate_config(print_only: bool, force: bool) -> Result<()> { 174 | let config_content = get_default_config(); 175 | 176 | if print_only { 177 | print!("{}", config_content); 178 | return Ok(()); 179 | } 180 | 181 | // Get config directory 182 | let config_home = env::var("XDG_CONFIG_HOME") 183 | .unwrap_or_else(|_| format!("{}/.config", env::var("HOME").unwrap())); 184 | 185 | let config_path = PathBuf::from(&config_home).join("gestures.kdl"); 186 | 187 | // Check if file exists 188 | if config_path.exists() && !force { 189 | return Err(miette::miette!( 190 | "Config file already exists at: {}\nUse --force to overwrite, or --print to view the default config", 191 | config_path.display() 192 | )); 193 | } 194 | 195 | // Write config file 196 | let mut file = fs::File::create(&config_path).map_err(|e| { 197 | miette::miette!( 198 | "Failed to create config file {}: {}", 199 | config_path.display(), 200 | e 201 | ) 202 | })?; 203 | 204 | file.write_all(config_content.as_bytes()) 205 | .map_err(|e| miette::miette!("Failed to write config file: {}", e))?; 206 | 207 | println!("✓ Configuration file created at: {}", config_path.display()); 208 | println!("\nEdit the file to customize your gestures:"); 209 | println!(" vim {}", config_path.display()); 210 | println!("\nAfter editing, reload the config:"); 211 | println!(" gestures reload"); 212 | println!("\nView full documentation:"); 213 | println!(" https://github.com/ferstar/gestures/blob/dev/config.md"); 214 | 215 | Ok(()) 216 | } 217 | 218 | fn main() -> Result<()> { 219 | let app = App::parse(); 220 | 221 | // Setup signal handlers for graceful shutdown 222 | signal_hook::flag::register(signal_hook::consts::SIGTERM, SHUTDOWN.clone()) 223 | .expect("Failed to register SIGTERM handler"); 224 | signal_hook::flag::register(signal_hook::consts::SIGINT, SHUTDOWN.clone()) 225 | .expect("Failed to register SIGINT handler"); 226 | 227 | { 228 | let mut l = Builder::from_default_env(); 229 | 230 | if app.verbose > 0 { 231 | l.filter_level(match app.verbose { 232 | 1 => LevelFilter::Info, 233 | 2 => LevelFilter::Debug, 234 | _ => LevelFilter::max(), 235 | }); 236 | } 237 | 238 | if app.debug { 239 | l.filter_level(LevelFilter::Debug); 240 | } 241 | 242 | l.init(); 243 | } 244 | 245 | let c = if let Some(p) = app.conf { 246 | Config::read_from_file(&p)? 247 | } else { 248 | config::Config::read_default_config().unwrap_or_else(|_| { 249 | log::error!("Could not read configuration file, using empty config!"); 250 | Config::default() 251 | }) 252 | }; 253 | log::debug!("{:#?}", &c); 254 | 255 | match app.command { 256 | c @ Commands::Reload => { 257 | ipc_client::handle_command(c); 258 | } 259 | Commands::Start => { 260 | let is_wayland = if app.wayland { 261 | log::info!("Forced Wayland mode via command line"); 262 | true 263 | } else if app.x11 { 264 | log::info!("Forced X11 mode via command line"); 265 | false 266 | } else { 267 | let detected = detect_wayland(); 268 | log::info!( 269 | "Auto-detected display server: {}", 270 | if detected { "Wayland" } else { "X11" } 271 | ); 272 | detected 273 | }; 274 | run_eh(Arc::new(RwLock::new(c)), is_wayland)?; 275 | } 276 | Commands::InstallService { print } => { 277 | install_service(print)?; 278 | } 279 | Commands::GenerateConfig { print, force } => { 280 | generate_config(print, force)?; 281 | } 282 | } 283 | 284 | Ok(()) 285 | } 286 | 287 | fn run_eh(config: Arc>, is_wayland: bool) -> Result<()> { 288 | let eh_thread = spawn_event_handler(config.clone(), is_wayland); 289 | ipc::create_socket(config); 290 | eh_thread.join().unwrap()?; 291 | Ok(()) 292 | } 293 | 294 | fn spawn_event_handler(config: Arc>, is_wayland: bool) -> JoinHandle> { 295 | thread::spawn(move || { 296 | log::debug!("Starting event handler in new thread"); 297 | let mut eh = event_handler::EventHandler::new(config); 298 | let mut interface = input::Libinput::new_with_udev(event_handler::Interface); 299 | eh.init(&mut interface)?; 300 | let _ = eh.main_loop(&mut interface, &mut start_handler(!is_wayland)); 301 | Ok(()) 302 | }) 303 | } 304 | 305 | #[derive(Parser, Debug)] 306 | #[command(author, version, about)] 307 | struct App { 308 | /// Verbosity, can be repeated 309 | #[arg(short, long, action = clap::ArgAction::Count)] 310 | verbose: u8, 311 | /// Debug mode 312 | #[arg(short, long)] 313 | debug: bool, 314 | /// Force Wayland mode (default: auto-detect via WAYLAND_DISPLAY/XDG_SESSION_TYPE) 315 | #[arg(short = 'w', long)] 316 | wayland: bool, 317 | /// Force X11 mode (default: auto-detect) 318 | #[arg(short = 'x', long, conflicts_with = "wayland")] 319 | x11: bool, 320 | /// Path to config file 321 | #[arg(short, long, value_name = "FILE")] 322 | conf: Option, 323 | #[command(subcommand)] 324 | command: Commands, 325 | } 326 | 327 | #[derive(Subcommand, Debug)] 328 | pub enum Commands { 329 | /// Reload the configuration 330 | Reload, 331 | /// Start the program 332 | Start, 333 | /// Install systemd user service 334 | InstallService { 335 | /// Print service file to stdout instead of installing 336 | #[arg(short = 'p', long)] 337 | print: bool, 338 | }, 339 | /// Generate default configuration file 340 | GenerateConfig { 341 | /// Print config to stdout instead of writing to file 342 | #[arg(short = 'p', long)] 343 | print: bool, 344 | /// Force overwrite existing config file 345 | #[arg(short = 'f', long)] 346 | force: bool, 347 | }, 348 | } 349 | -------------------------------------------------------------------------------- /src/event_handler.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | fs::OpenOptions, 3 | os::{ 4 | fd::{AsFd, OwnedFd}, 5 | unix::prelude::OpenOptionsExt, 6 | }, 7 | path::Path, 8 | sync::Arc, 9 | }; 10 | 11 | use input::{ 12 | event::{ 13 | gesture::{ 14 | GestureEndEvent, GestureEventCoordinates, GestureEventTrait, GestureHoldEvent, 15 | GesturePinchEvent, GesturePinchEventTrait, GestureSwipeEvent, 16 | }, 17 | Event, EventTrait, GestureEvent, 18 | }, 19 | DeviceCapability, Libinput, LibinputInterface, 20 | }; 21 | use miette::{miette, Result}; 22 | use nix::{ 23 | fcntl::OFlag, 24 | poll::{poll, PollFd, PollFlags, PollTimeout}, 25 | }; 26 | 27 | use crate::config::Config; 28 | use crate::gestures::{hold::*, pinch::*, swipe::*, *}; 29 | use crate::mouse_handler::MouseHandler; 30 | use crate::utils::exec_command_from_string; 31 | 32 | use parking_lot::RwLock; 33 | use std::collections::HashMap; 34 | 35 | #[derive(Debug)] 36 | struct GestureCache { 37 | swipe_gestures: HashMap>, 38 | last_update: std::time::Instant, 39 | } 40 | 41 | impl GestureCache { 42 | fn new() -> Self { 43 | Self { 44 | swipe_gestures: HashMap::new(), 45 | last_update: std::time::Instant::now(), 46 | } 47 | } 48 | } 49 | 50 | #[derive(Debug)] 51 | struct ThrottleState { 52 | last_update: std::time::Instant, 53 | min_interval: std::time::Duration, 54 | } 55 | 56 | impl ThrottleState { 57 | fn new(fps: u32) -> Self { 58 | Self { 59 | last_update: std::time::Instant::now(), 60 | min_interval: std::time::Duration::from_micros(1_000_000 / fps as u64), 61 | } 62 | } 63 | 64 | fn should_update(&mut self) -> bool { 65 | let now = std::time::Instant::now(); 66 | if now.duration_since(self.last_update) >= self.min_interval { 67 | self.last_update = now; 68 | true 69 | } else { 70 | false 71 | } 72 | } 73 | } 74 | 75 | #[derive(Debug)] 76 | pub struct EventHandler { 77 | config: Arc>, 78 | event: Gesture, 79 | cache: GestureCache, 80 | throttle: ThrottleState, 81 | } 82 | 83 | impl EventHandler { 84 | pub fn new(config: Arc>) -> Self { 85 | Self { 86 | config, 87 | event: Gesture::None, 88 | cache: GestureCache::new(), 89 | throttle: ThrottleState::new(60), 90 | } 91 | } 92 | 93 | pub fn init(&mut self, input: &mut Libinput) -> Result<()> { 94 | log::debug!("{:?} {:?}", &self, &input); 95 | self.init_ctx(input).expect("Could not initialize libinput"); 96 | if self.has_gesture_device(input) { 97 | Ok(()) 98 | } else { 99 | Err(miette!("Could not find gesture device")) 100 | } 101 | } 102 | 103 | fn init_ctx(&mut self, input: &mut Libinput) -> Result<(), ()> { 104 | input.udev_assign_seat("seat0")?; 105 | Ok(()) 106 | } 107 | 108 | fn has_gesture_device(&mut self, input: &mut Libinput) -> bool { 109 | log::debug!("Looking for gesture device"); 110 | if let Err(e) = input.dispatch() { 111 | log::error!("Failed to dispatch input events: {}", e); 112 | return false; 113 | } 114 | 115 | for event in &mut *input { 116 | if let Event::Device(e) = event { 117 | log::debug!("Device: {:?}", &e); 118 | if e.device().has_capability(DeviceCapability::Gesture) { 119 | log::debug!("Found gesture device"); 120 | return true; 121 | } 122 | } 123 | } 124 | 125 | log::debug!("No gesture device found"); 126 | false 127 | } 128 | 129 | pub fn main_loop(&mut self, input: &mut Libinput, mh: &mut MouseHandler) -> Result<()> { 130 | loop { 131 | if crate::SHUTDOWN.load(std::sync::atomic::Ordering::Relaxed) { 132 | log::info!("Received shutdown signal, exiting event loop"); 133 | break; 134 | } 135 | 136 | let mut fds = [PollFd::new(input.as_fd(), PollFlags::POLLIN)]; 137 | match poll(&mut fds, PollTimeout::try_from(100).unwrap()) { 138 | Ok(_) => { 139 | self.handle_event(input, mh)?; 140 | } 141 | Err(e) => { 142 | if e != nix::errno::Errno::EINTR { 143 | return Err(miette!("Poll error: {}", e)); 144 | } 145 | } 146 | } 147 | } 148 | Ok(()) 149 | } 150 | 151 | pub fn handle_event(&mut self, input: &mut Libinput, mh: &mut MouseHandler) -> Result<()> { 152 | input.dispatch().unwrap(); 153 | for event in input { 154 | if let Event::Gesture(e) = event { 155 | match e { 156 | GestureEvent::Pinch(e) => self.handle_pinch_event(e)?, 157 | GestureEvent::Swipe(e) => self.handle_swipe_event(e, mh)?, 158 | GestureEvent::Hold(e) => self.handle_hold_event(e)?, 159 | _ => (), 160 | } 161 | } 162 | } 163 | Ok(()) 164 | } 165 | 166 | fn handle_hold_event(&mut self, event: GestureHoldEvent) -> Result<()> { 167 | match event { 168 | GestureHoldEvent::Begin(e) => { 169 | self.event = Gesture::Hold(Hold { 170 | fingers: e.finger_count(), 171 | action: None, 172 | }) 173 | } 174 | GestureHoldEvent::End(_e) => { 175 | if let Gesture::Hold(s) = &self.event { 176 | log::debug!("Hold: {:?}", &s.fingers); 177 | for i in &self.config.clone().read().gestures { 178 | if let Gesture::Hold(j) = i { 179 | if j.fingers == s.fingers { 180 | exec_command_from_string( 181 | &j.action.clone().unwrap_or_default(), 182 | 0.0, 183 | 0.0, 184 | 0.0, 185 | 0.0, 186 | )?; 187 | } 188 | } 189 | } 190 | } 191 | } 192 | _ => (), 193 | } 194 | Ok(()) 195 | } 196 | 197 | fn handle_pinch_event(&mut self, event: GesturePinchEvent) -> Result<()> { 198 | match event { 199 | GesturePinchEvent::Begin(e) => { 200 | self.event = Gesture::Pinch(Pinch { 201 | fingers: e.finger_count(), 202 | direction: PinchDir::Any, 203 | update: None, 204 | start: None, 205 | end: None, 206 | }); 207 | if let Gesture::Pinch(s) = &self.event { 208 | for i in &self.config.clone().read().gestures { 209 | if let Gesture::Pinch(j) = i { 210 | if (j.direction == s.direction || j.direction == PinchDir::Any) 211 | && j.fingers == s.fingers 212 | { 213 | exec_command_from_string( 214 | &j.start.clone().unwrap_or_default(), 215 | 0.0, 216 | 0.0, 217 | 0.0, 218 | 0.0, 219 | )?; 220 | } 221 | } 222 | } 223 | } 224 | } 225 | GesturePinchEvent::Update(e) => { 226 | let scale = e.scale(); 227 | let delta_angle = e.angle_delta(); 228 | if let Gesture::Pinch(s) = &self.event { 229 | let dir = PinchDir::dir(scale, delta_angle); 230 | log::debug!( 231 | "Pinch: scale={:?} angle={:?} direction={:?} fingers={:?}", 232 | &scale, 233 | &delta_angle, 234 | &dir, 235 | &s.fingers 236 | ); 237 | for i in &self.config.clone().read().gestures { 238 | if let Gesture::Pinch(j) = i { 239 | if (j.direction == dir || j.direction == PinchDir::Any) 240 | && j.fingers == s.fingers 241 | { 242 | exec_command_from_string( 243 | &j.update.clone().unwrap_or_default(), 244 | 0.0, 245 | 0.0, 246 | delta_angle, 247 | scale, 248 | )?; 249 | } 250 | } 251 | } 252 | self.event = Gesture::Pinch(Pinch { 253 | fingers: s.fingers, 254 | direction: dir, 255 | update: None, 256 | start: None, 257 | end: None, 258 | }) 259 | } 260 | } 261 | GesturePinchEvent::End(_e) => { 262 | if let Gesture::Pinch(s) = &self.event { 263 | for i in &self.config.clone().read().gestures { 264 | if let Gesture::Pinch(j) = i { 265 | if (j.direction == s.direction || j.direction == PinchDir::Any) 266 | && j.fingers == s.fingers 267 | { 268 | exec_command_from_string( 269 | &j.end.clone().unwrap_or_default(), 270 | 0.0, 271 | 0.0, 272 | 0.0, 273 | 0.0, 274 | )?; 275 | } 276 | } 277 | } 278 | } 279 | } 280 | _ => (), 281 | } 282 | Ok(()) 283 | } 284 | 285 | fn handle_swipe_event( 286 | &mut self, 287 | event: GestureSwipeEvent, 288 | mh: &mut MouseHandler, 289 | ) -> Result<()> { 290 | match event { 291 | GestureSwipeEvent::Begin(e) => self.handle_swipe_begin(e.finger_count(), mh), 292 | GestureSwipeEvent::Update(e) => self.handle_swipe_update(e.dx(), e.dy(), mh), 293 | GestureSwipeEvent::End(e) => { 294 | if !e.cancelled() { 295 | self.handle_swipe_end(mh) 296 | } else { 297 | Ok(()) 298 | } 299 | } 300 | _ => Ok(()), 301 | } 302 | } 303 | 304 | fn update_cache(&mut self) { 305 | let config = self.config.read(); 306 | let mut swipe_map: HashMap> = HashMap::new(); 307 | 308 | for gesture in &config.gestures { 309 | if let Gesture::Swipe(swipe) = gesture { 310 | swipe_map 311 | .entry(swipe.fingers) 312 | .or_default() 313 | .push(gesture.clone()); 314 | } 315 | } 316 | 317 | self.cache.swipe_gestures = swipe_map; 318 | self.cache.last_update = std::time::Instant::now(); 319 | } 320 | 321 | fn handle_matching_gesture( 322 | &mut self, 323 | fingers: i32, 324 | mh: &mut MouseHandler, 325 | handler: F, 326 | ) -> Result<()> 327 | where 328 | F: Fn(&Gesture, &mut MouseHandler) -> Result<()>, 329 | { 330 | if self.cache.last_update.elapsed() > std::time::Duration::from_secs(1) { 331 | self.update_cache(); 332 | } 333 | 334 | if let Gesture::Swipe(_) = &self.event { 335 | if let Some(gestures) = self.cache.swipe_gestures.get(&fingers) { 336 | for gesture in gestures { 337 | handler(gesture, mh)?; 338 | } 339 | } 340 | } 341 | Ok(()) 342 | } 343 | 344 | fn is_direct_mouse_gesture(gesture: &Gesture) -> bool { 345 | if let Gesture::Swipe(j) = gesture { 346 | j.acceleration.is_some() && j.mouse_up_delay.is_some() && j.direction == SwipeDir::Any 347 | } else { 348 | false 349 | } 350 | } 351 | 352 | fn handle_swipe_begin(&mut self, fingers: i32, mh: &mut MouseHandler) -> Result<()> { 353 | self.event = Gesture::Swipe(Swipe::new(fingers)); 354 | 355 | self.handle_matching_gesture(fingers, mh, |gesture, mh| { 356 | if Self::is_direct_mouse_gesture(gesture) { 357 | log::debug!("Using direct mouse control"); 358 | mh.mouse_down(1); 359 | } else if let Gesture::Swipe(j) = gesture { 360 | if j.direction == SwipeDir::Any { 361 | exec_command_from_string(j.start.as_deref().unwrap_or(""), 0.0, 0.0, 0.0, 0.0)?; 362 | } 363 | } 364 | Ok(()) 365 | }) 366 | } 367 | 368 | fn handle_swipe_update(&mut self, dx: f64, dy: f64, mh: &mut MouseHandler) -> Result<()> { 369 | let swipe_dir = SwipeDir::dir(dx, dy); 370 | let (fingers, current_dir) = if let Gesture::Swipe(s) = &self.event { 371 | (s.fingers, swipe_dir.clone()) 372 | } else { 373 | return Ok(()); 374 | }; 375 | 376 | log::debug!("{:?} {:?}", ¤t_dir, &fingers); 377 | 378 | let should_throttle_update = !self.throttle.should_update(); 379 | 380 | let current_dir = current_dir.clone(); 381 | self.handle_matching_gesture(fingers, mh, move |gesture, mh| { 382 | if let Gesture::Swipe(j) = gesture { 383 | if Self::is_direct_mouse_gesture(gesture) { 384 | let acceleration = j.acceleration.unwrap_or_default() as f64 / 10.0; 385 | mh.move_mouse_relative((dx * acceleration) as i32, (dy * acceleration) as i32); 386 | } else if (j.direction == current_dir || j.direction == SwipeDir::Any) 387 | && !should_throttle_update 388 | { 389 | exec_command_from_string(j.update.as_deref().unwrap_or(""), dx, dy, 0.0, 0.0)?; 390 | } 391 | } 392 | Ok(()) 393 | })?; 394 | 395 | self.event = Gesture::Swipe(Swipe::with_direction(fingers, swipe_dir)); 396 | Ok(()) 397 | } 398 | 399 | fn handle_swipe_end(&mut self, mh: &mut MouseHandler) -> Result<()> { 400 | let (fingers, direction) = if let Gesture::Swipe(s) = &self.event { 401 | (s.fingers, s.direction.clone()) 402 | } else { 403 | return Ok(()); 404 | }; 405 | self.handle_matching_gesture(fingers, mh, |gesture, mh| { 406 | if let Gesture::Swipe(j) = gesture { 407 | if Self::is_direct_mouse_gesture(gesture) { 408 | let delay = j.mouse_up_delay.unwrap_or_default(); 409 | mh.mouse_up_delay(1, delay); 410 | } else if j.direction == direction || j.direction == SwipeDir::Any { 411 | exec_command_from_string(j.end.as_deref().unwrap_or(""), 0.0, 0.0, 0.0, 0.0)?; 412 | } 413 | } 414 | Ok(()) 415 | }) 416 | } 417 | } 418 | 419 | // Add this helper impl 420 | impl Swipe { 421 | fn new(fingers: i32) -> Self { 422 | Self { 423 | direction: SwipeDir::Any, 424 | fingers, 425 | update: None, 426 | start: None, 427 | end: None, 428 | acceleration: None, 429 | mouse_up_delay: None, 430 | } 431 | } 432 | 433 | fn with_direction(fingers: i32, direction: SwipeDir) -> Self { 434 | Self { 435 | direction, 436 | fingers, 437 | update: None, 438 | start: None, 439 | end: None, 440 | acceleration: None, 441 | mouse_up_delay: None, 442 | } 443 | } 444 | } 445 | 446 | pub struct Interface; 447 | 448 | impl LibinputInterface for Interface { 449 | #[inline] 450 | fn open_restricted(&mut self, path: &Path, flags: i32) -> Result { 451 | OpenOptions::new() 452 | .custom_flags(flags) 453 | .read(flags & OFlag::O_RDWR.bits() != 0) 454 | .write((flags & OFlag::O_WRONLY.bits() != 0) | (flags & OFlag::O_RDWR.bits() != 0)) 455 | .open(path) 456 | .map(Into::into) 457 | .map_err(|err| err.raw_os_error().unwrap_or(-1)) 458 | } 459 | 460 | #[inline] 461 | fn close_restricted(&mut self, fd: OwnedFd) { 462 | drop(fd); 463 | } 464 | } 465 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 4 4 | 5 | [[package]] 6 | name = "addr2line" 7 | version = "0.25.1" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "1b5d307320b3181d6d7954e663bd7c774a838b8220fe0593c86d9fb09f498b4b" 10 | dependencies = [ 11 | "gimli", 12 | ] 13 | 14 | [[package]] 15 | name = "adler2" 16 | version = "2.0.1" 17 | source = "registry+https://github.com/rust-lang/crates.io-index" 18 | checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" 19 | 20 | [[package]] 21 | name = "ahash" 22 | version = "0.8.12" 23 | source = "registry+https://github.com/rust-lang/crates.io-index" 24 | checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" 25 | dependencies = [ 26 | "cfg-if", 27 | "once_cell", 28 | "version_check", 29 | "zerocopy", 30 | ] 31 | 32 | [[package]] 33 | name = "aho-corasick" 34 | version = "1.1.3" 35 | source = "registry+https://github.com/rust-lang/crates.io-index" 36 | checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" 37 | dependencies = [ 38 | "memchr", 39 | ] 40 | 41 | [[package]] 42 | name = "allocator-api2" 43 | version = "0.2.21" 44 | source = "registry+https://github.com/rust-lang/crates.io-index" 45 | checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" 46 | 47 | [[package]] 48 | name = "android_system_properties" 49 | version = "0.1.5" 50 | source = "registry+https://github.com/rust-lang/crates.io-index" 51 | checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" 52 | dependencies = [ 53 | "libc", 54 | ] 55 | 56 | [[package]] 57 | name = "anstream" 58 | version = "0.6.21" 59 | source = "registry+https://github.com/rust-lang/crates.io-index" 60 | checksum = "43d5b281e737544384e969a5ccad3f1cdd24b48086a0fc1b2a5262a26b8f4f4a" 61 | dependencies = [ 62 | "anstyle", 63 | "anstyle-parse", 64 | "anstyle-query", 65 | "anstyle-wincon", 66 | "colorchoice", 67 | "is_terminal_polyfill", 68 | "utf8parse", 69 | ] 70 | 71 | [[package]] 72 | name = "anstyle" 73 | version = "1.0.13" 74 | source = "registry+https://github.com/rust-lang/crates.io-index" 75 | checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78" 76 | 77 | [[package]] 78 | name = "anstyle-parse" 79 | version = "0.2.7" 80 | source = "registry+https://github.com/rust-lang/crates.io-index" 81 | checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" 82 | dependencies = [ 83 | "utf8parse", 84 | ] 85 | 86 | [[package]] 87 | name = "anstyle-query" 88 | version = "1.1.4" 89 | source = "registry+https://github.com/rust-lang/crates.io-index" 90 | checksum = "9e231f6134f61b71076a3eab506c379d4f36122f2af15a9ff04415ea4c3339e2" 91 | dependencies = [ 92 | "windows-sys 0.60.2", 93 | ] 94 | 95 | [[package]] 96 | name = "anstyle-wincon" 97 | version = "3.0.10" 98 | source = "registry+https://github.com/rust-lang/crates.io-index" 99 | checksum = "3e0633414522a32ffaac8ac6cc8f748e090c5717661fddeea04219e2344f5f2a" 100 | dependencies = [ 101 | "anstyle", 102 | "once_cell_polyfill", 103 | "windows-sys 0.60.2", 104 | ] 105 | 106 | [[package]] 107 | name = "autocfg" 108 | version = "1.5.0" 109 | source = "registry+https://github.com/rust-lang/crates.io-index" 110 | checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" 111 | 112 | [[package]] 113 | name = "backtrace" 114 | version = "0.3.76" 115 | source = "registry+https://github.com/rust-lang/crates.io-index" 116 | checksum = "bb531853791a215d7c62a30daf0dde835f381ab5de4589cfe7c649d2cbe92bd6" 117 | dependencies = [ 118 | "addr2line", 119 | "cfg-if", 120 | "libc", 121 | "miniz_oxide", 122 | "object", 123 | "rustc-demangle", 124 | "windows-link", 125 | ] 126 | 127 | [[package]] 128 | name = "backtrace-ext" 129 | version = "0.2.1" 130 | source = "registry+https://github.com/rust-lang/crates.io-index" 131 | checksum = "537beee3be4a18fb023b570f80e3ae28003db9167a751266b259926e25539d50" 132 | dependencies = [ 133 | "backtrace", 134 | ] 135 | 136 | [[package]] 137 | name = "base64" 138 | version = "0.21.7" 139 | source = "registry+https://github.com/rust-lang/crates.io-index" 140 | checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" 141 | 142 | [[package]] 143 | name = "bitflags" 144 | version = "2.10.0" 145 | source = "registry+https://github.com/rust-lang/crates.io-index" 146 | checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" 147 | 148 | [[package]] 149 | name = "bumpalo" 150 | version = "3.19.0" 151 | source = "registry+https://github.com/rust-lang/crates.io-index" 152 | checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" 153 | 154 | [[package]] 155 | name = "cc" 156 | version = "1.2.43" 157 | source = "registry+https://github.com/rust-lang/crates.io-index" 158 | checksum = "739eb0f94557554b3ca9a86d2d37bebd49c5e6d0c1d2bda35ba5bdac830befc2" 159 | dependencies = [ 160 | "find-msvc-tools", 161 | "shlex", 162 | ] 163 | 164 | [[package]] 165 | name = "cfg-if" 166 | version = "1.0.4" 167 | source = "registry+https://github.com/rust-lang/crates.io-index" 168 | checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" 169 | 170 | [[package]] 171 | name = "cfg_aliases" 172 | version = "0.2.1" 173 | source = "registry+https://github.com/rust-lang/crates.io-index" 174 | checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" 175 | 176 | [[package]] 177 | name = "chrono" 178 | version = "0.4.42" 179 | source = "registry+https://github.com/rust-lang/crates.io-index" 180 | checksum = "145052bdd345b87320e369255277e3fb5152762ad123a901ef5c262dd38fe8d2" 181 | dependencies = [ 182 | "iana-time-zone", 183 | "js-sys", 184 | "num-traits", 185 | "wasm-bindgen", 186 | "windows-link", 187 | ] 188 | 189 | [[package]] 190 | name = "chumsky" 191 | version = "0.9.3" 192 | source = "registry+https://github.com/rust-lang/crates.io-index" 193 | checksum = "8eebd66744a15ded14960ab4ccdbfb51ad3b81f51f3f04a80adac98c985396c9" 194 | dependencies = [ 195 | "hashbrown", 196 | ] 197 | 198 | [[package]] 199 | name = "clap" 200 | version = "4.5.50" 201 | source = "registry+https://github.com/rust-lang/crates.io-index" 202 | checksum = "0c2cfd7bf8a6017ddaa4e32ffe7403d547790db06bd171c1c53926faab501623" 203 | dependencies = [ 204 | "clap_builder", 205 | "clap_derive", 206 | ] 207 | 208 | [[package]] 209 | name = "clap_builder" 210 | version = "4.5.50" 211 | source = "registry+https://github.com/rust-lang/crates.io-index" 212 | checksum = "0a4c05b9e80c5ccd3a7ef080ad7b6ba7d6fc00a985b8b157197075677c82c7a0" 213 | dependencies = [ 214 | "anstream", 215 | "anstyle", 216 | "clap_lex", 217 | "strsim", 218 | ] 219 | 220 | [[package]] 221 | name = "clap_derive" 222 | version = "4.5.49" 223 | source = "registry+https://github.com/rust-lang/crates.io-index" 224 | checksum = "2a0b5487afeab2deb2ff4e03a807ad1a03ac532ff5a2cee5d86884440c7f7671" 225 | dependencies = [ 226 | "heck 0.5.0", 227 | "proc-macro2", 228 | "quote", 229 | "syn 2.0.108", 230 | ] 231 | 232 | [[package]] 233 | name = "clap_lex" 234 | version = "0.7.6" 235 | source = "registry+https://github.com/rust-lang/crates.io-index" 236 | checksum = "a1d728cc89cf3aee9ff92b05e62b19ee65a02b5702cff7d5a377e32c6ae29d8d" 237 | 238 | [[package]] 239 | name = "colorchoice" 240 | version = "1.0.4" 241 | source = "registry+https://github.com/rust-lang/crates.io-index" 242 | checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" 243 | 244 | [[package]] 245 | name = "core-foundation-sys" 246 | version = "0.8.7" 247 | source = "registry+https://github.com/rust-lang/crates.io-index" 248 | checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" 249 | 250 | [[package]] 251 | name = "ctrlc" 252 | version = "3.5.0" 253 | source = "registry+https://github.com/rust-lang/crates.io-index" 254 | checksum = "881c5d0a13b2f1498e2306e82cbada78390e152d4b1378fb28a84f4dcd0dc4f3" 255 | dependencies = [ 256 | "dispatch", 257 | "nix", 258 | "windows-sys 0.61.2", 259 | ] 260 | 261 | [[package]] 262 | name = "dispatch" 263 | version = "0.2.0" 264 | source = "registry+https://github.com/rust-lang/crates.io-index" 265 | checksum = "bd0c93bb4b0c6d9b77f4435b0ae98c24d17f1c45b2ff844c6151a07256ca923b" 266 | 267 | [[package]] 268 | name = "env_filter" 269 | version = "0.1.4" 270 | source = "registry+https://github.com/rust-lang/crates.io-index" 271 | checksum = "1bf3c259d255ca70051b30e2e95b5446cdb8949ac4cd22c0d7fd634d89f568e2" 272 | dependencies = [ 273 | "log", 274 | "regex", 275 | ] 276 | 277 | [[package]] 278 | name = "env_logger" 279 | version = "0.11.8" 280 | source = "registry+https://github.com/rust-lang/crates.io-index" 281 | checksum = "13c863f0904021b108aa8b2f55046443e6b1ebde8fd4a15c399893aae4fa069f" 282 | dependencies = [ 283 | "anstream", 284 | "anstyle", 285 | "env_filter", 286 | "jiff", 287 | "log", 288 | ] 289 | 290 | [[package]] 291 | name = "errno" 292 | version = "0.3.14" 293 | source = "registry+https://github.com/rust-lang/crates.io-index" 294 | checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" 295 | dependencies = [ 296 | "libc", 297 | "windows-sys 0.61.2", 298 | ] 299 | 300 | [[package]] 301 | name = "find-msvc-tools" 302 | version = "0.1.4" 303 | source = "registry+https://github.com/rust-lang/crates.io-index" 304 | checksum = "52051878f80a721bb68ebfbc930e07b65ba72f2da88968ea5c06fd6ca3d3a127" 305 | 306 | [[package]] 307 | name = "gestures" 308 | version = "0.8.1" 309 | dependencies = [ 310 | "chrono", 311 | "clap", 312 | "ctrlc", 313 | "env_logger", 314 | "input", 315 | "knuffel", 316 | "libxdo", 317 | "log", 318 | "miette 7.6.0", 319 | "nix", 320 | "once_cell", 321 | "parking_lot", 322 | "regex", 323 | "signal-hook", 324 | "threadpool", 325 | "timer", 326 | ] 327 | 328 | [[package]] 329 | name = "gimli" 330 | version = "0.32.3" 331 | source = "registry+https://github.com/rust-lang/crates.io-index" 332 | checksum = "e629b9b98ef3dd8afe6ca2bd0f89306cec16d43d907889945bc5d6687f2f13c7" 333 | 334 | [[package]] 335 | name = "hashbrown" 336 | version = "0.14.5" 337 | source = "registry+https://github.com/rust-lang/crates.io-index" 338 | checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" 339 | dependencies = [ 340 | "ahash", 341 | "allocator-api2", 342 | ] 343 | 344 | [[package]] 345 | name = "heck" 346 | version = "0.4.1" 347 | source = "registry+https://github.com/rust-lang/crates.io-index" 348 | checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" 349 | dependencies = [ 350 | "unicode-segmentation", 351 | ] 352 | 353 | [[package]] 354 | name = "heck" 355 | version = "0.5.0" 356 | source = "registry+https://github.com/rust-lang/crates.io-index" 357 | checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" 358 | 359 | [[package]] 360 | name = "hermit-abi" 361 | version = "0.3.9" 362 | source = "registry+https://github.com/rust-lang/crates.io-index" 363 | checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" 364 | 365 | [[package]] 366 | name = "hermit-abi" 367 | version = "0.5.2" 368 | source = "registry+https://github.com/rust-lang/crates.io-index" 369 | checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" 370 | 371 | [[package]] 372 | name = "iana-time-zone" 373 | version = "0.1.64" 374 | source = "registry+https://github.com/rust-lang/crates.io-index" 375 | checksum = "33e57f83510bb73707521ebaffa789ec8caf86f9657cad665b092b581d40e9fb" 376 | dependencies = [ 377 | "android_system_properties", 378 | "core-foundation-sys", 379 | "iana-time-zone-haiku", 380 | "js-sys", 381 | "log", 382 | "wasm-bindgen", 383 | "windows-core", 384 | ] 385 | 386 | [[package]] 387 | name = "iana-time-zone-haiku" 388 | version = "0.1.2" 389 | source = "registry+https://github.com/rust-lang/crates.io-index" 390 | checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" 391 | dependencies = [ 392 | "cc", 393 | ] 394 | 395 | [[package]] 396 | name = "input" 397 | version = "0.9.1" 398 | source = "registry+https://github.com/rust-lang/crates.io-index" 399 | checksum = "fbdc09524a91f9cacd26f16734ff63d7dc650daffadd2b6f84d17a285bd875a9" 400 | dependencies = [ 401 | "bitflags", 402 | "input-sys", 403 | "libc", 404 | "log", 405 | "udev", 406 | ] 407 | 408 | [[package]] 409 | name = "input-sys" 410 | version = "1.18.0" 411 | source = "registry+https://github.com/rust-lang/crates.io-index" 412 | checksum = "bd4f5b4d1c00331c5245163aacfe5f20be75b564c7112d45893d4ae038119eb0" 413 | 414 | [[package]] 415 | name = "io-lifetimes" 416 | version = "1.0.11" 417 | source = "registry+https://github.com/rust-lang/crates.io-index" 418 | checksum = "eae7b9aee968036d54dce06cebaefd919e4472e753296daccd6d344e3e2df0c2" 419 | dependencies = [ 420 | "hermit-abi 0.3.9", 421 | "libc", 422 | "windows-sys 0.48.0", 423 | ] 424 | 425 | [[package]] 426 | name = "is_ci" 427 | version = "1.2.0" 428 | source = "registry+https://github.com/rust-lang/crates.io-index" 429 | checksum = "7655c9839580ee829dfacba1d1278c2b7883e50a277ff7541299489d6bdfdc45" 430 | 431 | [[package]] 432 | name = "is_terminal_polyfill" 433 | version = "1.70.2" 434 | source = "registry+https://github.com/rust-lang/crates.io-index" 435 | checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" 436 | 437 | [[package]] 438 | name = "jiff" 439 | version = "0.2.15" 440 | source = "registry+https://github.com/rust-lang/crates.io-index" 441 | checksum = "be1f93b8b1eb69c77f24bbb0afdf66f54b632ee39af40ca21c4365a1d7347e49" 442 | dependencies = [ 443 | "jiff-static", 444 | "log", 445 | "portable-atomic", 446 | "portable-atomic-util", 447 | "serde", 448 | ] 449 | 450 | [[package]] 451 | name = "jiff-static" 452 | version = "0.2.15" 453 | source = "registry+https://github.com/rust-lang/crates.io-index" 454 | checksum = "03343451ff899767262ec32146f6d559dd759fdadf42ff0e227c7c48f72594b4" 455 | dependencies = [ 456 | "proc-macro2", 457 | "quote", 458 | "syn 2.0.108", 459 | ] 460 | 461 | [[package]] 462 | name = "js-sys" 463 | version = "0.3.81" 464 | source = "registry+https://github.com/rust-lang/crates.io-index" 465 | checksum = "ec48937a97411dcb524a265206ccd4c90bb711fca92b2792c407f268825b9305" 466 | dependencies = [ 467 | "once_cell", 468 | "wasm-bindgen", 469 | ] 470 | 471 | [[package]] 472 | name = "knuffel" 473 | version = "3.2.0" 474 | source = "registry+https://github.com/rust-lang/crates.io-index" 475 | checksum = "04bee6ddc6071011314b1ce4f7705fef6c009401dba4fd22cb0009db6a177413" 476 | dependencies = [ 477 | "base64", 478 | "chumsky", 479 | "knuffel-derive", 480 | "miette 5.10.0", 481 | "thiserror", 482 | "unicode-width 0.1.14", 483 | ] 484 | 485 | [[package]] 486 | name = "knuffel-derive" 487 | version = "3.2.0" 488 | source = "registry+https://github.com/rust-lang/crates.io-index" 489 | checksum = "91977f56c49cfb961e3d840e2e7c6e4a56bde7283898cf606861f1421348283d" 490 | dependencies = [ 491 | "heck 0.4.1", 492 | "proc-macro-error", 493 | "proc-macro2", 494 | "quote", 495 | "syn 1.0.109", 496 | ] 497 | 498 | [[package]] 499 | name = "libc" 500 | version = "0.2.177" 501 | source = "registry+https://github.com/rust-lang/crates.io-index" 502 | checksum = "2874a2af47a2325c2001a6e6fad9b16a53b802102b528163885171cf92b15976" 503 | 504 | [[package]] 505 | name = "libudev-sys" 506 | version = "0.1.4" 507 | source = "registry+https://github.com/rust-lang/crates.io-index" 508 | checksum = "3c8469b4a23b962c1396b9b451dda50ef5b283e8dd309d69033475fa9b334324" 509 | dependencies = [ 510 | "libc", 511 | "pkg-config", 512 | ] 513 | 514 | [[package]] 515 | name = "libxdo" 516 | version = "0.6.0" 517 | source = "registry+https://github.com/rust-lang/crates.io-index" 518 | checksum = "00333b8756a3d28e78def82067a377de7fa61b24909000aeaa2b446a948d14db" 519 | dependencies = [ 520 | "libxdo-sys", 521 | ] 522 | 523 | [[package]] 524 | name = "libxdo-sys" 525 | version = "0.11.0" 526 | source = "registry+https://github.com/rust-lang/crates.io-index" 527 | checksum = "db23b9e7e2b7831bbd8aac0bbeeeb7b68cbebc162b227e7052e8e55829a09212" 528 | dependencies = [ 529 | "libc", 530 | "x11", 531 | ] 532 | 533 | [[package]] 534 | name = "linux-raw-sys" 535 | version = "0.11.0" 536 | source = "registry+https://github.com/rust-lang/crates.io-index" 537 | checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" 538 | 539 | [[package]] 540 | name = "lock_api" 541 | version = "0.4.14" 542 | source = "registry+https://github.com/rust-lang/crates.io-index" 543 | checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" 544 | dependencies = [ 545 | "scopeguard", 546 | ] 547 | 548 | [[package]] 549 | name = "log" 550 | version = "0.4.28" 551 | source = "registry+https://github.com/rust-lang/crates.io-index" 552 | checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432" 553 | 554 | [[package]] 555 | name = "memchr" 556 | version = "2.7.6" 557 | source = "registry+https://github.com/rust-lang/crates.io-index" 558 | checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" 559 | 560 | [[package]] 561 | name = "miette" 562 | version = "5.10.0" 563 | source = "registry+https://github.com/rust-lang/crates.io-index" 564 | checksum = "59bb584eaeeab6bd0226ccf3509a69d7936d148cf3d036ad350abe35e8c6856e" 565 | dependencies = [ 566 | "miette-derive 5.10.0", 567 | "once_cell", 568 | "thiserror", 569 | "unicode-width 0.1.14", 570 | ] 571 | 572 | [[package]] 573 | name = "miette" 574 | version = "7.6.0" 575 | source = "registry+https://github.com/rust-lang/crates.io-index" 576 | checksum = "5f98efec8807c63c752b5bd61f862c165c115b0a35685bdcfd9238c7aeb592b7" 577 | dependencies = [ 578 | "backtrace", 579 | "backtrace-ext", 580 | "cfg-if", 581 | "miette-derive 7.6.0", 582 | "owo-colors", 583 | "supports-color", 584 | "supports-hyperlinks", 585 | "supports-unicode", 586 | "terminal_size", 587 | "textwrap", 588 | "unicode-width 0.1.14", 589 | ] 590 | 591 | [[package]] 592 | name = "miette-derive" 593 | version = "5.10.0" 594 | source = "registry+https://github.com/rust-lang/crates.io-index" 595 | checksum = "49e7bc1560b95a3c4a25d03de42fe76ca718ab92d1a22a55b9b4cf67b3ae635c" 596 | dependencies = [ 597 | "proc-macro2", 598 | "quote", 599 | "syn 2.0.108", 600 | ] 601 | 602 | [[package]] 603 | name = "miette-derive" 604 | version = "7.6.0" 605 | source = "registry+https://github.com/rust-lang/crates.io-index" 606 | checksum = "db5b29714e950dbb20d5e6f74f9dcec4edbcc1067bb7f8ed198c097b8c1a818b" 607 | dependencies = [ 608 | "proc-macro2", 609 | "quote", 610 | "syn 2.0.108", 611 | ] 612 | 613 | [[package]] 614 | name = "miniz_oxide" 615 | version = "0.8.9" 616 | source = "registry+https://github.com/rust-lang/crates.io-index" 617 | checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" 618 | dependencies = [ 619 | "adler2", 620 | ] 621 | 622 | [[package]] 623 | name = "nix" 624 | version = "0.30.1" 625 | source = "registry+https://github.com/rust-lang/crates.io-index" 626 | checksum = "74523f3a35e05aba87a1d978330aef40f67b0304ac79c1c00b294c9830543db6" 627 | dependencies = [ 628 | "bitflags", 629 | "cfg-if", 630 | "cfg_aliases", 631 | "libc", 632 | ] 633 | 634 | [[package]] 635 | name = "num-traits" 636 | version = "0.2.19" 637 | source = "registry+https://github.com/rust-lang/crates.io-index" 638 | checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" 639 | dependencies = [ 640 | "autocfg", 641 | ] 642 | 643 | [[package]] 644 | name = "num_cpus" 645 | version = "1.17.0" 646 | source = "registry+https://github.com/rust-lang/crates.io-index" 647 | checksum = "91df4bbde75afed763b708b7eee1e8e7651e02d97f6d5dd763e89367e957b23b" 648 | dependencies = [ 649 | "hermit-abi 0.5.2", 650 | "libc", 651 | ] 652 | 653 | [[package]] 654 | name = "object" 655 | version = "0.37.3" 656 | source = "registry+https://github.com/rust-lang/crates.io-index" 657 | checksum = "ff76201f031d8863c38aa7f905eca4f53abbfa15f609db4277d44cd8938f33fe" 658 | dependencies = [ 659 | "memchr", 660 | ] 661 | 662 | [[package]] 663 | name = "once_cell" 664 | version = "1.21.3" 665 | source = "registry+https://github.com/rust-lang/crates.io-index" 666 | checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" 667 | 668 | [[package]] 669 | name = "once_cell_polyfill" 670 | version = "1.70.2" 671 | source = "registry+https://github.com/rust-lang/crates.io-index" 672 | checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" 673 | 674 | [[package]] 675 | name = "owo-colors" 676 | version = "4.2.3" 677 | source = "registry+https://github.com/rust-lang/crates.io-index" 678 | checksum = "9c6901729fa79e91a0913333229e9ca5dc725089d1c363b2f4b4760709dc4a52" 679 | 680 | [[package]] 681 | name = "parking_lot" 682 | version = "0.12.5" 683 | source = "registry+https://github.com/rust-lang/crates.io-index" 684 | checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" 685 | dependencies = [ 686 | "lock_api", 687 | "parking_lot_core", 688 | ] 689 | 690 | [[package]] 691 | name = "parking_lot_core" 692 | version = "0.9.12" 693 | source = "registry+https://github.com/rust-lang/crates.io-index" 694 | checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" 695 | dependencies = [ 696 | "cfg-if", 697 | "libc", 698 | "redox_syscall", 699 | "smallvec", 700 | "windows-link", 701 | ] 702 | 703 | [[package]] 704 | name = "pkg-config" 705 | version = "0.3.32" 706 | source = "registry+https://github.com/rust-lang/crates.io-index" 707 | checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" 708 | 709 | [[package]] 710 | name = "portable-atomic" 711 | version = "1.11.1" 712 | source = "registry+https://github.com/rust-lang/crates.io-index" 713 | checksum = "f84267b20a16ea918e43c6a88433c2d54fa145c92a811b5b047ccbe153674483" 714 | 715 | [[package]] 716 | name = "portable-atomic-util" 717 | version = "0.2.4" 718 | source = "registry+https://github.com/rust-lang/crates.io-index" 719 | checksum = "d8a2f0d8d040d7848a709caf78912debcc3f33ee4b3cac47d73d1e1069e83507" 720 | dependencies = [ 721 | "portable-atomic", 722 | ] 723 | 724 | [[package]] 725 | name = "proc-macro-error" 726 | version = "1.0.4" 727 | source = "registry+https://github.com/rust-lang/crates.io-index" 728 | checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" 729 | dependencies = [ 730 | "proc-macro-error-attr", 731 | "proc-macro2", 732 | "quote", 733 | "syn 1.0.109", 734 | "version_check", 735 | ] 736 | 737 | [[package]] 738 | name = "proc-macro-error-attr" 739 | version = "1.0.4" 740 | source = "registry+https://github.com/rust-lang/crates.io-index" 741 | checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" 742 | dependencies = [ 743 | "proc-macro2", 744 | "quote", 745 | "version_check", 746 | ] 747 | 748 | [[package]] 749 | name = "proc-macro2" 750 | version = "1.0.103" 751 | source = "registry+https://github.com/rust-lang/crates.io-index" 752 | checksum = "5ee95bc4ef87b8d5ba32e8b7714ccc834865276eab0aed5c9958d00ec45f49e8" 753 | dependencies = [ 754 | "unicode-ident", 755 | ] 756 | 757 | [[package]] 758 | name = "quote" 759 | version = "1.0.41" 760 | source = "registry+https://github.com/rust-lang/crates.io-index" 761 | checksum = "ce25767e7b499d1b604768e7cde645d14cc8584231ea6b295e9c9eb22c02e1d1" 762 | dependencies = [ 763 | "proc-macro2", 764 | ] 765 | 766 | [[package]] 767 | name = "redox_syscall" 768 | version = "0.5.18" 769 | source = "registry+https://github.com/rust-lang/crates.io-index" 770 | checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" 771 | dependencies = [ 772 | "bitflags", 773 | ] 774 | 775 | [[package]] 776 | name = "regex" 777 | version = "1.12.2" 778 | source = "registry+https://github.com/rust-lang/crates.io-index" 779 | checksum = "843bc0191f75f3e22651ae5f1e72939ab2f72a4bc30fa80a066bd66edefc24d4" 780 | dependencies = [ 781 | "aho-corasick", 782 | "memchr", 783 | "regex-automata", 784 | "regex-syntax", 785 | ] 786 | 787 | [[package]] 788 | name = "regex-automata" 789 | version = "0.4.13" 790 | source = "registry+https://github.com/rust-lang/crates.io-index" 791 | checksum = "5276caf25ac86c8d810222b3dbb938e512c55c6831a10f3e6ed1c93b84041f1c" 792 | dependencies = [ 793 | "aho-corasick", 794 | "memchr", 795 | "regex-syntax", 796 | ] 797 | 798 | [[package]] 799 | name = "regex-syntax" 800 | version = "0.8.8" 801 | source = "registry+https://github.com/rust-lang/crates.io-index" 802 | checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58" 803 | 804 | [[package]] 805 | name = "rustc-demangle" 806 | version = "0.1.26" 807 | source = "registry+https://github.com/rust-lang/crates.io-index" 808 | checksum = "56f7d92ca342cea22a06f2121d944b4fd82af56988c270852495420f961d4ace" 809 | 810 | [[package]] 811 | name = "rustix" 812 | version = "1.1.2" 813 | source = "registry+https://github.com/rust-lang/crates.io-index" 814 | checksum = "cd15f8a2c5551a84d56efdc1cd049089e409ac19a3072d5037a17fd70719ff3e" 815 | dependencies = [ 816 | "bitflags", 817 | "errno", 818 | "libc", 819 | "linux-raw-sys", 820 | "windows-sys 0.61.2", 821 | ] 822 | 823 | [[package]] 824 | name = "rustversion" 825 | version = "1.0.22" 826 | source = "registry+https://github.com/rust-lang/crates.io-index" 827 | checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" 828 | 829 | [[package]] 830 | name = "scopeguard" 831 | version = "1.2.0" 832 | source = "registry+https://github.com/rust-lang/crates.io-index" 833 | checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" 834 | 835 | [[package]] 836 | name = "serde" 837 | version = "1.0.228" 838 | source = "registry+https://github.com/rust-lang/crates.io-index" 839 | checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" 840 | dependencies = [ 841 | "serde_core", 842 | ] 843 | 844 | [[package]] 845 | name = "serde_core" 846 | version = "1.0.228" 847 | source = "registry+https://github.com/rust-lang/crates.io-index" 848 | checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" 849 | dependencies = [ 850 | "serde_derive", 851 | ] 852 | 853 | [[package]] 854 | name = "serde_derive" 855 | version = "1.0.228" 856 | source = "registry+https://github.com/rust-lang/crates.io-index" 857 | checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" 858 | dependencies = [ 859 | "proc-macro2", 860 | "quote", 861 | "syn 2.0.108", 862 | ] 863 | 864 | [[package]] 865 | name = "shlex" 866 | version = "1.3.0" 867 | source = "registry+https://github.com/rust-lang/crates.io-index" 868 | checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" 869 | 870 | [[package]] 871 | name = "signal-hook" 872 | version = "0.3.18" 873 | source = "registry+https://github.com/rust-lang/crates.io-index" 874 | checksum = "d881a16cf4426aa584979d30bd82cb33429027e42122b169753d6ef1085ed6e2" 875 | dependencies = [ 876 | "libc", 877 | "signal-hook-registry", 878 | ] 879 | 880 | [[package]] 881 | name = "signal-hook-registry" 882 | version = "1.4.6" 883 | source = "registry+https://github.com/rust-lang/crates.io-index" 884 | checksum = "b2a4719bff48cee6b39d12c020eeb490953ad2443b7055bd0b21fca26bd8c28b" 885 | dependencies = [ 886 | "libc", 887 | ] 888 | 889 | [[package]] 890 | name = "smallvec" 891 | version = "1.15.1" 892 | source = "registry+https://github.com/rust-lang/crates.io-index" 893 | checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" 894 | 895 | [[package]] 896 | name = "strsim" 897 | version = "0.11.1" 898 | source = "registry+https://github.com/rust-lang/crates.io-index" 899 | checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" 900 | 901 | [[package]] 902 | name = "supports-color" 903 | version = "3.0.2" 904 | source = "registry+https://github.com/rust-lang/crates.io-index" 905 | checksum = "c64fc7232dd8d2e4ac5ce4ef302b1d81e0b80d055b9d77c7c4f51f6aa4c867d6" 906 | dependencies = [ 907 | "is_ci", 908 | ] 909 | 910 | [[package]] 911 | name = "supports-hyperlinks" 912 | version = "3.1.0" 913 | source = "registry+https://github.com/rust-lang/crates.io-index" 914 | checksum = "804f44ed3c63152de6a9f90acbea1a110441de43006ea51bcce8f436196a288b" 915 | 916 | [[package]] 917 | name = "supports-unicode" 918 | version = "3.0.0" 919 | source = "registry+https://github.com/rust-lang/crates.io-index" 920 | checksum = "b7401a30af6cb5818bb64852270bb722533397edcfc7344954a38f420819ece2" 921 | 922 | [[package]] 923 | name = "syn" 924 | version = "1.0.109" 925 | source = "registry+https://github.com/rust-lang/crates.io-index" 926 | checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" 927 | dependencies = [ 928 | "proc-macro2", 929 | "quote", 930 | "unicode-ident", 931 | ] 932 | 933 | [[package]] 934 | name = "syn" 935 | version = "2.0.108" 936 | source = "registry+https://github.com/rust-lang/crates.io-index" 937 | checksum = "da58917d35242480a05c2897064da0a80589a2a0476c9a3f2fdc83b53502e917" 938 | dependencies = [ 939 | "proc-macro2", 940 | "quote", 941 | "unicode-ident", 942 | ] 943 | 944 | [[package]] 945 | name = "terminal_size" 946 | version = "0.4.3" 947 | source = "registry+https://github.com/rust-lang/crates.io-index" 948 | checksum = "60b8cb979cb11c32ce1603f8137b22262a9d131aaa5c37b5678025f22b8becd0" 949 | dependencies = [ 950 | "rustix", 951 | "windows-sys 0.60.2", 952 | ] 953 | 954 | [[package]] 955 | name = "textwrap" 956 | version = "0.16.2" 957 | source = "registry+https://github.com/rust-lang/crates.io-index" 958 | checksum = "c13547615a44dc9c452a8a534638acdf07120d4b6847c8178705da06306a3057" 959 | dependencies = [ 960 | "unicode-linebreak", 961 | "unicode-width 0.2.2", 962 | ] 963 | 964 | [[package]] 965 | name = "thiserror" 966 | version = "1.0.69" 967 | source = "registry+https://github.com/rust-lang/crates.io-index" 968 | checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" 969 | dependencies = [ 970 | "thiserror-impl", 971 | ] 972 | 973 | [[package]] 974 | name = "thiserror-impl" 975 | version = "1.0.69" 976 | source = "registry+https://github.com/rust-lang/crates.io-index" 977 | checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" 978 | dependencies = [ 979 | "proc-macro2", 980 | "quote", 981 | "syn 2.0.108", 982 | ] 983 | 984 | [[package]] 985 | name = "threadpool" 986 | version = "1.8.1" 987 | source = "registry+https://github.com/rust-lang/crates.io-index" 988 | checksum = "d050e60b33d41c19108b32cea32164033a9013fe3b46cbd4457559bfbf77afaa" 989 | dependencies = [ 990 | "num_cpus", 991 | ] 992 | 993 | [[package]] 994 | name = "timer" 995 | version = "0.2.0" 996 | source = "registry+https://github.com/rust-lang/crates.io-index" 997 | checksum = "31d42176308937165701f50638db1c31586f183f1aab416268216577aec7306b" 998 | dependencies = [ 999 | "chrono", 1000 | ] 1001 | 1002 | [[package]] 1003 | name = "udev" 1004 | version = "0.9.3" 1005 | source = "registry+https://github.com/rust-lang/crates.io-index" 1006 | checksum = "af4e37e9ea4401fc841ff54b9ddfc9be1079b1e89434c1a6a865dd68980f7e9f" 1007 | dependencies = [ 1008 | "io-lifetimes", 1009 | "libc", 1010 | "libudev-sys", 1011 | "pkg-config", 1012 | ] 1013 | 1014 | [[package]] 1015 | name = "unicode-ident" 1016 | version = "1.0.20" 1017 | source = "registry+https://github.com/rust-lang/crates.io-index" 1018 | checksum = "462eeb75aeb73aea900253ce739c8e18a67423fadf006037cd3ff27e82748a06" 1019 | 1020 | [[package]] 1021 | name = "unicode-linebreak" 1022 | version = "0.1.5" 1023 | source = "registry+https://github.com/rust-lang/crates.io-index" 1024 | checksum = "3b09c83c3c29d37506a3e260c08c03743a6bb66a9cd432c6934ab501a190571f" 1025 | 1026 | [[package]] 1027 | name = "unicode-segmentation" 1028 | version = "1.12.0" 1029 | source = "registry+https://github.com/rust-lang/crates.io-index" 1030 | checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" 1031 | 1032 | [[package]] 1033 | name = "unicode-width" 1034 | version = "0.1.14" 1035 | source = "registry+https://github.com/rust-lang/crates.io-index" 1036 | checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" 1037 | 1038 | [[package]] 1039 | name = "unicode-width" 1040 | version = "0.2.2" 1041 | source = "registry+https://github.com/rust-lang/crates.io-index" 1042 | checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" 1043 | 1044 | [[package]] 1045 | name = "utf8parse" 1046 | version = "0.2.2" 1047 | source = "registry+https://github.com/rust-lang/crates.io-index" 1048 | checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" 1049 | 1050 | [[package]] 1051 | name = "version_check" 1052 | version = "0.9.5" 1053 | source = "registry+https://github.com/rust-lang/crates.io-index" 1054 | checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" 1055 | 1056 | [[package]] 1057 | name = "wasm-bindgen" 1058 | version = "0.2.104" 1059 | source = "registry+https://github.com/rust-lang/crates.io-index" 1060 | checksum = "c1da10c01ae9f1ae40cbfac0bac3b1e724b320abfcf52229f80b547c0d250e2d" 1061 | dependencies = [ 1062 | "cfg-if", 1063 | "once_cell", 1064 | "rustversion", 1065 | "wasm-bindgen-macro", 1066 | "wasm-bindgen-shared", 1067 | ] 1068 | 1069 | [[package]] 1070 | name = "wasm-bindgen-backend" 1071 | version = "0.2.104" 1072 | source = "registry+https://github.com/rust-lang/crates.io-index" 1073 | checksum = "671c9a5a66f49d8a47345ab942e2cb93c7d1d0339065d4f8139c486121b43b19" 1074 | dependencies = [ 1075 | "bumpalo", 1076 | "log", 1077 | "proc-macro2", 1078 | "quote", 1079 | "syn 2.0.108", 1080 | "wasm-bindgen-shared", 1081 | ] 1082 | 1083 | [[package]] 1084 | name = "wasm-bindgen-macro" 1085 | version = "0.2.104" 1086 | source = "registry+https://github.com/rust-lang/crates.io-index" 1087 | checksum = "7ca60477e4c59f5f2986c50191cd972e3a50d8a95603bc9434501cf156a9a119" 1088 | dependencies = [ 1089 | "quote", 1090 | "wasm-bindgen-macro-support", 1091 | ] 1092 | 1093 | [[package]] 1094 | name = "wasm-bindgen-macro-support" 1095 | version = "0.2.104" 1096 | source = "registry+https://github.com/rust-lang/crates.io-index" 1097 | checksum = "9f07d2f20d4da7b26400c9f4a0511e6e0345b040694e8a75bd41d578fa4421d7" 1098 | dependencies = [ 1099 | "proc-macro2", 1100 | "quote", 1101 | "syn 2.0.108", 1102 | "wasm-bindgen-backend", 1103 | "wasm-bindgen-shared", 1104 | ] 1105 | 1106 | [[package]] 1107 | name = "wasm-bindgen-shared" 1108 | version = "0.2.104" 1109 | source = "registry+https://github.com/rust-lang/crates.io-index" 1110 | checksum = "bad67dc8b2a1a6e5448428adec4c3e84c43e561d8c9ee8a9e5aabeb193ec41d1" 1111 | dependencies = [ 1112 | "unicode-ident", 1113 | ] 1114 | 1115 | [[package]] 1116 | name = "windows-core" 1117 | version = "0.62.2" 1118 | source = "registry+https://github.com/rust-lang/crates.io-index" 1119 | checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" 1120 | dependencies = [ 1121 | "windows-implement", 1122 | "windows-interface", 1123 | "windows-link", 1124 | "windows-result", 1125 | "windows-strings", 1126 | ] 1127 | 1128 | [[package]] 1129 | name = "windows-implement" 1130 | version = "0.60.2" 1131 | source = "registry+https://github.com/rust-lang/crates.io-index" 1132 | checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" 1133 | dependencies = [ 1134 | "proc-macro2", 1135 | "quote", 1136 | "syn 2.0.108", 1137 | ] 1138 | 1139 | [[package]] 1140 | name = "windows-interface" 1141 | version = "0.59.3" 1142 | source = "registry+https://github.com/rust-lang/crates.io-index" 1143 | checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" 1144 | dependencies = [ 1145 | "proc-macro2", 1146 | "quote", 1147 | "syn 2.0.108", 1148 | ] 1149 | 1150 | [[package]] 1151 | name = "windows-link" 1152 | version = "0.2.1" 1153 | source = "registry+https://github.com/rust-lang/crates.io-index" 1154 | checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" 1155 | 1156 | [[package]] 1157 | name = "windows-result" 1158 | version = "0.4.1" 1159 | source = "registry+https://github.com/rust-lang/crates.io-index" 1160 | checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" 1161 | dependencies = [ 1162 | "windows-link", 1163 | ] 1164 | 1165 | [[package]] 1166 | name = "windows-strings" 1167 | version = "0.5.1" 1168 | source = "registry+https://github.com/rust-lang/crates.io-index" 1169 | checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" 1170 | dependencies = [ 1171 | "windows-link", 1172 | ] 1173 | 1174 | [[package]] 1175 | name = "windows-sys" 1176 | version = "0.48.0" 1177 | source = "registry+https://github.com/rust-lang/crates.io-index" 1178 | checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" 1179 | dependencies = [ 1180 | "windows-targets 0.48.5", 1181 | ] 1182 | 1183 | [[package]] 1184 | name = "windows-sys" 1185 | version = "0.60.2" 1186 | source = "registry+https://github.com/rust-lang/crates.io-index" 1187 | checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" 1188 | dependencies = [ 1189 | "windows-targets 0.53.5", 1190 | ] 1191 | 1192 | [[package]] 1193 | name = "windows-sys" 1194 | version = "0.61.2" 1195 | source = "registry+https://github.com/rust-lang/crates.io-index" 1196 | checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" 1197 | dependencies = [ 1198 | "windows-link", 1199 | ] 1200 | 1201 | [[package]] 1202 | name = "windows-targets" 1203 | version = "0.48.5" 1204 | source = "registry+https://github.com/rust-lang/crates.io-index" 1205 | checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" 1206 | dependencies = [ 1207 | "windows_aarch64_gnullvm 0.48.5", 1208 | "windows_aarch64_msvc 0.48.5", 1209 | "windows_i686_gnu 0.48.5", 1210 | "windows_i686_msvc 0.48.5", 1211 | "windows_x86_64_gnu 0.48.5", 1212 | "windows_x86_64_gnullvm 0.48.5", 1213 | "windows_x86_64_msvc 0.48.5", 1214 | ] 1215 | 1216 | [[package]] 1217 | name = "windows-targets" 1218 | version = "0.53.5" 1219 | source = "registry+https://github.com/rust-lang/crates.io-index" 1220 | checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" 1221 | dependencies = [ 1222 | "windows-link", 1223 | "windows_aarch64_gnullvm 0.53.1", 1224 | "windows_aarch64_msvc 0.53.1", 1225 | "windows_i686_gnu 0.53.1", 1226 | "windows_i686_gnullvm", 1227 | "windows_i686_msvc 0.53.1", 1228 | "windows_x86_64_gnu 0.53.1", 1229 | "windows_x86_64_gnullvm 0.53.1", 1230 | "windows_x86_64_msvc 0.53.1", 1231 | ] 1232 | 1233 | [[package]] 1234 | name = "windows_aarch64_gnullvm" 1235 | version = "0.48.5" 1236 | source = "registry+https://github.com/rust-lang/crates.io-index" 1237 | checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" 1238 | 1239 | [[package]] 1240 | name = "windows_aarch64_gnullvm" 1241 | version = "0.53.1" 1242 | source = "registry+https://github.com/rust-lang/crates.io-index" 1243 | checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" 1244 | 1245 | [[package]] 1246 | name = "windows_aarch64_msvc" 1247 | version = "0.48.5" 1248 | source = "registry+https://github.com/rust-lang/crates.io-index" 1249 | checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" 1250 | 1251 | [[package]] 1252 | name = "windows_aarch64_msvc" 1253 | version = "0.53.1" 1254 | source = "registry+https://github.com/rust-lang/crates.io-index" 1255 | checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" 1256 | 1257 | [[package]] 1258 | name = "windows_i686_gnu" 1259 | version = "0.48.5" 1260 | source = "registry+https://github.com/rust-lang/crates.io-index" 1261 | checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" 1262 | 1263 | [[package]] 1264 | name = "windows_i686_gnu" 1265 | version = "0.53.1" 1266 | source = "registry+https://github.com/rust-lang/crates.io-index" 1267 | checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" 1268 | 1269 | [[package]] 1270 | name = "windows_i686_gnullvm" 1271 | version = "0.53.1" 1272 | source = "registry+https://github.com/rust-lang/crates.io-index" 1273 | checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" 1274 | 1275 | [[package]] 1276 | name = "windows_i686_msvc" 1277 | version = "0.48.5" 1278 | source = "registry+https://github.com/rust-lang/crates.io-index" 1279 | checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" 1280 | 1281 | [[package]] 1282 | name = "windows_i686_msvc" 1283 | version = "0.53.1" 1284 | source = "registry+https://github.com/rust-lang/crates.io-index" 1285 | checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" 1286 | 1287 | [[package]] 1288 | name = "windows_x86_64_gnu" 1289 | version = "0.48.5" 1290 | source = "registry+https://github.com/rust-lang/crates.io-index" 1291 | checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" 1292 | 1293 | [[package]] 1294 | name = "windows_x86_64_gnu" 1295 | version = "0.53.1" 1296 | source = "registry+https://github.com/rust-lang/crates.io-index" 1297 | checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" 1298 | 1299 | [[package]] 1300 | name = "windows_x86_64_gnullvm" 1301 | version = "0.48.5" 1302 | source = "registry+https://github.com/rust-lang/crates.io-index" 1303 | checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" 1304 | 1305 | [[package]] 1306 | name = "windows_x86_64_gnullvm" 1307 | version = "0.53.1" 1308 | source = "registry+https://github.com/rust-lang/crates.io-index" 1309 | checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" 1310 | 1311 | [[package]] 1312 | name = "windows_x86_64_msvc" 1313 | version = "0.48.5" 1314 | source = "registry+https://github.com/rust-lang/crates.io-index" 1315 | checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" 1316 | 1317 | [[package]] 1318 | name = "windows_x86_64_msvc" 1319 | version = "0.53.1" 1320 | source = "registry+https://github.com/rust-lang/crates.io-index" 1321 | checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" 1322 | 1323 | [[package]] 1324 | name = "x11" 1325 | version = "2.21.0" 1326 | source = "registry+https://github.com/rust-lang/crates.io-index" 1327 | checksum = "502da5464ccd04011667b11c435cb992822c2c0dbde1770c988480d312a0db2e" 1328 | dependencies = [ 1329 | "libc", 1330 | "pkg-config", 1331 | ] 1332 | 1333 | [[package]] 1334 | name = "zerocopy" 1335 | version = "0.8.27" 1336 | source = "registry+https://github.com/rust-lang/crates.io-index" 1337 | checksum = "0894878a5fa3edfd6da3f88c4805f4c8558e2b996227a3d864f47fe11e38282c" 1338 | dependencies = [ 1339 | "zerocopy-derive", 1340 | ] 1341 | 1342 | [[package]] 1343 | name = "zerocopy-derive" 1344 | version = "0.8.27" 1345 | source = "registry+https://github.com/rust-lang/crates.io-index" 1346 | checksum = "88d2b8d9c68ad2b9e4340d7832716a4d21a22a1154777ad56ea55c51a9cf3831" 1347 | dependencies = [ 1348 | "proc-macro2", 1349 | "quote", 1350 | "syn 2.0.108", 1351 | ] 1352 | --------------------------------------------------------------------------------