Initial commit
This commit is contained in:
Generated
+686
@@ -0,0 +1,686 @@
|
|||||||
|
# This file is automatically @generated by Cargo.
|
||||||
|
# It is not intended for manual editing.
|
||||||
|
version = 4
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "android_system_properties"
|
||||||
|
version = "0.1.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "arrayref"
|
||||||
|
version = "0.3.9"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "76a2e8124351fda1ef8aaaa3bbd7ebbcb486bbcd4225aca0aa0d84bb2db8fecb"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "arrayvec"
|
||||||
|
version = "0.5.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "23b62fc65de8e4e7f52534fb52b0f3ed04746ae267519eef2a83941e8085068b"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "autocfg"
|
||||||
|
version = "1.5.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "base64"
|
||||||
|
version = "0.13.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "bitflags"
|
||||||
|
version = "0.9.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "4efd02e230a02e18f92fc2735f44597385ed02ad8f831e7c1c1156ee5e1ab3a5"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "bitflags"
|
||||||
|
version = "1.3.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "blake2b_simd"
|
||||||
|
version = "0.5.11"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "afa748e348ad3be8263be728124b24a24f268266f6f5d58af9d75f6a40b5c587"
|
||||||
|
dependencies = [
|
||||||
|
"arrayref",
|
||||||
|
"arrayvec",
|
||||||
|
"constant_time_eq",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "block"
|
||||||
|
version = "0.1.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "0d8c1fef690941d3e7788d328517591fecc684c084084702d6ff1641e993699a"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "bumpalo"
|
||||||
|
version = "3.20.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cc"
|
||||||
|
version = "1.2.65"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e228eec9be7c17ccb640b59b36a5cd805ea2a564a4c5e162c2f659fea30d3b96"
|
||||||
|
dependencies = [
|
||||||
|
"find-msvc-tools",
|
||||||
|
"shlex",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cfg-if"
|
||||||
|
version = "1.0.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "chrono"
|
||||||
|
version = "0.4.45"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1aa79e62e7697b8e29b513a68abacf485adcd1fe8284a4316c5ae868e6633327"
|
||||||
|
dependencies = [
|
||||||
|
"iana-time-zone",
|
||||||
|
"js-sys",
|
||||||
|
"num-traits",
|
||||||
|
"wasm-bindgen",
|
||||||
|
"windows-link 0.2.1",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "constant_time_eq"
|
||||||
|
version = "0.1.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "245097e9a4535ee1e3e3931fcfcd55a796a44c643e8596ff6566d68f09b87bbc"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "core-foundation-sys"
|
||||||
|
version = "0.8.7"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "crossbeam-utils"
|
||||||
|
version = "0.8.21"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "dbus"
|
||||||
|
version = "0.8.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5cd9e78c210146a1860f897db03412fd5091fd73100778e43ee255cca252cf32"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
"libdbus-sys",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "dirs"
|
||||||
|
version = "1.0.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "3fd78930633bd1c6e35c4b42b1df7b0cbc6bc191146e512bb3bedf243fcc3901"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
"redox_users",
|
||||||
|
"winapi",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "evdev-rs"
|
||||||
|
version = "0.4.0"
|
||||||
|
source = "git+https://github.com/ndesh26/evdev-rs.git?rev=8e995b8bf#8e995b8bfc1f8ed844ae47edaa4286c99e8c88b5"
|
||||||
|
dependencies = [
|
||||||
|
"bitflags 1.3.2",
|
||||||
|
"evdev-sys",
|
||||||
|
"libc",
|
||||||
|
"log",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "evdev-sys"
|
||||||
|
version = "0.2.1"
|
||||||
|
source = "git+https://github.com/ndesh26/evdev-rs.git?rev=8e995b8bf#8e995b8bfc1f8ed844ae47edaa4286c99e8c88b5"
|
||||||
|
dependencies = [
|
||||||
|
"cc",
|
||||||
|
"libc",
|
||||||
|
"pkg-config",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "find-msvc-tools"
|
||||||
|
version = "0.1.9"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "futures-core"
|
||||||
|
version = "0.3.32"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "futures-task"
|
||||||
|
version = "0.3.32"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "futures-util"
|
||||||
|
version = "0.3.32"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6"
|
||||||
|
dependencies = [
|
||||||
|
"futures-core",
|
||||||
|
"futures-task",
|
||||||
|
"pin-project-lite",
|
||||||
|
"slab",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "getrandom"
|
||||||
|
version = "0.1.16"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8fc3cb4d91f53b50155bdcfd23f6a4c39ae1969c2ae85982b135750cccaf5fce"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
"libc",
|
||||||
|
"wasi",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "hidapi"
|
||||||
|
version = "1.5.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "798154e4b6570af74899d71155fb0072d5b17e6aa12f39c8ef22c60fb8ec99e7"
|
||||||
|
dependencies = [
|
||||||
|
"cc",
|
||||||
|
"libc",
|
||||||
|
"pkg-config",
|
||||||
|
"winapi",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "iana-time-zone"
|
||||||
|
version = "0.1.65"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470"
|
||||||
|
dependencies = [
|
||||||
|
"android_system_properties",
|
||||||
|
"core-foundation-sys",
|
||||||
|
"iana-time-zone-haiku",
|
||||||
|
"js-sys",
|
||||||
|
"log",
|
||||||
|
"wasm-bindgen",
|
||||||
|
"windows-core",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "iana-time-zone-haiku"
|
||||||
|
version = "0.1.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f"
|
||||||
|
dependencies = [
|
||||||
|
"cc",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "js-sys"
|
||||||
|
version = "0.3.103"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "53b44bfcdb3f8d5837a46dae1ca9660a837176eee74a28b229bc626816589102"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
"futures-util",
|
||||||
|
"wasm-bindgen",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "libc"
|
||||||
|
version = "0.2.186"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "libdbus-sys"
|
||||||
|
version = "0.2.7"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "328c4789d42200f1eeec05bd86c9c13c7f091d2ba9a6ea35acdf51f31bc0f043"
|
||||||
|
dependencies = [
|
||||||
|
"pkg-config",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "libudev-sys"
|
||||||
|
version = "0.1.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "3c8469b4a23b962c1396b9b451dda50ef5b283e8dd309d69033475fa9b334324"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
"pkg-config",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "log"
|
||||||
|
version = "0.4.33"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "mac-notification-sys"
|
||||||
|
version = "0.3.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "3dfb6b71a9a89cd38b395d994214297447e8e63b1ba5708a9a2b0b1048ceda76"
|
||||||
|
dependencies = [
|
||||||
|
"cc",
|
||||||
|
"chrono",
|
||||||
|
"dirs",
|
||||||
|
"objc-foundation",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "malloc_buf"
|
||||||
|
version = "0.0.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "62bb907fe88d54d8d9ce32a3cceab4218ed2f6b7d35617cafe9adf84e43919cb"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "notify-rust"
|
||||||
|
version = "4.0.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "144acee6a0543dc74893e4b8a33936b5b0a94cc2d4ab024afd0c6daff7afc3c0"
|
||||||
|
dependencies = [
|
||||||
|
"dbus",
|
||||||
|
"mac-notification-sys",
|
||||||
|
"winrt-notification",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "num-traits"
|
||||||
|
version = "0.2.19"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
|
||||||
|
dependencies = [
|
||||||
|
"autocfg",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "objc"
|
||||||
|
version = "0.2.7"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "915b1b472bc21c53464d6c8461c9d3af805ba1ef837e1cac254428f4a77177b1"
|
||||||
|
dependencies = [
|
||||||
|
"malloc_buf",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "objc-foundation"
|
||||||
|
version = "0.1.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1add1b659e36c9607c7aab864a76c7a4c2760cd0cd2e120f3fb8b952c7e22bf9"
|
||||||
|
dependencies = [
|
||||||
|
"block",
|
||||||
|
"objc",
|
||||||
|
"objc_id",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "objc_id"
|
||||||
|
version = "0.1.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c92d4ddb4bd7b50d730c215ff871754d0da6b2178849f8a2a2ab69712d0c073b"
|
||||||
|
dependencies = [
|
||||||
|
"objc",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "once_cell"
|
||||||
|
version = "1.21.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pin-project-lite"
|
||||||
|
version = "0.2.17"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pkg-config"
|
||||||
|
version = "0.3.33"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "proc-macro2"
|
||||||
|
version = "1.0.106"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934"
|
||||||
|
dependencies = [
|
||||||
|
"unicode-ident",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "quote"
|
||||||
|
version = "0.3.15"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7a6e920b65c65f10b2ae65c831a81a073a89edd28c7cce89475bff467ab4167a"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "quote"
|
||||||
|
version = "1.0.46"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "dfbc457d0c7a0759a614551b11a6409e5951f6c7537be1f1b7682b9ae9230368"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "redox_syscall"
|
||||||
|
version = "0.1.57"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "41cc0f7e4d5d4544e8861606a285bb08d3e70712ccc7d2b84d7c0ccfaf4b05ce"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "redox_users"
|
||||||
|
version = "0.3.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "de0737333e7a9502c789a36d7c7fa6092a49895d4faa31ca5df163857ded2e9d"
|
||||||
|
dependencies = [
|
||||||
|
"getrandom",
|
||||||
|
"redox_syscall",
|
||||||
|
"rust-argon2",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rust-argon2"
|
||||||
|
version = "0.8.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "4b18820d944b33caa75a71378964ac46f58517c92b6ae5f762636247c09e78fb"
|
||||||
|
dependencies = [
|
||||||
|
"base64",
|
||||||
|
"blake2b_simd",
|
||||||
|
"constant_time_eq",
|
||||||
|
"crossbeam-utils",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rustversion"
|
||||||
|
version = "1.0.22"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "shlex"
|
||||||
|
version = "2.0.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "slab"
|
||||||
|
version = "0.4.12"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "strum"
|
||||||
|
version = "0.8.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "4ca6e4730f517e041e547ffe23d29daab8de6b73af4b6ae2a002108169f5e7da"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "strum_macros"
|
||||||
|
version = "0.8.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "3384590878eb0cab3b128e844412e2d010821e7e091211b9d87324173ada7db8"
|
||||||
|
dependencies = [
|
||||||
|
"quote 0.3.15",
|
||||||
|
"syn 0.11.11",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "surface-dial-renewed"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"evdev-rs",
|
||||||
|
"hidapi",
|
||||||
|
"notify-rust",
|
||||||
|
"udev",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "syn"
|
||||||
|
version = "0.11.11"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d3b891b9015c88c576343b9b3e41c2c11a51c219ef067b264bd9c8aa9b441dad"
|
||||||
|
dependencies = [
|
||||||
|
"quote 0.3.15",
|
||||||
|
"synom",
|
||||||
|
"unicode-xid",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "syn"
|
||||||
|
version = "2.0.118"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1b9ae57f904213ebb649ce6895b8a66c66f0203b9319718f69a5612a065b1422"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote 1.0.46",
|
||||||
|
"unicode-ident",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "synom"
|
||||||
|
version = "0.11.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a393066ed9010ebaed60b9eafa373d4b1baac186dd7e008555b0f702b51945b6"
|
||||||
|
dependencies = [
|
||||||
|
"unicode-xid",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "udev"
|
||||||
|
version = "0.5.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "048df778e99eea028c08cca7853b9b521df6948b59bb29ab8bb737c057f58e6d"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
"libudev-sys",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "unicode-ident"
|
||||||
|
version = "1.0.24"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "unicode-xid"
|
||||||
|
version = "0.0.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8c1f860d7d29cf02cb2f3f359fd35991af3d30bac52c57d265a3c461074cb4dc"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wasi"
|
||||||
|
version = "0.9.0+wasi-snapshot-preview1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wasm-bindgen"
|
||||||
|
version = "0.2.126"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "4b067c0c11094aef6b7a801c1e34a26affafdf3d051dba08456b868789aaf9a4"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
"once_cell",
|
||||||
|
"rustversion",
|
||||||
|
"wasm-bindgen-macro",
|
||||||
|
"wasm-bindgen-shared",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wasm-bindgen-macro"
|
||||||
|
version = "0.2.126"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "167ce5e579f6bcf889c4f7175a8a5a585de84e8ff93976ce393efa5f2837aab1"
|
||||||
|
dependencies = [
|
||||||
|
"quote 1.0.46",
|
||||||
|
"wasm-bindgen-macro-support",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wasm-bindgen-macro-support"
|
||||||
|
version = "0.2.126"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f3997c7839262f4ef12cf90b818d6340c18e80f263f1a94bf157d0ec4420380e"
|
||||||
|
dependencies = [
|
||||||
|
"bumpalo",
|
||||||
|
"proc-macro2",
|
||||||
|
"quote 1.0.46",
|
||||||
|
"syn 2.0.118",
|
||||||
|
"wasm-bindgen-shared",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wasm-bindgen-shared"
|
||||||
|
version = "0.2.126"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "dc1b4cb0cc549fcf58d7dfc081778139b3d283a081644e833e84682ad71cea24"
|
||||||
|
dependencies = [
|
||||||
|
"unicode-ident",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "winapi"
|
||||||
|
version = "0.3.9"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
|
||||||
|
dependencies = [
|
||||||
|
"winapi-i686-pc-windows-gnu",
|
||||||
|
"winapi-x86_64-pc-windows-gnu",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "winapi-i686-pc-windows-gnu"
|
||||||
|
version = "0.4.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "winapi-x86_64-pc-windows-gnu"
|
||||||
|
version = "0.4.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows-core"
|
||||||
|
version = "0.61.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3"
|
||||||
|
dependencies = [
|
||||||
|
"windows-implement",
|
||||||
|
"windows-interface",
|
||||||
|
"windows-link 0.1.3",
|
||||||
|
"windows-result",
|
||||||
|
"windows-strings",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows-implement"
|
||||||
|
version = "0.60.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote 1.0.46",
|
||||||
|
"syn 2.0.118",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows-interface"
|
||||||
|
version = "0.59.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote 1.0.46",
|
||||||
|
"syn 2.0.118",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows-link"
|
||||||
|
version = "0.1.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows-link"
|
||||||
|
version = "0.2.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows-result"
|
||||||
|
version = "0.3.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6"
|
||||||
|
dependencies = [
|
||||||
|
"windows-link 0.1.3",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows-strings"
|
||||||
|
version = "0.4.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57"
|
||||||
|
dependencies = [
|
||||||
|
"windows-link 0.1.3",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "winrt"
|
||||||
|
version = "0.4.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7e30cba82e22b083dc5a422c2ee77e20dc7927271a0dc981360c57c1453cb48d"
|
||||||
|
dependencies = [
|
||||||
|
"winapi",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "winrt-notification"
|
||||||
|
version = "0.2.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "57790eb281688a4682dab44df2a1ba8b78373233bd71cb291c3e75fecb1a01c4"
|
||||||
|
dependencies = [
|
||||||
|
"strum",
|
||||||
|
"strum_macros",
|
||||||
|
"winapi",
|
||||||
|
"winrt",
|
||||||
|
"xml-rs",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "xml-rs"
|
||||||
|
version = "0.6.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e1945e12e16b951721d7976520b0832496ef79c31602c7a29d950de79ba74621"
|
||||||
|
dependencies = [
|
||||||
|
"bitflags 0.9.1",
|
||||||
|
]
|
||||||
+13
@@ -0,0 +1,13 @@
|
|||||||
|
[package]
|
||||||
|
name = "surface-dial-renewed"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
evdev-rs = { git = "https://github.com/ndesh26/evdev-rs.git", rev = "8e995b8bf" }
|
||||||
|
hidapi = { version = "1.2.3", default-features = false, features = ["linux-shared-hidraw"] }
|
||||||
|
notify-rust = "=4.0.0"
|
||||||
|
udev = "0.5"
|
||||||
|
|
||||||
|
[profile.release.package.evdev-rs]
|
||||||
|
opt-level = 0
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
# Surface Dial Renewed Driver
|
||||||
|
|
||||||
|
This is a concept rewrite of the Surface Dial userspace driver. It is kept
|
||||||
|
separate from the existing daemon so the new event model can be tested without
|
||||||
|
touching the current code.
|
||||||
|
|
||||||
|
## Goals
|
||||||
|
|
||||||
|
- Multitouch scrolling first.
|
||||||
|
- Regular wheel scrolling as a fallback mode.
|
||||||
|
- Dial sensitivity adjustment by holding the button and rotating.
|
||||||
|
- Desktop notification feedback for sensitivity changes.
|
||||||
|
- Small, explicit state machines instead of mode/thread sprawl.
|
||||||
|
|
||||||
|
## Run
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cargo run --manifest-path renewed_driver/Cargo.toml -- --mode multitouch
|
||||||
|
```
|
||||||
|
|
||||||
|
Regular wheel mode:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cargo run --manifest-path renewed_driver/Cargo.toml -- --mode wheel
|
||||||
|
```
|
||||||
|
|
||||||
|
The process still needs access to the Surface Dial event device and
|
||||||
|
`/dev/uinput`, just like the original daemon.
|
||||||
|
|
||||||
|
## Controls
|
||||||
|
|
||||||
|
- Rotate: scroll.
|
||||||
|
- Hold and rotate: change sensitivity.
|
||||||
|
- Release after changing sensitivity: resume scroll mode.
|
||||||
|
|
||||||
|
Sensitivity levels are 1 through 32. KDE displays the current value through the
|
||||||
|
native OSD text path, with desktop notifications used only as a fallback.
|
||||||
|
|
||||||
|
## Documentation
|
||||||
|
|
||||||
|
Detailed architecture, behavior, protocol, install, and troubleshooting notes
|
||||||
|
live under [`doc/`](doc/README.md).
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
# Renewed Driver Documentation
|
||||||
|
|
||||||
|
This folder documents the current concept driver implementation.
|
||||||
|
|
||||||
|
- [Architecture](architecture.md): process layout, module responsibilities, and data flow.
|
||||||
|
- [Behavior](behavior.md): user-visible controls, scrolling model, sensitivity, haptics, and OSD.
|
||||||
|
- [Input and Output Protocol](input-output.md): Surface Dial event decoding and virtual uinput devices.
|
||||||
|
- [Installation and Service](installation-service.md): install files, udev rules, systemd user service, and logs.
|
||||||
|
- [Troubleshooting](troubleshooting.md): raw event inspection and common failure modes.
|
||||||
|
|
||||||
|
The driver intentionally stays small. The main design choice is to keep the
|
||||||
|
event loop explicit and avoid the configurable mode framework from the older
|
||||||
|
daemon until there is a concrete need for it.
|
||||||
@@ -0,0 +1,86 @@
|
|||||||
|
# Architecture
|
||||||
|
|
||||||
|
`surface-dial-renewed` is a userspace Linux driver. It reads the Microsoft
|
||||||
|
Surface Dial through evdev, emits virtual input devices through uinput, uses HID
|
||||||
|
for haptic feedback, and uses KDE's OSD service for sensitivity feedback.
|
||||||
|
|
||||||
|
## Runtime Threads
|
||||||
|
|
||||||
|
The process has one main event loop and one input worker thread.
|
||||||
|
|
||||||
|
The input worker is created by `DialReader::spawn` in `src/dial.rs`. It finds
|
||||||
|
the `Surface Dial System Multi Axis` event node through udev, blocks on evdev
|
||||||
|
events, and sends simplified raw events through a channel.
|
||||||
|
|
||||||
|
The main thread owns all output state:
|
||||||
|
|
||||||
|
- `DriverState` in `src/main.rs`
|
||||||
|
- `VirtualInput` in `src/virtual_input.rs`
|
||||||
|
- `Haptics` in `src/haptics.rs`
|
||||||
|
- `SensitivityNotifier` in `src/notify.rs`
|
||||||
|
|
||||||
|
Keeping output ownership on one thread avoids locking around uinput devices and
|
||||||
|
keeps gesture lifecycle decisions local to the main loop.
|
||||||
|
|
||||||
|
## Module Responsibilities
|
||||||
|
|
||||||
|
`src/main.rs`
|
||||||
|
|
||||||
|
The state machine. It decides whether a Dial rotation means scroll or
|
||||||
|
sensitivity adjustment. It starts and ends multitouch gestures, updates
|
||||||
|
sensitivity, and calls haptics/OSD hooks.
|
||||||
|
|
||||||
|
`src/dial.rs`
|
||||||
|
|
||||||
|
Device discovery and event decoding. It converts evdev events into
|
||||||
|
`DialEventKind` values: connect, disconnect, button press/release, long press,
|
||||||
|
rotation, and ignored noise.
|
||||||
|
|
||||||
|
`src/virtual_input.rs`
|
||||||
|
|
||||||
|
Creates two uinput devices:
|
||||||
|
|
||||||
|
- `Surface Dial Renewed Touchpad` for multitouch scrolling.
|
||||||
|
- `Surface Dial Renewed Wheel` for regular wheel mode.
|
||||||
|
|
||||||
|
The touchpad device is intentionally not advertised as a buttonpad and does not
|
||||||
|
expose `BTN_LEFT`, reducing the chance that very short gestures become tap or
|
||||||
|
right-click behavior.
|
||||||
|
|
||||||
|
`src/sensitivity.rs`
|
||||||
|
|
||||||
|
Defines the 1-32 sensitivity scale and maps each level to scroll scaling.
|
||||||
|
|
||||||
|
`src/haptics.rs`
|
||||||
|
|
||||||
|
Opens the Dial's HID device and sends manual haptic buzz reports. Haptics are
|
||||||
|
best-effort; failure disables haptics but does not stop scrolling.
|
||||||
|
|
||||||
|
`src/notify.rs`
|
||||||
|
|
||||||
|
Shows the sensitivity value. KDE uses `qdbus6` with
|
||||||
|
`org.kde.osdService.showText`. If that fails, the code falls back to
|
||||||
|
FreeDesktop notifications.
|
||||||
|
|
||||||
|
`src/error.rs`
|
||||||
|
|
||||||
|
Small shared error enum for the concept driver.
|
||||||
|
|
||||||
|
## Data Flow
|
||||||
|
|
||||||
|
1. `DialReader` finds `Surface Dial System Multi Axis`.
|
||||||
|
2. Raw evdev events are sent to the main loop.
|
||||||
|
3. `main.rs` decodes intent:
|
||||||
|
- button held + rotate: sensitivity adjustment
|
||||||
|
- rotate alone: scrolling
|
||||||
|
- connect/disconnect: haptics and gesture cleanup
|
||||||
|
4. `VirtualInput` emits either multitouch or wheel events.
|
||||||
|
5. `Haptics` buzzes on selector entry and sensitivity level changes.
|
||||||
|
6. `SensitivityNotifier` displays `Surface Dial sensitivity N/32`.
|
||||||
|
|
||||||
|
## Reconnect Model
|
||||||
|
|
||||||
|
The input worker scans for an existing Dial at startup. If the device is removed
|
||||||
|
or an input read fails, it sends a disconnect event, sleeps briefly, and retries
|
||||||
|
discovery/monitoring. The main loop ends any active multitouch gesture and drops
|
||||||
|
the HID haptics handle on disconnect. On reconnect it reopens haptics.
|
||||||
@@ -0,0 +1,85 @@
|
|||||||
|
# Driver Behavior
|
||||||
|
|
||||||
|
## Default Mode
|
||||||
|
|
||||||
|
The default mode is multitouch scrolling. Run explicitly with:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
surface-dial-renewed --mode multitouch
|
||||||
|
```
|
||||||
|
|
||||||
|
Regular wheel mode exists for fallback/testing:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
surface-dial-renewed --mode wheel
|
||||||
|
```
|
||||||
|
|
||||||
|
## Normal Rotation
|
||||||
|
|
||||||
|
Normal Dial rotation emits a synthetic two-finger touchpad scroll through
|
||||||
|
`Surface Dial Renewed Touchpad`.
|
||||||
|
|
||||||
|
The driver starts a virtual two-finger contact on the first rotation event,
|
||||||
|
moves both virtual fingers vertically, and ends the touch gesture after no
|
||||||
|
rotation has arrived for 2 seconds.
|
||||||
|
|
||||||
|
The 2 second hold is intentional. Very slow physical Dial movement can emit
|
||||||
|
sparse `REL_DIAL` events. If the virtual fingers are lifted too quickly, libinput
|
||||||
|
can treat those sparse movements as many tiny gestures and ignore them. Holding
|
||||||
|
the contact makes slow and precise movement accumulate into one gesture.
|
||||||
|
|
||||||
|
## Sensitivity Selector
|
||||||
|
|
||||||
|
Hold the Dial down and rotate to change sensitivity.
|
||||||
|
|
||||||
|
- Range: `1/32` through `32/32`
|
||||||
|
- Default: `7/32`
|
||||||
|
- Step threshold: `48` raw Dial units per sensitivity level
|
||||||
|
- Display: KDE OSD text, falling back to notification
|
||||||
|
- Haptics: one buzz when entering the selector, one buzz per accepted level
|
||||||
|
|
||||||
|
While the button is held, rotation never scrolls. If a multitouch scroll gesture
|
||||||
|
is active when the button is pressed, it is ended before sensitivity adjustment.
|
||||||
|
|
||||||
|
## Sensitivity Scaling
|
||||||
|
|
||||||
|
The sensitivity level maps to milli-pixels per raw Dial unit:
|
||||||
|
|
||||||
|
```text
|
||||||
|
500 + (level - 1) * 250
|
||||||
|
```
|
||||||
|
|
||||||
|
This means level 1 is `0.5` virtual coordinate units per raw Dial unit, level 7
|
||||||
|
is `2.0`, and level 32 is `8.25`.
|
||||||
|
|
||||||
|
The driver stores fractional remainders while a gesture is active. A nonzero raw
|
||||||
|
Dial event is rounded away from zero if it would otherwise produce no integer
|
||||||
|
coordinate movement.
|
||||||
|
|
||||||
|
## Haptics
|
||||||
|
|
||||||
|
Haptics are deliberately limited:
|
||||||
|
|
||||||
|
- Enter sensitivity selector: buzz once.
|
||||||
|
- Change sensitivity level: buzz once.
|
||||||
|
|
||||||
|
Normal scrolling does not buzz. It was tested and removed because it was
|
||||||
|
distracting and hard to align with physical rotation consistently.
|
||||||
|
|
||||||
|
## OSD
|
||||||
|
|
||||||
|
On KDE, the driver calls:
|
||||||
|
|
||||||
|
```text
|
||||||
|
org.kde.plasmashell /org/kde/osdService org.kde.osdService.showText
|
||||||
|
```
|
||||||
|
|
||||||
|
The displayed text is:
|
||||||
|
|
||||||
|
```text
|
||||||
|
Surface Dial sensitivity N/32
|
||||||
|
```
|
||||||
|
|
||||||
|
KDE exposes polished volume/brightness OSD methods, but those are semantically
|
||||||
|
volume/brightness-specific. The generic custom progress API is not callable as a
|
||||||
|
normal method on this system, so the driver uses text OSD instead.
|
||||||
@@ -0,0 +1,100 @@
|
|||||||
|
# Input and Output Protocol
|
||||||
|
|
||||||
|
## Physical Dial Input
|
||||||
|
|
||||||
|
The driver reads the `Surface Dial System Multi Axis` evdev device.
|
||||||
|
|
||||||
|
Find it with:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
rg -n -C 8 "Surface Dial System Multi Axis" /proc/bus/input/devices
|
||||||
|
```
|
||||||
|
|
||||||
|
Then inspect raw data:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo evtest /dev/input/eventXX
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected raw events:
|
||||||
|
|
||||||
|
```text
|
||||||
|
EV_REL / REL_DIAL: rotation
|
||||||
|
EV_KEY / BTN_0: button press and release
|
||||||
|
EV_SYN and EV_MSC: ignored framing/scancode events
|
||||||
|
```
|
||||||
|
|
||||||
|
The `Surface Dial System Control` node is not used for scrolling. It exposes
|
||||||
|
sleep/wakeup-style keys, not rotation.
|
||||||
|
|
||||||
|
## Virtual Touchpad
|
||||||
|
|
||||||
|
The multitouch output device is named:
|
||||||
|
|
||||||
|
```text
|
||||||
|
Surface Dial Renewed Touchpad
|
||||||
|
```
|
||||||
|
|
||||||
|
It exposes a type-B multitouch touchpad with slots. On gesture start it creates
|
||||||
|
two active tracking IDs. On movement it moves both slots along the Y axis and
|
||||||
|
updates the single-touch compatibility `ABS_Y` value.
|
||||||
|
|
||||||
|
Important implementation details:
|
||||||
|
|
||||||
|
- Two fingers are used because libinput interprets that as scroll.
|
||||||
|
- `INPUT_PROP_POINTER` is enabled.
|
||||||
|
- `INPUT_PROP_BUTTONPAD` is intentionally not enabled.
|
||||||
|
- `BTN_LEFT` is intentionally not exposed.
|
||||||
|
- Pressure/touch-major/tool-width axes are exposed to look more like a real
|
||||||
|
touchpad.
|
||||||
|
|
||||||
|
The lack of buttonpad/left-button capability reduces accidental tap/click
|
||||||
|
interpretation when a scroll gesture is short.
|
||||||
|
|
||||||
|
## Gesture Lifecycle
|
||||||
|
|
||||||
|
`multitouch_start`
|
||||||
|
|
||||||
|
Creates slot 0 and slot 1 with tracking IDs, positions, pressure, touch major,
|
||||||
|
and two-finger tool state.
|
||||||
|
|
||||||
|
`multitouch_move`
|
||||||
|
|
||||||
|
Moves both slots to the same new Y position. The driver keeps an accumulated
|
||||||
|
`mt_position` relative to `MT_BASELINE`.
|
||||||
|
|
||||||
|
`multitouch_settle`
|
||||||
|
|
||||||
|
Emits one final stationary frame before ending. This reduces the chance that the
|
||||||
|
final movement is interpreted as a flick.
|
||||||
|
|
||||||
|
`multitouch_end`
|
||||||
|
|
||||||
|
Sets both tracking IDs to `-1`, releases `BTN_TOUCH` and tool keys, and reports
|
||||||
|
zero pressure.
|
||||||
|
|
||||||
|
## Coordinate Model
|
||||||
|
|
||||||
|
`MT_BASELINE` is `10_000`, with coordinates from `0` to `20_000`.
|
||||||
|
|
||||||
|
The driver recenters the synthetic gesture if `mt_position` exceeds
|
||||||
|
`MT_RECENTER_THRESHOLD`. This avoids running into coordinate limits during long
|
||||||
|
scrolls.
|
||||||
|
|
||||||
|
## Virtual Wheel
|
||||||
|
|
||||||
|
The fallback wheel device is named:
|
||||||
|
|
||||||
|
```text
|
||||||
|
Surface Dial Renewed Wheel
|
||||||
|
```
|
||||||
|
|
||||||
|
It emits both:
|
||||||
|
|
||||||
|
```text
|
||||||
|
REL_WHEEL
|
||||||
|
REL_WHEEL_HI_RES
|
||||||
|
```
|
||||||
|
|
||||||
|
Wheel mode is implemented but has not received the same tuning as multitouch
|
||||||
|
mode.
|
||||||
@@ -0,0 +1,87 @@
|
|||||||
|
# Installation and Systemd Service
|
||||||
|
|
||||||
|
## Files
|
||||||
|
|
||||||
|
Install helpers:
|
||||||
|
|
||||||
|
```text
|
||||||
|
renewed_driver/install.sh
|
||||||
|
renewed_driver/uninstall.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
Systemd user service:
|
||||||
|
|
||||||
|
```text
|
||||||
|
renewed_driver/install/surface-dial-renewed.service
|
||||||
|
```
|
||||||
|
|
||||||
|
udev rules:
|
||||||
|
|
||||||
|
```text
|
||||||
|
renewed_driver/install/70-surface-dial-renewed-uinput.rules
|
||||||
|
renewed_driver/install/70-surface-dial-renewed-input.rules
|
||||||
|
renewed_driver/install/70-surface-dial-renewed-hidraw.rules
|
||||||
|
```
|
||||||
|
|
||||||
|
## Install
|
||||||
|
|
||||||
|
From the repository root:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./renewed_driver/install.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
This performs:
|
||||||
|
|
||||||
|
1. `cargo install --path renewed_driver`
|
||||||
|
2. udev rule installation under `/etc/udev/rules.d`
|
||||||
|
3. udev reload and trigger
|
||||||
|
4. systemd user service installation
|
||||||
|
5. `systemctl --user enable --now surface-dial-renewed.service`
|
||||||
|
|
||||||
|
The service runs as the desktop user, not root. That is required for access to
|
||||||
|
the user session D-Bus used by KDE OSD.
|
||||||
|
|
||||||
|
## Service
|
||||||
|
|
||||||
|
The service runs:
|
||||||
|
|
||||||
|
```text
|
||||||
|
%h/.cargo/bin/surface-dial-renewed --mode multitouch
|
||||||
|
```
|
||||||
|
|
||||||
|
It restarts on failure with a 2 second delay.
|
||||||
|
|
||||||
|
Useful commands:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
systemctl --user status surface-dial-renewed.service
|
||||||
|
systemctl --user restart surface-dial-renewed.service
|
||||||
|
journalctl --user -u surface-dial-renewed.service -f
|
||||||
|
```
|
||||||
|
|
||||||
|
## Permissions
|
||||||
|
|
||||||
|
The udev rules grant user-session access to:
|
||||||
|
|
||||||
|
- `/dev/uinput`
|
||||||
|
- Surface Dial input event nodes
|
||||||
|
- Surface Dial HID raw node for haptics
|
||||||
|
|
||||||
|
If input permissions still fail, add the user to the group that owns
|
||||||
|
`/dev/input/event*`, then log out and back in:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo gpasswd -a "$(whoami)" "$(stat -c '%G' /dev/input/event0)"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Uninstall
|
||||||
|
|
||||||
|
From the repository root:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./renewed_driver/uninstall.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
This disables the user service, removes service/rule files, reloads udev, and
|
||||||
|
uninstalls the Cargo binary.
|
||||||
@@ -0,0 +1,82 @@
|
|||||||
|
# Troubleshooting
|
||||||
|
|
||||||
|
## Check the Service
|
||||||
|
|
||||||
|
```bash
|
||||||
|
systemctl --user status surface-dial-renewed.service
|
||||||
|
journalctl --user -u surface-dial-renewed.service -n 100 --no-pager
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected startup lines:
|
||||||
|
|
||||||
|
```text
|
||||||
|
surface-dial-renewed: running in Multitouch mode
|
||||||
|
surface-dial-renewed: dial connected
|
||||||
|
```
|
||||||
|
|
||||||
|
## Check Raw Dial Rotation
|
||||||
|
|
||||||
|
Find the current event node:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
rg -n -C 8 "Surface Dial System Multi Axis" /proc/bus/input/devices
|
||||||
|
```
|
||||||
|
|
||||||
|
Then run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo evtest /dev/input/eventXX
|
||||||
|
```
|
||||||
|
|
||||||
|
Rotation should produce:
|
||||||
|
|
||||||
|
```text
|
||||||
|
type 2 (EV_REL), code 7 (REL_DIAL), value ...
|
||||||
|
```
|
||||||
|
|
||||||
|
If `REL_DIAL` does not appear, the driver cannot scroll. Reconnect the Dial over
|
||||||
|
Bluetooth and check again.
|
||||||
|
|
||||||
|
## Wrong Event Node
|
||||||
|
|
||||||
|
The Control node is not the rotation node. It looks like:
|
||||||
|
|
||||||
|
```text
|
||||||
|
Surface Dial System Control
|
||||||
|
```
|
||||||
|
|
||||||
|
and exposes `KEY_SLEEP`/`KEY_WAKEUP`. Use `Surface Dial System Multi Axis` for
|
||||||
|
rotation.
|
||||||
|
|
||||||
|
## Haptics Work But Scroll Does Not
|
||||||
|
|
||||||
|
Haptics use HID raw access; scrolling uses evdev input plus uinput output.
|
||||||
|
Haptics working only proves the HID path works.
|
||||||
|
|
||||||
|
Check:
|
||||||
|
|
||||||
|
1. Raw `REL_DIAL` events with `evtest`.
|
||||||
|
2. Service logs for `dial connected`.
|
||||||
|
3. `/dev/uinput` permissions.
|
||||||
|
4. Whether `Surface Dial Renewed Touchpad` appears in `/proc/bus/input/devices`.
|
||||||
|
|
||||||
|
## Sensitivity OSD Warnings
|
||||||
|
|
||||||
|
The service may log messages from `qdbus6` about locale fallback, for example
|
||||||
|
`Detected locale "C"`. The OSD still works. This is caused by the systemd user
|
||||||
|
service environment lacking a UTF-8 locale variable.
|
||||||
|
|
||||||
|
If it becomes noisy, set locale environment for the user service or replace the
|
||||||
|
shell-out OSD backend with a direct D-Bus implementation.
|
||||||
|
|
||||||
|
## Reconnect Oddities
|
||||||
|
|
||||||
|
After Bluetooth reconnects, event numbers can change. Always rediscover with:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
rg -n -C 8 "Surface Dial" /proc/bus/input/devices
|
||||||
|
```
|
||||||
|
|
||||||
|
If button events appear but rotation does not, disconnect and reconnect the Dial
|
||||||
|
again. The driver cannot synthesize rotation if the kernel is not emitting
|
||||||
|
`REL_DIAL`.
|
||||||
Executable
+24
@@ -0,0 +1,24 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
repo_dir="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||||
|
driver_dir="${repo_dir}/renewed_driver"
|
||||||
|
|
||||||
|
cargo install --path "${driver_dir}"
|
||||||
|
|
||||||
|
sudo install -Dm644 "${driver_dir}/install/70-surface-dial-renewed-uinput.rules" \
|
||||||
|
/etc/udev/rules.d/70-surface-dial-renewed-uinput.rules
|
||||||
|
sudo install -Dm644 "${driver_dir}/install/70-surface-dial-renewed-input.rules" \
|
||||||
|
/etc/udev/rules.d/70-surface-dial-renewed-input.rules
|
||||||
|
sudo install -Dm644 "${driver_dir}/install/70-surface-dial-renewed-hidraw.rules" \
|
||||||
|
/etc/udev/rules.d/70-surface-dial-renewed-hidraw.rules
|
||||||
|
sudo udevadm control --reload-rules
|
||||||
|
sudo udevadm trigger
|
||||||
|
|
||||||
|
install -Dm644 "${driver_dir}/install/surface-dial-renewed.service" \
|
||||||
|
"${HOME}/.config/systemd/user/surface-dial-renewed.service"
|
||||||
|
|
||||||
|
systemctl --user daemon-reload
|
||||||
|
systemctl --user enable --now surface-dial-renewed.service
|
||||||
|
|
||||||
|
systemctl --user --no-pager status surface-dial-renewed.service
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
KERNEL=="hidraw*", SUBSYSTEM=="hidraw", ATTRS{idVendor}=="045e", ATTRS{idProduct}=="091b", TAG+="uaccess", TAG+="udev-acl"
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
KERNEL=="event*", SUBSYSTEM=="input", ATTRS{name}=="Surface Dial System Multi Axis", TAG+="uaccess", TAG+="udev-acl"
|
||||||
|
KERNEL=="event*", SUBSYSTEM=="input", ATTRS{name}=="Surface Dial System Control", TAG+="uaccess", TAG+="udev-acl"
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
KERNEL=="uinput", SUBSYSTEM=="misc", OPTIONS+="static_node=uinput", TAG+="uaccess", TAG+="udev-acl"
|
||||||
@@ -0,0 +1,77 @@
|
|||||||
|
# Installing `surface-dial-renewed`
|
||||||
|
|
||||||
|
These files install the renewed driver as a systemd user service. The service
|
||||||
|
runs as your desktop user so it can access your session D-Bus for KDE OSD.
|
||||||
|
|
||||||
|
## Build and Install the Binary
|
||||||
|
|
||||||
|
Automatic install from the repository root:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./renewed_driver/install.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
Manual install:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cargo install --path renewed_driver
|
||||||
|
```
|
||||||
|
|
||||||
|
This installs:
|
||||||
|
|
||||||
|
```text
|
||||||
|
~/.cargo/bin/surface-dial-renewed
|
||||||
|
```
|
||||||
|
|
||||||
|
## Install Device Permission Rules
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo cp renewed_driver/install/70-surface-dial-renewed-uinput.rules /etc/udev/rules.d/
|
||||||
|
sudo cp renewed_driver/install/70-surface-dial-renewed-input.rules /etc/udev/rules.d/
|
||||||
|
sudo cp renewed_driver/install/70-surface-dial-renewed-hidraw.rules /etc/udev/rules.d/
|
||||||
|
sudo udevadm control --reload-rules
|
||||||
|
sudo udevadm trigger
|
||||||
|
```
|
||||||
|
|
||||||
|
If the service still cannot access input devices, add your user to the group
|
||||||
|
that owns `/dev/input/event*` on your system, then log out and back in:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo gpasswd -a "$(whoami)" "$(stat -c '%G' /dev/input/event0)"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Install the User Service
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mkdir -p ~/.config/systemd/user
|
||||||
|
cp renewed_driver/install/surface-dial-renewed.service ~/.config/systemd/user/
|
||||||
|
systemctl --user daemon-reload
|
||||||
|
systemctl --user enable --now surface-dial-renewed.service
|
||||||
|
```
|
||||||
|
|
||||||
|
## Check Status and Logs
|
||||||
|
|
||||||
|
```bash
|
||||||
|
systemctl --user status surface-dial-renewed.service
|
||||||
|
journalctl --user -u surface-dial-renewed.service -f
|
||||||
|
```
|
||||||
|
|
||||||
|
## Uninstall
|
||||||
|
|
||||||
|
Automatic uninstall from the repository root:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./renewed_driver/uninstall.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
Manual uninstall:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
systemctl --user disable --now surface-dial-renewed.service
|
||||||
|
rm ~/.config/systemd/user/surface-dial-renewed.service
|
||||||
|
sudo rm /etc/udev/rules.d/70-surface-dial-renewed-uinput.rules
|
||||||
|
sudo rm /etc/udev/rules.d/70-surface-dial-renewed-input.rules
|
||||||
|
sudo rm /etc/udev/rules.d/70-surface-dial-renewed-hidraw.rules
|
||||||
|
systemctl --user daemon-reload
|
||||||
|
sudo udevadm control --reload-rules
|
||||||
|
```
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=Surface Dial Renewed Driver
|
||||||
|
After=graphical-session.target
|
||||||
|
PartOf=graphical-session.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=simple
|
||||||
|
ExecStart=%h/.cargo/bin/surface-dial-renewed --mode multitouch
|
||||||
|
Restart=on-failure
|
||||||
|
RestartSec=2
|
||||||
|
StandardOutput=journal
|
||||||
|
StandardError=journal
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=default.target
|
||||||
+261
@@ -0,0 +1,261 @@
|
|||||||
|
use std::fs;
|
||||||
|
use std::sync::mpsc;
|
||||||
|
use std::time::{Duration, Instant};
|
||||||
|
|
||||||
|
use evdev_rs::enums::{EventCode, EventType, EV_KEY, EV_REL};
|
||||||
|
use evdev_rs::{InputEvent, ReadFlag};
|
||||||
|
|
||||||
|
use crate::error::{Error, Result};
|
||||||
|
|
||||||
|
const MULTI_AXIS_NAME: &str = r#""Surface Dial System Multi Axis""#;
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct DialEvent {
|
||||||
|
pub kind: DialEventKind,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum DialEventKind {
|
||||||
|
Connected,
|
||||||
|
Disconnected,
|
||||||
|
Ignored,
|
||||||
|
ButtonPress,
|
||||||
|
ButtonRelease,
|
||||||
|
ButtonLongPress,
|
||||||
|
Rotate(i32),
|
||||||
|
}
|
||||||
|
|
||||||
|
enum RawDialEvent {
|
||||||
|
Connected,
|
||||||
|
Disconnected,
|
||||||
|
Input(InputEvent),
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct DialReader {
|
||||||
|
rx: mpsc::Receiver<RawDialEvent>,
|
||||||
|
long_press_timeout: Duration,
|
||||||
|
// Long-press is not used by the current UX, but keeping the detection here
|
||||||
|
// leaves room for a future mode/menu without complicating the main loop.
|
||||||
|
long_press_deadline: Option<Instant>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DialReader {
|
||||||
|
pub fn spawn(long_press_timeout: Duration) -> Result<Self> {
|
||||||
|
let (tx, rx) = mpsc::channel();
|
||||||
|
|
||||||
|
std::thread::Builder::new()
|
||||||
|
.name("dial-input".to_string())
|
||||||
|
.spawn(move || {
|
||||||
|
if let Err(err) = input_worker(tx) {
|
||||||
|
eprintln!("surface-dial-renewed: input worker stopped: {err}");
|
||||||
|
}
|
||||||
|
})?;
|
||||||
|
|
||||||
|
Ok(Self {
|
||||||
|
rx,
|
||||||
|
long_press_timeout,
|
||||||
|
long_press_deadline: None,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn next_event(&mut self, idle_timeout: Option<Duration>) -> Result<Option<DialEvent>> {
|
||||||
|
let timeout = self.next_timeout(idle_timeout);
|
||||||
|
|
||||||
|
let raw = match timeout {
|
||||||
|
Some(timeout) => match self.rx.recv_timeout(timeout) {
|
||||||
|
Ok(raw) => raw,
|
||||||
|
Err(mpsc::RecvTimeoutError::Timeout) => {
|
||||||
|
if self.long_press_due() {
|
||||||
|
self.long_press_deadline = None;
|
||||||
|
return Ok(Some(DialEvent {
|
||||||
|
kind: DialEventKind::ButtonLongPress,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
return Ok(None);
|
||||||
|
}
|
||||||
|
Err(mpsc::RecvTimeoutError::Disconnected) => {
|
||||||
|
return Err(Error::UnexpectedEvent(
|
||||||
|
"input worker disconnected".to_string(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
None => self
|
||||||
|
.rx
|
||||||
|
.recv()
|
||||||
|
.map_err(|_| Error::UnexpectedEvent("input worker disconnected".to_string()))?,
|
||||||
|
};
|
||||||
|
|
||||||
|
let event = match raw {
|
||||||
|
RawDialEvent::Connected => DialEvent {
|
||||||
|
kind: DialEventKind::Connected,
|
||||||
|
},
|
||||||
|
RawDialEvent::Disconnected => DialEvent {
|
||||||
|
kind: DialEventKind::Disconnected,
|
||||||
|
},
|
||||||
|
RawDialEvent::Input(input) => decode_input(input)?,
|
||||||
|
};
|
||||||
|
|
||||||
|
match event.kind {
|
||||||
|
DialEventKind::ButtonPress => {
|
||||||
|
self.long_press_deadline = Some(Instant::now() + self.long_press_timeout);
|
||||||
|
}
|
||||||
|
DialEventKind::ButtonRelease | DialEventKind::Rotate(_) => {
|
||||||
|
self.long_press_deadline = None;
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(Some(event))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn next_timeout(&self, idle_timeout: Option<Duration>) -> Option<Duration> {
|
||||||
|
let long_press_timeout = self
|
||||||
|
.long_press_deadline
|
||||||
|
.map(|deadline| deadline.saturating_duration_since(Instant::now()));
|
||||||
|
|
||||||
|
match (idle_timeout, long_press_timeout) {
|
||||||
|
(Some(idle), Some(long_press)) => Some(idle.min(long_press)),
|
||||||
|
(Some(idle), None) => Some(idle),
|
||||||
|
(None, Some(long_press)) => Some(long_press),
|
||||||
|
(None, None) => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn long_press_due(&self) -> bool {
|
||||||
|
self.long_press_deadline
|
||||||
|
.is_some_and(|deadline| Instant::now() >= deadline)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn input_worker(tx: mpsc::Sender<RawDialEvent>) -> std::io::Result<()> {
|
||||||
|
loop {
|
||||||
|
// First handle already-connected Dials. Bluetooth reconnects can create
|
||||||
|
// new event node numbers, so each outer loop does a fresh udev scan.
|
||||||
|
match find_existing_device() {
|
||||||
|
Ok(Some(device)) => {
|
||||||
|
if let Err(err) = read_device(device, &tx) {
|
||||||
|
eprintln!("surface-dial-renewed: input read failed: {err}");
|
||||||
|
let _ = tx.send(RawDialEvent::Disconnected);
|
||||||
|
std::thread::sleep(Duration::from_millis(250));
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
Ok(None) => {}
|
||||||
|
Err(err) => {
|
||||||
|
eprintln!("surface-dial-renewed: input scan failed: {err}");
|
||||||
|
std::thread::sleep(Duration::from_millis(250));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If there is no current Dial, block on udev add events and immediately
|
||||||
|
// read the first matching device. Errors are logged and retried so a
|
||||||
|
// transient Bluetooth/udev issue does not kill the daemon.
|
||||||
|
if let Err(err) = wait_for_added_device(&tx) {
|
||||||
|
eprintln!("surface-dial-renewed: input monitor failed: {err}");
|
||||||
|
std::thread::sleep(Duration::from_millis(250));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn find_existing_device() -> std::io::Result<Option<evdev_rs::Device>> {
|
||||||
|
let mut enumerator = udev::Enumerator::new()?;
|
||||||
|
enumerator.match_subsystem("input")?;
|
||||||
|
|
||||||
|
for device in enumerator.scan_devices()? {
|
||||||
|
if let Some(device) = udev_to_evdev(&device)? {
|
||||||
|
return Ok(Some(device));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(None)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn wait_for_added_device(tx: &mpsc::Sender<RawDialEvent>) -> std::io::Result<()> {
|
||||||
|
let mut socket = udev::MonitorBuilder::new()?
|
||||||
|
.match_subsystem("input")?
|
||||||
|
.listen()?;
|
||||||
|
|
||||||
|
loop {
|
||||||
|
let Some(event) = socket.next() else {
|
||||||
|
std::thread::sleep(Duration::from_millis(25));
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
|
||||||
|
if !matches!(event.event_type(), udev::EventType::Add) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let Some(device) = udev_to_evdev(&event.device())? else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
|
||||||
|
read_device(device, tx)?;
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn udev_to_evdev(device: &udev::Device) -> std::io::Result<Option<evdev_rs::Device>> {
|
||||||
|
let Some(devnode) = device.devnode() else {
|
||||||
|
return Ok(None);
|
||||||
|
};
|
||||||
|
|
||||||
|
let Some(parent) = device.parent() else {
|
||||||
|
return Ok(None);
|
||||||
|
};
|
||||||
|
|
||||||
|
// The /dev/input/eventXX node itself is a child. The readable device name
|
||||||
|
// lives on its parent input device.
|
||||||
|
let name = parent
|
||||||
|
.property_value("NAME")
|
||||||
|
.unwrap_or_else(|| std::ffi::OsStr::new(""))
|
||||||
|
.to_string_lossy();
|
||||||
|
|
||||||
|
if name != MULTI_AXIS_NAME {
|
||||||
|
return Ok(None);
|
||||||
|
}
|
||||||
|
|
||||||
|
let file = fs::File::open(devnode)?;
|
||||||
|
evdev_rs::Device::new_from_fd(file).map(Some)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn read_device(device: evdev_rs::Device, tx: &mpsc::Sender<RawDialEvent>) -> std::io::Result<()> {
|
||||||
|
let _ = tx.send(RawDialEvent::Connected);
|
||||||
|
|
||||||
|
loop {
|
||||||
|
match device.next_event(ReadFlag::BLOCKING) {
|
||||||
|
Ok((_status, event)) => {
|
||||||
|
let _ = tx.send(RawDialEvent::Input(event));
|
||||||
|
}
|
||||||
|
// ENODEV means the Bluetooth input device disappeared.
|
||||||
|
Err(err) if err.raw_os_error() == Some(19) => break,
|
||||||
|
Err(err) => return Err(err),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let _ = tx.send(RawDialEvent::Disconnected);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn decode_input(input: InputEvent) -> Result<DialEvent> {
|
||||||
|
// Keep the public event surface deliberately small: BTN_0 is the button,
|
||||||
|
// REL_DIAL is rotation, SYN/MSC framing is noise for this driver.
|
||||||
|
let kind = match input.event_type {
|
||||||
|
EventType::EV_SYN | EventType::EV_MSC => DialEventKind::Ignored,
|
||||||
|
EventType::EV_KEY => match input.event_code {
|
||||||
|
EventCode::EV_KEY(EV_KEY::BTN_0) => match input.value {
|
||||||
|
0 => DialEventKind::ButtonRelease,
|
||||||
|
1 => DialEventKind::ButtonPress,
|
||||||
|
_ => return Err(Error::UnexpectedEvent(format!("{input:?}"))),
|
||||||
|
},
|
||||||
|
_ => DialEventKind::Ignored,
|
||||||
|
},
|
||||||
|
EventType::EV_REL => match input.event_code {
|
||||||
|
EventCode::EV_REL(EV_REL::REL_DIAL) => DialEventKind::Rotate(input.value),
|
||||||
|
_ => DialEventKind::Ignored,
|
||||||
|
},
|
||||||
|
_ => DialEventKind::Ignored,
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(DialEvent { kind })
|
||||||
|
}
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
use std::fmt;
|
||||||
|
|
||||||
|
pub type Result<T> = std::result::Result<T, Error>;
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum Error {
|
||||||
|
Io(std::io::Error),
|
||||||
|
Hid(hidapi::HidError),
|
||||||
|
Notify(notify_rust::error::Error),
|
||||||
|
UnexpectedEvent(String),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for Error {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
match self {
|
||||||
|
Error::Io(err) => write!(f, "{err}"),
|
||||||
|
Error::Hid(err) => write!(f, "{err}"),
|
||||||
|
Error::Notify(err) => write!(f, "{err}"),
|
||||||
|
Error::UnexpectedEvent(event) => write!(f, "unexpected input event: {event}"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::error::Error for Error {}
|
||||||
|
|
||||||
|
impl From<std::io::Error> for Error {
|
||||||
|
fn from(err: std::io::Error) -> Self {
|
||||||
|
Error::Io(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<notify_rust::error::Error> for Error {
|
||||||
|
fn from(err: notify_rust::error::Error) -> Self {
|
||||||
|
Error::Notify(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<hidapi::HidError> for Error {
|
||||||
|
fn from(err: hidapi::HidError) -> Self {
|
||||||
|
Error::Hid(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
use hidapi::{HidApi, HidDevice};
|
||||||
|
|
||||||
|
const SURFACE_DIAL_VENDOR_ID: u16 = 0x045e;
|
||||||
|
const SURFACE_DIAL_PRODUCT_ID: u16 = 0x091b;
|
||||||
|
|
||||||
|
pub struct Haptics {
|
||||||
|
device: Option<DialHid>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Haptics {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self { device: None }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn reconnect(&mut self) {
|
||||||
|
// HID raw is separate from evdev. Treat it as optional: lack of hidraw
|
||||||
|
// permission should not break scrolling.
|
||||||
|
if self.device.is_some() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.device = match DialHid::open() {
|
||||||
|
Ok(device) => Some(device),
|
||||||
|
Err(err) => {
|
||||||
|
eprintln!("surface-dial-renewed: haptics unavailable: {err}");
|
||||||
|
None
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn disconnect(&mut self) {
|
||||||
|
self.device = None;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn buzz(&mut self, repeat: u8) {
|
||||||
|
let Some(device) = &self.device else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
// If the Bluetooth HID handle went stale, drop it. The next Dial
|
||||||
|
// reconnect event will try to reopen haptics.
|
||||||
|
if let Err(err) = device.buzz(repeat) {
|
||||||
|
eprintln!("surface-dial-renewed: haptics failed: {err}");
|
||||||
|
self.device = None;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct DialHid {
|
||||||
|
// Keep HidApi alive for at least as long as HidDevice. Some hidapi backends
|
||||||
|
// tie device lifetime to the owning API context.
|
||||||
|
device: HidDevice,
|
||||||
|
_api: HidApi,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DialHid {
|
||||||
|
fn open() -> Result<Self, hidapi::HidError> {
|
||||||
|
let api = HidApi::new()?;
|
||||||
|
let device = api.open(SURFACE_DIAL_VENDOR_ID, SURFACE_DIAL_PRODUCT_ID)?;
|
||||||
|
Ok(Self { device, _api: api })
|
||||||
|
}
|
||||||
|
|
||||||
|
fn buzz(&self, repeat: u8) -> Result<(), hidapi::HidError> {
|
||||||
|
// Manual haptic trigger report, ported from the old driver. We only use
|
||||||
|
// this for selector entry and level changes.
|
||||||
|
let buf = [
|
||||||
|
0x01, // Report ID
|
||||||
|
repeat, // RepeatCount
|
||||||
|
0x03, // ManualTrigger
|
||||||
|
0x00, // RetriggerPeriod low byte
|
||||||
|
0x00, // RetriggerPeriod high byte
|
||||||
|
];
|
||||||
|
self.device.write(&buf)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
+275
@@ -0,0 +1,275 @@
|
|||||||
|
mod dial;
|
||||||
|
mod error;
|
||||||
|
mod haptics;
|
||||||
|
mod notify;
|
||||||
|
mod sensitivity;
|
||||||
|
mod virtual_input;
|
||||||
|
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
use dial::{DialEvent, DialEventKind, DialReader};
|
||||||
|
use error::Result;
|
||||||
|
use haptics::Haptics;
|
||||||
|
use notify::SensitivityNotifier;
|
||||||
|
use sensitivity::Sensitivity;
|
||||||
|
use virtual_input::{VirtualInput, WheelDirection};
|
||||||
|
|
||||||
|
const MULTITOUCH_IDLE_TIMEOUT: Duration = Duration::from_millis(2_000);
|
||||||
|
const MULTITOUCH_SETTLE_TIMEOUT: Duration = Duration::from_millis(80);
|
||||||
|
const SENSITIVITY_STEP_UNITS: i32 = 48;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
|
||||||
|
enum OutputMode {
|
||||||
|
Multitouch,
|
||||||
|
Wheel,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
struct DriverState {
|
||||||
|
mode: OutputMode,
|
||||||
|
sensitivity: Sensitivity,
|
||||||
|
button_down: bool,
|
||||||
|
adjusted_while_pressed: bool,
|
||||||
|
sensitivity_accumulator: i32,
|
||||||
|
// Active synthetic touchpad gesture state. These are kept in the main loop
|
||||||
|
// so all uinput writes happen from one place and gesture cleanup is explicit.
|
||||||
|
mt_active: bool,
|
||||||
|
mt_position: i32,
|
||||||
|
mt_remainder_milli: i32,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DriverState {
|
||||||
|
fn new(mode: OutputMode) -> Self {
|
||||||
|
Self {
|
||||||
|
mode,
|
||||||
|
sensitivity: Sensitivity::default(),
|
||||||
|
button_down: false,
|
||||||
|
adjusted_while_pressed: false,
|
||||||
|
sensitivity_accumulator: 0,
|
||||||
|
mt_active: false,
|
||||||
|
mt_position: 0,
|
||||||
|
mt_remainder_milli: 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
if let Err(err) = run() {
|
||||||
|
eprintln!("surface-dial-renewed: {err}");
|
||||||
|
std::process::exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn run() -> Result<()> {
|
||||||
|
let mode = parse_mode(std::env::args().skip(1));
|
||||||
|
let mut state = DriverState::new(mode);
|
||||||
|
// Input reading runs on a worker thread; output devices stay owned by the
|
||||||
|
// main thread to avoid sharing uinput handles across threads.
|
||||||
|
let mut dial = DialReader::spawn(Duration::from_millis(750))?;
|
||||||
|
let mut output = VirtualInput::new()?;
|
||||||
|
let mut notifier = SensitivityNotifier::new();
|
||||||
|
let mut haptics = Haptics::new();
|
||||||
|
|
||||||
|
eprintln!("surface-dial-renewed: running in {mode:?} mode");
|
||||||
|
|
||||||
|
loop {
|
||||||
|
// While a synthetic two-finger gesture is active, recv with a timeout.
|
||||||
|
// Timeout means "the user stopped rotating", so the virtual fingers can
|
||||||
|
// be lifted. The 2s delay is deliberate: very slow Dial movement emits
|
||||||
|
// sparse REL_DIAL events, and libinput needs one continuous gesture to
|
||||||
|
// recognize those as precise scroll movement.
|
||||||
|
let timeout = if state.mt_active {
|
||||||
|
Some(MULTITOUCH_IDLE_TIMEOUT)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
|
match dial.next_event(timeout)? {
|
||||||
|
Some(event) => {
|
||||||
|
handle_event(event, &mut state, &mut output, &mut notifier, &mut haptics)?
|
||||||
|
}
|
||||||
|
None => end_multitouch_if_active(&mut state, &mut output)?,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn handle_event(
|
||||||
|
event: DialEvent,
|
||||||
|
state: &mut DriverState,
|
||||||
|
output: &mut VirtualInput,
|
||||||
|
notifier: &mut SensitivityNotifier,
|
||||||
|
haptics: &mut Haptics,
|
||||||
|
) -> Result<()> {
|
||||||
|
match event.kind {
|
||||||
|
DialEventKind::Connected => {
|
||||||
|
eprintln!("surface-dial-renewed: dial connected");
|
||||||
|
// Haptics are tied to the HID raw device, not the evdev input node,
|
||||||
|
// so reconnect them when the input side confirms the Dial is back.
|
||||||
|
haptics.reconnect();
|
||||||
|
}
|
||||||
|
DialEventKind::Disconnected => {
|
||||||
|
end_multitouch_if_active(state, output)?;
|
||||||
|
haptics.disconnect();
|
||||||
|
eprintln!("surface-dial-renewed: dial disconnected");
|
||||||
|
}
|
||||||
|
DialEventKind::ButtonPress => {
|
||||||
|
// Holding the button turns rotation into sensitivity adjustment. The
|
||||||
|
// buzz is the tactile "selector entered" cue.
|
||||||
|
state.button_down = true;
|
||||||
|
state.adjusted_while_pressed = false;
|
||||||
|
state.sensitivity_accumulator = 0;
|
||||||
|
haptics.buzz(1);
|
||||||
|
}
|
||||||
|
DialEventKind::ButtonRelease => {
|
||||||
|
state.button_down = false;
|
||||||
|
state.adjusted_while_pressed = false;
|
||||||
|
state.sensitivity_accumulator = 0;
|
||||||
|
}
|
||||||
|
DialEventKind::ButtonLongPress => {
|
||||||
|
// The concept driver reserves long-press for future mode switching.
|
||||||
|
}
|
||||||
|
DialEventKind::Rotate(delta) => {
|
||||||
|
if state.button_down {
|
||||||
|
// Never scroll while the sensitivity selector is active. End a
|
||||||
|
// pending touchpad gesture before showing the selector so the
|
||||||
|
// desktop does not see overlapping scroll and adjustment input.
|
||||||
|
end_multitouch_if_active(state, output)?;
|
||||||
|
adjust_sensitivity(delta, state, notifier, haptics)?;
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
match state.mode {
|
||||||
|
OutputMode::Multitouch => multitouch_scroll(delta, state, output)?,
|
||||||
|
OutputMode::Wheel => wheel_scroll(delta, state, output)?,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
DialEventKind::Ignored => {}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn adjust_sensitivity(
|
||||||
|
delta: i32,
|
||||||
|
state: &mut DriverState,
|
||||||
|
notifier: &mut SensitivityNotifier,
|
||||||
|
haptics: &mut Haptics,
|
||||||
|
) -> Result<()> {
|
||||||
|
// The Dial emits many REL_DIAL events per visible detent. Accumulating raw
|
||||||
|
// units makes the 1-32 selector controllable instead of changing a level for
|
||||||
|
// every tiny event.
|
||||||
|
state.sensitivity_accumulator = state.sensitivity_accumulator.saturating_add(delta);
|
||||||
|
|
||||||
|
while state.sensitivity_accumulator.abs() >= SENSITIVITY_STEP_UNITS {
|
||||||
|
let direction = state.sensitivity_accumulator.signum();
|
||||||
|
state.sensitivity_accumulator -= direction * SENSITIVITY_STEP_UNITS;
|
||||||
|
|
||||||
|
if state.sensitivity.adjust(direction) {
|
||||||
|
notifier.show(state.sensitivity)?;
|
||||||
|
haptics.buzz(1);
|
||||||
|
eprintln!(
|
||||||
|
"surface-dial-renewed: sensitivity level {}",
|
||||||
|
state.sensitivity.level()
|
||||||
|
);
|
||||||
|
state.adjusted_while_pressed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn multitouch_scroll(delta: i32, state: &mut DriverState, output: &mut VirtualInput) -> Result<()> {
|
||||||
|
if !state.mt_active {
|
||||||
|
state.mt_position = 0;
|
||||||
|
output.multitouch_start()?;
|
||||||
|
state.mt_active = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sensitivity is expressed in milli-pixels so low sensitivity can preserve
|
||||||
|
// fractional motion. A nonzero raw delta rounds away from zero; otherwise a
|
||||||
|
// tiny slow rotation could be observed by evtest but never move the virtual
|
||||||
|
// fingers enough to give libinput a chance to scroll.
|
||||||
|
let scaled_delta = delta
|
||||||
|
.saturating_mul(state.sensitivity.multitouch_milli_pixels())
|
||||||
|
.saturating_add(state.mt_remainder_milli);
|
||||||
|
let mut whole_delta = scaled_delta / 1_000;
|
||||||
|
state.mt_remainder_milli = scaled_delta % 1_000;
|
||||||
|
|
||||||
|
if whole_delta == 0 && scaled_delta != 0 {
|
||||||
|
whole_delta = scaled_delta.signum();
|
||||||
|
state.mt_remainder_milli = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
state.mt_position = state.mt_position.saturating_add(whole_delta);
|
||||||
|
|
||||||
|
if state.mt_position.abs() > virtual_input::MT_RECENTER_THRESHOLD {
|
||||||
|
// Long scrolls would eventually hit the virtual touchpad coordinate
|
||||||
|
// limits. Recreate the two-finger contact near the baseline and keep
|
||||||
|
// only the latest movement.
|
||||||
|
output.multitouch_end()?;
|
||||||
|
output.multitouch_start()?;
|
||||||
|
state.mt_position = whole_delta;
|
||||||
|
state.mt_remainder_milli = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
output.multitouch_move(state.mt_position)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn wheel_scroll(delta: i32, state: &DriverState, output: &mut VirtualInput) -> Result<()> {
|
||||||
|
let ticks = delta.abs().saturating_mul(state.sensitivity.wheel_ticks());
|
||||||
|
let direction = if delta > 0 {
|
||||||
|
WheelDirection::Up
|
||||||
|
} else {
|
||||||
|
WheelDirection::Down
|
||||||
|
};
|
||||||
|
|
||||||
|
for _ in 0..ticks.max(1) {
|
||||||
|
output.wheel_step(direction)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn end_multitouch_if_active(state: &mut DriverState, output: &mut VirtualInput) -> Result<()> {
|
||||||
|
if state.mt_active {
|
||||||
|
// Emit one stationary frame and wait briefly before lifting fingers.
|
||||||
|
// This made libinput less likely to interpret the final delta as a
|
||||||
|
// touchpad flick/coast.
|
||||||
|
output.multitouch_settle(state.mt_position)?;
|
||||||
|
std::thread::sleep(MULTITOUCH_SETTLE_TIMEOUT);
|
||||||
|
output.multitouch_end()?;
|
||||||
|
state.mt_active = false;
|
||||||
|
state.mt_position = 0;
|
||||||
|
state.mt_remainder_milli = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_mode(args: impl Iterator<Item = String>) -> OutputMode {
|
||||||
|
let mut mode = OutputMode::Multitouch;
|
||||||
|
let mut previous = String::new();
|
||||||
|
|
||||||
|
for arg in args {
|
||||||
|
if previous == "--mode" {
|
||||||
|
mode = parse_mode_value(&arg);
|
||||||
|
} else if let Some(value) = arg.strip_prefix("--mode=") {
|
||||||
|
mode = parse_mode_value(value);
|
||||||
|
}
|
||||||
|
previous = arg;
|
||||||
|
}
|
||||||
|
|
||||||
|
mode
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_mode_value(value: &str) -> OutputMode {
|
||||||
|
match value {
|
||||||
|
"wheel" | "scroll" | "regular" => OutputMode::Wheel,
|
||||||
|
"multitouch" | "mt" | "touchpad" => OutputMode::Multitouch,
|
||||||
|
other => {
|
||||||
|
eprintln!("surface-dial-renewed: unknown mode '{other}', using multitouch");
|
||||||
|
OutputMode::Multitouch
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
use std::process::Command;
|
||||||
|
|
||||||
|
use notify_rust::{Hint, Notification, NotificationHandle, Timeout};
|
||||||
|
|
||||||
|
use crate::error::Result;
|
||||||
|
use crate::sensitivity::Sensitivity;
|
||||||
|
|
||||||
|
pub struct SensitivityNotifier {
|
||||||
|
handle: Option<NotificationHandle>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SensitivityNotifier {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self { handle: None }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn show(&mut self, sensitivity: Sensitivity) -> Result<()> {
|
||||||
|
let text = sensitivity_text(sensitivity);
|
||||||
|
// KDE notifications can collapse/suppress rapid replacement updates.
|
||||||
|
// Plasma's OSD text path is more reliable for a repeatedly-updated
|
||||||
|
// sensitivity readout, so try it first.
|
||||||
|
if show_kde_osd(&text) {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Non-KDE fallback. Replacement by notification id prevents stacking
|
||||||
|
// many sensitivity notifications.
|
||||||
|
let mut notification = Notification::new();
|
||||||
|
notification
|
||||||
|
.appname("surface-dial-renewed")
|
||||||
|
.summary(&format!(
|
||||||
|
"Surface Dial sensitivity: {}",
|
||||||
|
sensitivity.level()
|
||||||
|
))
|
||||||
|
.body(&text)
|
||||||
|
.icon("input-mouse")
|
||||||
|
.hint(Hint::Transient(true))
|
||||||
|
.hint(Hint::Category("device".to_string()))
|
||||||
|
.hint(Hint::CustomInt(
|
||||||
|
"value".to_string(),
|
||||||
|
sensitivity.percent().clamp(0, 100),
|
||||||
|
))
|
||||||
|
.timeout(Timeout::Milliseconds(1_800));
|
||||||
|
|
||||||
|
if let Some(handle) = &self.handle {
|
||||||
|
notification.id(handle.id());
|
||||||
|
}
|
||||||
|
|
||||||
|
self.handle = Some(notification.show()?);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn show_kde_osd(text: &str) -> bool {
|
||||||
|
// Shelling out keeps the dependency set simple. A future direct D-Bus
|
||||||
|
// backend would avoid qdbus locale warnings in journal output.
|
||||||
|
Command::new("qdbus6")
|
||||||
|
.args([
|
||||||
|
"org.kde.plasmashell",
|
||||||
|
"/org/kde/osdService",
|
||||||
|
"org.kde.osdService.showText",
|
||||||
|
"input-mouse",
|
||||||
|
text,
|
||||||
|
])
|
||||||
|
.status()
|
||||||
|
.is_ok_and(|status| status.success())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn sensitivity_text(sensitivity: Sensitivity) -> String {
|
||||||
|
format!(
|
||||||
|
"Surface Dial sensitivity {}/{}",
|
||||||
|
sensitivity.level(),
|
||||||
|
Sensitivity::MAX
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
#[derive(Debug, Clone, Copy)]
|
||||||
|
pub struct Sensitivity {
|
||||||
|
level: i32,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for Sensitivity {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self { level: 7 }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Sensitivity {
|
||||||
|
pub const MIN: i32 = 1;
|
||||||
|
pub const MAX: i32 = 32;
|
||||||
|
|
||||||
|
pub fn level(self) -> i32 {
|
||||||
|
self.level
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn percent(self) -> i32 {
|
||||||
|
((self.level - Self::MIN) * 100) / (Self::MAX - Self::MIN)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn adjust(&mut self, direction: i32) -> bool {
|
||||||
|
let old = self.level;
|
||||||
|
self.level = (self.level + direction).clamp(Self::MIN, Self::MAX);
|
||||||
|
old != self.level
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn multitouch_milli_pixels(self) -> i32 {
|
||||||
|
// Milli-pixels allow fractional scaling at low levels while the main
|
||||||
|
// gesture code still writes integer evdev coordinates.
|
||||||
|
500 + (self.level - Self::MIN) * 250
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn wheel_ticks(self) -> i32 {
|
||||||
|
if self.level >= 8 {
|
||||||
|
2
|
||||||
|
} else {
|
||||||
|
1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,308 @@
|
|||||||
|
use std::io;
|
||||||
|
|
||||||
|
use evdev_rs::enums::*;
|
||||||
|
use evdev_rs::{AbsInfo, Device, InputEvent, TimeVal, UInputDevice};
|
||||||
|
|
||||||
|
use crate::error::Result;
|
||||||
|
|
||||||
|
pub const MT_BASELINE: i32 = 10_000;
|
||||||
|
pub const MT_RECENTER_THRESHOLD: i32 = MT_BASELINE / 2;
|
||||||
|
|
||||||
|
// The touchpad coordinate space is synthetic. Large enough coordinates make
|
||||||
|
// long scrolls possible; recentering in main.rs prevents limit exhaustion.
|
||||||
|
const TOUCHPAD_MAX: i32 = MT_BASELINE * 2;
|
||||||
|
const SLOT_0_TRACKING_ID: i32 = 10;
|
||||||
|
const SLOT_1_TRACKING_ID: i32 = 11;
|
||||||
|
const FINGER_GAP: i32 = MT_BASELINE / 4;
|
||||||
|
const TOUCH_PRESSURE: i32 = 30;
|
||||||
|
const TOUCH_MAJOR: i32 = 700;
|
||||||
|
const TOOL_WIDTH: i32 = 4;
|
||||||
|
const HI_RES_WHEEL_STEP: i32 = 120;
|
||||||
|
|
||||||
|
pub struct VirtualInput {
|
||||||
|
wheel: UInputDevice,
|
||||||
|
touchpad: UInputDevice,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy)]
|
||||||
|
pub enum WheelDirection {
|
||||||
|
Up,
|
||||||
|
Down,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl VirtualInput {
|
||||||
|
pub fn new() -> Result<Self> {
|
||||||
|
let wheel = create_wheel_device()?;
|
||||||
|
let touchpad = create_touchpad_device()?;
|
||||||
|
|
||||||
|
// Give udev/libinput a brief chance to notice the new virtual devices
|
||||||
|
// before the first synthetic event is written.
|
||||||
|
std::thread::sleep(std::time::Duration::from_millis(250));
|
||||||
|
|
||||||
|
Ok(Self { wheel, touchpad })
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn wheel_step(&mut self, direction: WheelDirection) -> Result<()> {
|
||||||
|
let value = match direction {
|
||||||
|
WheelDirection::Up => 1,
|
||||||
|
WheelDirection::Down => -1,
|
||||||
|
};
|
||||||
|
|
||||||
|
self.wheel
|
||||||
|
.write_event(&event_rel(EV_REL::REL_WHEEL, value))?;
|
||||||
|
self.wheel.write_event(&event_rel(
|
||||||
|
EV_REL::REL_WHEEL_HI_RES,
|
||||||
|
value * HI_RES_WHEEL_STEP,
|
||||||
|
))?;
|
||||||
|
self.wheel_syn()?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn multitouch_start(&mut self) -> Result<()> {
|
||||||
|
// Type-B multitouch start: create slot 0, report one-finger state, then
|
||||||
|
// create slot 1 and switch to two-finger state. This shape mirrors a
|
||||||
|
// real touchpad closely enough for libinput to classify it as scrolling.
|
||||||
|
self.touchpad
|
||||||
|
.write_event(&event_abs(EV_ABS::ABS_MT_SLOT, 0))?;
|
||||||
|
self.touchpad
|
||||||
|
.write_event(&event_abs(EV_ABS::ABS_MT_TRACKING_ID, SLOT_0_TRACKING_ID))?;
|
||||||
|
self.touchpad.write_event(&event_abs(
|
||||||
|
EV_ABS::ABS_MT_POSITION_X,
|
||||||
|
MT_BASELINE - FINGER_GAP,
|
||||||
|
))?;
|
||||||
|
self.touchpad
|
||||||
|
.write_event(&event_abs(EV_ABS::ABS_MT_POSITION_Y, MT_BASELINE))?;
|
||||||
|
self.touchpad
|
||||||
|
.write_event(&event_abs(EV_ABS::ABS_MT_PRESSURE, TOUCH_PRESSURE))?;
|
||||||
|
self.touchpad
|
||||||
|
.write_event(&event_abs(EV_ABS::ABS_MT_TOUCH_MAJOR, TOUCH_MAJOR))?;
|
||||||
|
self.touchpad
|
||||||
|
.write_event(&event_key(EV_KEY::BTN_TOUCH, 1))?;
|
||||||
|
self.touchpad
|
||||||
|
.write_event(&event_key(EV_KEY::BTN_TOOL_FINGER, 1))?;
|
||||||
|
self.touchpad
|
||||||
|
.write_event(&event_key(EV_KEY::BTN_TOOL_DOUBLETAP, 1))?;
|
||||||
|
self.touchpad
|
||||||
|
.write_event(&event_abs(EV_ABS::ABS_X, MT_BASELINE - FINGER_GAP))?;
|
||||||
|
self.touchpad
|
||||||
|
.write_event(&event_abs(EV_ABS::ABS_Y, MT_BASELINE))?;
|
||||||
|
self.touchpad
|
||||||
|
.write_event(&event_abs(EV_ABS::ABS_PRESSURE, TOUCH_PRESSURE))?;
|
||||||
|
self.touch_syn()?;
|
||||||
|
|
||||||
|
self.touchpad
|
||||||
|
.write_event(&event_abs(EV_ABS::ABS_MT_SLOT, 1))?;
|
||||||
|
self.touchpad
|
||||||
|
.write_event(&event_abs(EV_ABS::ABS_MT_TRACKING_ID, SLOT_1_TRACKING_ID))?;
|
||||||
|
self.touchpad.write_event(&event_abs(
|
||||||
|
EV_ABS::ABS_MT_POSITION_X,
|
||||||
|
MT_BASELINE + FINGER_GAP,
|
||||||
|
))?;
|
||||||
|
self.touchpad
|
||||||
|
.write_event(&event_abs(EV_ABS::ABS_MT_POSITION_Y, MT_BASELINE))?;
|
||||||
|
self.touchpad
|
||||||
|
.write_event(&event_abs(EV_ABS::ABS_MT_PRESSURE, TOUCH_PRESSURE))?;
|
||||||
|
self.touchpad
|
||||||
|
.write_event(&event_abs(EV_ABS::ABS_MT_TOUCH_MAJOR, TOUCH_MAJOR))?;
|
||||||
|
self.touchpad
|
||||||
|
.write_event(&event_abs(EV_ABS::ABS_TOOL_WIDTH, TOOL_WIDTH))?;
|
||||||
|
self.touchpad
|
||||||
|
.write_event(&event_key(EV_KEY::BTN_TOOL_FINGER, 0))?;
|
||||||
|
self.touchpad
|
||||||
|
.write_event(&event_key(EV_KEY::BTN_TOOL_DOUBLETAP, 1))?;
|
||||||
|
self.touch_syn()?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn multitouch_move(&mut self, y_delta: i32) -> Result<()> {
|
||||||
|
let y = (MT_BASELINE + y_delta).clamp(1, TOUCHPAD_MAX - 1);
|
||||||
|
|
||||||
|
self.write_multitouch_y(y)?;
|
||||||
|
self.touch_syn()?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn multitouch_settle(&mut self, y_delta: i32) -> Result<()> {
|
||||||
|
// A final stationary frame before lift-off reduces accidental flicking
|
||||||
|
// or coasting at the end of a synthetic scroll gesture.
|
||||||
|
let y = (MT_BASELINE + y_delta).clamp(1, TOUCHPAD_MAX - 1);
|
||||||
|
|
||||||
|
self.write_multitouch_y(y)?;
|
||||||
|
self.touchpad
|
||||||
|
.write_event(&event_abs(EV_ABS::ABS_PRESSURE, TOUCH_PRESSURE))?;
|
||||||
|
self.touch_syn()?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn write_multitouch_y(&mut self, y: i32) -> Result<()> {
|
||||||
|
// Move both virtual fingers together. For two-finger scrolling, libinput
|
||||||
|
// cares about the gesture movement, not about preserving a changing
|
||||||
|
// distance between fingers.
|
||||||
|
self.touchpad
|
||||||
|
.write_event(&event_abs(EV_ABS::ABS_MT_SLOT, 0))?;
|
||||||
|
self.touchpad
|
||||||
|
.write_event(&event_abs(EV_ABS::ABS_MT_POSITION_Y, y))?;
|
||||||
|
self.touchpad
|
||||||
|
.write_event(&event_abs(EV_ABS::ABS_MT_SLOT, 1))?;
|
||||||
|
self.touchpad
|
||||||
|
.write_event(&event_abs(EV_ABS::ABS_MT_POSITION_Y, y))?;
|
||||||
|
self.touchpad.write_event(&event_abs(EV_ABS::ABS_Y, y))?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn multitouch_end(&mut self) -> Result<()> {
|
||||||
|
// End both tracking IDs and clear touch/tool state. If this is skipped
|
||||||
|
// because the process is killed, the uinput device disappearing should
|
||||||
|
// still clear the state in the compositor.
|
||||||
|
self.touchpad
|
||||||
|
.write_event(&event_abs(EV_ABS::ABS_MT_SLOT, 0))?;
|
||||||
|
self.touchpad
|
||||||
|
.write_event(&event_abs(EV_ABS::ABS_MT_TRACKING_ID, -1))?;
|
||||||
|
self.touchpad
|
||||||
|
.write_event(&event_abs(EV_ABS::ABS_MT_SLOT, 1))?;
|
||||||
|
self.touchpad
|
||||||
|
.write_event(&event_abs(EV_ABS::ABS_MT_TRACKING_ID, -1))?;
|
||||||
|
self.touchpad
|
||||||
|
.write_event(&event_key(EV_KEY::BTN_TOUCH, 0))?;
|
||||||
|
self.touchpad
|
||||||
|
.write_event(&event_key(EV_KEY::BTN_TOOL_FINGER, 0))?;
|
||||||
|
self.touchpad
|
||||||
|
.write_event(&event_key(EV_KEY::BTN_TOOL_DOUBLETAP, 0))?;
|
||||||
|
self.touchpad
|
||||||
|
.write_event(&event_key(EV_KEY::BTN_TOOL_TRIPLETAP, 0))?;
|
||||||
|
self.touchpad
|
||||||
|
.write_event(&event_key(EV_KEY::BTN_TOOL_QUADTAP, 0))?;
|
||||||
|
self.touchpad
|
||||||
|
.write_event(&event_abs(EV_ABS::ABS_PRESSURE, 0))?;
|
||||||
|
self.touch_syn()?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn wheel_syn(&mut self) -> io::Result<()> {
|
||||||
|
self.wheel.write_event(&event_syn())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn touch_syn(&mut self) -> io::Result<()> {
|
||||||
|
self.touchpad.write_event(&event_syn())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn create_wheel_device() -> io::Result<UInputDevice> {
|
||||||
|
let device = Device::new().expect("evdev device allocation failed");
|
||||||
|
device.set_name("Surface Dial Renewed Wheel");
|
||||||
|
|
||||||
|
device.enable(&EventType::EV_SYN)?;
|
||||||
|
device.enable(&EventCode::EV_SYN(EV_SYN::SYN_REPORT))?;
|
||||||
|
device.enable(&EventType::EV_REL)?;
|
||||||
|
device.enable(&EventCode::EV_REL(EV_REL::REL_WHEEL))?;
|
||||||
|
device.enable(&EventCode::EV_REL(EV_REL::REL_WHEEL_HI_RES))?;
|
||||||
|
|
||||||
|
UInputDevice::create_from_device(&device)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn create_touchpad_device() -> io::Result<UInputDevice> {
|
||||||
|
let device = Device::new().expect("evdev device allocation failed");
|
||||||
|
device.set_name("Surface Dial Renewed Touchpad");
|
||||||
|
|
||||||
|
device.enable(&InputProp::INPUT_PROP_POINTER)?;
|
||||||
|
// Do not advertise BUTTONPAD or BTN_LEFT. Those made very short gestures
|
||||||
|
// easier for the desktop stack to treat as tap/click behavior.
|
||||||
|
|
||||||
|
device.enable(&EventType::EV_SYN)?;
|
||||||
|
device.enable(&EventCode::EV_SYN(EV_SYN::SYN_REPORT))?;
|
||||||
|
|
||||||
|
device.enable(&EventType::EV_KEY)?;
|
||||||
|
device.enable(&EventCode::EV_KEY(EV_KEY::BTN_TOUCH))?;
|
||||||
|
device.enable(&EventCode::EV_KEY(EV_KEY::BTN_TOOL_FINGER))?;
|
||||||
|
device.enable(&EventCode::EV_KEY(EV_KEY::BTN_TOOL_DOUBLETAP))?;
|
||||||
|
device.enable(&EventCode::EV_KEY(EV_KEY::BTN_TOOL_TRIPLETAP))?;
|
||||||
|
device.enable(&EventCode::EV_KEY(EV_KEY::BTN_TOOL_QUADTAP))?;
|
||||||
|
|
||||||
|
device.enable(&EventType::EV_ABS)?;
|
||||||
|
enable_abs(&device, EV_ABS::ABS_X, 0, TOUCHPAD_MAX, MT_BASELINE)?;
|
||||||
|
enable_abs(&device, EV_ABS::ABS_Y, 0, TOUCHPAD_MAX, MT_BASELINE)?;
|
||||||
|
enable_abs(&device, EV_ABS::ABS_PRESSURE, 0, 255, 0)?;
|
||||||
|
enable_abs(&device, EV_ABS::ABS_TOOL_WIDTH, 0, 15, 0)?;
|
||||||
|
enable_abs(&device, EV_ABS::ABS_MT_SLOT, 0, 4, 0)?;
|
||||||
|
enable_abs(&device, EV_ABS::ABS_MT_TOUCH_MAJOR, 0, 2_500, 0)?;
|
||||||
|
enable_abs(&device, EV_ABS::ABS_MT_TRACKING_ID, 0, 65535, 0)?;
|
||||||
|
enable_abs(
|
||||||
|
&device,
|
||||||
|
EV_ABS::ABS_MT_POSITION_X,
|
||||||
|
0,
|
||||||
|
TOUCHPAD_MAX,
|
||||||
|
MT_BASELINE,
|
||||||
|
)?;
|
||||||
|
enable_abs(
|
||||||
|
&device,
|
||||||
|
EV_ABS::ABS_MT_POSITION_Y,
|
||||||
|
0,
|
||||||
|
TOUCHPAD_MAX,
|
||||||
|
MT_BASELINE,
|
||||||
|
)?;
|
||||||
|
enable_abs(&device, EV_ABS::ABS_MT_PRESSURE, 0, 255, 0)?;
|
||||||
|
|
||||||
|
UInputDevice::create_from_device(&device)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn enable_abs(
|
||||||
|
device: &Device,
|
||||||
|
code: EV_ABS,
|
||||||
|
minimum: i32,
|
||||||
|
maximum: i32,
|
||||||
|
value: i32,
|
||||||
|
) -> io::Result<()> {
|
||||||
|
// Resolution affects how libinput interprets physical movement. Keep this
|
||||||
|
// consistent across axes unless the gesture scaling is retuned deliberately.
|
||||||
|
device.enable_event_code(
|
||||||
|
&EventCode::EV_ABS(code),
|
||||||
|
Some(&AbsInfo {
|
||||||
|
value,
|
||||||
|
minimum,
|
||||||
|
maximum,
|
||||||
|
fuzz: 0,
|
||||||
|
flat: 0,
|
||||||
|
resolution: 32,
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn event_syn() -> InputEvent {
|
||||||
|
InputEvent {
|
||||||
|
time: TimeVal::new(0, 0),
|
||||||
|
event_code: EventCode::EV_SYN(EV_SYN::SYN_REPORT),
|
||||||
|
event_type: EventType::EV_SYN,
|
||||||
|
value: 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn event_key(code: EV_KEY, value: i32) -> InputEvent {
|
||||||
|
InputEvent {
|
||||||
|
time: TimeVal::new(0, 0),
|
||||||
|
event_code: EventCode::EV_KEY(code),
|
||||||
|
event_type: EventType::EV_KEY,
|
||||||
|
value,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn event_rel(code: EV_REL, value: i32) -> InputEvent {
|
||||||
|
InputEvent {
|
||||||
|
time: TimeVal::new(0, 0),
|
||||||
|
event_code: EventCode::EV_REL(code),
|
||||||
|
event_type: EventType::EV_REL,
|
||||||
|
value,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn event_abs(code: EV_ABS, value: i32) -> InputEvent {
|
||||||
|
InputEvent {
|
||||||
|
time: TimeVal::new(0, 0),
|
||||||
|
event_code: EventCode::EV_ABS(code),
|
||||||
|
event_type: EventType::EV_ABS,
|
||||||
|
value,
|
||||||
|
}
|
||||||
|
}
|
||||||
Executable
+13
@@ -0,0 +1,13 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
systemctl --user disable --now surface-dial-renewed.service || true
|
||||||
|
rm -f "${HOME}/.config/systemd/user/surface-dial-renewed.service"
|
||||||
|
systemctl --user daemon-reload
|
||||||
|
|
||||||
|
sudo rm -f /etc/udev/rules.d/70-surface-dial-renewed-uinput.rules
|
||||||
|
sudo rm -f /etc/udev/rules.d/70-surface-dial-renewed-input.rules
|
||||||
|
sudo rm -f /etc/udev/rules.d/70-surface-dial-renewed-hidraw.rules
|
||||||
|
sudo udevadm control --reload-rules
|
||||||
|
|
||||||
|
cargo uninstall surface-dial-renewed || true
|
||||||
Reference in New Issue
Block a user