Initial commit

This commit is contained in:
2026-06-26 15:35:13 +02:00
commit b56dd2a34e
23 changed files with 2407 additions and 0 deletions
Generated
+686
View File
@@ -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
View File
@@ -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
+42
View File
@@ -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).
+13
View File
@@ -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.
+86
View File
@@ -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.
+85
View File
@@ -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.
+100
View File
@@ -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.
+87
View File
@@ -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.
+82
View File
@@ -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
View File
@@ -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"
+77
View File
@@ -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
```
+15
View File
@@ -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
View File
@@ -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 })
}
+42
View File
@@ -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)
}
}
+76
View File
@@ -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
View File
@@ -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
}
}
}
+75
View File
@@ -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
)
}
+43
View File
@@ -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
}
}
}
+308
View File
@@ -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
View File
@@ -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