Compare commits
7 commits
Author | SHA1 | Date | |
---|---|---|---|
dde97be4a0 | |||
ca87295359 | |||
43bdc008a1 | |||
63824510a5 | |||
3f3382ffa7 | |||
08aac599a6 | |||
af21756cef |
50 changed files with 2278 additions and 1588 deletions
2
.gitignore
vendored
2
.gitignore
vendored
|
@ -1,2 +1,2 @@
|
||||||
build/
|
build/
|
||||||
target/
|
target/
|
||||||
|
|
481
Cargo.lock
generated
Normal file
481
Cargo.lock
generated
Normal file
|
@ -0,0 +1,481 @@
|
||||||
|
# This file is automatically @generated by Cargo.
|
||||||
|
# It is not intended for manual editing.
|
||||||
|
version = 4
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "anstream"
|
||||||
|
version = "0.6.19"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "301af1932e46185686725e0fad2f8f2aa7da69dd70bf6ecc44d6b703844a3933"
|
||||||
|
dependencies = [
|
||||||
|
"anstyle",
|
||||||
|
"anstyle-parse",
|
||||||
|
"anstyle-query",
|
||||||
|
"anstyle-wincon",
|
||||||
|
"colorchoice",
|
||||||
|
"is_terminal_polyfill",
|
||||||
|
"utf8parse",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "anstyle"
|
||||||
|
version = "1.0.11"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "862ed96ca487e809f1c8e5a8447f6ee2cf102f846893800b20cebdf541fc6bbd"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "anstyle-parse"
|
||||||
|
version = "0.2.7"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2"
|
||||||
|
dependencies = [
|
||||||
|
"utf8parse",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "anstyle-query"
|
||||||
|
version = "1.1.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "6c8bdeb6047d8983be085bab0ba1472e6dc604e7041dbf6fcd5e71523014fae9"
|
||||||
|
dependencies = [
|
||||||
|
"windows-sys",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "anstyle-wincon"
|
||||||
|
version = "3.0.9"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "403f75924867bb1033c59fbf0797484329750cfbe3c4325cd33127941fabc882"
|
||||||
|
dependencies = [
|
||||||
|
"anstyle",
|
||||||
|
"once_cell_polyfill",
|
||||||
|
"windows-sys",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "bitflags"
|
||||||
|
version = "2.9.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "bitvec"
|
||||||
|
version = "1.0.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1bc2832c24239b0141d5674bb9174f9d68a8b5b3f2753311927c172ca46f7e9c"
|
||||||
|
dependencies = [
|
||||||
|
"funty",
|
||||||
|
"radium",
|
||||||
|
"tap",
|
||||||
|
"wyz",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "bstr"
|
||||||
|
version = "1.12.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "234113d19d0d7d613b40e86fb654acf958910802bcceab913a4f9e7cda03b1a4"
|
||||||
|
dependencies = [
|
||||||
|
"memchr",
|
||||||
|
"regex-automata",
|
||||||
|
"serde",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cfg-if"
|
||||||
|
version = "1.0.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9555578bc9e57714c812a1f84e4fc5b4d21fcb063490c624de019f7464c91268"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cfg_aliases"
|
||||||
|
version = "0.2.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "clap"
|
||||||
|
version = "4.5.42"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ed87a9d530bb41a67537289bafcac159cb3ee28460e0a4571123d2a778a6a882"
|
||||||
|
dependencies = [
|
||||||
|
"clap_builder",
|
||||||
|
"clap_derive",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "clap_builder"
|
||||||
|
version = "4.5.42"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "64f4f3f3c77c94aff3c7e9aac9a2ca1974a5adf392a8bb751e827d6d127ab966"
|
||||||
|
dependencies = [
|
||||||
|
"anstream",
|
||||||
|
"anstyle",
|
||||||
|
"clap_lex",
|
||||||
|
"strsim",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "clap_derive"
|
||||||
|
version = "4.5.41"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ef4f52386a59ca4c860f7393bcf8abd8dfd91ecccc0f774635ff68e92eeef491"
|
||||||
|
dependencies = [
|
||||||
|
"heck",
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "clap_lex"
|
||||||
|
version = "0.7.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b94f61472cee1439c0b966b47e3aca9ae07e45d070759512cd390ea2bebc6675"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "colorchoice"
|
||||||
|
version = "1.0.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "dirs"
|
||||||
|
version = "6.0.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c3e8aa94d75141228480295a7d0e7feb620b1a5ad9f12bc40be62411e38cce4e"
|
||||||
|
dependencies = [
|
||||||
|
"dirs-sys",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "dirs-sys"
|
||||||
|
version = "0.5.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
"option-ext",
|
||||||
|
"redox_users",
|
||||||
|
"windows-sys",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "evdev"
|
||||||
|
version = "0.13.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a3c10865aeab1a7399b3c2d6046e8dcc7f5227b656f235ed63ef5ee45a47b8f8"
|
||||||
|
dependencies = [
|
||||||
|
"bitvec",
|
||||||
|
"cfg-if",
|
||||||
|
"libc",
|
||||||
|
"nix",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "funty"
|
||||||
|
version = "2.0.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "getrandom"
|
||||||
|
version = "0.2.16"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
"libc",
|
||||||
|
"wasi",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "heck"
|
||||||
|
version = "0.5.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "is_terminal_polyfill"
|
||||||
|
version = "1.70.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "joyful"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"clap",
|
||||||
|
"evdev",
|
||||||
|
"shellexpand",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "libc"
|
||||||
|
version = "0.2.174"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1171693293099992e19cddea4e8b849964e9846f4acee11b3948bcc337be8776"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "libredox"
|
||||||
|
version = "0.1.9"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "391290121bad3d37fbddad76d8f5d1c1c314cfc646d143d7e07a3086ddff0ce3"
|
||||||
|
dependencies = [
|
||||||
|
"bitflags",
|
||||||
|
"libc",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "memchr"
|
||||||
|
version = "2.7.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "32a282da65faaf38286cf3be983213fcf1d2e2a58700e808f83f4ea9a4804bc0"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "nix"
|
||||||
|
version = "0.29.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46"
|
||||||
|
dependencies = [
|
||||||
|
"bitflags",
|
||||||
|
"cfg-if",
|
||||||
|
"cfg_aliases",
|
||||||
|
"libc",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "once_cell_polyfill"
|
||||||
|
version = "1.70.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a4895175b425cb1f87721b59f0f286c2092bd4af812243672510e1ac53e2e0ad"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "option-ext"
|
||||||
|
version = "0.2.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "os_str_bytes"
|
||||||
|
version = "6.6.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e2355d85b9a3786f481747ced0e0ff2ba35213a1f9bd406ed906554d7af805a1"
|
||||||
|
dependencies = [
|
||||||
|
"memchr",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "proc-macro2"
|
||||||
|
version = "1.0.95"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778"
|
||||||
|
dependencies = [
|
||||||
|
"unicode-ident",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "quote"
|
||||||
|
version = "1.0.40"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "radium"
|
||||||
|
version = "0.7.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "redox_users"
|
||||||
|
version = "0.5.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a4e608c6638b9c18977b00b475ac1f28d14e84b27d8d42f70e0bf1e3dec127ac"
|
||||||
|
dependencies = [
|
||||||
|
"getrandom",
|
||||||
|
"libredox",
|
||||||
|
"thiserror",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "regex-automata"
|
||||||
|
version = "0.4.9"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "serde"
|
||||||
|
version = "1.0.219"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6"
|
||||||
|
dependencies = [
|
||||||
|
"serde_derive",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "serde_derive"
|
||||||
|
version = "1.0.219"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "shellexpand"
|
||||||
|
version = "3.1.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8b1fdf65dd6331831494dd616b30351c38e96e45921a27745cf98490458b90bb"
|
||||||
|
dependencies = [
|
||||||
|
"bstr",
|
||||||
|
"dirs",
|
||||||
|
"os_str_bytes",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "strsim"
|
||||||
|
version = "0.11.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "syn"
|
||||||
|
version = "2.0.104"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "17b6f705963418cdb9927482fa304bc562ece2fdd4f616084c50b7023b435a40"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"unicode-ident",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tap"
|
||||||
|
version = "1.0.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "thiserror"
|
||||||
|
version = "2.0.12"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708"
|
||||||
|
dependencies = [
|
||||||
|
"thiserror-impl",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "thiserror-impl"
|
||||||
|
version = "2.0.12"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "unicode-ident"
|
||||||
|
version = "1.0.18"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "utf8parse"
|
||||||
|
version = "0.2.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wasi"
|
||||||
|
version = "0.11.1+wasi-snapshot-preview1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows-sys"
|
||||||
|
version = "0.59.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b"
|
||||||
|
dependencies = [
|
||||||
|
"windows-targets",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows-targets"
|
||||||
|
version = "0.52.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"
|
||||||
|
dependencies = [
|
||||||
|
"windows_aarch64_gnullvm",
|
||||||
|
"windows_aarch64_msvc",
|
||||||
|
"windows_i686_gnu",
|
||||||
|
"windows_i686_gnullvm",
|
||||||
|
"windows_i686_msvc",
|
||||||
|
"windows_x86_64_gnu",
|
||||||
|
"windows_x86_64_gnullvm",
|
||||||
|
"windows_x86_64_msvc",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows_aarch64_gnullvm"
|
||||||
|
version = "0.52.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows_aarch64_msvc"
|
||||||
|
version = "0.52.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows_i686_gnu"
|
||||||
|
version = "0.52.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows_i686_gnullvm"
|
||||||
|
version = "0.52.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows_i686_msvc"
|
||||||
|
version = "0.52.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows_x86_64_gnu"
|
||||||
|
version = "0.52.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows_x86_64_gnullvm"
|
||||||
|
version = "0.52.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows_x86_64_msvc"
|
||||||
|
version = "0.52.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wyz"
|
||||||
|
version = "0.5.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "05f360fc0b24296329c78fda852a1e9ae82de9cf7b27dae4b7f62f118f77b9ed"
|
||||||
|
dependencies = [
|
||||||
|
"tap",
|
||||||
|
]
|
10
Cargo.toml
Normal file
10
Cargo.toml
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
[package]
|
||||||
|
name = "joyful"
|
||||||
|
version = "0.1.0"
|
||||||
|
description = "Joystick remapper"
|
||||||
|
edition = "2024"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
clap = { version = "4.5.42", features = ["derive"] }
|
||||||
|
evdev = { version = "0.13.1" }
|
||||||
|
shellexpand = { version = "3.1.1", features = ["full"] }
|
|
@ -5,8 +5,7 @@ import (
|
||||||
"slices"
|
"slices"
|
||||||
|
|
||||||
// TODO: using config here feels like bad coupling... ButtonFromIndex might need a refactor / move
|
// TODO: using config here feels like bad coupling... ButtonFromIndex might need a refactor / move
|
||||||
|
"git.annabunches.net/annabunches/joyful/internal/config"
|
||||||
"git.annabunches.net/annabunches/joyful/internal/eventcodes"
|
|
||||||
"git.annabunches.net/annabunches/joyful/internal/logger"
|
"git.annabunches.net/annabunches/joyful/internal/logger"
|
||||||
"github.com/holoplot/go-evdev"
|
"github.com/holoplot/go-evdev"
|
||||||
flag "github.com/spf13/pflag"
|
flag "github.com/spf13/pflag"
|
||||||
|
@ -21,7 +20,7 @@ func isJoystickLike(device *evdev.InputDevice) bool {
|
||||||
if slices.Contains(types, evdev.EV_KEY) {
|
if slices.Contains(types, evdev.EV_KEY) {
|
||||||
buttons := device.CapableEvents(evdev.EV_KEY)
|
buttons := device.CapableEvents(evdev.EV_KEY)
|
||||||
|
|
||||||
for _, code := range eventcodes.ButtonFromIndex {
|
for _, code := range config.ButtonFromIndex {
|
||||||
if slices.Contains(buttons, code) {
|
if slices.Contains(buttons, code) {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,146 +0,0 @@
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"sync"
|
|
||||||
|
|
||||||
"git.annabunches.net/annabunches/joyful/internal/configparser"
|
|
||||||
"git.annabunches.net/annabunches/joyful/internal/logger"
|
|
||||||
"git.annabunches.net/annabunches/joyful/internal/mappingrules"
|
|
||||||
"git.annabunches.net/annabunches/joyful/internal/virtualdevice"
|
|
||||||
"github.com/holoplot/go-evdev"
|
|
||||||
)
|
|
||||||
|
|
||||||
func initPhysicalDevices(conf *configparser.Config) map[string]*evdev.InputDevice {
|
|
||||||
pDeviceMap := make(map[string]*evdev.InputDevice)
|
|
||||||
|
|
||||||
for _, devConfig := range conf.Devices {
|
|
||||||
if devConfig.Type != configparser.DeviceTypePhysical {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
innerConfig := devConfig.Config.(configparser.DeviceConfigPhysical)
|
|
||||||
name, device, err := initPhysicalDevice(innerConfig)
|
|
||||||
if err != nil {
|
|
||||||
logger.LogError(err, "Failed to initialize physical device")
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
pDeviceMap[name] = device
|
|
||||||
|
|
||||||
displayName := innerConfig.DeviceName
|
|
||||||
if innerConfig.DevicePath != "" {
|
|
||||||
displayName = innerConfig.DevicePath
|
|
||||||
}
|
|
||||||
logger.Logf("Connected to '%s' as '%s'", displayName, name)
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(pDeviceMap) == 0 {
|
|
||||||
logger.Log("Warning: no physical devices found in configuration. No rules will work.")
|
|
||||||
}
|
|
||||||
return pDeviceMap
|
|
||||||
}
|
|
||||||
|
|
||||||
func initPhysicalDevice(config configparser.DeviceConfigPhysical) (string, *evdev.InputDevice, error) {
|
|
||||||
name := config.Name
|
|
||||||
var device *evdev.InputDevice
|
|
||||||
var err error
|
|
||||||
|
|
||||||
if config.DevicePath != "" {
|
|
||||||
device, err = evdev.Open(config.DevicePath)
|
|
||||||
} else {
|
|
||||||
device, err = evdev.OpenByName(config.DeviceName)
|
|
||||||
}
|
|
||||||
|
|
||||||
if config.Lock && err == nil {
|
|
||||||
grabErr := device.Grab()
|
|
||||||
logger.LogIfError(grabErr, "Failed to lock device for exclusive access")
|
|
||||||
}
|
|
||||||
|
|
||||||
return name, device, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: juggling all these maps is a pain. Is there a better solution here?
|
|
||||||
func initVirtualBuffers(config *configparser.Config) (map[string]*evdev.InputDevice,
|
|
||||||
map[string]*virtualdevice.EventBuffer,
|
|
||||||
map[*evdev.InputDevice]*virtualdevice.EventBuffer) {
|
|
||||||
|
|
||||||
vDevicesByName := make(map[string]*evdev.InputDevice)
|
|
||||||
vBuffersByName := make(map[string]*virtualdevice.EventBuffer)
|
|
||||||
vBuffersByDevice := make(map[*evdev.InputDevice]*virtualdevice.EventBuffer)
|
|
||||||
|
|
||||||
for _, devConfig := range config.Devices {
|
|
||||||
if devConfig.Type != configparser.DeviceTypeVirtual {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
vConfig := devConfig.Config.(configparser.DeviceConfigVirtual)
|
|
||||||
buffer, err := virtualdevice.NewEventBuffer(vConfig)
|
|
||||||
if err != nil {
|
|
||||||
logger.LogError(err, "Failed to create virtual device, skipping")
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
vDevicesByName[buffer.Name] = buffer.Device.(*evdev.InputDevice)
|
|
||||||
vBuffersByName[buffer.Name] = buffer
|
|
||||||
vBuffersByDevice[buffer.Device.(*evdev.InputDevice)] = buffer
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(vDevicesByName) == 0 {
|
|
||||||
logger.Log("Warning: no virtual devices found in configuration. No rules will work.")
|
|
||||||
}
|
|
||||||
|
|
||||||
return vDevicesByName, vBuffersByName, vBuffersByDevice
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: At some point it would *very likely* make sense to map each rule to all of the physical devices that can
|
|
||||||
// trigger it, and return that instead. Something like a map[Device][]mappingrule.MappingRule.
|
|
||||||
// This would speed up rule matching by only checking relevant rules for a given input event.
|
|
||||||
// We could take this further and make it a map[<struct of *inputdevice, type, and code>][]rule
|
|
||||||
// For very large rule-bases this may be helpful for staying performant.
|
|
||||||
func loadRules(
|
|
||||||
config *configparser.Config,
|
|
||||||
pDevices map[string]*evdev.InputDevice,
|
|
||||||
vDevices map[string]*evdev.InputDevice,
|
|
||||||
modes []string) ([]mappingrules.MappingRule, <-chan ChannelEvent, func(), *sync.WaitGroup) {
|
|
||||||
|
|
||||||
var wg sync.WaitGroup
|
|
||||||
eventChannel := make(chan ChannelEvent, 1000)
|
|
||||||
ctx, cancel := context.WithCancel(context.Background())
|
|
||||||
|
|
||||||
// Setup device mapping for the mappingrules package
|
|
||||||
pDevs := mappingrules.ConvertDeviceMap(pDevices)
|
|
||||||
vDevs := mappingrules.ConvertDeviceMap(vDevices)
|
|
||||||
|
|
||||||
// Initialize rules
|
|
||||||
rules := make([]mappingrules.MappingRule, 0)
|
|
||||||
for _, ruleConfig := range config.Rules {
|
|
||||||
newRule, err := mappingrules.NewRule(ruleConfig, pDevs, vDevs, modes)
|
|
||||||
if err != nil {
|
|
||||||
logger.LogError(err, "Failed to create rule, skipping")
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
rules = append(rules, newRule)
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.Logf("Created %d mapping rules.", len(rules))
|
|
||||||
|
|
||||||
// start listening for events on devices and timers
|
|
||||||
for _, device := range pDevices {
|
|
||||||
wg.Add(1)
|
|
||||||
go eventWatcher(device, eventChannel, ctx, &wg)
|
|
||||||
}
|
|
||||||
|
|
||||||
timerCount := 0
|
|
||||||
for _, rule := range rules {
|
|
||||||
if timedRule, ok := rule.(mappingrules.TimedEventEmitter); ok {
|
|
||||||
wg.Add(1)
|
|
||||||
go timerWatcher(timedRule, eventChannel, ctx, &wg)
|
|
||||||
timerCount++
|
|
||||||
}
|
|
||||||
}
|
|
||||||
logger.Logf("Registered %d timers.", timerCount)
|
|
||||||
|
|
||||||
go consoleWatcher(eventChannel)
|
|
||||||
|
|
||||||
return rules, eventChannel, cancel, &wg
|
|
||||||
}
|
|
|
@ -1,15 +1,19 @@
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
|
"sync"
|
||||||
|
|
||||||
"github.com/holoplot/go-evdev"
|
"github.com/holoplot/go-evdev"
|
||||||
flag "github.com/spf13/pflag"
|
flag "github.com/spf13/pflag"
|
||||||
|
|
||||||
"git.annabunches.net/annabunches/joyful/internal/configparser"
|
"git.annabunches.net/annabunches/joyful/internal/config"
|
||||||
"git.annabunches.net/annabunches/joyful/internal/logger"
|
"git.annabunches.net/annabunches/joyful/internal/logger"
|
||||||
|
"git.annabunches.net/annabunches/joyful/internal/mappingrules"
|
||||||
|
"git.annabunches.net/annabunches/joyful/internal/virtualdevice"
|
||||||
)
|
)
|
||||||
|
|
||||||
func getConfigDir(dir string) string {
|
func getConfigDir(dir string) string {
|
||||||
|
@ -17,6 +21,45 @@ func getConfigDir(dir string) string {
|
||||||
return os.ExpandEnv(configDir)
|
return os.ExpandEnv(configDir)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func readConfig(configDir string) *config.ConfigParser {
|
||||||
|
parser := &config.ConfigParser{}
|
||||||
|
err := parser.Parse(configDir)
|
||||||
|
logger.FatalIfError(err, "Failed to parse config")
|
||||||
|
return parser
|
||||||
|
}
|
||||||
|
|
||||||
|
func initVirtualBuffers(config *config.ConfigParser) (map[string]*virtualdevice.EventBuffer, map[*evdev.InputDevice]*virtualdevice.EventBuffer) {
|
||||||
|
vDevices := config.CreateVirtualDevices()
|
||||||
|
if len(vDevices) == 0 {
|
||||||
|
logger.Log("Warning: no virtual devices found in configuration. No rules will work.")
|
||||||
|
}
|
||||||
|
|
||||||
|
vBuffersByName := make(map[string]*virtualdevice.EventBuffer)
|
||||||
|
vBuffersByDevice := make(map[*evdev.InputDevice]*virtualdevice.EventBuffer)
|
||||||
|
for name, device := range vDevices {
|
||||||
|
vBuffersByName[name] = virtualdevice.NewEventBuffer(device)
|
||||||
|
vBuffersByDevice[device] = vBuffersByName[name]
|
||||||
|
}
|
||||||
|
return vBuffersByName, vBuffersByDevice
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extracts the evdev devices from a list of virtual buffers and returns them.
|
||||||
|
func getVirtualDevices(buffers map[string]*virtualdevice.EventBuffer) map[string]*evdev.InputDevice {
|
||||||
|
devices := make(map[string]*evdev.InputDevice)
|
||||||
|
for name, buffer := range buffers {
|
||||||
|
devices[name] = buffer.Device.(*evdev.InputDevice)
|
||||||
|
}
|
||||||
|
return devices
|
||||||
|
}
|
||||||
|
|
||||||
|
func initPhysicalDevices(config *config.ConfigParser) map[string]*evdev.InputDevice {
|
||||||
|
pDeviceMap := config.ConnectPhysicalDevices()
|
||||||
|
if len(pDeviceMap) == 0 {
|
||||||
|
logger.Log("Warning: no physical devices found in configuration. No rules will work.")
|
||||||
|
}
|
||||||
|
return pDeviceMap
|
||||||
|
}
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
// parse command-line
|
// parse command-line
|
||||||
var configFlag string
|
var configFlag string
|
||||||
|
@ -27,39 +70,32 @@ func main() {
|
||||||
|
|
||||||
// parse configs
|
// parse configs
|
||||||
configDir := getConfigDir(configFlag)
|
configDir := getConfigDir(configFlag)
|
||||||
config, err := configparser.ParseConfig(configDir)
|
config := readConfig(configDir)
|
||||||
logger.FatalIfError(err, "Failed to parse configuration")
|
|
||||||
|
|
||||||
// initialize TTS
|
// initialize TTS
|
||||||
tts, err := newTTS(ttsOps)
|
tts, err := newTTS(ttsOps)
|
||||||
logger.LogIfError(err, "Failed to initialize TTS")
|
logger.LogIfError(err, "Failed to initialize TTS")
|
||||||
|
|
||||||
// Initialize virtual devices with event buffers
|
// Initialize virtual devices with event buffers
|
||||||
vDevicesByName, vBuffersByName, vBuffersByDevice := initVirtualBuffers(config)
|
vBuffersByName, vBuffersByDevice := initVirtualBuffers(config)
|
||||||
|
|
||||||
// Initialize physical devices
|
// Initialize physical devices
|
||||||
pDevices := initPhysicalDevices(config)
|
pDevices := initPhysicalDevices(config)
|
||||||
|
|
||||||
// initialize the mode variables
|
|
||||||
var mode string
|
|
||||||
modes := config.Modes
|
|
||||||
if len(modes) == 0 {
|
|
||||||
mode = "*"
|
|
||||||
} else {
|
|
||||||
mode = config.Modes[0]
|
|
||||||
}
|
|
||||||
|
|
||||||
// Load the rules
|
// Load the rules
|
||||||
rules, eventChannel, cancel, wg := loadRules(config, pDevices, vDevicesByName, modes)
|
rules, eventChannel, cancel, wg := loadRules(config, pDevices, getVirtualDevices(vBuffersByName))
|
||||||
|
|
||||||
|
// initialize the mode variable
|
||||||
|
mode := config.GetModes()[0]
|
||||||
|
|
||||||
// initialize TTS phrases for modes
|
// initialize TTS phrases for modes
|
||||||
for _, m := range modes {
|
for _, m := range config.GetModes() {
|
||||||
tts.AddMessage(m)
|
tts.AddMessage(m)
|
||||||
logger.LogDebugf("Added TTS message '%s'", m)
|
logger.LogDebugf("Added TTS message '%s'", m)
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Println("Joyful Running! Press Ctrl+C to quit. Press Enter to reload rules.")
|
fmt.Println("Joyful Running! Press Ctrl+C to quit. Press Enter to reload rules.")
|
||||||
if len(modes) > 0 {
|
if len(config.GetModes()) > 1 {
|
||||||
logger.Logf("Initial mode set to '%s'", mode)
|
logger.Logf("Initial mode set to '%s'", mode)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -97,18 +133,13 @@ func main() {
|
||||||
|
|
||||||
case ChannelEventReload:
|
case ChannelEventReload:
|
||||||
// stop existing channels
|
// stop existing channels
|
||||||
config, err := configparser.ParseConfig(configDir) // reload the config
|
|
||||||
if err != nil {
|
|
||||||
logger.LogError(err, "Failed to parse config, no changes made")
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
fmt.Println("Reloading rules.")
|
fmt.Println("Reloading rules.")
|
||||||
cancel()
|
cancel()
|
||||||
fmt.Println("Waiting for existing listeners to exit. Provide input from each of your devices.")
|
fmt.Println("Waiting for existing listeners to exit. Provide input from each of your devices.")
|
||||||
wg.Wait()
|
wg.Wait()
|
||||||
fmt.Println("Listeners exited. Loading new rules.")
|
fmt.Println("Listeners exited. Parsing config.")
|
||||||
rules, eventChannel, cancel, wg = loadRules(config, pDevices, vDevicesByName, modes)
|
config := readConfig(configDir) // reload the config
|
||||||
|
rules, eventChannel, cancel, wg = loadRules(config, pDevices, getVirtualDevices(vBuffersByName))
|
||||||
fmt.Println("Config re-loaded. Only rule changes applied. Device and Mode changes require restart.")
|
fmt.Println("Config re-loaded. Only rule changes applied. Device and Mode changes require restart.")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -117,3 +148,37 @@ func main() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func loadRules(
|
||||||
|
config *config.ConfigParser,
|
||||||
|
pDevices map[string]*evdev.InputDevice,
|
||||||
|
vDevices map[string]*evdev.InputDevice) ([]mappingrules.MappingRule, <-chan ChannelEvent, func(), *sync.WaitGroup) {
|
||||||
|
|
||||||
|
var wg sync.WaitGroup
|
||||||
|
eventChannel := make(chan ChannelEvent, 1000)
|
||||||
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
|
||||||
|
// Initialize rules
|
||||||
|
rules := config.BuildRules(pDevices, vDevices)
|
||||||
|
logger.Logf("Created %d mapping rules.", len(rules))
|
||||||
|
|
||||||
|
// start listening for events on devices and timers
|
||||||
|
for _, device := range pDevices {
|
||||||
|
wg.Add(1)
|
||||||
|
go eventWatcher(device, eventChannel, ctx, &wg)
|
||||||
|
}
|
||||||
|
|
||||||
|
timerCount := 0
|
||||||
|
for _, rule := range rules {
|
||||||
|
if timedRule, ok := rule.(mappingrules.TimedEventEmitter); ok {
|
||||||
|
wg.Add(1)
|
||||||
|
go timerWatcher(timedRule, eventChannel, ctx, &wg)
|
||||||
|
timerCount++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
logger.Logf("Registered %d timers.", timerCount)
|
||||||
|
|
||||||
|
go consoleWatcher(eventChannel)
|
||||||
|
|
||||||
|
return rules, eventChannel, cancel, &wg
|
||||||
|
}
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
package eventcodes
|
package config
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
@ -8,17 +8,17 @@ import (
|
||||||
"github.com/holoplot/go-evdev"
|
"github.com/holoplot/go-evdev"
|
||||||
)
|
)
|
||||||
|
|
||||||
func ParseCodeButton(code string) (evdev.EvCode, error) {
|
func parseCodeButton(code string) (evdev.EvCode, error) {
|
||||||
prefix := CodePrefixButton
|
prefix := CodePrefixButton
|
||||||
|
|
||||||
if strings.HasPrefix(code, CodePrefixKey+"_") {
|
if strings.HasPrefix(code, CodePrefixKey+"_") {
|
||||||
prefix = CodePrefixKey
|
prefix = CodePrefixKey
|
||||||
}
|
}
|
||||||
|
|
||||||
return ParseCode(code, prefix)
|
return parseCode(code, prefix)
|
||||||
}
|
}
|
||||||
|
|
||||||
func ParseCode(code, prefix string) (evdev.EvCode, error) {
|
func parseCode(code, prefix string) (evdev.EvCode, error) {
|
||||||
code = strings.ToUpper(code)
|
code = strings.ToUpper(code)
|
||||||
|
|
||||||
var codeLookup map[string]evdev.EvCode
|
var codeLookup map[string]evdev.EvCode
|
||||||
|
@ -70,8 +70,3 @@ func ParseCode(code, prefix string) (evdev.EvCode, error) {
|
||||||
return eventCode, nil
|
return eventCode, nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// hasError exists solely to switch on errors in conditional and case statements
|
|
||||||
func hasError(_ any, err error) bool {
|
|
||||||
return err != nil
|
|
||||||
}
|
|
|
@ -1,4 +1,4 @@
|
||||||
package eventcodes
|
package config
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
@ -18,7 +18,7 @@ func TestRunnerEventCodeParserTests(t *testing.T) {
|
||||||
|
|
||||||
func parseCodeTestCase(t *EventCodeParserTests, in string, out evdev.EvCode, prefix string) {
|
func parseCodeTestCase(t *EventCodeParserTests, in string, out evdev.EvCode, prefix string) {
|
||||||
t.Run(fmt.Sprintf("%s: %s", prefix, in), func() {
|
t.Run(fmt.Sprintf("%s: %s", prefix, in), func() {
|
||||||
code, err := ParseCode(in, prefix)
|
code, err := parseCode(in, prefix)
|
||||||
t.Nil(err)
|
t.Nil(err)
|
||||||
t.EqualValues(out, code)
|
t.EqualValues(out, code)
|
||||||
})
|
})
|
||||||
|
@ -38,7 +38,7 @@ func (t *EventCodeParserTests) TestParseCodeButton() {
|
||||||
|
|
||||||
for _, testCase := range testCases {
|
for _, testCase := range testCases {
|
||||||
t.Run(testCase.in, func() {
|
t.Run(testCase.in, func() {
|
||||||
code, err := ParseCodeButton(testCase.in)
|
code, err := parseCodeButton(testCase.in)
|
||||||
t.Nil(err)
|
t.Nil(err)
|
||||||
t.EqualValues(code, testCase.out)
|
t.EqualValues(code, testCase.out)
|
||||||
})
|
})
|
||||||
|
@ -134,7 +134,7 @@ func (t *EventCodeParserTests) TestParseCode() {
|
||||||
|
|
||||||
for _, testCase := range testCases {
|
for _, testCase := range testCases {
|
||||||
t.Run(fmt.Sprintf("%s - '%s'", testCase.prefix, testCase.in), func() {
|
t.Run(fmt.Sprintf("%s - '%s'", testCase.prefix, testCase.in), func() {
|
||||||
_, err := ParseCode(testCase.in, testCase.prefix)
|
_, err := parseCode(testCase.in, testCase.prefix)
|
||||||
t.NotNil(err)
|
t.NotNil(err)
|
||||||
})
|
})
|
||||||
}
|
}
|
77
internal/config/configparser.go
Normal file
77
internal/config/configparser.go
Normal file
|
@ -0,0 +1,77 @@
|
||||||
|
// The ConfigParser is the main structure you'll interact with when using this package.
|
||||||
|
//
|
||||||
|
// Example usage:
|
||||||
|
// config := &config.ConfigParser{}
|
||||||
|
// config.Parse(<some directory containing YAML files>)
|
||||||
|
// virtualDevices := config.CreateVirtualDevices()
|
||||||
|
// physicalDevices := config.ConnectVirtualDevices()
|
||||||
|
// modes := config.GetModes()
|
||||||
|
// rules := config.BuildRules(physicalDevices, virtualDevices, modes)
|
||||||
|
//
|
||||||
|
// nb: there are methods defined on ConfigParser in other files in this package!
|
||||||
|
|
||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"git.annabunches.net/annabunches/joyful/internal/logger"
|
||||||
|
"github.com/goccy/go-yaml"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ConfigParser struct {
|
||||||
|
config Config
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse all the config files and store the config data for further use
|
||||||
|
func (parser *ConfigParser) Parse(directory string) error {
|
||||||
|
parser.config = Config{}
|
||||||
|
|
||||||
|
// Find the config files in the directory
|
||||||
|
dirEntries, err := os.ReadDir(directory)
|
||||||
|
if err != nil {
|
||||||
|
err = os.Mkdir(directory, 0755)
|
||||||
|
if err != nil {
|
||||||
|
return errors.New("Failed to create config directory at " + directory)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Open each yaml file and add its contents to the global config
|
||||||
|
for _, file := range dirEntries {
|
||||||
|
name := file.Name()
|
||||||
|
if file.IsDir() || !(strings.HasSuffix(name, ".yaml") || strings.HasSuffix(name, ".yml")) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
filePath := filepath.Join(directory, name)
|
||||||
|
if strings.HasSuffix(filePath, ".yaml") || strings.HasSuffix(filePath, ".yml") {
|
||||||
|
data, err := os.ReadFile(filePath)
|
||||||
|
if err != nil {
|
||||||
|
logger.LogError(err, "Error while opening config file")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
newConfig := Config{}
|
||||||
|
err = yaml.Unmarshal(data, &newConfig)
|
||||||
|
logger.LogIfError(err, "Error parsing YAML")
|
||||||
|
parser.config.Rules = append(parser.config.Rules, newConfig.Rules...)
|
||||||
|
parser.config.Devices = append(parser.config.Devices, newConfig.Devices...)
|
||||||
|
parser.config.Modes = append(parser.config.Modes, newConfig.Modes...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(parser.config.Devices) == 0 {
|
||||||
|
return errors.New("Found no devices in configuration. Please add configuration at " + directory)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (parser *ConfigParser) GetModes() []string {
|
||||||
|
if len(parser.config.Modes) == 0 {
|
||||||
|
return []string{"*"}
|
||||||
|
}
|
||||||
|
return parser.config.Modes
|
||||||
|
}
|
217
internal/config/devices.go
Normal file
217
internal/config/devices.go
Normal file
|
@ -0,0 +1,217 @@
|
||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"git.annabunches.net/annabunches/joyful/internal/logger"
|
||||||
|
"github.com/holoplot/go-evdev"
|
||||||
|
)
|
||||||
|
|
||||||
|
// CreateVirtualDevices will register any configured devices with type = virtual
|
||||||
|
// using /dev/uinput, and return a map of those devices.
|
||||||
|
//
|
||||||
|
// This function assumes you have already called Parse() on the config directory.
|
||||||
|
//
|
||||||
|
// This function should only be called once, unless you want to create duplicate devices for some reason.
|
||||||
|
func (parser *ConfigParser) CreateVirtualDevices() map[string]*evdev.InputDevice {
|
||||||
|
deviceMap := make(map[string]*evdev.InputDevice)
|
||||||
|
|
||||||
|
for _, deviceConfig := range parser.config.Devices {
|
||||||
|
if strings.ToLower(deviceConfig.Type) != DeviceTypeVirtual {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
name := fmt.Sprintf("joyful-%s", deviceConfig.Name)
|
||||||
|
|
||||||
|
var capabilities map[evdev.EvType][]evdev.EvCode
|
||||||
|
|
||||||
|
// todo: add tests for presets
|
||||||
|
switch deviceConfig.Preset {
|
||||||
|
case DevicePresetGamepad:
|
||||||
|
capabilities = CapabilitiesPresetGamepad
|
||||||
|
case DevicePresetKeyboard:
|
||||||
|
capabilities = CapabilitiesPresetKeyboard
|
||||||
|
case DevicePresetJoystick:
|
||||||
|
capabilities = CapabilitiesPresetJoystick
|
||||||
|
case DevicePresetMouse:
|
||||||
|
capabilities = CapabilitiesPresetMouse
|
||||||
|
default:
|
||||||
|
capabilities = map[evdev.EvType][]evdev.EvCode{
|
||||||
|
evdev.EV_KEY: makeButtons(deviceConfig.NumButtons, deviceConfig.Buttons),
|
||||||
|
evdev.EV_ABS: makeAxes(deviceConfig.NumAxes, deviceConfig.Axes),
|
||||||
|
evdev.EV_REL: makeRelativeAxes(deviceConfig.NumRelativeAxes, deviceConfig.RelativeAxes),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
device, err := evdev.CreateDevice(
|
||||||
|
name,
|
||||||
|
// TODO: who knows what these should actually be
|
||||||
|
evdev.InputID{
|
||||||
|
BusType: 0x03,
|
||||||
|
Vendor: 0x4711,
|
||||||
|
Product: 0x0816,
|
||||||
|
Version: 1,
|
||||||
|
},
|
||||||
|
capabilities,
|
||||||
|
)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
logger.LogIfError(err, "Failed to create virtual device")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
deviceMap[deviceConfig.Name] = device
|
||||||
|
logger.Log(fmt.Sprintf(
|
||||||
|
"Created virtual device '%s' with %d buttons, %d axes, and %d relative axes",
|
||||||
|
name,
|
||||||
|
len(capabilities[evdev.EV_KEY]),
|
||||||
|
len(capabilities[evdev.EV_ABS]),
|
||||||
|
len(capabilities[evdev.EV_REL]),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
return deviceMap
|
||||||
|
}
|
||||||
|
|
||||||
|
// ConnectPhysicalDevices will create InputDevices corresponding to any registered
|
||||||
|
// devices with type = physical.
|
||||||
|
//
|
||||||
|
// This function assumes you have already called Parse() on the config directory.
|
||||||
|
//
|
||||||
|
// This function should only be called once.
|
||||||
|
func (parser *ConfigParser) ConnectPhysicalDevices() map[string]*evdev.InputDevice {
|
||||||
|
deviceMap := make(map[string]*evdev.InputDevice)
|
||||||
|
|
||||||
|
for _, deviceConfig := range parser.config.Devices {
|
||||||
|
if strings.ToLower(deviceConfig.Type) != DeviceTypePhysical {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
var infoName string
|
||||||
|
var device *evdev.InputDevice
|
||||||
|
var err error
|
||||||
|
|
||||||
|
if deviceConfig.DevicePath != "" {
|
||||||
|
infoName = deviceConfig.DevicePath
|
||||||
|
device, err = evdev.Open(deviceConfig.DevicePath)
|
||||||
|
} else {
|
||||||
|
infoName = deviceConfig.DeviceName
|
||||||
|
device, err = evdev.OpenByName(deviceConfig.DeviceName)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
logger.LogError(err, "Failed to open physical device, skipping. Confirm the device name or path with 'evinfo'")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if deviceConfig.Lock {
|
||||||
|
logger.LogDebugf("Locking device '%s'", infoName)
|
||||||
|
err := device.Grab()
|
||||||
|
if err != nil {
|
||||||
|
logger.LogError(err, "Failed to grab device for exclusive access")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.Log(fmt.Sprintf("Connected to '%s' as '%s'", infoName, deviceConfig.Name))
|
||||||
|
deviceMap[deviceConfig.Name] = device
|
||||||
|
}
|
||||||
|
|
||||||
|
return deviceMap
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: these functions have a lot of duplication; we need to figure out how to refactor it cleanly
|
||||||
|
// without losing logging context...
|
||||||
|
func makeButtons(numButtons int, buttonList []string) []evdev.EvCode {
|
||||||
|
if numButtons > 0 && len(buttonList) > 0 {
|
||||||
|
logger.Log("'num_buttons' and 'buttons' both specified, ignoring 'num_buttons'")
|
||||||
|
}
|
||||||
|
|
||||||
|
if numButtons > VirtualDeviceMaxButtons {
|
||||||
|
numButtons = VirtualDeviceMaxButtons
|
||||||
|
logger.Logf("Limiting virtual device buttons to %d", VirtualDeviceMaxButtons)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(buttonList) > 0 {
|
||||||
|
buttons := make([]evdev.EvCode, 0, len(buttonList))
|
||||||
|
for _, codeStr := range buttonList {
|
||||||
|
code, err := parseCode(codeStr, "BTN")
|
||||||
|
if err != nil {
|
||||||
|
logger.LogError(err, "Failed to create button, skipping")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
buttons = append(buttons, code)
|
||||||
|
}
|
||||||
|
return buttons
|
||||||
|
}
|
||||||
|
|
||||||
|
buttons := make([]evdev.EvCode, numButtons)
|
||||||
|
|
||||||
|
for i := 0; i < numButtons; i++ {
|
||||||
|
buttons[i] = ButtonFromIndex[i]
|
||||||
|
}
|
||||||
|
|
||||||
|
return buttons
|
||||||
|
}
|
||||||
|
|
||||||
|
func makeAxes(numAxes int, axisList []string) []evdev.EvCode {
|
||||||
|
if numAxes > 0 && len(axisList) > 0 {
|
||||||
|
logger.Log("'num_axes' and 'axes' both specified, ignoring 'num_axes'")
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(axisList) > 0 {
|
||||||
|
axes := make([]evdev.EvCode, 0, len(axisList))
|
||||||
|
for _, codeStr := range axisList {
|
||||||
|
code, err := parseCode(codeStr, "ABS")
|
||||||
|
if err != nil {
|
||||||
|
logger.LogError(err, "Failed to create axis, skipping")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
axes = append(axes, code)
|
||||||
|
}
|
||||||
|
return axes
|
||||||
|
}
|
||||||
|
|
||||||
|
if numAxes > 8 {
|
||||||
|
numAxes = 8
|
||||||
|
logger.Log("Limiting virtual device axes to 8")
|
||||||
|
}
|
||||||
|
|
||||||
|
axes := make([]evdev.EvCode, numAxes)
|
||||||
|
for i := 0; i < numAxes; i++ {
|
||||||
|
axes[i] = evdev.EvCode(i)
|
||||||
|
}
|
||||||
|
|
||||||
|
return axes
|
||||||
|
}
|
||||||
|
|
||||||
|
func makeRelativeAxes(numAxes int, axisList []string) []evdev.EvCode {
|
||||||
|
if numAxes > 0 && len(axisList) > 0 {
|
||||||
|
logger.Log("'num_rel_axes' and 'rel_axes' both specified, ignoring 'num_rel_axes'")
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(axisList) > 0 {
|
||||||
|
axes := make([]evdev.EvCode, 0, len(axisList))
|
||||||
|
for _, codeStr := range axisList {
|
||||||
|
code, err := parseCode(codeStr, "REL")
|
||||||
|
if err != nil {
|
||||||
|
logger.LogError(err, "Failed to create axis, skipping")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
axes = append(axes, code)
|
||||||
|
}
|
||||||
|
return axes
|
||||||
|
}
|
||||||
|
|
||||||
|
if numAxes > 10 {
|
||||||
|
numAxes = 10
|
||||||
|
logger.Log("Limiting virtual device relative axes to 10")
|
||||||
|
}
|
||||||
|
|
||||||
|
axes := make([]evdev.EvCode, numAxes)
|
||||||
|
for i := 0; i < numAxes; i++ {
|
||||||
|
axes[i] = evdev.EvCode(i)
|
||||||
|
}
|
||||||
|
|
||||||
|
return axes
|
||||||
|
}
|
|
@ -1,4 +1,4 @@
|
||||||
package virtualdevice
|
package config
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"testing"
|
"testing"
|
||||||
|
@ -7,15 +7,15 @@ import (
|
||||||
"github.com/stretchr/testify/suite"
|
"github.com/stretchr/testify/suite"
|
||||||
)
|
)
|
||||||
|
|
||||||
type InitTests struct {
|
type DevicesConfigTests struct {
|
||||||
suite.Suite
|
suite.Suite
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestRunnerInit(t *testing.T) {
|
func TestRunnerDevicesConfig(t *testing.T) {
|
||||||
suite.Run(t, new(InitTests))
|
suite.Run(t, new(DevicesConfigTests))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *InitTests) TestMakeButtons() {
|
func (t *DevicesConfigTests) TestMakeButtons() {
|
||||||
t.Run("Maximum buttons", func() {
|
t.Run("Maximum buttons", func() {
|
||||||
buttons := makeButtons(VirtualDeviceMaxButtons, []string{})
|
buttons := makeButtons(VirtualDeviceMaxButtons, []string{})
|
||||||
t.Equal(VirtualDeviceMaxButtons, len(buttons))
|
t.Equal(VirtualDeviceMaxButtons, len(buttons))
|
||||||
|
@ -44,7 +44,7 @@ func (t *InitTests) TestMakeButtons() {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *InitTests) TestMakeAxes() {
|
func (t *DevicesConfigTests) TestMakeAxes() {
|
||||||
t.Run("8 axes", func() {
|
t.Run("8 axes", func() {
|
||||||
axes := makeAxes(8, []string{})
|
axes := makeAxes(8, []string{})
|
||||||
t.Equal(8, len(axes))
|
t.Equal(8, len(axes))
|
||||||
|
@ -81,7 +81,7 @@ func (t *InitTests) TestMakeAxes() {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *InitTests) TestMakeRelativeAxes() {
|
func (t *DevicesConfigTests) TestMakeRelativeAxes() {
|
||||||
t.Run("10 axes", func() {
|
t.Run("10 axes", func() {
|
||||||
axes := makeRelativeAxes(10, []string{})
|
axes := makeRelativeAxes(10, []string{})
|
||||||
t.Equal(10, len(axes))
|
t.Equal(10, len(axes))
|
7
internal/config/interfaces.go
Normal file
7
internal/config/interfaces.go
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
package config
|
||||||
|
|
||||||
|
import "github.com/holoplot/go-evdev"
|
||||||
|
|
||||||
|
type Device interface {
|
||||||
|
AbsInfos() (map[evdev.EvCode]evdev.AbsInfo, error)
|
||||||
|
}
|
146
internal/config/make_rule_targets.go
Normal file
146
internal/config/make_rule_targets.go
Normal file
|
@ -0,0 +1,146 @@
|
||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"git.annabunches.net/annabunches/joyful/internal/mappingrules"
|
||||||
|
"github.com/holoplot/go-evdev"
|
||||||
|
)
|
||||||
|
|
||||||
|
func makeRuleTargetButton(targetConfig RuleTargetConfig, devs map[string]Device) (*mappingrules.RuleTargetButton, error) {
|
||||||
|
device, ok := devs[targetConfig.Device]
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf("non-existent device '%s'", targetConfig.Device)
|
||||||
|
}
|
||||||
|
|
||||||
|
eventCode, err := parseCodeButton(targetConfig.Button)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return mappingrules.NewRuleTargetButton(
|
||||||
|
targetConfig.Device,
|
||||||
|
device,
|
||||||
|
eventCode,
|
||||||
|
targetConfig.Inverted,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func makeRuleTargetAxis(targetConfig RuleTargetConfig, devs map[string]Device) (*mappingrules.RuleTargetAxis, error) {
|
||||||
|
device, ok := devs[targetConfig.Device]
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf("non-existent device '%s'", targetConfig.Device)
|
||||||
|
}
|
||||||
|
|
||||||
|
if targetConfig.DeadzoneEnd < targetConfig.DeadzoneStart {
|
||||||
|
return nil, errors.New("deadzone_end must be greater than deadzone_start")
|
||||||
|
}
|
||||||
|
|
||||||
|
eventCode, err := parseCode(targetConfig.Axis, CodePrefixAxis)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
deadzoneStart, deadzoneEnd, err := calculateDeadzones(targetConfig, device, eventCode)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return mappingrules.NewRuleTargetAxis(
|
||||||
|
targetConfig.Device,
|
||||||
|
device,
|
||||||
|
eventCode,
|
||||||
|
targetConfig.Inverted,
|
||||||
|
deadzoneStart,
|
||||||
|
deadzoneEnd,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func makeRuleTargetRelaxis(targetConfig RuleTargetConfig, devs map[string]Device) (*mappingrules.RuleTargetRelaxis, error) {
|
||||||
|
device, ok := devs[targetConfig.Device]
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf("non-existent device '%s'", targetConfig.Device)
|
||||||
|
}
|
||||||
|
|
||||||
|
eventCode, err := parseCode(targetConfig.Axis, CodePrefixRelaxis)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return mappingrules.NewRuleTargetRelaxis(
|
||||||
|
targetConfig.Device,
|
||||||
|
device,
|
||||||
|
eventCode,
|
||||||
|
targetConfig.Inverted,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func makeRuleTargetModeSelect(targetConfig RuleTargetConfig, allModes []string) (*mappingrules.RuleTargetModeSelect, error) {
|
||||||
|
if ok := validateModes(targetConfig.Modes, allModes); !ok {
|
||||||
|
return nil, errors.New("undefined mode in mode select list")
|
||||||
|
}
|
||||||
|
|
||||||
|
return mappingrules.NewRuleTargetModeSelect(targetConfig.Modes)
|
||||||
|
}
|
||||||
|
|
||||||
|
// hasError exists solely to switch on errors in case statements
|
||||||
|
func hasError(_ any, err error) bool {
|
||||||
|
return err != nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// calculateDeadzones produces the deadzone start and end values in absolute terms
|
||||||
|
// TODO: on the one hand, this logic feels betten encapsulated in mappingrules. On the other hand,
|
||||||
|
// passing even more parameters to NewRuleTargetAxis feels terrible
|
||||||
|
func calculateDeadzones(targetConfig RuleTargetConfig, device Device, axis evdev.EvCode) (int32, int32, error) {
|
||||||
|
|
||||||
|
var deadzoneStart, deadzoneEnd int32
|
||||||
|
deadzoneStart = 0
|
||||||
|
deadzoneEnd = 0
|
||||||
|
|
||||||
|
if targetConfig.DeadzoneStart != 0 || targetConfig.DeadzoneEnd != 0 {
|
||||||
|
return targetConfig.DeadzoneStart, targetConfig.DeadzoneEnd, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var min, max int32
|
||||||
|
absInfoMap, err := device.AbsInfos()
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
min = mappingrules.AxisValueMin
|
||||||
|
max = mappingrules.AxisValueMax
|
||||||
|
} else {
|
||||||
|
absInfo := absInfoMap[axis]
|
||||||
|
min = absInfo.Minimum
|
||||||
|
max = absInfo.Maximum
|
||||||
|
}
|
||||||
|
|
||||||
|
if targetConfig.DeadzoneCenter < min || targetConfig.DeadzoneCenter > max {
|
||||||
|
return 0, 0, fmt.Errorf("deadzone_center '%d' is out of bounds", targetConfig.DeadzoneCenter)
|
||||||
|
}
|
||||||
|
|
||||||
|
switch {
|
||||||
|
case targetConfig.DeadzoneSize != 0:
|
||||||
|
deadzoneStart = targetConfig.DeadzoneCenter - targetConfig.DeadzoneSize/2
|
||||||
|
deadzoneEnd = targetConfig.DeadzoneCenter + targetConfig.DeadzoneSize/2
|
||||||
|
case targetConfig.DeadzoneSizePercent != 0:
|
||||||
|
deadzoneSize := (max - min) / targetConfig.DeadzoneSizePercent
|
||||||
|
deadzoneStart = targetConfig.DeadzoneCenter - deadzoneSize/2
|
||||||
|
deadzoneEnd = targetConfig.DeadzoneCenter + deadzoneSize/2
|
||||||
|
}
|
||||||
|
|
||||||
|
deadzoneStart, deadzoneEnd = clampAndShift(deadzoneStart, deadzoneEnd, min, max)
|
||||||
|
return deadzoneStart, deadzoneEnd, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func clampAndShift(start, end, min, max int32) (int32, int32) {
|
||||||
|
if start < min {
|
||||||
|
end += min - start
|
||||||
|
start = min
|
||||||
|
}
|
||||||
|
if end > max {
|
||||||
|
start -= end - max
|
||||||
|
end = max
|
||||||
|
}
|
||||||
|
|
||||||
|
return start, end
|
||||||
|
}
|
244
internal/config/make_rule_targets_test.go
Normal file
244
internal/config/make_rule_targets_test.go
Normal file
|
@ -0,0 +1,244 @@
|
||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/holoplot/go-evdev"
|
||||||
|
"github.com/stretchr/testify/mock"
|
||||||
|
"github.com/stretchr/testify/suite"
|
||||||
|
)
|
||||||
|
|
||||||
|
type MakeRuleTargetsTests struct {
|
||||||
|
suite.Suite
|
||||||
|
devs map[string]Device
|
||||||
|
deviceMock *DeviceMock
|
||||||
|
config RuleTargetConfig
|
||||||
|
}
|
||||||
|
|
||||||
|
type DeviceMock struct {
|
||||||
|
mock.Mock
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *DeviceMock) AbsInfos() (map[evdev.EvCode]evdev.AbsInfo, error) {
|
||||||
|
args := m.Called()
|
||||||
|
return args.Get(0).(map[evdev.EvCode]evdev.AbsInfo), args.Error(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRunnerMakeRuleTargets(t *testing.T) {
|
||||||
|
suite.Run(t, new(MakeRuleTargetsTests))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *MakeRuleTargetsTests) SetupSuite() {
|
||||||
|
t.deviceMock = new(DeviceMock)
|
||||||
|
t.deviceMock.On("AbsInfos").Return(
|
||||||
|
map[evdev.EvCode]evdev.AbsInfo{
|
||||||
|
evdev.ABS_X: {
|
||||||
|
Minimum: 0,
|
||||||
|
Maximum: 10000,
|
||||||
|
},
|
||||||
|
evdev.ABS_Y: {
|
||||||
|
Minimum: 0,
|
||||||
|
Maximum: 10000,
|
||||||
|
},
|
||||||
|
}, nil,
|
||||||
|
)
|
||||||
|
t.devs = map[string]Device{
|
||||||
|
"test": t.deviceMock,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *MakeRuleTargetsTests) SetupSubTest() {
|
||||||
|
t.config = RuleTargetConfig{
|
||||||
|
Device: "test",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *MakeRuleTargetsTests) TestMakeRuleTargetButton() {
|
||||||
|
t.Run("Standard keycode", func() {
|
||||||
|
t.config.Button = "BTN_TRIGGER"
|
||||||
|
rule, err := makeRuleTargetButton(t.config, t.devs)
|
||||||
|
t.Nil(err)
|
||||||
|
t.EqualValues(evdev.BTN_TRIGGER, rule.Button)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Hex code", func() {
|
||||||
|
t.config.Button = "0x2fd"
|
||||||
|
rule, err := makeRuleTargetButton(t.config, t.devs)
|
||||||
|
t.Nil(err)
|
||||||
|
t.EqualValues(evdev.EvCode(0x2fd), rule.Button)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Index", func() {
|
||||||
|
t.config.Button = "3"
|
||||||
|
rule, err := makeRuleTargetButton(t.config, t.devs)
|
||||||
|
t.Nil(err)
|
||||||
|
t.EqualValues(evdev.BTN_TOP, rule.Button)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Index too high", func() {
|
||||||
|
t.config.Button = "74"
|
||||||
|
_, err := makeRuleTargetButton(t.config, t.devs)
|
||||||
|
t.NotNil(err)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Un-prefixed keycode", func() {
|
||||||
|
t.config.Button = "pinkie"
|
||||||
|
rule, err := makeRuleTargetButton(t.config, t.devs)
|
||||||
|
t.Nil(err)
|
||||||
|
t.EqualValues(evdev.BTN_PINKIE, rule.Button)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Invalid keycode", func() {
|
||||||
|
t.config.Button = "foo"
|
||||||
|
_, err := makeRuleTargetButton(t.config, t.devs)
|
||||||
|
t.NotNil(err)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *MakeRuleTargetsTests) TestMakeRuleTargetAxis() {
|
||||||
|
t.Run("Standard code", func() {
|
||||||
|
t.config.Axis = "ABS_X"
|
||||||
|
rule, err := makeRuleTargetAxis(t.config, t.devs)
|
||||||
|
t.Nil(err)
|
||||||
|
t.EqualValues(evdev.ABS_X, rule.Axis)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Hex code", func() {
|
||||||
|
t.config.Axis = "0x01"
|
||||||
|
rule, err := makeRuleTargetAxis(t.config, t.devs)
|
||||||
|
t.Nil(err)
|
||||||
|
t.EqualValues(evdev.ABS_Y, rule.Axis)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Un-prefixed code", func() {
|
||||||
|
t.config.Axis = "x"
|
||||||
|
rule, err := makeRuleTargetAxis(t.config, t.devs)
|
||||||
|
t.Nil(err)
|
||||||
|
t.EqualValues(evdev.ABS_X, rule.Axis)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Invalid code", func() {
|
||||||
|
t.config.Axis = "foo"
|
||||||
|
_, err := makeRuleTargetAxis(t.config, t.devs)
|
||||||
|
t.NotNil(err)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Invalid deadzone", func() {
|
||||||
|
t.config.Axis = "x"
|
||||||
|
t.config.DeadzoneEnd = 100
|
||||||
|
t.config.DeadzoneStart = 1000
|
||||||
|
_, err := makeRuleTargetAxis(t.config, t.devs)
|
||||||
|
t.NotNil(err)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Deadzone center/size", func() {
|
||||||
|
t.config.Axis = "x"
|
||||||
|
t.config.DeadzoneCenter = 5000
|
||||||
|
t.config.DeadzoneSize = 1000
|
||||||
|
rule, err := makeRuleTargetAxis(t.config, t.devs)
|
||||||
|
t.Nil(err)
|
||||||
|
t.EqualValues(4500, rule.DeadzoneStart)
|
||||||
|
t.EqualValues(5500, rule.DeadzoneEnd)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Deadzone center/size lower boundary", func() {
|
||||||
|
t.config.Axis = "x"
|
||||||
|
t.config.DeadzoneCenter = 0
|
||||||
|
t.config.DeadzoneSize = 500
|
||||||
|
rule, err := makeRuleTargetAxis(t.config, t.devs)
|
||||||
|
t.Nil(err)
|
||||||
|
t.EqualValues(0, rule.DeadzoneStart)
|
||||||
|
t.EqualValues(500, rule.DeadzoneEnd)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Deadzone center/size upper boundary", func() {
|
||||||
|
t.config.Axis = "x"
|
||||||
|
t.config.DeadzoneCenter = 10000
|
||||||
|
t.config.DeadzoneSize = 500
|
||||||
|
rule, err := makeRuleTargetAxis(t.config, t.devs)
|
||||||
|
t.Nil(err)
|
||||||
|
t.EqualValues(9500, rule.DeadzoneStart)
|
||||||
|
t.EqualValues(10000, rule.DeadzoneEnd)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Deadzone center/size invalid center", func() {
|
||||||
|
t.config.Axis = "x"
|
||||||
|
t.config.DeadzoneCenter = 20000
|
||||||
|
t.config.DeadzoneSize = 500
|
||||||
|
_, err := makeRuleTargetAxis(t.config, t.devs)
|
||||||
|
t.NotNil(err)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Deadzone center/percent", func() {
|
||||||
|
t.config.Axis = "x"
|
||||||
|
t.config.DeadzoneCenter = 5000
|
||||||
|
t.config.DeadzoneSizePercent = 10
|
||||||
|
rule, err := makeRuleTargetAxis(t.config, t.devs)
|
||||||
|
t.Nil(err)
|
||||||
|
t.EqualValues(4500, rule.DeadzoneStart)
|
||||||
|
t.EqualValues(5500, rule.DeadzoneEnd)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Deadzone center/percent lower boundary", func() {
|
||||||
|
t.config.Axis = "x"
|
||||||
|
t.config.DeadzoneCenter = 0
|
||||||
|
t.config.DeadzoneSizePercent = 10
|
||||||
|
rule, err := makeRuleTargetAxis(t.config, t.devs)
|
||||||
|
t.Nil(err)
|
||||||
|
t.EqualValues(0, rule.DeadzoneStart)
|
||||||
|
t.EqualValues(1000, rule.DeadzoneEnd)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Deadzone center/percent upper boundary", func() {
|
||||||
|
t.config.Axis = "x"
|
||||||
|
t.config.DeadzoneCenter = 10000
|
||||||
|
t.config.DeadzoneSizePercent = 10
|
||||||
|
rule, err := makeRuleTargetAxis(t.config, t.devs)
|
||||||
|
t.Nil(err)
|
||||||
|
t.EqualValues(9000, rule.DeadzoneStart)
|
||||||
|
t.EqualValues(10000, rule.DeadzoneEnd)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Deadzone center/percent invalid center", func() {
|
||||||
|
t.config.Axis = "x"
|
||||||
|
t.config.DeadzoneCenter = 20000
|
||||||
|
t.config.DeadzoneSizePercent = 10
|
||||||
|
_, err := makeRuleTargetAxis(t.config, t.devs)
|
||||||
|
t.NotNil(err)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *MakeRuleTargetsTests) TestMakeRuleTargetRelaxis() {
|
||||||
|
t.Run("Standard keycode", func() {
|
||||||
|
t.config.Axis = "REL_WHEEL"
|
||||||
|
rule, err := makeRuleTargetRelaxis(t.config, t.devs)
|
||||||
|
t.Nil(err)
|
||||||
|
t.EqualValues(evdev.REL_WHEEL, rule.Axis)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Hex keycode", func() {
|
||||||
|
t.config.Axis = "0x00"
|
||||||
|
rule, err := makeRuleTargetRelaxis(t.config, t.devs)
|
||||||
|
t.Nil(err)
|
||||||
|
t.EqualValues(evdev.REL_X, rule.Axis)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Un-prefixed keycode", func() {
|
||||||
|
t.config.Axis = "wheel"
|
||||||
|
rule, err := makeRuleTargetRelaxis(t.config, t.devs)
|
||||||
|
t.Nil(err)
|
||||||
|
t.EqualValues(evdev.REL_WHEEL, rule.Axis)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Invalid keycode", func() {
|
||||||
|
t.config.Axis = "foo"
|
||||||
|
_, err := makeRuleTargetRelaxis(t.config, t.devs)
|
||||||
|
t.NotNil(err)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Incorrect axis type", func() {
|
||||||
|
t.config.Axis = "ABS_X"
|
||||||
|
_, err := makeRuleTargetRelaxis(t.config, t.devs)
|
||||||
|
t.NotNil(err)
|
||||||
|
})
|
||||||
|
}
|
230
internal/config/make_rules.go
Normal file
230
internal/config/make_rules.go
Normal file
|
@ -0,0 +1,230 @@
|
||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"git.annabunches.net/annabunches/joyful/internal/logger"
|
||||||
|
"git.annabunches.net/annabunches/joyful/internal/mappingrules"
|
||||||
|
"github.com/holoplot/go-evdev"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TODO: At some point it would *very likely* make sense to map each rule to all of the physical devices that can
|
||||||
|
// trigger it, and return that instead. Something like a map[Device][]mappingrule.MappingRule.
|
||||||
|
// This would speed up rule matching by only checking relevant rules for a given input event.
|
||||||
|
// We could take this further and make it a map[<struct of *inputdevice, type, and code>][]rule
|
||||||
|
// For very large rule-bases this may be helpful for staying performant.
|
||||||
|
func (parser *ConfigParser) BuildRules(pInputDevs map[string]*evdev.InputDevice, vInputDevs map[string]*evdev.InputDevice) []mappingrules.MappingRule {
|
||||||
|
rules := make([]mappingrules.MappingRule, 0)
|
||||||
|
modes := parser.GetModes()
|
||||||
|
|
||||||
|
// Golang can't inspect the concrete map type to determine interface conformance,
|
||||||
|
// so we handle that here.
|
||||||
|
pDevs := make(map[string]Device)
|
||||||
|
for name, dev := range pInputDevs {
|
||||||
|
pDevs[name] = dev
|
||||||
|
}
|
||||||
|
vDevs := make(map[string]Device)
|
||||||
|
for name, dev := range vInputDevs {
|
||||||
|
vDevs[name] = dev
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, ruleConfig := range parser.config.Rules {
|
||||||
|
var newRule mappingrules.MappingRule
|
||||||
|
var err error
|
||||||
|
|
||||||
|
if ok := validateModes(ruleConfig.Modes, modes); !ok {
|
||||||
|
logger.Logf("Skipping rule '%s', mode list specifies undefined mode.", ruleConfig.Name)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
base := mappingrules.NewMappingRuleBase(ruleConfig.Name, ruleConfig.Modes)
|
||||||
|
|
||||||
|
switch strings.ToLower(ruleConfig.Type) {
|
||||||
|
case RuleTypeButton:
|
||||||
|
newRule, err = makeMappingRuleButton(ruleConfig, pDevs, vDevs, base)
|
||||||
|
case RuleTypeButtonCombo:
|
||||||
|
newRule, err = makeMappingRuleCombo(ruleConfig, pDevs, vDevs, base)
|
||||||
|
case RuleTypeLatched:
|
||||||
|
newRule, err = makeMappingRuleLatched(ruleConfig, pDevs, vDevs, base)
|
||||||
|
case RuleTypeAxis:
|
||||||
|
newRule, err = makeMappingRuleAxis(ruleConfig, pDevs, vDevs, base)
|
||||||
|
case RuleTypeAxisCombined:
|
||||||
|
newRule, err = makeMappingRuleAxisCombined(ruleConfig, pDevs, vDevs, base)
|
||||||
|
case RuleTypeAxisToButton:
|
||||||
|
newRule, err = makeMappingRuleAxisToButton(ruleConfig, pDevs, vDevs, base)
|
||||||
|
case RuleTypeAxisToRelaxis:
|
||||||
|
newRule, err = makeMappingRuleAxisToRelaxis(ruleConfig, pDevs, vDevs, base)
|
||||||
|
case RuleTypeModeSelect:
|
||||||
|
newRule, err = makeMappingRuleModeSelect(ruleConfig, pDevs, modes, base)
|
||||||
|
default:
|
||||||
|
err = fmt.Errorf("bad rule type '%s' for rule '%s'", ruleConfig.Type, ruleConfig.Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
logger.LogErrorf(err, "Failed to build rule '%s'", ruleConfig.Name)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
rules = append(rules, newRule)
|
||||||
|
}
|
||||||
|
|
||||||
|
return rules
|
||||||
|
}
|
||||||
|
|
||||||
|
func makeMappingRuleButton(ruleConfig RuleConfig,
|
||||||
|
pDevs map[string]Device,
|
||||||
|
vDevs map[string]Device,
|
||||||
|
base mappingrules.MappingRuleBase) (*mappingrules.MappingRuleButton, error) {
|
||||||
|
|
||||||
|
input, err := makeRuleTargetButton(ruleConfig.Input, pDevs)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
output, err := makeRuleTargetButton(ruleConfig.Output, vDevs)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return mappingrules.NewMappingRuleButton(base, input, output), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func makeMappingRuleCombo(ruleConfig RuleConfig,
|
||||||
|
pDevs map[string]Device,
|
||||||
|
vDevs map[string]Device,
|
||||||
|
base mappingrules.MappingRuleBase) (*mappingrules.MappingRuleButtonCombo, error) {
|
||||||
|
|
||||||
|
inputs := make([]*mappingrules.RuleTargetButton, 0)
|
||||||
|
for _, inputConfig := range ruleConfig.Inputs {
|
||||||
|
input, err := makeRuleTargetButton(inputConfig, pDevs)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
inputs = append(inputs, input)
|
||||||
|
}
|
||||||
|
|
||||||
|
output, err := makeRuleTargetButton(ruleConfig.Output, vDevs)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return mappingrules.NewMappingRuleButtonCombo(base, inputs, output), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func makeMappingRuleLatched(ruleConfig RuleConfig,
|
||||||
|
pDevs map[string]Device,
|
||||||
|
vDevs map[string]Device,
|
||||||
|
base mappingrules.MappingRuleBase) (*mappingrules.MappingRuleButtonLatched, error) {
|
||||||
|
|
||||||
|
input, err := makeRuleTargetButton(ruleConfig.Input, pDevs)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
output, err := makeRuleTargetButton(ruleConfig.Output, vDevs)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return mappingrules.NewMappingRuleButtonLatched(base, input, output), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func makeMappingRuleAxis(ruleConfig RuleConfig,
|
||||||
|
pDevs map[string]Device,
|
||||||
|
vDevs map[string]Device,
|
||||||
|
base mappingrules.MappingRuleBase) (*mappingrules.MappingRuleAxis, error) {
|
||||||
|
|
||||||
|
input, err := makeRuleTargetAxis(ruleConfig.Input, pDevs)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
output, err := makeRuleTargetAxis(ruleConfig.Output, vDevs)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return mappingrules.NewMappingRuleAxis(base, input, output), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func makeMappingRuleAxisCombined(ruleConfig RuleConfig,
|
||||||
|
pDevs map[string]Device,
|
||||||
|
vDevs map[string]Device,
|
||||||
|
base mappingrules.MappingRuleBase) (*mappingrules.MappingRuleAxisCombined, error) {
|
||||||
|
|
||||||
|
inputLower, err := makeRuleTargetAxis(ruleConfig.InputLower, pDevs)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
inputUpper, err := makeRuleTargetAxis(ruleConfig.InputUpper, pDevs)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
output, err := makeRuleTargetAxis(ruleConfig.Output, vDevs)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return mappingrules.NewMappingRuleAxisCombined(base, inputLower, inputUpper, output), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func makeMappingRuleAxisToButton(ruleConfig RuleConfig,
|
||||||
|
pDevs map[string]Device,
|
||||||
|
vDevs map[string]Device,
|
||||||
|
base mappingrules.MappingRuleBase) (*mappingrules.MappingRuleAxisToButton, error) {
|
||||||
|
|
||||||
|
input, err := makeRuleTargetAxis(ruleConfig.Input, pDevs)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
output, err := makeRuleTargetButton(ruleConfig.Output, vDevs)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return mappingrules.NewMappingRuleAxisToButton(base, input, output, ruleConfig.RepeatRateMin, ruleConfig.RepeatRateMax), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func makeMappingRuleAxisToRelaxis(ruleConfig RuleConfig,
|
||||||
|
pDevs map[string]Device,
|
||||||
|
vDevs map[string]Device,
|
||||||
|
base mappingrules.MappingRuleBase) (*mappingrules.MappingRuleAxisToRelaxis, error) {
|
||||||
|
|
||||||
|
input, err := makeRuleTargetAxis(ruleConfig.Input, pDevs)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
output, err := makeRuleTargetRelaxis(ruleConfig.Output, vDevs)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return mappingrules.NewMappingRuleAxisToRelaxis(base,
|
||||||
|
input, output,
|
||||||
|
ruleConfig.RepeatRateMin,
|
||||||
|
ruleConfig.RepeatRateMax,
|
||||||
|
ruleConfig.Increment), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func makeMappingRuleModeSelect(ruleConfig RuleConfig,
|
||||||
|
pDevs map[string]Device,
|
||||||
|
modes []string,
|
||||||
|
base mappingrules.MappingRuleBase) (*mappingrules.MappingRuleModeSelect, error) {
|
||||||
|
|
||||||
|
input, err := makeRuleTargetButton(ruleConfig.Input, pDevs)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
output, err := makeRuleTargetModeSelect(ruleConfig.Output, modes)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return mappingrules.NewMappingRuleModeSelect(base, input, output), nil
|
||||||
|
}
|
19
internal/config/modes.go
Normal file
19
internal/config/modes.go
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
package config
|
||||||
|
|
||||||
|
import "slices"
|
||||||
|
|
||||||
|
// validateModes checks the provided modes against a larger subset of modes (usually all defined ones)
|
||||||
|
// and returns false if any of the modes are not defined.
|
||||||
|
func validateModes(modes []string, allModes []string) bool {
|
||||||
|
if len(modes) == 0 {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, mode := range modes {
|
||||||
|
if !slices.Contains(allModes, mode) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
99
internal/config/schema.go
Normal file
99
internal/config/schema.go
Normal file
|
@ -0,0 +1,99 @@
|
||||||
|
// These types comprise the YAML schema for configuring Joyful.
|
||||||
|
// The config files will be combined and then unmarshalled into this
|
||||||
|
//
|
||||||
|
// TODO: currently the types in here aren't especially strong; each one is
|
||||||
|
// decomposed into a different object based on the Type fields. We should implement
|
||||||
|
// some sort of delayed unmarshalling technique, for example see ideas at
|
||||||
|
// https://stackoverflow.com/questions/70635636/unmarshaling-yaml-into-different-struct-based-off-yaml-field
|
||||||
|
// Then we can be more explicit about the interface here.
|
||||||
|
|
||||||
|
package config
|
||||||
|
|
||||||
|
type Config struct {
|
||||||
|
Devices []DeviceConfig `yaml:"devices"`
|
||||||
|
Modes []string `yaml:"modes,omitempty"`
|
||||||
|
Rules []RuleConfig `yaml:"rules"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type DeviceConfig struct {
|
||||||
|
Name string `yaml:"name"`
|
||||||
|
Type string `yaml:"type"`
|
||||||
|
DeviceName string `yaml:"device_name,omitempty"`
|
||||||
|
DevicePath string `yaml:"device_path,omitempty"`
|
||||||
|
Preset string `yaml:"preset,omitempty"`
|
||||||
|
NumButtons int `yaml:"num_buttons,omitempty"`
|
||||||
|
NumAxes int `yaml:"num_axes,omitempty"`
|
||||||
|
NumRelativeAxes int `yaml:"num_rel_axes"`
|
||||||
|
Buttons []string `yaml:"buttons,omitempty"`
|
||||||
|
Axes []string `yaml:"axes,omitempty"`
|
||||||
|
RelativeAxes []string `yaml:"rel_axes,omitempty"`
|
||||||
|
Lock bool `yaml:"lock,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type RuleConfig struct {
|
||||||
|
Name string `yaml:"name,omitempty"`
|
||||||
|
Type string `yaml:"type"`
|
||||||
|
Input RuleTargetConfig `yaml:"input,omitempty"`
|
||||||
|
InputLower RuleTargetConfig `yaml:"input_lower,omitempty"`
|
||||||
|
InputUpper RuleTargetConfig `yaml:"input_upper,omitempty"`
|
||||||
|
Inputs []RuleTargetConfig `yaml:"inputs,omitempty"`
|
||||||
|
Output RuleTargetConfig `yaml:"output"`
|
||||||
|
Modes []string `yaml:"modes,omitempty"`
|
||||||
|
RepeatRateMin int `yaml:"repeat_rate_min,omitempty"`
|
||||||
|
RepeatRateMax int `yaml:"repeat_rate_max,omitempty"`
|
||||||
|
Increment int `yaml:"increment,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type RuleTargetConfig struct {
|
||||||
|
Device string `yaml:"device,omitempty"`
|
||||||
|
Button string `yaml:"button,omitempty"`
|
||||||
|
Axis string `yaml:"axis,omitempty"`
|
||||||
|
DeadzoneCenter int32 `yaml:"deadzone_center,omitempty"`
|
||||||
|
DeadzoneSize int32 `yaml:"deadzone_size,omitempty"`
|
||||||
|
DeadzoneSizePercent int32 `yaml:"deadzone_size_percent,omitempty"`
|
||||||
|
DeadzoneStart int32 `yaml:"deadzone_start,omitempty"`
|
||||||
|
DeadzoneEnd int32 `yaml:"deadzone_end,omitempty"`
|
||||||
|
Inverted bool `yaml:"inverted,omitempty"`
|
||||||
|
Modes []string `yaml:"modes,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: custom yaml unmarshaling is obtuse; do we really need to do all of this work
|
||||||
|
// just to set a single default value?
|
||||||
|
func (dc *DeviceConfig) UnmarshalYAML(unmarshal func(data interface{}) error) error {
|
||||||
|
var raw struct {
|
||||||
|
Name string
|
||||||
|
Type string
|
||||||
|
DeviceName string `yaml:"device_name"`
|
||||||
|
DevicePath string `yaml:"device_path"`
|
||||||
|
Preset string
|
||||||
|
NumButtons int `yaml:"num_buttons"`
|
||||||
|
NumAxes int `yaml:"num_axes"`
|
||||||
|
NumRelativeAxes int `yaml:"num_rel_axes"`
|
||||||
|
Buttons []string
|
||||||
|
Axes []string
|
||||||
|
RelativeAxes []string `yaml:"relative_axes"`
|
||||||
|
Lock bool `yaml:"lock,omitempty"`
|
||||||
|
}
|
||||||
|
raw.Lock = true
|
||||||
|
|
||||||
|
err := unmarshal(&raw)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
*dc = DeviceConfig{
|
||||||
|
Name: raw.Name,
|
||||||
|
Type: raw.Type,
|
||||||
|
DeviceName: raw.DeviceName,
|
||||||
|
DevicePath: raw.DevicePath,
|
||||||
|
Preset: raw.Preset,
|
||||||
|
NumButtons: raw.NumButtons,
|
||||||
|
NumAxes: raw.NumAxes,
|
||||||
|
NumRelativeAxes: raw.NumRelativeAxes,
|
||||||
|
Buttons: raw.Buttons,
|
||||||
|
Axes: raw.Axes,
|
||||||
|
RelativeAxes: raw.RelativeAxes,
|
||||||
|
Lock: raw.Lock,
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
|
@ -1,16 +1,114 @@
|
||||||
package virtualdevice
|
package config
|
||||||
|
|
||||||
import "github.com/holoplot/go-evdev"
|
import (
|
||||||
|
"github.com/holoplot/go-evdev"
|
||||||
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
DeviceTypePhysical = "physical"
|
||||||
|
DeviceTypeVirtual = "virtual"
|
||||||
|
|
||||||
DevicePresetKeyboard = "keyboard"
|
DevicePresetKeyboard = "keyboard"
|
||||||
DevicePresetGamepad = "gamepad"
|
DevicePresetGamepad = "gamepad"
|
||||||
DevicePresetJoystick = "joystick"
|
DevicePresetJoystick = "joystick"
|
||||||
DevicePresetMouse = "mouse"
|
DevicePresetMouse = "mouse"
|
||||||
|
|
||||||
|
RuleTypeButton = "button"
|
||||||
|
RuleTypeButtonCombo = "button-combo"
|
||||||
|
RuleTypeLatched = "button-latched"
|
||||||
|
RuleTypeAxis = "axis"
|
||||||
|
RuleTypeAxisCombined = "axis-combined"
|
||||||
|
RuleTypeModeSelect = "mode-select"
|
||||||
|
RuleTypeAxisToButton = "axis-to-button"
|
||||||
|
RuleTypeAxisToRelaxis = "axis-to-relaxis"
|
||||||
|
|
||||||
|
CodePrefixButton = "BTN"
|
||||||
|
CodePrefixKey = "KEY"
|
||||||
|
CodePrefixAxis = "ABS"
|
||||||
|
CodePrefixRelaxis = "REL"
|
||||||
|
|
||||||
VirtualDeviceMaxButtons = 74
|
VirtualDeviceMaxButtons = 74
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
ButtonFromIndex = []evdev.EvCode{
|
||||||
|
evdev.BTN_TRIGGER,
|
||||||
|
evdev.BTN_THUMB,
|
||||||
|
evdev.BTN_THUMB2,
|
||||||
|
evdev.BTN_TOP,
|
||||||
|
evdev.BTN_TOP2,
|
||||||
|
evdev.BTN_PINKIE,
|
||||||
|
evdev.BTN_BASE,
|
||||||
|
evdev.BTN_BASE2,
|
||||||
|
evdev.BTN_BASE3,
|
||||||
|
evdev.BTN_BASE4,
|
||||||
|
evdev.BTN_BASE5,
|
||||||
|
evdev.BTN_BASE6,
|
||||||
|
evdev.EvCode(0x12c), // decimal 300
|
||||||
|
evdev.EvCode(0x12d), // decimal 301
|
||||||
|
evdev.EvCode(0x12e), // decimal 302
|
||||||
|
evdev.BTN_DEAD,
|
||||||
|
evdev.BTN_TRIGGER_HAPPY1,
|
||||||
|
evdev.BTN_TRIGGER_HAPPY2,
|
||||||
|
evdev.BTN_TRIGGER_HAPPY3,
|
||||||
|
evdev.BTN_TRIGGER_HAPPY4,
|
||||||
|
evdev.BTN_TRIGGER_HAPPY5,
|
||||||
|
evdev.BTN_TRIGGER_HAPPY6,
|
||||||
|
evdev.BTN_TRIGGER_HAPPY7,
|
||||||
|
evdev.BTN_TRIGGER_HAPPY8,
|
||||||
|
evdev.BTN_TRIGGER_HAPPY9,
|
||||||
|
evdev.BTN_TRIGGER_HAPPY10,
|
||||||
|
evdev.BTN_TRIGGER_HAPPY11,
|
||||||
|
evdev.BTN_TRIGGER_HAPPY12,
|
||||||
|
evdev.BTN_TRIGGER_HAPPY13,
|
||||||
|
evdev.BTN_TRIGGER_HAPPY14,
|
||||||
|
evdev.BTN_TRIGGER_HAPPY15,
|
||||||
|
evdev.BTN_TRIGGER_HAPPY16,
|
||||||
|
evdev.BTN_TRIGGER_HAPPY17,
|
||||||
|
evdev.BTN_TRIGGER_HAPPY18,
|
||||||
|
evdev.BTN_TRIGGER_HAPPY19,
|
||||||
|
evdev.BTN_TRIGGER_HAPPY20,
|
||||||
|
evdev.BTN_TRIGGER_HAPPY21,
|
||||||
|
evdev.BTN_TRIGGER_HAPPY22,
|
||||||
|
evdev.BTN_TRIGGER_HAPPY23,
|
||||||
|
evdev.BTN_TRIGGER_HAPPY24,
|
||||||
|
evdev.BTN_TRIGGER_HAPPY25,
|
||||||
|
evdev.BTN_TRIGGER_HAPPY26,
|
||||||
|
evdev.BTN_TRIGGER_HAPPY27,
|
||||||
|
evdev.BTN_TRIGGER_HAPPY28,
|
||||||
|
evdev.BTN_TRIGGER_HAPPY29,
|
||||||
|
evdev.BTN_TRIGGER_HAPPY30,
|
||||||
|
evdev.BTN_TRIGGER_HAPPY31,
|
||||||
|
evdev.BTN_TRIGGER_HAPPY32,
|
||||||
|
evdev.BTN_TRIGGER_HAPPY33,
|
||||||
|
evdev.BTN_TRIGGER_HAPPY34,
|
||||||
|
evdev.BTN_TRIGGER_HAPPY35,
|
||||||
|
evdev.BTN_TRIGGER_HAPPY36,
|
||||||
|
evdev.BTN_TRIGGER_HAPPY37,
|
||||||
|
evdev.BTN_TRIGGER_HAPPY38,
|
||||||
|
evdev.BTN_TRIGGER_HAPPY39,
|
||||||
|
evdev.BTN_TRIGGER_HAPPY40,
|
||||||
|
evdev.EvCode(0x2e8),
|
||||||
|
evdev.EvCode(0x2e9),
|
||||||
|
evdev.EvCode(0x2f0),
|
||||||
|
evdev.EvCode(0x2f1),
|
||||||
|
evdev.EvCode(0x2f2),
|
||||||
|
evdev.EvCode(0x2f3),
|
||||||
|
evdev.EvCode(0x2f4),
|
||||||
|
evdev.EvCode(0x2f5),
|
||||||
|
evdev.EvCode(0x2f6),
|
||||||
|
evdev.EvCode(0x2f7),
|
||||||
|
evdev.EvCode(0x2f8),
|
||||||
|
evdev.EvCode(0x2f9),
|
||||||
|
evdev.EvCode(0x2fa),
|
||||||
|
evdev.EvCode(0x2fb),
|
||||||
|
evdev.EvCode(0x2fc),
|
||||||
|
evdev.EvCode(0x2fd),
|
||||||
|
evdev.EvCode(0x2fe),
|
||||||
|
evdev.EvCode(0x2ff),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
// Device Presets
|
// Device Presets
|
||||||
var (
|
var (
|
||||||
CapabilitiesPresetGamepad = map[evdev.EvType][]evdev.EvCode{
|
CapabilitiesPresetGamepad = map[evdev.EvType][]evdev.EvCode{
|
|
@ -1,67 +0,0 @@
|
||||||
package configparser
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"git.annabunches.net/annabunches/joyful/internal/logger"
|
|
||||||
"github.com/goccy/go-yaml"
|
|
||||||
)
|
|
||||||
|
|
||||||
func ParseConfig(directory string) (*Config, error) {
|
|
||||||
config := new(Config)
|
|
||||||
|
|
||||||
configFiles, err := getConfigFilePaths(directory)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Open each yaml file and add its contents to the global config
|
|
||||||
for _, filePath := range configFiles {
|
|
||||||
data, err := os.ReadFile(filePath)
|
|
||||||
if err != nil {
|
|
||||||
logger.LogError(err, "Error while opening config file")
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
newConfig := Config{}
|
|
||||||
err = yaml.Unmarshal(data, &newConfig)
|
|
||||||
logger.LogIfError(err, "Error parsing YAML")
|
|
||||||
config.Rules = append(config.Rules, newConfig.Rules...)
|
|
||||||
config.Devices = append(config.Devices, newConfig.Devices...)
|
|
||||||
config.Modes = append(config.Modes, newConfig.Modes...)
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(config.Devices) == 0 {
|
|
||||||
return nil, errors.New("Found no devices in configuration. Please add configuration at " + directory)
|
|
||||||
}
|
|
||||||
|
|
||||||
return config, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func getConfigFilePaths(directory string) ([]string, error) {
|
|
||||||
paths := make([]string, 0)
|
|
||||||
|
|
||||||
dirEntries, err := os.ReadDir(directory)
|
|
||||||
if err != nil {
|
|
||||||
err = os.Mkdir(directory, 0755)
|
|
||||||
if err != nil {
|
|
||||||
return nil, errors.New("failed to create config directory at " + directory)
|
|
||||||
} else {
|
|
||||||
return nil, errors.New("no config files found at " + directory)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, file := range dirEntries {
|
|
||||||
name := strings.ToLower(file.Name())
|
|
||||||
if file.IsDir() || !(strings.HasSuffix(name, ".yaml") || strings.HasSuffix(name, ".yml")) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
paths = append(paths, filepath.Join(directory, file.Name()))
|
|
||||||
}
|
|
||||||
|
|
||||||
return paths, nil
|
|
||||||
}
|
|
|
@ -1,31 +0,0 @@
|
||||||
package configparser
|
|
||||||
|
|
||||||
// These top-level structs use custom unmarshaling to unpack each available sub-type
|
|
||||||
type DeviceConfig struct {
|
|
||||||
Type DeviceType
|
|
||||||
Config interface{}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (dc *DeviceConfig) UnmarshalYAML(unmarshal func(data interface{}) error) error {
|
|
||||||
metaConfig := &struct {
|
|
||||||
Type DeviceType
|
|
||||||
}{}
|
|
||||||
err := unmarshal(metaConfig)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
dc.Type = metaConfig.Type
|
|
||||||
|
|
||||||
err = nil
|
|
||||||
switch metaConfig.Type {
|
|
||||||
case DeviceTypePhysical:
|
|
||||||
config := DeviceConfigPhysical{}
|
|
||||||
err = unmarshal(&config)
|
|
||||||
dc.Config = config
|
|
||||||
case DeviceTypeVirtual:
|
|
||||||
config := DeviceConfigVirtual{}
|
|
||||||
err = unmarshal(&config)
|
|
||||||
dc.Config = config
|
|
||||||
}
|
|
||||||
return err
|
|
||||||
}
|
|
|
@ -1,35 +0,0 @@
|
||||||
package configparser
|
|
||||||
|
|
||||||
type DeviceConfigPhysical struct {
|
|
||||||
Name string
|
|
||||||
DeviceName string `yaml:"device_name,omitempty"`
|
|
||||||
DevicePath string `yaml:"device_path,omitempty"`
|
|
||||||
Lock bool
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: custom yaml unmarshaling is obtuse; do we really need to do all of this work
|
|
||||||
// just to set a single default value?
|
|
||||||
func (dc *DeviceConfigPhysical) UnmarshalYAML(unmarshal func(data interface{}) error) error {
|
|
||||||
var raw struct {
|
|
||||||
Name string
|
|
||||||
DeviceName string `yaml:"device_name"`
|
|
||||||
DevicePath string `yaml:"device_path"`
|
|
||||||
Lock bool `yaml:"lock,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set non-standard defaults
|
|
||||||
raw.Lock = true
|
|
||||||
|
|
||||||
err := unmarshal(&raw)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
*dc = DeviceConfigPhysical{
|
|
||||||
Name: raw.Name,
|
|
||||||
DeviceName: raw.DeviceName,
|
|
||||||
DevicePath: raw.DevicePath,
|
|
||||||
Lock: raw.Lock,
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
|
@ -1,40 +0,0 @@
|
||||||
package configparser
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"strings"
|
|
||||||
)
|
|
||||||
|
|
||||||
type DeviceType string
|
|
||||||
|
|
||||||
const (
|
|
||||||
DeviceTypeNone DeviceType = ""
|
|
||||||
DeviceTypePhysical DeviceType = "physical"
|
|
||||||
DeviceTypeVirtual DeviceType = "virtual"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
deviceTypeMap = map[string]DeviceType{
|
|
||||||
"physical": DeviceTypePhysical,
|
|
||||||
"virtual": DeviceTypeVirtual,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
func ParseDeviceType(in string) (DeviceType, error) {
|
|
||||||
deviceType, ok := deviceTypeMap[strings.ToLower(in)]
|
|
||||||
if !ok {
|
|
||||||
return DeviceTypeNone, fmt.Errorf("invalid rule type '%s'", in)
|
|
||||||
}
|
|
||||||
return deviceType, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (rt *DeviceType) UnmarshalYAML(unmarshal func(data interface{}) error) error {
|
|
||||||
var raw string
|
|
||||||
err := unmarshal(&raw)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
*rt, err = ParseDeviceType(raw)
|
|
||||||
return err
|
|
||||||
}
|
|
|
@ -1,60 +0,0 @@
|
||||||
package configparser
|
|
||||||
|
|
||||||
type RuleConfig struct {
|
|
||||||
Type RuleType
|
|
||||||
Name string
|
|
||||||
Modes []string
|
|
||||||
Config interface{}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (dc *RuleConfig) UnmarshalYAML(unmarshal func(data interface{}) error) error {
|
|
||||||
metaConfig := &struct {
|
|
||||||
Type RuleType
|
|
||||||
Name string
|
|
||||||
Modes []string
|
|
||||||
}{}
|
|
||||||
err := unmarshal(metaConfig)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
dc.Type = metaConfig.Type
|
|
||||||
dc.Name = metaConfig.Name
|
|
||||||
dc.Modes = metaConfig.Modes
|
|
||||||
|
|
||||||
switch dc.Type {
|
|
||||||
case RuleTypeButton:
|
|
||||||
config := RuleConfigButton{}
|
|
||||||
err = unmarshal(&config)
|
|
||||||
dc.Config = config
|
|
||||||
case RuleTypeButtonCombo:
|
|
||||||
config := RuleConfigButtonCombo{}
|
|
||||||
err = unmarshal(&config)
|
|
||||||
dc.Config = config
|
|
||||||
case RuleTypeButtonLatched:
|
|
||||||
config := RuleConfigButtonLatched{}
|
|
||||||
err = unmarshal(&config)
|
|
||||||
dc.Config = config
|
|
||||||
case RuleTypeAxis:
|
|
||||||
config := RuleConfigAxis{}
|
|
||||||
err = unmarshal(&config)
|
|
||||||
dc.Config = config
|
|
||||||
case RuleTypeAxisCombined:
|
|
||||||
config := RuleConfigAxisCombined{}
|
|
||||||
err = unmarshal(&config)
|
|
||||||
dc.Config = config
|
|
||||||
case RuleTypeAxisToButton:
|
|
||||||
config := RuleConfigAxisToButton{}
|
|
||||||
err = unmarshal(&config)
|
|
||||||
dc.Config = config
|
|
||||||
case RuleTypeAxisToRelaxis:
|
|
||||||
config := RuleConfigAxisToRelaxis{}
|
|
||||||
err = unmarshal(&config)
|
|
||||||
dc.Config = config
|
|
||||||
case RuleTypeModeSelect:
|
|
||||||
config := RuleConfigModeSelect{}
|
|
||||||
err = unmarshal(&config)
|
|
||||||
dc.Config = config
|
|
||||||
}
|
|
||||||
|
|
||||||
return err
|
|
||||||
}
|
|
|
@ -1,53 +0,0 @@
|
||||||
package configparser
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"strings"
|
|
||||||
)
|
|
||||||
|
|
||||||
// TODO: maybe these want to live somewhere other than configparser?
|
|
||||||
type RuleType string
|
|
||||||
|
|
||||||
const (
|
|
||||||
RuleTypeNone RuleType = ""
|
|
||||||
RuleTypeButton RuleType = "button"
|
|
||||||
RuleTypeButtonCombo RuleType = "button-combo"
|
|
||||||
RuleTypeButtonLatched RuleType = "button-latched"
|
|
||||||
RuleTypeAxis RuleType = "axis"
|
|
||||||
RuleTypeAxisCombined RuleType = "axis-combined"
|
|
||||||
RuleTypeAxisToButton RuleType = "axis-to-button"
|
|
||||||
RuleTypeAxisToRelaxis RuleType = "axis-to-relaxis"
|
|
||||||
RuleTypeModeSelect RuleType = "mode-select"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
ruleTypeMap = map[string]RuleType{
|
|
||||||
"button": RuleTypeButton,
|
|
||||||
"button-combo": RuleTypeButtonCombo,
|
|
||||||
"button-latched": RuleTypeButtonLatched,
|
|
||||||
"axis": RuleTypeAxis,
|
|
||||||
"axis-combined": RuleTypeAxisCombined,
|
|
||||||
"axis-to-button": RuleTypeAxisToButton,
|
|
||||||
"axis-to-relaxis": RuleTypeAxisToRelaxis,
|
|
||||||
"mode-select": RuleTypeModeSelect,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
func ParseRuleType(in string) (RuleType, error) {
|
|
||||||
ruleType, ok := ruleTypeMap[strings.ToLower(in)]
|
|
||||||
if !ok {
|
|
||||||
return RuleTypeNone, fmt.Errorf("invalid rule type '%s'", in)
|
|
||||||
}
|
|
||||||
return ruleType, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (rt *RuleType) UnmarshalYAML(unmarshal func(data interface{}) error) error {
|
|
||||||
var raw string
|
|
||||||
err := unmarshal(&raw)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
*rt, err = ParseRuleType(raw)
|
|
||||||
return err
|
|
||||||
}
|
|
|
@ -1,93 +0,0 @@
|
||||||
// These types comprise the YAML schema that doesn't need custom unmarshalling.
|
|
||||||
|
|
||||||
package configparser
|
|
||||||
|
|
||||||
type Config struct {
|
|
||||||
Devices []DeviceConfig
|
|
||||||
Modes []string
|
|
||||||
Rules []RuleConfig
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: configure custom unmarshaling so we can overload Buttons, Axes, and RelativeAxes...
|
|
||||||
type DeviceConfigVirtual struct {
|
|
||||||
Name string
|
|
||||||
Preset string
|
|
||||||
NumButtons int `yaml:"num_buttons,omitempty"`
|
|
||||||
NumAxes int `yaml:"num_axes,omitempty"`
|
|
||||||
NumRelativeAxes int `yaml:"num_rel_axes"`
|
|
||||||
Buttons []string
|
|
||||||
Axes []string
|
|
||||||
RelativeAxes []string `yaml:"rel_axes,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type RuleConfigButton struct {
|
|
||||||
Input RuleTargetConfigButton
|
|
||||||
Output RuleTargetConfigButton
|
|
||||||
}
|
|
||||||
|
|
||||||
type RuleConfigButtonCombo struct {
|
|
||||||
Inputs []RuleTargetConfigButton
|
|
||||||
Output RuleTargetConfigButton
|
|
||||||
}
|
|
||||||
|
|
||||||
type RuleConfigButtonLatched struct {
|
|
||||||
Input RuleTargetConfigButton
|
|
||||||
Output RuleTargetConfigButton
|
|
||||||
}
|
|
||||||
|
|
||||||
type RuleConfigAxis struct {
|
|
||||||
Input RuleTargetConfigAxis
|
|
||||||
Output RuleTargetConfigAxis
|
|
||||||
}
|
|
||||||
|
|
||||||
type RuleConfigAxisCombined struct {
|
|
||||||
InputLower RuleTargetConfigAxis `yaml:"input_lower,omitempty"`
|
|
||||||
InputUpper RuleTargetConfigAxis `yaml:"input_upper,omitempty"`
|
|
||||||
Output RuleTargetConfigAxis
|
|
||||||
}
|
|
||||||
|
|
||||||
type RuleConfigAxisToButton struct {
|
|
||||||
RepeatRateMin int `yaml:"repeat_rate_min,omitempty"`
|
|
||||||
RepeatRateMax int `yaml:"repeat_rate_max,omitempty"`
|
|
||||||
Input RuleTargetConfigAxis
|
|
||||||
Output RuleTargetConfigButton
|
|
||||||
}
|
|
||||||
|
|
||||||
type RuleConfigAxisToRelaxis struct {
|
|
||||||
RepeatRateMin int `yaml:"repeat_rate_min"`
|
|
||||||
RepeatRateMax int `yaml:"repeat_rate_max"`
|
|
||||||
Increment int
|
|
||||||
Input RuleTargetConfigAxis
|
|
||||||
Output RuleTargetConfigRelaxis
|
|
||||||
}
|
|
||||||
|
|
||||||
type RuleConfigModeSelect struct {
|
|
||||||
Input RuleTargetConfigButton
|
|
||||||
Output RuleTargetConfigModeSelect
|
|
||||||
}
|
|
||||||
|
|
||||||
type RuleTargetConfigButton struct {
|
|
||||||
Device string
|
|
||||||
Button string
|
|
||||||
Inverted bool
|
|
||||||
}
|
|
||||||
|
|
||||||
type RuleTargetConfigAxis struct {
|
|
||||||
Device string
|
|
||||||
Axis string
|
|
||||||
DeadzoneCenter int32 `yaml:"deadzone_center,omitempty"`
|
|
||||||
DeadzoneSize int32 `yaml:"deadzone_size,omitempty"`
|
|
||||||
DeadzoneSizePercent int32 `yaml:"deadzone_size_percent,omitempty"`
|
|
||||||
DeadzoneStart int32 `yaml:"deadzone_start,omitempty"`
|
|
||||||
DeadzoneEnd int32 `yaml:"deadzone_end,omitempty"`
|
|
||||||
Inverted bool
|
|
||||||
}
|
|
||||||
|
|
||||||
type RuleTargetConfigRelaxis struct {
|
|
||||||
Device string
|
|
||||||
Axis string
|
|
||||||
}
|
|
||||||
|
|
||||||
type RuleTargetConfigModeSelect struct {
|
|
||||||
Modes []string
|
|
||||||
}
|
|
|
@ -1,90 +0,0 @@
|
||||||
package eventcodes
|
|
||||||
|
|
||||||
import "github.com/holoplot/go-evdev"
|
|
||||||
|
|
||||||
const (
|
|
||||||
CodePrefixButton = "BTN"
|
|
||||||
CodePrefixKey = "KEY"
|
|
||||||
CodePrefixAxis = "ABS"
|
|
||||||
CodePrefixRelaxis = "REL"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
// Map joystick buttons to integer indices
|
|
||||||
ButtonFromIndex = []evdev.EvCode{
|
|
||||||
evdev.BTN_TRIGGER,
|
|
||||||
evdev.BTN_THUMB,
|
|
||||||
evdev.BTN_THUMB2,
|
|
||||||
evdev.BTN_TOP,
|
|
||||||
evdev.BTN_TOP2,
|
|
||||||
evdev.BTN_PINKIE,
|
|
||||||
evdev.BTN_BASE,
|
|
||||||
evdev.BTN_BASE2,
|
|
||||||
evdev.BTN_BASE3,
|
|
||||||
evdev.BTN_BASE4,
|
|
||||||
evdev.BTN_BASE5,
|
|
||||||
evdev.BTN_BASE6,
|
|
||||||
evdev.EvCode(0x12c), // decimal 300
|
|
||||||
evdev.EvCode(0x12d), // decimal 301
|
|
||||||
evdev.EvCode(0x12e), // decimal 302
|
|
||||||
evdev.BTN_DEAD,
|
|
||||||
evdev.BTN_TRIGGER_HAPPY1,
|
|
||||||
evdev.BTN_TRIGGER_HAPPY2,
|
|
||||||
evdev.BTN_TRIGGER_HAPPY3,
|
|
||||||
evdev.BTN_TRIGGER_HAPPY4,
|
|
||||||
evdev.BTN_TRIGGER_HAPPY5,
|
|
||||||
evdev.BTN_TRIGGER_HAPPY6,
|
|
||||||
evdev.BTN_TRIGGER_HAPPY7,
|
|
||||||
evdev.BTN_TRIGGER_HAPPY8,
|
|
||||||
evdev.BTN_TRIGGER_HAPPY9,
|
|
||||||
evdev.BTN_TRIGGER_HAPPY10,
|
|
||||||
evdev.BTN_TRIGGER_HAPPY11,
|
|
||||||
evdev.BTN_TRIGGER_HAPPY12,
|
|
||||||
evdev.BTN_TRIGGER_HAPPY13,
|
|
||||||
evdev.BTN_TRIGGER_HAPPY14,
|
|
||||||
evdev.BTN_TRIGGER_HAPPY15,
|
|
||||||
evdev.BTN_TRIGGER_HAPPY16,
|
|
||||||
evdev.BTN_TRIGGER_HAPPY17,
|
|
||||||
evdev.BTN_TRIGGER_HAPPY18,
|
|
||||||
evdev.BTN_TRIGGER_HAPPY19,
|
|
||||||
evdev.BTN_TRIGGER_HAPPY20,
|
|
||||||
evdev.BTN_TRIGGER_HAPPY21,
|
|
||||||
evdev.BTN_TRIGGER_HAPPY22,
|
|
||||||
evdev.BTN_TRIGGER_HAPPY23,
|
|
||||||
evdev.BTN_TRIGGER_HAPPY24,
|
|
||||||
evdev.BTN_TRIGGER_HAPPY25,
|
|
||||||
evdev.BTN_TRIGGER_HAPPY26,
|
|
||||||
evdev.BTN_TRIGGER_HAPPY27,
|
|
||||||
evdev.BTN_TRIGGER_HAPPY28,
|
|
||||||
evdev.BTN_TRIGGER_HAPPY29,
|
|
||||||
evdev.BTN_TRIGGER_HAPPY30,
|
|
||||||
evdev.BTN_TRIGGER_HAPPY31,
|
|
||||||
evdev.BTN_TRIGGER_HAPPY32,
|
|
||||||
evdev.BTN_TRIGGER_HAPPY33,
|
|
||||||
evdev.BTN_TRIGGER_HAPPY34,
|
|
||||||
evdev.BTN_TRIGGER_HAPPY35,
|
|
||||||
evdev.BTN_TRIGGER_HAPPY36,
|
|
||||||
evdev.BTN_TRIGGER_HAPPY37,
|
|
||||||
evdev.BTN_TRIGGER_HAPPY38,
|
|
||||||
evdev.BTN_TRIGGER_HAPPY39,
|
|
||||||
evdev.BTN_TRIGGER_HAPPY40,
|
|
||||||
evdev.EvCode(0x2e8),
|
|
||||||
evdev.EvCode(0x2e9),
|
|
||||||
evdev.EvCode(0x2f0),
|
|
||||||
evdev.EvCode(0x2f1),
|
|
||||||
evdev.EvCode(0x2f2),
|
|
||||||
evdev.EvCode(0x2f3),
|
|
||||||
evdev.EvCode(0x2f4),
|
|
||||||
evdev.EvCode(0x2f5),
|
|
||||||
evdev.EvCode(0x2f6),
|
|
||||||
evdev.EvCode(0x2f7),
|
|
||||||
evdev.EvCode(0x2f8),
|
|
||||||
evdev.EvCode(0x2f9),
|
|
||||||
evdev.EvCode(0x2fa),
|
|
||||||
evdev.EvCode(0x2fb),
|
|
||||||
evdev.EvCode(0x2fc),
|
|
||||||
evdev.EvCode(0x2fd),
|
|
||||||
evdev.EvCode(0x2fe),
|
|
||||||
evdev.EvCode(0x2ff),
|
|
||||||
}
|
|
||||||
)
|
|
|
@ -1,246 +0,0 @@
|
||||||
// TODO: these tests should live with their rule_target_* counterparts
|
|
||||||
|
|
||||||
package mappingrules
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"git.annabunches.net/annabunches/joyful/internal/configparser"
|
|
||||||
"github.com/holoplot/go-evdev"
|
|
||||||
"github.com/stretchr/testify/mock"
|
|
||||||
"github.com/stretchr/testify/suite"
|
|
||||||
)
|
|
||||||
|
|
||||||
type MakeRuleTargetsTests struct {
|
|
||||||
suite.Suite
|
|
||||||
devs map[string]Device
|
|
||||||
deviceMock *DeviceMock
|
|
||||||
}
|
|
||||||
|
|
||||||
type DeviceMock struct {
|
|
||||||
mock.Mock
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *DeviceMock) AbsInfos() (map[evdev.EvCode]evdev.AbsInfo, error) {
|
|
||||||
args := m.Called()
|
|
||||||
return args.Get(0).(map[evdev.EvCode]evdev.AbsInfo), args.Error(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestRunnerMakeRuleTargets(t *testing.T) {
|
|
||||||
suite.Run(t, new(MakeRuleTargetsTests))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *MakeRuleTargetsTests) SetupSuite() {
|
|
||||||
t.deviceMock = new(DeviceMock)
|
|
||||||
t.deviceMock.On("AbsInfos").Return(
|
|
||||||
map[evdev.EvCode]evdev.AbsInfo{
|
|
||||||
evdev.ABS_X: {
|
|
||||||
Minimum: 0,
|
|
||||||
Maximum: 10000,
|
|
||||||
},
|
|
||||||
evdev.ABS_Y: {
|
|
||||||
Minimum: 0,
|
|
||||||
Maximum: 10000,
|
|
||||||
},
|
|
||||||
}, nil,
|
|
||||||
)
|
|
||||||
t.devs = map[string]Device{
|
|
||||||
"test": t.deviceMock,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *MakeRuleTargetsTests) TestMakeRuleTargetButton() {
|
|
||||||
config := configparser.RuleTargetConfigButton{Device: "test"}
|
|
||||||
|
|
||||||
t.Run("Standard keycode", func() {
|
|
||||||
config.Button = "BTN_TRIGGER"
|
|
||||||
rule, err := NewRuleTargetButtonFromConfig(config, t.devs)
|
|
||||||
t.Nil(err)
|
|
||||||
t.EqualValues(evdev.BTN_TRIGGER, rule.Button)
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("Hex code", func() {
|
|
||||||
config.Button = "0x2fd"
|
|
||||||
rule, err := NewRuleTargetButtonFromConfig(config, t.devs)
|
|
||||||
t.Nil(err)
|
|
||||||
t.EqualValues(evdev.EvCode(0x2fd), rule.Button)
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("Index", func() {
|
|
||||||
config.Button = "3"
|
|
||||||
rule, err := NewRuleTargetButtonFromConfig(config, t.devs)
|
|
||||||
t.Nil(err)
|
|
||||||
t.EqualValues(evdev.BTN_TOP, rule.Button)
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("Index too high", func() {
|
|
||||||
config.Button = "74"
|
|
||||||
_, err := NewRuleTargetButtonFromConfig(config, t.devs)
|
|
||||||
t.NotNil(err)
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("Un-prefixed keycode", func() {
|
|
||||||
config.Button = "pinkie"
|
|
||||||
rule, err := NewRuleTargetButtonFromConfig(config, t.devs)
|
|
||||||
t.Nil(err)
|
|
||||||
t.EqualValues(evdev.BTN_PINKIE, rule.Button)
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("Invalid keycode", func() {
|
|
||||||
config.Button = "foo"
|
|
||||||
_, err := NewRuleTargetButtonFromConfig(config, t.devs)
|
|
||||||
t.NotNil(err)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *MakeRuleTargetsTests) TestMakeRuleTargetAxis() {
|
|
||||||
codeTestCases := []struct {
|
|
||||||
input string
|
|
||||||
output evdev.EvCode
|
|
||||||
}{
|
|
||||||
{"ABS_X", evdev.ABS_X},
|
|
||||||
{"0x01", evdev.ABS_Y},
|
|
||||||
{"x", evdev.ABS_X},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tc := range codeTestCases {
|
|
||||||
t.Run(fmt.Sprintf("KeyCode %s", tc.input), func() {
|
|
||||||
config := configparser.RuleTargetConfigAxis{Device: "test"}
|
|
||||||
config.Axis = tc.input
|
|
||||||
rule, err := NewRuleTargetAxisFromConfig(config, t.devs)
|
|
||||||
t.Nil(err)
|
|
||||||
t.EqualValues(tc.output, rule.Axis)
|
|
||||||
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
t.Run("Invalid code", func() {
|
|
||||||
config := configparser.RuleTargetConfigAxis{Device: "test"}
|
|
||||||
config.Axis = "foo"
|
|
||||||
_, err := NewRuleTargetAxisFromConfig(config, t.devs)
|
|
||||||
t.NotNil(err)
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("Invalid deadzone", func() {
|
|
||||||
config := configparser.RuleTargetConfigAxis{Device: "test"}
|
|
||||||
config.Axis = "x"
|
|
||||||
config.DeadzoneEnd = 100
|
|
||||||
config.DeadzoneStart = 1000
|
|
||||||
_, err := NewRuleTargetAxisFromConfig(config, t.devs)
|
|
||||||
t.NotNil(err)
|
|
||||||
})
|
|
||||||
|
|
||||||
relDeadzoneTestCases := []struct {
|
|
||||||
inCenter int32
|
|
||||||
inSize int32
|
|
||||||
outStart int32
|
|
||||||
outEnd int32
|
|
||||||
}{
|
|
||||||
{5000, 1000, 4500, 5500},
|
|
||||||
{0, 500, 0, 500},
|
|
||||||
{10000, 500, 9500, 10000},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tc := range relDeadzoneTestCases {
|
|
||||||
t.Run(fmt.Sprintf("Relative Deadzone %d +- %d", tc.inCenter, tc.inSize), func() {
|
|
||||||
config := configparser.RuleTargetConfigAxis{
|
|
||||||
Device: "test",
|
|
||||||
Axis: "x",
|
|
||||||
DeadzoneCenter: tc.inCenter,
|
|
||||||
DeadzoneSize: tc.inSize,
|
|
||||||
}
|
|
||||||
rule, err := NewRuleTargetAxisFromConfig(config, t.devs)
|
|
||||||
|
|
||||||
t.Nil(err)
|
|
||||||
t.Equal(tc.outStart, rule.DeadzoneStart)
|
|
||||||
t.Equal(tc.outEnd, rule.DeadzoneEnd)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
t.Run("Deadzone center/size invalid center", func() {
|
|
||||||
config := configparser.RuleTargetConfigAxis{
|
|
||||||
Device: "test",
|
|
||||||
Axis: "x",
|
|
||||||
DeadzoneCenter: 20000,
|
|
||||||
DeadzoneSize: 500,
|
|
||||||
}
|
|
||||||
_, err := NewRuleTargetAxisFromConfig(config, t.devs)
|
|
||||||
t.NotNil(err)
|
|
||||||
})
|
|
||||||
|
|
||||||
relDeadzonePercentTestCases := []struct {
|
|
||||||
inCenter int32
|
|
||||||
inSizePercent int32
|
|
||||||
outStart int32
|
|
||||||
outEnd int32
|
|
||||||
}{
|
|
||||||
{5000, 10, 4500, 5500},
|
|
||||||
{0, 10, 0, 1000},
|
|
||||||
{10000, 10, 9000, 10000},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tc := range relDeadzonePercentTestCases {
|
|
||||||
t.Run(fmt.Sprintf("Relative percent deadzone %d +- %d%%", tc.inCenter, tc.inSizePercent), func() {
|
|
||||||
config := configparser.RuleTargetConfigAxis{
|
|
||||||
Device: "test",
|
|
||||||
Axis: "x",
|
|
||||||
DeadzoneCenter: tc.inCenter,
|
|
||||||
DeadzoneSizePercent: tc.inSizePercent,
|
|
||||||
}
|
|
||||||
rule, err := NewRuleTargetAxisFromConfig(config, t.devs)
|
|
||||||
|
|
||||||
t.Nil(err)
|
|
||||||
t.Equal(tc.outStart, rule.DeadzoneStart)
|
|
||||||
t.Equal(tc.outEnd, rule.DeadzoneEnd)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
t.Run("Deadzone center/percent invalid center", func() {
|
|
||||||
config := configparser.RuleTargetConfigAxis{
|
|
||||||
Device: "test",
|
|
||||||
Axis: "x",
|
|
||||||
DeadzoneCenter: 20000,
|
|
||||||
DeadzoneSizePercent: 10,
|
|
||||||
}
|
|
||||||
_, err := NewRuleTargetAxisFromConfig(config, t.devs)
|
|
||||||
t.NotNil(err)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *MakeRuleTargetsTests) TestMakeRuleTargetRelaxis() {
|
|
||||||
config := configparser.RuleTargetConfigRelaxis{Device: "test"}
|
|
||||||
|
|
||||||
t.Run("Standard keycode", func() {
|
|
||||||
config.Axis = "REL_WHEEL"
|
|
||||||
rule, err := NewRuleTargetRelaxisFromConfig(config, t.devs)
|
|
||||||
t.Nil(err)
|
|
||||||
t.EqualValues(evdev.REL_WHEEL, rule.Axis)
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("Hex keycode", func() {
|
|
||||||
config.Axis = "0x00"
|
|
||||||
rule, err := NewRuleTargetRelaxisFromConfig(config, t.devs)
|
|
||||||
t.Nil(err)
|
|
||||||
t.EqualValues(evdev.REL_X, rule.Axis)
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("Un-prefixed keycode", func() {
|
|
||||||
config.Axis = "wheel"
|
|
||||||
rule, err := NewRuleTargetRelaxisFromConfig(config, t.devs)
|
|
||||||
t.Nil(err)
|
|
||||||
t.EqualValues(evdev.REL_WHEEL, rule.Axis)
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("Invalid keycode", func() {
|
|
||||||
config.Axis = "foo"
|
|
||||||
_, err := NewRuleTargetRelaxisFromConfig(config, t.devs)
|
|
||||||
t.NotNil(err)
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("Incorrect axis type", func() {
|
|
||||||
config.Axis = "ABS_X"
|
|
||||||
_, err := NewRuleTargetRelaxisFromConfig(config, t.devs)
|
|
||||||
t.NotNil(err)
|
|
||||||
})
|
|
||||||
}
|
|
|
@ -1,79 +0,0 @@
|
||||||
package mappingrules
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"slices"
|
|
||||||
|
|
||||||
"git.annabunches.net/annabunches/joyful/internal/configparser"
|
|
||||||
"git.annabunches.net/annabunches/joyful/internal/logger"
|
|
||||||
"github.com/holoplot/go-evdev"
|
|
||||||
)
|
|
||||||
|
|
||||||
func ConvertDeviceMap(inputDevs map[string]*evdev.InputDevice) map[string]Device {
|
|
||||||
// Golang can't inspect the concrete map type to determine interface conformance,
|
|
||||||
// so we handle that here.
|
|
||||||
devices := make(map[string]Device)
|
|
||||||
for name, dev := range inputDevs {
|
|
||||||
devices[name] = dev
|
|
||||||
}
|
|
||||||
return devices
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewRule parses a RuleConfig struct and creates and returns the appropriate rule type.
|
|
||||||
// You can remap a map[string]*evdev.InputDevice to our interface type with ConvertDeviceMap
|
|
||||||
func NewRule(config configparser.RuleConfig, pDevs map[string]Device, vDevs map[string]Device, modes []string) (MappingRule, error) {
|
|
||||||
var newRule MappingRule
|
|
||||||
var err error
|
|
||||||
|
|
||||||
if !validateModes(config.Modes, modes) {
|
|
||||||
return nil, errors.New("mode list specifies undefined mode")
|
|
||||||
}
|
|
||||||
|
|
||||||
base := NewMappingRuleBase(config.Name, config.Modes)
|
|
||||||
|
|
||||||
switch config.Type {
|
|
||||||
case configparser.RuleTypeButton:
|
|
||||||
newRule, err = NewMappingRuleButton(config.Config.(configparser.RuleConfigButton), pDevs, vDevs, base)
|
|
||||||
case configparser.RuleTypeButtonCombo:
|
|
||||||
newRule, err = NewMappingRuleButtonCombo(config.Config.(configparser.RuleConfigButtonCombo), pDevs, vDevs, base)
|
|
||||||
case configparser.RuleTypeButtonLatched:
|
|
||||||
newRule, err = NewMappingRuleButtonLatched(config.Config.(configparser.RuleConfigButtonLatched), pDevs, vDevs, base)
|
|
||||||
case configparser.RuleTypeAxis:
|
|
||||||
newRule, err = NewMappingRuleAxis(config.Config.(configparser.RuleConfigAxis), pDevs, vDevs, base)
|
|
||||||
case configparser.RuleTypeAxisCombined:
|
|
||||||
newRule, err = NewMappingRuleAxisCombined(config.Config.(configparser.RuleConfigAxisCombined), pDevs, vDevs, base)
|
|
||||||
case configparser.RuleTypeAxisToButton:
|
|
||||||
newRule, err = NewMappingRuleAxisToButton(config.Config.(configparser.RuleConfigAxisToButton), pDevs, vDevs, base)
|
|
||||||
case configparser.RuleTypeAxisToRelaxis:
|
|
||||||
newRule, err = NewMappingRuleAxisToRelaxis(config.Config.(configparser.RuleConfigAxisToRelaxis), pDevs, vDevs, base)
|
|
||||||
case configparser.RuleTypeModeSelect:
|
|
||||||
newRule, err = NewMappingRuleModeSelect(config.Config.(configparser.RuleConfigModeSelect), pDevs, modes, base)
|
|
||||||
default:
|
|
||||||
// Shouldn't actually be possible to get here...
|
|
||||||
err = fmt.Errorf("bad rule type '%s' for rule '%s'", config.Type, config.Name)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
logger.LogErrorf(err, "Failed to build rule '%s'", config.Name)
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return newRule, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// validateModes checks the provided modes against a larger subset of modes (usually all defined ones)
|
|
||||||
// and returns false if any of the modes are not defined.
|
|
||||||
func validateModes(modes []string, allModes []string) bool {
|
|
||||||
if len(modes) == 0 {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, mode := range modes {
|
|
||||||
if !slices.Contains(allModes, mode) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return true
|
|
||||||
}
|
|
|
@ -1,9 +1,6 @@
|
||||||
package mappingrules
|
package mappingrules
|
||||||
|
|
||||||
import (
|
import "github.com/holoplot/go-evdev"
|
||||||
"git.annabunches.net/annabunches/joyful/internal/configparser"
|
|
||||||
"github.com/holoplot/go-evdev"
|
|
||||||
)
|
|
||||||
|
|
||||||
// A Simple Mapping Rule can map a button to a button or an axis to an axis.
|
// A Simple Mapping Rule can map a button to a button or an axis to an axis.
|
||||||
type MappingRuleAxis struct {
|
type MappingRuleAxis struct {
|
||||||
|
@ -12,26 +9,12 @@ type MappingRuleAxis struct {
|
||||||
Output *RuleTargetAxis
|
Output *RuleTargetAxis
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewMappingRuleAxis(ruleConfig configparser.RuleConfigAxis,
|
func NewMappingRuleAxis(base MappingRuleBase, input *RuleTargetAxis, output *RuleTargetAxis) *MappingRuleAxis {
|
||||||
pDevs map[string]Device,
|
|
||||||
vDevs map[string]Device,
|
|
||||||
base MappingRuleBase) (*MappingRuleAxis, error) {
|
|
||||||
|
|
||||||
input, err := NewRuleTargetAxisFromConfig(ruleConfig.Input, pDevs)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
output, err := NewRuleTargetAxisFromConfig(ruleConfig.Output, vDevs)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return &MappingRuleAxis{
|
return &MappingRuleAxis{
|
||||||
MappingRuleBase: base,
|
MappingRuleBase: base,
|
||||||
Input: input,
|
Input: input,
|
||||||
Output: output,
|
Output: output,
|
||||||
}, nil
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (rule *MappingRuleAxis) MatchEvent(device Device, event *evdev.InputEvent, mode *string) (*evdev.InputDevice, *evdev.InputEvent) {
|
func (rule *MappingRuleAxis) MatchEvent(device Device, event *evdev.InputEvent, mode *string) (*evdev.InputDevice, *evdev.InputEvent) {
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
package mappingrules
|
package mappingrules
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"git.annabunches.net/annabunches/joyful/internal/configparser"
|
|
||||||
"git.annabunches.net/annabunches/joyful/internal/logger"
|
"git.annabunches.net/annabunches/joyful/internal/logger"
|
||||||
"github.com/holoplot/go-evdev"
|
"github.com/holoplot/go-evdev"
|
||||||
)
|
)
|
||||||
|
@ -13,26 +12,7 @@ type MappingRuleAxisCombined struct {
|
||||||
Output *RuleTargetAxis
|
Output *RuleTargetAxis
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewMappingRuleAxisCombined(ruleConfig configparser.RuleConfigAxisCombined,
|
func NewMappingRuleAxisCombined(base MappingRuleBase, inputLower *RuleTargetAxis, inputUpper *RuleTargetAxis, output *RuleTargetAxis) *MappingRuleAxisCombined {
|
||||||
pDevs map[string]Device,
|
|
||||||
vDevs map[string]Device,
|
|
||||||
base MappingRuleBase) (*MappingRuleAxisCombined, error) {
|
|
||||||
|
|
||||||
inputLower, err := NewRuleTargetAxisFromConfig(ruleConfig.InputLower, pDevs)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
inputUpper, err := NewRuleTargetAxisFromConfig(ruleConfig.InputUpper, pDevs)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
output, err := NewRuleTargetAxisFromConfig(ruleConfig.Output, vDevs)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
inputLower.OutputMax = 0
|
inputLower.OutputMax = 0
|
||||||
inputUpper.OutputMin = 0
|
inputUpper.OutputMin = 0
|
||||||
return &MappingRuleAxisCombined{
|
return &MappingRuleAxisCombined{
|
||||||
|
@ -40,7 +20,7 @@ func NewMappingRuleAxisCombined(ruleConfig configparser.RuleConfigAxisCombined,
|
||||||
InputLower: inputLower,
|
InputLower: inputLower,
|
||||||
InputUpper: inputUpper,
|
InputUpper: inputUpper,
|
||||||
Output: output,
|
Output: output,
|
||||||
}, nil
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (rule *MappingRuleAxisCombined) MatchEvent(device Device, event *evdev.InputEvent, mode *string) (*evdev.InputDevice, *evdev.InputEvent) {
|
func (rule *MappingRuleAxisCombined) MatchEvent(device Device, event *evdev.InputEvent, mode *string) (*evdev.InputDevice, *evdev.InputEvent) {
|
||||||
|
|
|
@ -38,9 +38,7 @@ func (t *MappingRuleAxisCombinedTests) SetupTest() {
|
||||||
}, nil)
|
}, nil)
|
||||||
|
|
||||||
t.inputTargetLower, _ = NewRuleTargetAxis("test-input", t.inputDevice, evdev.ABS_X, true, 0, 0)
|
t.inputTargetLower, _ = NewRuleTargetAxis("test-input", t.inputDevice, evdev.ABS_X, true, 0, 0)
|
||||||
t.inputTargetLower.OutputMax = 0
|
|
||||||
t.inputTargetUpper, _ = NewRuleTargetAxis("test-input", t.inputDevice, evdev.ABS_Y, false, 0, 0)
|
t.inputTargetUpper, _ = NewRuleTargetAxis("test-input", t.inputDevice, evdev.ABS_Y, false, 0, 0)
|
||||||
t.inputTargetUpper.OutputMin = 0
|
|
||||||
|
|
||||||
t.outputDevice = &evdev.InputDevice{}
|
t.outputDevice = &evdev.InputDevice{}
|
||||||
t.outputTarget, _ = NewRuleTargetAxis("test-output", t.outputDevice, evdev.ABS_X, false, 0, 0)
|
t.outputTarget, _ = NewRuleTargetAxis("test-output", t.outputDevice, evdev.ABS_X, false, 0, 0)
|
||||||
|
@ -59,30 +57,19 @@ func (t *MappingRuleAxisCombinedTests) TearDownSubTest() {
|
||||||
t.inputDevice.Reset()
|
t.inputDevice.Reset()
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: this test sucks
|
|
||||||
func (t *MappingRuleAxisCombinedTests) TestNewMappingRuleAxisCombined() {
|
func (t *MappingRuleAxisCombinedTests) TestNewMappingRuleAxisCombined() {
|
||||||
t.inputDevice.Stub("AbsInfos").Return(map[evdev.EvCode]evdev.AbsInfo{
|
t.inputDevice.Stub("AbsInfos").Return(map[evdev.EvCode]evdev.AbsInfo{
|
||||||
evdev.ABS_X: {Minimum: 0, Maximum: 10000},
|
evdev.ABS_X: {Minimum: 0, Maximum: 10000},
|
||||||
evdev.ABS_Y: {Minimum: 0, Maximum: 10000},
|
evdev.ABS_Y: {Minimum: 0, Maximum: 10000},
|
||||||
}, nil)
|
}, nil)
|
||||||
|
|
||||||
rule := &MappingRuleAxisCombined{
|
rule := NewMappingRuleAxisCombined(t.base, t.inputTargetLower, t.inputTargetUpper, t.outputTarget)
|
||||||
MappingRuleBase: t.base,
|
|
||||||
InputLower: t.inputTargetLower,
|
|
||||||
InputUpper: t.inputTargetUpper,
|
|
||||||
Output: t.outputTarget,
|
|
||||||
}
|
|
||||||
t.EqualValues(0, rule.InputLower.OutputMax)
|
t.EqualValues(0, rule.InputLower.OutputMax)
|
||||||
t.EqualValues(0, rule.InputUpper.OutputMin)
|
t.EqualValues(0, rule.InputUpper.OutputMin)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *MappingRuleAxisCombinedTests) TestMatchEvent() {
|
func (t *MappingRuleAxisCombinedTests) TestMatchEvent() {
|
||||||
rule := &MappingRuleAxisCombined{
|
rule := NewMappingRuleAxisCombined(t.base, t.inputTargetLower, t.inputTargetUpper, t.outputTarget)
|
||||||
MappingRuleBase: t.base,
|
|
||||||
InputLower: t.inputTargetLower,
|
|
||||||
InputUpper: t.inputTargetUpper,
|
|
||||||
Output: t.outputTarget,
|
|
||||||
}
|
|
||||||
|
|
||||||
t.Run("Lower Input", func() {
|
t.Run("Lower Input", func() {
|
||||||
testCases := []struct{ in, out int32 }{
|
testCases := []struct{ in, out int32 }{
|
||||||
|
|
|
@ -3,7 +3,6 @@ package mappingrules
|
||||||
import (
|
import (
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"git.annabunches.net/annabunches/joyful/internal/configparser"
|
|
||||||
"github.com/holoplot/go-evdev"
|
"github.com/holoplot/go-evdev"
|
||||||
"github.com/jonboulle/clockwork"
|
"github.com/jonboulle/clockwork"
|
||||||
)
|
)
|
||||||
|
@ -24,34 +23,20 @@ type MappingRuleAxisToButton struct {
|
||||||
clock clockwork.Clock
|
clock clockwork.Clock
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewMappingRuleAxisToButton(ruleConfig configparser.RuleConfigAxisToButton,
|
func NewMappingRuleAxisToButton(base MappingRuleBase, input *RuleTargetAxis, output *RuleTargetButton, repeatRateMin, repeatRateMax int) *MappingRuleAxisToButton {
|
||||||
pDevs map[string]Device,
|
|
||||||
vDevs map[string]Device,
|
|
||||||
base MappingRuleBase) (*MappingRuleAxisToButton, error) {
|
|
||||||
|
|
||||||
input, err := NewRuleTargetAxisFromConfig(ruleConfig.Input, pDevs)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
output, err := NewRuleTargetButtonFromConfig(ruleConfig.Output, vDevs)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return &MappingRuleAxisToButton{
|
return &MappingRuleAxisToButton{
|
||||||
MappingRuleBase: base,
|
MappingRuleBase: base,
|
||||||
Input: input,
|
Input: input,
|
||||||
Output: output,
|
Output: output,
|
||||||
RepeatRateMin: ruleConfig.RepeatRateMin,
|
RepeatRateMin: repeatRateMin,
|
||||||
RepeatRateMax: ruleConfig.RepeatRateMax,
|
RepeatRateMax: repeatRateMax,
|
||||||
lastEvent: time.Now(),
|
lastEvent: time.Now(),
|
||||||
nextEvent: NoNextEvent,
|
nextEvent: NoNextEvent,
|
||||||
repeat: ruleConfig.RepeatRateMin != 0 && ruleConfig.RepeatRateMax != 0,
|
repeat: repeatRateMin != 0 && repeatRateMax != 0,
|
||||||
pressed: false,
|
pressed: false,
|
||||||
active: false,
|
active: false,
|
||||||
clock: clockwork.NewRealClock(),
|
clock: clockwork.NewRealClock(),
|
||||||
}, nil
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (rule *MappingRuleAxisToButton) MatchEvent(device Device, event *evdev.InputEvent, mode *string) (*evdev.InputDevice, *evdev.InputEvent) {
|
func (rule *MappingRuleAxisToButton) MatchEvent(device Device, event *evdev.InputEvent, mode *string) (*evdev.InputDevice, *evdev.InputEvent) {
|
||||||
|
|
|
@ -19,44 +19,6 @@ type MappingRuleAxisToButtonTests struct {
|
||||||
base MappingRuleBase
|
base MappingRuleBase
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestRunnerMappingRuleAxisToButtonTests(t *testing.T) {
|
|
||||||
suite.Run(t, new(MappingRuleAxisToButtonTests))
|
|
||||||
}
|
|
||||||
|
|
||||||
// buildTimerRule creates a MappingRuleAxisToButton with a mocked clock
|
|
||||||
func (t *MappingRuleAxisToButtonTests) buildTimerRule(
|
|
||||||
repeatMin,
|
|
||||||
repeatMax int,
|
|
||||||
nextEvent time.Duration) (*MappingRuleAxisToButton, *clockwork.FakeClock) {
|
|
||||||
|
|
||||||
mockClock := clockwork.NewFakeClock()
|
|
||||||
testRule := t.buildRule(repeatMin, repeatMax)
|
|
||||||
testRule.clock = mockClock
|
|
||||||
testRule.lastEvent = testRule.clock.Now()
|
|
||||||
testRule.nextEvent = nextEvent
|
|
||||||
if nextEvent != NoNextEvent {
|
|
||||||
testRule.active = true
|
|
||||||
}
|
|
||||||
return testRule, mockClock
|
|
||||||
}
|
|
||||||
|
|
||||||
// Todo: don't love this repeated logic...
|
|
||||||
func (t *MappingRuleAxisToButtonTests) buildRule(repeatMin, repeatMax int) *MappingRuleAxisToButton {
|
|
||||||
return &MappingRuleAxisToButton{
|
|
||||||
MappingRuleBase: t.base,
|
|
||||||
Input: t.inputRule,
|
|
||||||
Output: t.outputRule,
|
|
||||||
RepeatRateMin: repeatMin,
|
|
||||||
RepeatRateMax: repeatMax,
|
|
||||||
lastEvent: time.Now(),
|
|
||||||
nextEvent: NoNextEvent,
|
|
||||||
repeat: repeatMin != 0 && repeatMax != 0,
|
|
||||||
pressed: false,
|
|
||||||
active: false,
|
|
||||||
clock: clockwork.NewRealClock(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *MappingRuleAxisToButtonTests) SetupTest() {
|
func (t *MappingRuleAxisToButtonTests) SetupTest() {
|
||||||
mode := "*"
|
mode := "*"
|
||||||
t.mode = &mode
|
t.mode = &mode
|
||||||
|
@ -78,7 +40,7 @@ func (t *MappingRuleAxisToButtonTests) TestMatchEvent() {
|
||||||
|
|
||||||
// A valid input should set a nextevent
|
// A valid input should set a nextevent
|
||||||
t.Run("No Repeat", func() {
|
t.Run("No Repeat", func() {
|
||||||
testRule := t.buildRule(0, 0)
|
testRule := NewMappingRuleAxisToButton(t.base, t.inputRule, t.outputRule, 0, 0)
|
||||||
|
|
||||||
t.Run("Valid Input", func() {
|
t.Run("Valid Input", func() {
|
||||||
testRule.MatchEvent(t.inputDevice, &evdev.InputEvent{
|
testRule.MatchEvent(t.inputDevice, &evdev.InputEvent{
|
||||||
|
@ -100,7 +62,7 @@ func (t *MappingRuleAxisToButtonTests) TestMatchEvent() {
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("Repeat", func() {
|
t.Run("Repeat", func() {
|
||||||
testRule := t.buildRule(750, 250)
|
testRule := NewMappingRuleAxisToButton(t.base, t.inputRule, t.outputRule, 750, 250)
|
||||||
testRule.MatchEvent(t.inputDevice, &evdev.InputEvent{
|
testRule.MatchEvent(t.inputDevice, &evdev.InputEvent{
|
||||||
Type: evdev.EV_ABS,
|
Type: evdev.EV_ABS,
|
||||||
Code: evdev.ABS_X,
|
Code: evdev.ABS_X,
|
||||||
|
@ -128,7 +90,7 @@ func (t *MappingRuleAxisToButtonTests) TestTimerEvent() {
|
||||||
t.Run("No Repeat", func() {
|
t.Run("No Repeat", func() {
|
||||||
// Get event if called immediately
|
// Get event if called immediately
|
||||||
t.Run("Event is available immediately", func() {
|
t.Run("Event is available immediately", func() {
|
||||||
testRule, _ := t.buildTimerRule(0, 0, 0)
|
testRule, _ := buildTimerRule(t, 0, 0, 0)
|
||||||
|
|
||||||
event := testRule.TimerEvent()
|
event := testRule.TimerEvent()
|
||||||
|
|
||||||
|
@ -138,7 +100,7 @@ func (t *MappingRuleAxisToButtonTests) TestTimerEvent() {
|
||||||
|
|
||||||
// Off event on second call
|
// Off event on second call
|
||||||
t.Run("Event emits off on second call", func() {
|
t.Run("Event emits off on second call", func() {
|
||||||
testRule, _ := t.buildTimerRule(0, 0, 0)
|
testRule, _ := buildTimerRule(t, 0, 0, 0)
|
||||||
|
|
||||||
testRule.TimerEvent()
|
testRule.TimerEvent()
|
||||||
event := testRule.TimerEvent()
|
event := testRule.TimerEvent()
|
||||||
|
@ -149,7 +111,7 @@ func (t *MappingRuleAxisToButtonTests) TestTimerEvent() {
|
||||||
|
|
||||||
// No further event, even if we wait a while
|
// No further event, even if we wait a while
|
||||||
t.Run("Additional events are not emitted while still active.", func() {
|
t.Run("Additional events are not emitted while still active.", func() {
|
||||||
testRule, mockClock := t.buildTimerRule(0, 0, 0)
|
testRule, mockClock := buildTimerRule(t, 0, 0, 0)
|
||||||
|
|
||||||
testRule.TimerEvent()
|
testRule.TimerEvent()
|
||||||
testRule.TimerEvent()
|
testRule.TimerEvent()
|
||||||
|
@ -163,13 +125,13 @@ func (t *MappingRuleAxisToButtonTests) TestTimerEvent() {
|
||||||
|
|
||||||
t.Run("Repeat", func() {
|
t.Run("Repeat", func() {
|
||||||
t.Run("No event if called immediately", func() {
|
t.Run("No event if called immediately", func() {
|
||||||
testRule, _ := t.buildTimerRule(100, 10, 50*time.Millisecond)
|
testRule, _ := buildTimerRule(t, 100, 10, 50*time.Millisecond)
|
||||||
event := testRule.TimerEvent()
|
event := testRule.TimerEvent()
|
||||||
t.Nil(event)
|
t.Nil(event)
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("No event after 49ms", func() {
|
t.Run("No event after 49ms", func() {
|
||||||
testRule, mockClock := t.buildTimerRule(100, 10, 50*time.Millisecond)
|
testRule, mockClock := buildTimerRule(t, 100, 10, 50*time.Millisecond)
|
||||||
mockClock.Advance(49 * time.Millisecond)
|
mockClock.Advance(49 * time.Millisecond)
|
||||||
|
|
||||||
event := testRule.TimerEvent()
|
event := testRule.TimerEvent()
|
||||||
|
@ -178,7 +140,7 @@ func (t *MappingRuleAxisToButtonTests) TestTimerEvent() {
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("Event after 50ms", func() {
|
t.Run("Event after 50ms", func() {
|
||||||
testRule, mockClock := t.buildTimerRule(100, 10, 50*time.Millisecond)
|
testRule, mockClock := buildTimerRule(t, 100, 10, 50*time.Millisecond)
|
||||||
mockClock.Advance(50 * time.Millisecond)
|
mockClock.Advance(50 * time.Millisecond)
|
||||||
|
|
||||||
event := testRule.TimerEvent()
|
event := testRule.TimerEvent()
|
||||||
|
@ -188,7 +150,7 @@ func (t *MappingRuleAxisToButtonTests) TestTimerEvent() {
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("Additional event at 100ms", func() {
|
t.Run("Additional event at 100ms", func() {
|
||||||
testRule, mockClock := t.buildTimerRule(100, 10, 50*time.Millisecond)
|
testRule, mockClock := buildTimerRule(t, 100, 10, 50*time.Millisecond)
|
||||||
|
|
||||||
mockClock.Advance(50 * time.Millisecond)
|
mockClock.Advance(50 * time.Millisecond)
|
||||||
testRule.TimerEvent()
|
testRule.TimerEvent()
|
||||||
|
@ -201,3 +163,24 @@ func (t *MappingRuleAxisToButtonTests) TestTimerEvent() {
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestRunnerMappingRuleAxisToButtonTests(t *testing.T) {
|
||||||
|
suite.Run(t, new(MappingRuleAxisToButtonTests))
|
||||||
|
}
|
||||||
|
|
||||||
|
// buildTimerRule creates a MappingRuleAxisToButton with a mocked clock
|
||||||
|
func buildTimerRule(t *MappingRuleAxisToButtonTests,
|
||||||
|
repeatMin,
|
||||||
|
repeatMax int,
|
||||||
|
nextEvent time.Duration) (*MappingRuleAxisToButton, *clockwork.FakeClock) {
|
||||||
|
|
||||||
|
mockClock := clockwork.NewFakeClock()
|
||||||
|
testRule := NewMappingRuleAxisToButton(t.base, t.inputRule, t.outputRule, repeatMin, repeatMax)
|
||||||
|
testRule.clock = mockClock
|
||||||
|
testRule.lastEvent = testRule.clock.Now()
|
||||||
|
testRule.nextEvent = nextEvent
|
||||||
|
if nextEvent != NoNextEvent {
|
||||||
|
testRule.active = true
|
||||||
|
}
|
||||||
|
return testRule, mockClock
|
||||||
|
}
|
||||||
|
|
|
@ -3,7 +3,6 @@ package mappingrules
|
||||||
import (
|
import (
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"git.annabunches.net/annabunches/joyful/internal/configparser"
|
|
||||||
"github.com/holoplot/go-evdev"
|
"github.com/holoplot/go-evdev"
|
||||||
"github.com/jonboulle/clockwork"
|
"github.com/jonboulle/clockwork"
|
||||||
)
|
)
|
||||||
|
@ -24,32 +23,23 @@ type MappingRuleAxisToRelaxis struct {
|
||||||
clock clockwork.Clock
|
clock clockwork.Clock
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewMappingRuleAxisToRelaxis(ruleConfig configparser.RuleConfigAxisToRelaxis,
|
func NewMappingRuleAxisToRelaxis(
|
||||||
pDevs map[string]Device,
|
base MappingRuleBase,
|
||||||
vDevs map[string]Device,
|
input *RuleTargetAxis,
|
||||||
base MappingRuleBase) (*MappingRuleAxisToRelaxis, error) {
|
output *RuleTargetRelaxis,
|
||||||
|
repeatRateMin, repeatRateMax, increment int) *MappingRuleAxisToRelaxis {
|
||||||
input, err := NewRuleTargetAxisFromConfig(ruleConfig.Input, pDevs)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
output, err := NewRuleTargetRelaxisFromConfig(ruleConfig.Output, vDevs)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return &MappingRuleAxisToRelaxis{
|
return &MappingRuleAxisToRelaxis{
|
||||||
MappingRuleBase: base,
|
MappingRuleBase: base,
|
||||||
Input: input,
|
Input: input,
|
||||||
Output: output,
|
Output: output,
|
||||||
RepeatRateMin: ruleConfig.RepeatRateMin,
|
RepeatRateMin: repeatRateMin,
|
||||||
RepeatRateMax: ruleConfig.RepeatRateMax,
|
RepeatRateMax: repeatRateMax,
|
||||||
Increment: int32(ruleConfig.Increment),
|
Increment: int32(increment),
|
||||||
lastEvent: time.Now(),
|
lastEvent: time.Now(),
|
||||||
nextEvent: NoNextEvent,
|
nextEvent: NoNextEvent,
|
||||||
clock: clockwork.NewRealClock(),
|
clock: clockwork.NewRealClock(),
|
||||||
}, nil
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (rule *MappingRuleAxisToRelaxis) MatchEvent(
|
func (rule *MappingRuleAxisToRelaxis) MatchEvent(
|
||||||
|
|
|
@ -1,9 +1,6 @@
|
||||||
package mappingrules
|
package mappingrules
|
||||||
|
|
||||||
import (
|
import "github.com/holoplot/go-evdev"
|
||||||
"git.annabunches.net/annabunches/joyful/internal/configparser"
|
|
||||||
"github.com/holoplot/go-evdev"
|
|
||||||
)
|
|
||||||
|
|
||||||
// A Simple Mapping Rule can map a button to a button or an axis to an axis.
|
// A Simple Mapping Rule can map a button to a button or an axis to an axis.
|
||||||
type MappingRuleButton struct {
|
type MappingRuleButton struct {
|
||||||
|
@ -12,26 +9,16 @@ type MappingRuleButton struct {
|
||||||
Output *RuleTargetButton
|
Output *RuleTargetButton
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewMappingRuleButton(ruleConfig configparser.RuleConfigButton,
|
func NewMappingRuleButton(
|
||||||
pDevs map[string]Device,
|
base MappingRuleBase,
|
||||||
vDevs map[string]Device,
|
input *RuleTargetButton,
|
||||||
base MappingRuleBase) (*MappingRuleButton, error) {
|
output *RuleTargetButton) *MappingRuleButton {
|
||||||
|
|
||||||
input, err := NewRuleTargetButtonFromConfig(ruleConfig.Input, pDevs)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
output, err := NewRuleTargetButtonFromConfig(ruleConfig.Output, vDevs)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return &MappingRuleButton{
|
return &MappingRuleButton{
|
||||||
MappingRuleBase: base,
|
MappingRuleBase: base,
|
||||||
Input: input,
|
Input: input,
|
||||||
Output: output,
|
Output: output,
|
||||||
}, nil
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (rule *MappingRuleButton) MatchEvent(device Device, event *evdev.InputEvent, mode *string) (*evdev.InputDevice, *evdev.InputEvent) {
|
func (rule *MappingRuleButton) MatchEvent(device Device, event *evdev.InputEvent, mode *string) (*evdev.InputDevice, *evdev.InputEvent) {
|
||||||
|
|
|
@ -1,9 +1,6 @@
|
||||||
package mappingrules
|
package mappingrules
|
||||||
|
|
||||||
import (
|
import "github.com/holoplot/go-evdev"
|
||||||
"git.annabunches.net/annabunches/joyful/internal/configparser"
|
|
||||||
"github.com/holoplot/go-evdev"
|
|
||||||
)
|
|
||||||
|
|
||||||
// A Combo Mapping Rule can require multiple physical button presses for a single output button
|
// A Combo Mapping Rule can require multiple physical button presses for a single output button
|
||||||
type MappingRuleButtonCombo struct {
|
type MappingRuleButtonCombo struct {
|
||||||
|
@ -13,31 +10,17 @@ type MappingRuleButtonCombo struct {
|
||||||
State int
|
State int
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewMappingRuleButtonCombo(ruleConfig configparser.RuleConfigButtonCombo,
|
func NewMappingRuleButtonCombo(
|
||||||
pDevs map[string]Device,
|
base MappingRuleBase,
|
||||||
vDevs map[string]Device,
|
inputs []*RuleTargetButton,
|
||||||
base MappingRuleBase) (*MappingRuleButtonCombo, error) {
|
output *RuleTargetButton) *MappingRuleButtonCombo {
|
||||||
|
|
||||||
inputs := make([]*RuleTargetButton, 0)
|
|
||||||
for _, inputConfig := range ruleConfig.Inputs {
|
|
||||||
input, err := NewRuleTargetButtonFromConfig(inputConfig, pDevs)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
inputs = append(inputs, input)
|
|
||||||
}
|
|
||||||
|
|
||||||
output, err := NewRuleTargetButtonFromConfig(ruleConfig.Output, vDevs)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return &MappingRuleButtonCombo{
|
return &MappingRuleButtonCombo{
|
||||||
MappingRuleBase: base,
|
MappingRuleBase: base,
|
||||||
Inputs: inputs,
|
Inputs: inputs,
|
||||||
Output: output,
|
Output: output,
|
||||||
State: 0,
|
State: 0,
|
||||||
}, nil
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (rule *MappingRuleButtonCombo) MatchEvent(device Device, event *evdev.InputEvent, mode *string) (*evdev.InputDevice, *evdev.InputEvent) {
|
func (rule *MappingRuleButtonCombo) MatchEvent(device Device, event *evdev.InputEvent, mode *string) (*evdev.InputDevice, *evdev.InputEvent) {
|
||||||
|
|
|
@ -1,9 +1,6 @@
|
||||||
package mappingrules
|
package mappingrules
|
||||||
|
|
||||||
import (
|
import "github.com/holoplot/go-evdev"
|
||||||
"git.annabunches.net/annabunches/joyful/internal/configparser"
|
|
||||||
"github.com/holoplot/go-evdev"
|
|
||||||
)
|
|
||||||
|
|
||||||
type MappingRuleButtonLatched struct {
|
type MappingRuleButtonLatched struct {
|
||||||
MappingRuleBase
|
MappingRuleBase
|
||||||
|
@ -12,27 +9,17 @@ type MappingRuleButtonLatched struct {
|
||||||
State bool
|
State bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewMappingRuleButtonLatched(ruleConfig configparser.RuleConfigButtonLatched,
|
func NewMappingRuleButtonLatched(
|
||||||
pDevs map[string]Device,
|
base MappingRuleBase,
|
||||||
vDevs map[string]Device,
|
input *RuleTargetButton,
|
||||||
base MappingRuleBase) (*MappingRuleButtonLatched, error) {
|
output *RuleTargetButton) *MappingRuleButtonLatched {
|
||||||
|
|
||||||
input, err := NewRuleTargetButtonFromConfig(ruleConfig.Input, pDevs)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
output, err := NewRuleTargetButtonFromConfig(ruleConfig.Output, vDevs)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return &MappingRuleButtonLatched{
|
return &MappingRuleButtonLatched{
|
||||||
MappingRuleBase: base,
|
MappingRuleBase: base,
|
||||||
Input: input,
|
Input: input,
|
||||||
Output: output,
|
Output: output,
|
||||||
State: false,
|
State: false,
|
||||||
}, nil
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (rule *MappingRuleButtonLatched) MatchEvent(device Device, event *evdev.InputEvent, mode *string) (*evdev.InputDevice, *evdev.InputEvent) {
|
func (rule *MappingRuleButtonLatched) MatchEvent(device Device, event *evdev.InputEvent, mode *string) (*evdev.InputDevice, *evdev.InputEvent) {
|
||||||
|
|
|
@ -28,11 +28,7 @@ func (t *MappingRuleButtonTests) SetupTest() {
|
||||||
func (t *MappingRuleButtonTests) TestMatchEvent() {
|
func (t *MappingRuleButtonTests) TestMatchEvent() {
|
||||||
inputButton, _ := NewRuleTargetButton("", t.inputDevice, evdev.BTN_TRIGGER, false)
|
inputButton, _ := NewRuleTargetButton("", t.inputDevice, evdev.BTN_TRIGGER, false)
|
||||||
outputButton, _ := NewRuleTargetButton("", t.outputDevice, evdev.BTN_TRIGGER, false)
|
outputButton, _ := NewRuleTargetButton("", t.outputDevice, evdev.BTN_TRIGGER, false)
|
||||||
testRule := &MappingRuleButton{
|
testRule := NewMappingRuleButton(t.base, inputButton, outputButton)
|
||||||
MappingRuleBase: t.base,
|
|
||||||
Input: inputButton,
|
|
||||||
Output: outputButton,
|
|
||||||
}
|
|
||||||
|
|
||||||
// A matching input event should produce an output event
|
// A matching input event should produce an output event
|
||||||
expected := &evdev.InputEvent{
|
expected := &evdev.InputEvent{
|
||||||
|
@ -62,11 +58,7 @@ func (t *MappingRuleButtonTests) TestMatchEvent() {
|
||||||
func (t *MappingRuleButtonTests) TestMatchEventInverted() {
|
func (t *MappingRuleButtonTests) TestMatchEventInverted() {
|
||||||
inputButton, _ := NewRuleTargetButton("", t.inputDevice, evdev.BTN_TRIGGER, true)
|
inputButton, _ := NewRuleTargetButton("", t.inputDevice, evdev.BTN_TRIGGER, true)
|
||||||
outputButton, _ := NewRuleTargetButton("", t.outputDevice, evdev.BTN_TRIGGER, false)
|
outputButton, _ := NewRuleTargetButton("", t.outputDevice, evdev.BTN_TRIGGER, false)
|
||||||
testRule := &MappingRuleButton{
|
testRule := NewMappingRuleButton(t.base, inputButton, outputButton)
|
||||||
MappingRuleBase: t.base,
|
|
||||||
Input: inputButton,
|
|
||||||
Output: outputButton,
|
|
||||||
}
|
|
||||||
|
|
||||||
// A matching input event should produce an output event
|
// A matching input event should produce an output event
|
||||||
expected := &evdev.InputEvent{
|
expected := &evdev.InputEvent{
|
||||||
|
|
|
@ -1,9 +1,6 @@
|
||||||
package mappingrules
|
package mappingrules
|
||||||
|
|
||||||
import (
|
import "github.com/holoplot/go-evdev"
|
||||||
"git.annabunches.net/annabunches/joyful/internal/configparser"
|
|
||||||
"github.com/holoplot/go-evdev"
|
|
||||||
)
|
|
||||||
|
|
||||||
type MappingRuleModeSelect struct {
|
type MappingRuleModeSelect struct {
|
||||||
MappingRuleBase
|
MappingRuleBase
|
||||||
|
@ -11,26 +8,17 @@ type MappingRuleModeSelect struct {
|
||||||
Output *RuleTargetModeSelect
|
Output *RuleTargetModeSelect
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewMappingRuleModeSelect(ruleConfig configparser.RuleConfigModeSelect,
|
func NewMappingRuleModeSelect(
|
||||||
pDevs map[string]Device,
|
base MappingRuleBase,
|
||||||
modes []string,
|
input *RuleTargetButton,
|
||||||
base MappingRuleBase) (*MappingRuleModeSelect, error) {
|
output *RuleTargetModeSelect,
|
||||||
|
) *MappingRuleModeSelect {
|
||||||
input, err := NewRuleTargetButtonFromConfig(ruleConfig.Input, pDevs)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
output, err := NewRuleTargetModeSelectFromConfig(ruleConfig.Output, modes)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return &MappingRuleModeSelect{
|
return &MappingRuleModeSelect{
|
||||||
MappingRuleBase: base,
|
MappingRuleBase: base,
|
||||||
Input: input,
|
Input: input,
|
||||||
Output: output,
|
Output: output,
|
||||||
}, nil
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (rule *MappingRuleModeSelect) MatchEvent(
|
func (rule *MappingRuleModeSelect) MatchEvent(
|
||||||
|
|
|
@ -28,16 +28,3 @@ func Clamp[T Numeric](value, min, max T) T {
|
||||||
}
|
}
|
||||||
return value
|
return value
|
||||||
}
|
}
|
||||||
|
|
||||||
func clampAndShift(start, end, min, max int32) (int32, int32) {
|
|
||||||
if start < min {
|
|
||||||
end += min - start
|
|
||||||
start = min
|
|
||||||
}
|
|
||||||
if end > max {
|
|
||||||
start -= end - max
|
|
||||||
end = max
|
|
||||||
}
|
|
||||||
|
|
||||||
return start, end
|
|
||||||
}
|
|
||||||
|
|
|
@ -4,8 +4,6 @@ import (
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"git.annabunches.net/annabunches/joyful/internal/configparser"
|
|
||||||
"git.annabunches.net/annabunches/joyful/internal/eventcodes"
|
|
||||||
"github.com/holoplot/go-evdev"
|
"github.com/holoplot/go-evdev"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -22,77 +20,6 @@ type RuleTargetAxis struct {
|
||||||
deadzoneSize int32
|
deadzoneSize int32
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewRuleTargetAxisFromConfig(targetConfig configparser.RuleTargetConfigAxis, devs map[string]Device) (*RuleTargetAxis, error) {
|
|
||||||
device, ok := devs[targetConfig.Device]
|
|
||||||
if !ok {
|
|
||||||
return nil, fmt.Errorf("non-existent device '%s'", targetConfig.Device)
|
|
||||||
}
|
|
||||||
|
|
||||||
if targetConfig.DeadzoneEnd < targetConfig.DeadzoneStart {
|
|
||||||
return nil, errors.New("deadzone_end must be greater than deadzone_start")
|
|
||||||
}
|
|
||||||
|
|
||||||
eventCode, err := eventcodes.ParseCode(targetConfig.Axis, eventcodes.CodePrefixAxis)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
deadzoneStart, deadzoneEnd, err := calculateDeadzones(targetConfig, device, eventCode)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return NewRuleTargetAxis(
|
|
||||||
targetConfig.Device,
|
|
||||||
device,
|
|
||||||
eventCode,
|
|
||||||
targetConfig.Inverted,
|
|
||||||
deadzoneStart,
|
|
||||||
deadzoneEnd,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// calculateDeadzones produces the deadzone start and end values in absolute terms
|
|
||||||
func calculateDeadzones(targetConfig configparser.RuleTargetConfigAxis, device Device, axis evdev.EvCode) (int32, int32, error) {
|
|
||||||
|
|
||||||
var deadzoneStart, deadzoneEnd int32
|
|
||||||
deadzoneStart = 0
|
|
||||||
deadzoneEnd = 0
|
|
||||||
|
|
||||||
if targetConfig.DeadzoneStart != 0 || targetConfig.DeadzoneEnd != 0 {
|
|
||||||
return targetConfig.DeadzoneStart, targetConfig.DeadzoneEnd, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
var min, max int32
|
|
||||||
absInfoMap, err := device.AbsInfos()
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
min = AxisValueMin
|
|
||||||
max = AxisValueMax
|
|
||||||
} else {
|
|
||||||
absInfo := absInfoMap[axis]
|
|
||||||
min = absInfo.Minimum
|
|
||||||
max = absInfo.Maximum
|
|
||||||
}
|
|
||||||
|
|
||||||
if targetConfig.DeadzoneCenter < min || targetConfig.DeadzoneCenter > max {
|
|
||||||
return 0, 0, fmt.Errorf("deadzone_center '%d' is out of bounds", targetConfig.DeadzoneCenter)
|
|
||||||
}
|
|
||||||
|
|
||||||
switch {
|
|
||||||
case targetConfig.DeadzoneSize != 0:
|
|
||||||
deadzoneStart = targetConfig.DeadzoneCenter - targetConfig.DeadzoneSize/2
|
|
||||||
deadzoneEnd = targetConfig.DeadzoneCenter + targetConfig.DeadzoneSize/2
|
|
||||||
case targetConfig.DeadzoneSizePercent != 0:
|
|
||||||
deadzoneSize := (max - min) / targetConfig.DeadzoneSizePercent
|
|
||||||
deadzoneStart = targetConfig.DeadzoneCenter - deadzoneSize/2
|
|
||||||
deadzoneEnd = targetConfig.DeadzoneCenter + deadzoneSize/2
|
|
||||||
}
|
|
||||||
|
|
||||||
deadzoneStart, deadzoneEnd = clampAndShift(deadzoneStart, deadzoneEnd, min, max)
|
|
||||||
return deadzoneStart, deadzoneEnd, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewRuleTargetAxis(device_name string,
|
func NewRuleTargetAxis(device_name string,
|
||||||
device Device,
|
device Device,
|
||||||
axis evdev.EvCode,
|
axis evdev.EvCode,
|
||||||
|
|
|
@ -1,12 +1,6 @@
|
||||||
package mappingrules
|
package mappingrules
|
||||||
|
|
||||||
import (
|
import "github.com/holoplot/go-evdev"
|
||||||
"fmt"
|
|
||||||
|
|
||||||
"git.annabunches.net/annabunches/joyful/internal/configparser"
|
|
||||||
"git.annabunches.net/annabunches/joyful/internal/eventcodes"
|
|
||||||
"github.com/holoplot/go-evdev"
|
|
||||||
)
|
|
||||||
|
|
||||||
type RuleTargetButton struct {
|
type RuleTargetButton struct {
|
||||||
DeviceName string
|
DeviceName string
|
||||||
|
@ -15,25 +9,6 @@ type RuleTargetButton struct {
|
||||||
Inverted bool
|
Inverted bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewRuleTargetButtonFromConfig(targetConfig configparser.RuleTargetConfigButton, devs map[string]Device) (*RuleTargetButton, error) {
|
|
||||||
device, ok := devs[targetConfig.Device]
|
|
||||||
if !ok {
|
|
||||||
return nil, fmt.Errorf("non-existent device '%s'", targetConfig.Device)
|
|
||||||
}
|
|
||||||
|
|
||||||
eventCode, err := eventcodes.ParseCodeButton(targetConfig.Button)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return NewRuleTargetButton(
|
|
||||||
targetConfig.Device,
|
|
||||||
device,
|
|
||||||
eventCode,
|
|
||||||
targetConfig.Inverted,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewRuleTargetButton(device_name string, device Device, code evdev.EvCode, inverted bool) (*RuleTargetButton, error) {
|
func NewRuleTargetButton(device_name string, device Device, code evdev.EvCode, inverted bool) (*RuleTargetButton, error) {
|
||||||
return &RuleTargetButton{
|
return &RuleTargetButton{
|
||||||
DeviceName: device_name,
|
DeviceName: device_name,
|
||||||
|
|
|
@ -4,7 +4,6 @@ import (
|
||||||
"errors"
|
"errors"
|
||||||
"slices"
|
"slices"
|
||||||
|
|
||||||
"git.annabunches.net/annabunches/joyful/internal/configparser"
|
|
||||||
"git.annabunches.net/annabunches/joyful/internal/logger"
|
"git.annabunches.net/annabunches/joyful/internal/logger"
|
||||||
"github.com/holoplot/go-evdev"
|
"github.com/holoplot/go-evdev"
|
||||||
)
|
)
|
||||||
|
@ -13,14 +12,6 @@ type RuleTargetModeSelect struct {
|
||||||
Modes []string
|
Modes []string
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewRuleTargetModeSelectFromConfig(targetConfig configparser.RuleTargetConfigModeSelect, allModes []string) (*RuleTargetModeSelect, error) {
|
|
||||||
if ok := validateModes(targetConfig.Modes, allModes); !ok {
|
|
||||||
return nil, errors.New("undefined mode in mode select list")
|
|
||||||
}
|
|
||||||
|
|
||||||
return NewRuleTargetModeSelect(targetConfig.Modes)
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewRuleTargetModeSelect(modes []string) (*RuleTargetModeSelect, error) {
|
func NewRuleTargetModeSelect(modes []string) (*RuleTargetModeSelect, error) {
|
||||||
if len(modes) == 0 {
|
if len(modes) == 0 {
|
||||||
return nil, errors.New("cannot create RuleTargetModeSelect: mode list is empty")
|
return nil, errors.New("cannot create RuleTargetModeSelect: mode list is empty")
|
||||||
|
|
|
@ -1,10 +1,6 @@
|
||||||
package mappingrules
|
package mappingrules
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
|
||||||
|
|
||||||
"git.annabunches.net/annabunches/joyful/internal/configparser"
|
|
||||||
"git.annabunches.net/annabunches/joyful/internal/eventcodes"
|
|
||||||
"github.com/holoplot/go-evdev"
|
"github.com/holoplot/go-evdev"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -12,34 +8,19 @@ type RuleTargetRelaxis struct {
|
||||||
DeviceName string
|
DeviceName string
|
||||||
Device Device
|
Device Device
|
||||||
Axis evdev.EvCode
|
Axis evdev.EvCode
|
||||||
|
Inverted bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewRuleTargetRelaxisFromConfig(targetConfig configparser.RuleTargetConfigRelaxis, devs map[string]Device) (*RuleTargetRelaxis, error) {
|
func NewRuleTargetRelaxis(device_name string,
|
||||||
device, ok := devs[targetConfig.Device]
|
|
||||||
if !ok {
|
|
||||||
return nil, fmt.Errorf("non-existent device '%s'", targetConfig.Device)
|
|
||||||
}
|
|
||||||
|
|
||||||
eventCode, err := eventcodes.ParseCode(targetConfig.Axis, eventcodes.CodePrefixRelaxis)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return NewRuleTargetRelaxis(
|
|
||||||
targetConfig.Device,
|
|
||||||
device,
|
|
||||||
eventCode,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewRuleTargetRelaxis(deviceName string,
|
|
||||||
device Device,
|
device Device,
|
||||||
axis evdev.EvCode) (*RuleTargetRelaxis, error) {
|
axis evdev.EvCode,
|
||||||
|
inverted bool) (*RuleTargetRelaxis, error) {
|
||||||
|
|
||||||
return &RuleTargetRelaxis{
|
return &RuleTargetRelaxis{
|
||||||
DeviceName: deviceName,
|
DeviceName: device_name,
|
||||||
Device: device,
|
Device: device,
|
||||||
Axis: axis,
|
Axis: axis,
|
||||||
|
Inverted: inverted,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
35
internal/virtualdevice/cleanup.go
Normal file
35
internal/virtualdevice/cleanup.go
Normal file
|
@ -0,0 +1,35 @@
|
||||||
|
// Functions for cleaning up stale virtual devices
|
||||||
|
|
||||||
|
package virtualdevice
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/holoplot/go-evdev"
|
||||||
|
)
|
||||||
|
|
||||||
|
func CleanupStaleVirtualDevices() {
|
||||||
|
devices, err := evdev.ListDevicePaths()
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("Couldn't list devices while running cleanup: %s\n", err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, devicePath := range devices {
|
||||||
|
if strings.HasPrefix(devicePath.Name, "joyful-joystick") {
|
||||||
|
device, err := evdev.Open(devicePath.Path)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("Failed to open existing joyful device at '%s': %s\n", devicePath.Path, err.Error())
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
err = evdev.DestroyDevice(device)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("Failed to destroy existing joyful device '%s' at '%s': %s\n", devicePath.Name, devicePath.Path, err.Error())
|
||||||
|
} else {
|
||||||
|
fmt.Printf("Destroyed stale joyful device '%s'\n", devicePath.Path)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -11,7 +11,13 @@ import (
|
||||||
type EventBuffer struct {
|
type EventBuffer struct {
|
||||||
events []*evdev.InputEvent
|
events []*evdev.InputEvent
|
||||||
Device VirtualDevice
|
Device VirtualDevice
|
||||||
Name string
|
}
|
||||||
|
|
||||||
|
func NewEventBuffer(device VirtualDevice) *EventBuffer {
|
||||||
|
return &EventBuffer{
|
||||||
|
events: make([]*evdev.InputEvent, 0, 100),
|
||||||
|
Device: device,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (buffer *EventBuffer) AddEvent(event *evdev.InputEvent) {
|
func (buffer *EventBuffer) AddEvent(event *evdev.InputEvent) {
|
||||||
|
|
|
@ -11,11 +11,10 @@ import (
|
||||||
|
|
||||||
type EventBufferTests struct {
|
type EventBufferTests struct {
|
||||||
suite.Suite
|
suite.Suite
|
||||||
device *VirtualDeviceMock
|
device *VirtualDeviceMock
|
||||||
buffer *EventBuffer
|
writeOneCall *mock.Call
|
||||||
}
|
}
|
||||||
|
|
||||||
// Mocks
|
|
||||||
type VirtualDeviceMock struct {
|
type VirtualDeviceMock struct {
|
||||||
mock.Mock
|
mock.Mock
|
||||||
}
|
}
|
||||||
|
@ -25,65 +24,65 @@ func (m *VirtualDeviceMock) WriteOne(event *evdev.InputEvent) error {
|
||||||
return args.Error(0)
|
return args.Error(0)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Setup
|
|
||||||
func TestRunnerEventBufferTests(t *testing.T) {
|
func TestRunnerEventBufferTests(t *testing.T) {
|
||||||
suite.Run(t, new(EventBufferTests))
|
suite.Run(t, new(EventBufferTests))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (t *EventBufferTests) SetupTest() {
|
||||||
|
t.device = new(VirtualDeviceMock)
|
||||||
|
}
|
||||||
|
|
||||||
func (t *EventBufferTests) SetupSubTest() {
|
func (t *EventBufferTests) SetupSubTest() {
|
||||||
t.device = new(VirtualDeviceMock)
|
t.device = new(VirtualDeviceMock)
|
||||||
t.buffer = &EventBuffer{Device: t.device}
|
t.writeOneCall = t.device.On("WriteOne").Return(nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *EventBufferTests) TearDownSubTest() {
|
||||||
|
t.writeOneCall.Unset()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Tests
|
|
||||||
func (t *EventBufferTests) TestNewEventBuffer() {
|
func (t *EventBufferTests) TestNewEventBuffer() {
|
||||||
t.Equal(t.device, t.buffer.Device)
|
buffer := NewEventBuffer(t.device)
|
||||||
t.Len(t.buffer.events, 0)
|
t.Equal(t.device, buffer.Device)
|
||||||
|
t.Len(buffer.events, 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *EventBufferTests) TestEventBuffer() {
|
func (t *EventBufferTests) TestEventBufferAddEvent() {
|
||||||
|
buffer := NewEventBuffer(t.device)
|
||||||
t.Run("AddEvent", func() {
|
buffer.AddEvent(&evdev.InputEvent{})
|
||||||
t.buffer.AddEvent(&evdev.InputEvent{})
|
buffer.AddEvent(&evdev.InputEvent{})
|
||||||
t.buffer.AddEvent(&evdev.InputEvent{})
|
buffer.AddEvent(&evdev.InputEvent{})
|
||||||
t.buffer.AddEvent(&evdev.InputEvent{})
|
t.Len(buffer.events, 3)
|
||||||
t.Len(t.buffer.events, 3)
|
}
|
||||||
})
|
|
||||||
|
func (t *EventBufferTests) TestEventBufferSendEvents() {
|
||||||
t.Run("SendEvents", func() {
|
t.Run("3 Events", func() {
|
||||||
t.Run("3 Events", func() {
|
buffer := NewEventBuffer(t.device)
|
||||||
writeOneCall := t.device.On("WriteOne").Return(nil)
|
buffer.AddEvent(&evdev.InputEvent{})
|
||||||
|
buffer.AddEvent(&evdev.InputEvent{})
|
||||||
t.buffer.AddEvent(&evdev.InputEvent{})
|
buffer.AddEvent(&evdev.InputEvent{})
|
||||||
t.buffer.AddEvent(&evdev.InputEvent{})
|
errs := buffer.SendEvents()
|
||||||
t.buffer.AddEvent(&evdev.InputEvent{})
|
|
||||||
errs := t.buffer.SendEvents()
|
t.Len(errs, 0)
|
||||||
|
t.device.AssertNumberOfCalls(t.T(), "WriteOne", 4)
|
||||||
t.Len(errs, 0)
|
})
|
||||||
t.device.AssertNumberOfCalls(t.T(), "WriteOne", 4)
|
|
||||||
|
t.Run("No Events", func() {
|
||||||
writeOneCall.Unset()
|
buffer := NewEventBuffer(t.device)
|
||||||
})
|
errs := buffer.SendEvents()
|
||||||
|
|
||||||
t.Run("No Events", func() {
|
t.Len(errs, 0)
|
||||||
writeOneCall := t.device.On("WriteOne").Return(nil)
|
t.device.AssertNumberOfCalls(t.T(), "WriteOne", 0)
|
||||||
|
})
|
||||||
errs := t.buffer.SendEvents()
|
|
||||||
|
t.Run("Bad Event", func() {
|
||||||
t.Len(errs, 0)
|
t.writeOneCall.Unset()
|
||||||
t.device.AssertNumberOfCalls(t.T(), "WriteOne", 0)
|
t.writeOneCall = t.device.On("WriteOne").Return(errors.New("Fail"))
|
||||||
|
|
||||||
writeOneCall.Unset()
|
buffer := NewEventBuffer(t.device)
|
||||||
})
|
buffer.AddEvent(&evdev.InputEvent{})
|
||||||
|
errs := buffer.SendEvents()
|
||||||
t.Run("Bad Event", func() {
|
t.Len(errs, 2)
|
||||||
writeOneCall := t.device.On("WriteOne").Return(errors.New("Fail"))
|
})
|
||||||
|
|
||||||
t.buffer.AddEvent(&evdev.InputEvent{})
|
|
||||||
errs := t.buffer.SendEvents()
|
|
||||||
t.Len(errs, 2)
|
|
||||||
|
|
||||||
writeOneCall.Unset()
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,165 +0,0 @@
|
||||||
package virtualdevice
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
|
|
||||||
"git.annabunches.net/annabunches/joyful/internal/configparser"
|
|
||||||
"git.annabunches.net/annabunches/joyful/internal/eventcodes"
|
|
||||||
"git.annabunches.net/annabunches/joyful/internal/logger"
|
|
||||||
"github.com/holoplot/go-evdev"
|
|
||||||
)
|
|
||||||
|
|
||||||
// NewEventBuffer takes a virtual device config specification, creates the underlying
|
|
||||||
// evdev.InputDevice, and wraps it in a buffered event emitter.
|
|
||||||
func NewEventBuffer(config configparser.DeviceConfigVirtual) (*EventBuffer, error) {
|
|
||||||
deviceMap := make(map[string]*evdev.InputDevice)
|
|
||||||
|
|
||||||
name := fmt.Sprintf("joyful-%s", config.Name)
|
|
||||||
|
|
||||||
var capabilities map[evdev.EvType][]evdev.EvCode
|
|
||||||
|
|
||||||
// todo: add tests for presets
|
|
||||||
switch config.Preset {
|
|
||||||
case DevicePresetGamepad:
|
|
||||||
capabilities = CapabilitiesPresetGamepad
|
|
||||||
case DevicePresetKeyboard:
|
|
||||||
capabilities = CapabilitiesPresetKeyboard
|
|
||||||
case DevicePresetJoystick:
|
|
||||||
capabilities = CapabilitiesPresetJoystick
|
|
||||||
case DevicePresetMouse:
|
|
||||||
capabilities = CapabilitiesPresetMouse
|
|
||||||
default:
|
|
||||||
capabilities = map[evdev.EvType][]evdev.EvCode{
|
|
||||||
evdev.EV_KEY: makeButtons(config.NumButtons, config.Buttons),
|
|
||||||
evdev.EV_ABS: makeAxes(config.NumAxes, config.Axes),
|
|
||||||
evdev.EV_REL: makeRelativeAxes(config.NumRelativeAxes, config.RelativeAxes),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
device, err := evdev.CreateDevice(
|
|
||||||
name,
|
|
||||||
// TODO: placeholders. Who knows what these should actually be...
|
|
||||||
evdev.InputID{
|
|
||||||
BusType: 0x03,
|
|
||||||
Vendor: 0x4711,
|
|
||||||
Product: 0x0816,
|
|
||||||
Version: 1,
|
|
||||||
},
|
|
||||||
capabilities,
|
|
||||||
)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
deviceMap[config.Name] = device
|
|
||||||
logger.Log(fmt.Sprintf(
|
|
||||||
"Created virtual device '%s' with %d buttons, %d axes, and %d relative axes",
|
|
||||||
name,
|
|
||||||
len(capabilities[evdev.EV_KEY]),
|
|
||||||
len(capabilities[evdev.EV_ABS]),
|
|
||||||
len(capabilities[evdev.EV_REL]),
|
|
||||||
))
|
|
||||||
|
|
||||||
return &EventBuffer{
|
|
||||||
events: make([]*evdev.InputEvent, 0, 100),
|
|
||||||
Device: device,
|
|
||||||
Name: config.Name,
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: these functions have a lot of duplication; we need to figure out how to refactor it cleanly
|
|
||||||
// without losing logging context...
|
|
||||||
func makeButtons(numButtons int, buttonList []string) []evdev.EvCode {
|
|
||||||
if numButtons > 0 && len(buttonList) > 0 {
|
|
||||||
logger.Log("'num_buttons' and 'buttons' both specified, ignoring 'num_buttons'")
|
|
||||||
}
|
|
||||||
|
|
||||||
if numButtons > VirtualDeviceMaxButtons {
|
|
||||||
numButtons = VirtualDeviceMaxButtons
|
|
||||||
logger.Logf("Limiting virtual device buttons to %d", VirtualDeviceMaxButtons)
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(buttonList) > 0 {
|
|
||||||
buttons := make([]evdev.EvCode, 0, len(buttonList))
|
|
||||||
for _, codeStr := range buttonList {
|
|
||||||
code, err := eventcodes.ParseCode(codeStr, "BTN")
|
|
||||||
if err != nil {
|
|
||||||
logger.LogError(err, "Failed to create button, skipping")
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
buttons = append(buttons, code)
|
|
||||||
}
|
|
||||||
return buttons
|
|
||||||
}
|
|
||||||
|
|
||||||
buttons := make([]evdev.EvCode, numButtons)
|
|
||||||
|
|
||||||
for i := 0; i < numButtons; i++ {
|
|
||||||
buttons[i] = eventcodes.ButtonFromIndex[i]
|
|
||||||
}
|
|
||||||
|
|
||||||
return buttons
|
|
||||||
}
|
|
||||||
|
|
||||||
func makeAxes(numAxes int, axisList []string) []evdev.EvCode {
|
|
||||||
if numAxes > 0 && len(axisList) > 0 {
|
|
||||||
logger.Log("'num_axes' and 'axes' both specified, ignoring 'num_axes'")
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(axisList) > 0 {
|
|
||||||
axes := make([]evdev.EvCode, 0, len(axisList))
|
|
||||||
for _, codeStr := range axisList {
|
|
||||||
code, err := eventcodes.ParseCode(codeStr, "ABS")
|
|
||||||
if err != nil {
|
|
||||||
logger.LogError(err, "Failed to create axis, skipping")
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
axes = append(axes, code)
|
|
||||||
}
|
|
||||||
return axes
|
|
||||||
}
|
|
||||||
|
|
||||||
if numAxes > 8 {
|
|
||||||
numAxes = 8
|
|
||||||
logger.Log("Limiting virtual device axes to 8")
|
|
||||||
}
|
|
||||||
|
|
||||||
axes := make([]evdev.EvCode, numAxes)
|
|
||||||
for i := 0; i < numAxes; i++ {
|
|
||||||
axes[i] = evdev.EvCode(i)
|
|
||||||
}
|
|
||||||
|
|
||||||
return axes
|
|
||||||
}
|
|
||||||
|
|
||||||
func makeRelativeAxes(numAxes int, axisList []string) []evdev.EvCode {
|
|
||||||
if numAxes > 0 && len(axisList) > 0 {
|
|
||||||
logger.Log("'num_rel_axes' and 'rel_axes' both specified, ignoring 'num_rel_axes'")
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(axisList) > 0 {
|
|
||||||
axes := make([]evdev.EvCode, 0, len(axisList))
|
|
||||||
for _, codeStr := range axisList {
|
|
||||||
code, err := eventcodes.ParseCode(codeStr, "REL")
|
|
||||||
if err != nil {
|
|
||||||
logger.LogError(err, "Failed to create axis, skipping")
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
axes = append(axes, code)
|
|
||||||
}
|
|
||||||
return axes
|
|
||||||
}
|
|
||||||
|
|
||||||
if numAxes > 10 {
|
|
||||||
numAxes = 10
|
|
||||||
logger.Log("Limiting virtual device relative axes to 10")
|
|
||||||
}
|
|
||||||
|
|
||||||
axes := make([]evdev.EvCode, numAxes)
|
|
||||||
for i := 0; i < numAxes; i++ {
|
|
||||||
axes[i] = evdev.EvCode(i)
|
|
||||||
}
|
|
||||||
|
|
||||||
return axes
|
|
||||||
}
|
|
84
src/bin/evinfo.rs
Normal file
84
src/bin/evinfo.rs
Normal file
|
@ -0,0 +1,84 @@
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
use clap::Parser;
|
||||||
|
use evdev::raw_stream::RawDevice;
|
||||||
|
|
||||||
|
#[derive(Parser, Debug)]
|
||||||
|
#[command(version, about, long_about = None)]
|
||||||
|
struct Args {
|
||||||
|
/// Print additional information about each device. (-vv for even more verbosity)
|
||||||
|
#[arg(short, long, action = clap::ArgAction::Count)]
|
||||||
|
verbose: u8,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
let args = Args::parse();
|
||||||
|
let devices = evdev::raw_stream::enumerate();
|
||||||
|
|
||||||
|
for (path, dev) in devices {
|
||||||
|
if is_joystick_like(&dev) {
|
||||||
|
print_device(path, dev, args.verbose)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static JOYSTICK_BUTTONS: [evdev::KeyCode; 11] = [
|
||||||
|
evdev::KeyCode::BTN_TRIGGER_HAPPY1,
|
||||||
|
evdev::KeyCode::BTN_TRIGGER_HAPPY2,
|
||||||
|
evdev::KeyCode::BTN_TRIGGER_HAPPY3,
|
||||||
|
evdev::KeyCode::BTN_TRIGGER_HAPPY4,
|
||||||
|
evdev::KeyCode::BTN_TRIGGER_HAPPY5,
|
||||||
|
evdev::KeyCode::BTN_TRIGGER_HAPPY6,
|
||||||
|
evdev::KeyCode::BTN_TRIGGER_HAPPY7,
|
||||||
|
evdev::KeyCode::BTN_TRIGGER_HAPPY8,
|
||||||
|
evdev::KeyCode::BTN_TRIGGER_HAPPY9,
|
||||||
|
evdev::KeyCode::BTN_TRIGGER_HAPPY10,
|
||||||
|
evdev::KeyCode::BTN_TRIGGER_HAPPY11,
|
||||||
|
];
|
||||||
|
|
||||||
|
fn is_joystick_like(device: &RawDevice) -> bool {
|
||||||
|
if let Some(_) = device.supported_absolute_axes() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(keys) = device.supported_keys() {
|
||||||
|
for key in keys.iter() {
|
||||||
|
if JOYSTICK_BUTTONS.contains(&key) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn print_device(path: PathBuf, device: RawDevice, verbose: u8) {
|
||||||
|
println!(
|
||||||
|
"{}: \"{}\"",
|
||||||
|
path.to_str().unwrap_or("unknown_device_path"),
|
||||||
|
device.name().unwrap_or("unknown_device_name")
|
||||||
|
);
|
||||||
|
|
||||||
|
if verbose > 0 {
|
||||||
|
let input_id = device.input_id();
|
||||||
|
println!("\tUUID:\t\t'{}'", device.unique_name().unwrap_or("n/a"));
|
||||||
|
println!("\tVendor:\t\t'0x{:x}'", input_id.vendor());
|
||||||
|
println!("\tProduct:\t'0x{:x}'", input_id.product());
|
||||||
|
println!("\tVersion:\t'{}'", input_id.version());
|
||||||
|
}
|
||||||
|
|
||||||
|
if verbose > 1 {
|
||||||
|
if let Ok(abs_info) = device.get_absinfo() {
|
||||||
|
if abs_info.count() > 0 {
|
||||||
|
println!("\tAxis Data:");
|
||||||
|
for (axis, info) in abs_info {
|
||||||
|
println!("\t\t{} {}-{}", axis, info.minimum(), info.maximum());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if verbose > 0 {
|
||||||
|
println!();
|
||||||
|
}
|
||||||
|
}
|
278
src/bin/joyful.rs
Normal file
278
src/bin/joyful.rs
Normal file
|
@ -0,0 +1,278 @@
|
||||||
|
use std::env;
|
||||||
|
use std::error;
|
||||||
|
use std::fs;
|
||||||
|
|
||||||
|
use clap::Parser;
|
||||||
|
use shellexpand;
|
||||||
|
|
||||||
|
type Result<T> = std::result::Result<T, Box<dyn error::Error>>;
|
||||||
|
|
||||||
|
#[derive(Parser, Debug)]
|
||||||
|
#[command(version, about, long_about)]
|
||||||
|
struct Args {
|
||||||
|
/// The directory that contains your YAML configuration.
|
||||||
|
#[arg(short, long, default_value = "~/.config/joyful/")]
|
||||||
|
config: String,
|
||||||
|
|
||||||
|
/// Print extra information to the console.
|
||||||
|
#[arg(short, long)]
|
||||||
|
debug: bool,
|
||||||
|
|
||||||
|
/// Volume for text-to-speech. (0-200)
|
||||||
|
#[arg(long, default_value_t = 100)]
|
||||||
|
tts_volume: u8,
|
||||||
|
|
||||||
|
/// Median pitch for text-to-speech. (0-100)
|
||||||
|
#[arg(long, default_value_t = 50)]
|
||||||
|
tts_pitch: u8,
|
||||||
|
|
||||||
|
/// Pitch range for text-to-speech. (0-100)
|
||||||
|
#[arg(long, default_value_t = 50)]
|
||||||
|
tts_range: u8,
|
||||||
|
|
||||||
|
/// Speaking speed for text-to-speech in words per minute.
|
||||||
|
#[arg(long, default_value_t = 175)]
|
||||||
|
tts_speed: u8,
|
||||||
|
|
||||||
|
/// The espeak-ng voice to use for text-to-speech.
|
||||||
|
#[arg(long, default_value = "en")]
|
||||||
|
tts_voice: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
// Parse Command-line
|
||||||
|
let args = Args::parse();
|
||||||
|
|
||||||
|
// Parse configs
|
||||||
|
let config_files = get_config_files(args.config);
|
||||||
|
|
||||||
|
// Initialize TTS
|
||||||
|
|
||||||
|
// Create Virtual Devices
|
||||||
|
|
||||||
|
// Create Physical Devices
|
||||||
|
|
||||||
|
// Create Rules
|
||||||
|
|
||||||
|
// Create listening threads?
|
||||||
|
|
||||||
|
// Loop: Parse Input
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_config_files(config_dir: String) -> Result<Vec<String>> {
|
||||||
|
let config_dir = shellexpand::full(&config_dir)?;
|
||||||
|
let paths = fs::read_dir(config_dir)?;
|
||||||
|
|
||||||
|
let mut files: Vec<String> = Vec::new();
|
||||||
|
|
||||||
|
for path in paths {
|
||||||
|
let path = match path {
|
||||||
|
Ok(path) => {
|
||||||
|
// DEBUG
|
||||||
|
println!("{:?}", path.path());
|
||||||
|
path.path()
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
println!("{err}");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if path.ends_with("yml") || path.ends_with("yaml") {
|
||||||
|
let path = match path.to_str() {
|
||||||
|
Some(path) => path,
|
||||||
|
None => {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
files.push(path.to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return files;
|
||||||
|
}
|
||||||
|
|
||||||
|
// package main
|
||||||
|
|
||||||
|
// import (
|
||||||
|
// "context"
|
||||||
|
// "fmt"
|
||||||
|
// "os"
|
||||||
|
// "strings"
|
||||||
|
// "sync"
|
||||||
|
|
||||||
|
// "github.com/holoplot/go-evdev"
|
||||||
|
// flag "github.com/spf13/pflag"
|
||||||
|
|
||||||
|
// "git.annabunches.net/annabunches/joyful/internal/config"
|
||||||
|
// "git.annabunches.net/annabunches/joyful/internal/logger"
|
||||||
|
// "git.annabunches.net/annabunches/joyful/internal/mappingrules"
|
||||||
|
// "git.annabunches.net/annabunches/joyful/internal/virtualdevice"
|
||||||
|
// )
|
||||||
|
|
||||||
|
// func getConfigDir(dir string) string {
|
||||||
|
// configDir := strings.ReplaceAll(dir, "~", "${HOME}")
|
||||||
|
// return os.ExpandEnv(configDir)
|
||||||
|
// }
|
||||||
|
|
||||||
|
// func readConfig(configDir string) *config.ConfigParser {
|
||||||
|
// parser := &config.ConfigParser{}
|
||||||
|
// err := parser.Parse(configDir)
|
||||||
|
// logger.FatalIfError(err, "Failed to parse config")
|
||||||
|
// return parser
|
||||||
|
// }
|
||||||
|
|
||||||
|
// func initVirtualBuffers(config *config.ConfigParser) (map[string]*virtualdevice.EventBuffer, map[*evdev.InputDevice]*virtualdevice.EventBuffer) {
|
||||||
|
// vDevices := config.CreateVirtualDevices()
|
||||||
|
// if len(vDevices) == 0 {
|
||||||
|
// logger.Log("Warning: no virtual devices found in configuration. No rules will work.")
|
||||||
|
// }
|
||||||
|
|
||||||
|
// vBuffersByName := make(map[string]*virtualdevice.EventBuffer)
|
||||||
|
// vBuffersByDevice := make(map[*evdev.InputDevice]*virtualdevice.EventBuffer)
|
||||||
|
// for name, device := range vDevices {
|
||||||
|
// vBuffersByName[name] = virtualdevice.NewEventBuffer(device)
|
||||||
|
// vBuffersByDevice[device] = vBuffersByName[name]
|
||||||
|
// }
|
||||||
|
// return vBuffersByName, vBuffersByDevice
|
||||||
|
// }
|
||||||
|
|
||||||
|
// // Extracts the evdev devices from a list of virtual buffers and returns them.
|
||||||
|
// func getVirtualDevices(buffers map[string]*virtualdevice.EventBuffer) map[string]*evdev.InputDevice {
|
||||||
|
// devices := make(map[string]*evdev.InputDevice)
|
||||||
|
// for name, buffer := range buffers {
|
||||||
|
// devices[name] = buffer.Device.(*evdev.InputDevice)
|
||||||
|
// }
|
||||||
|
// return devices
|
||||||
|
// }
|
||||||
|
|
||||||
|
// func initPhysicalDevices(config *config.ConfigParser) map[string]*evdev.InputDevice {
|
||||||
|
// pDeviceMap := config.ConnectPhysicalDevices()
|
||||||
|
// if len(pDeviceMap) == 0 {
|
||||||
|
// logger.Log("Warning: no physical devices found in configuration. No rules will work.")
|
||||||
|
// }
|
||||||
|
// return pDeviceMap
|
||||||
|
// }
|
||||||
|
|
||||||
|
// func main() {
|
||||||
|
// // parse command-line
|
||||||
|
// var configFlag string
|
||||||
|
// flag.BoolVarP(&logger.IsDebugMode, "debug", "d", false, "Output very verbose debug messages.")
|
||||||
|
// flag.StringVarP(&configFlag, "config", "c", "~/.config/joyful", "Directory to read configuration from.")
|
||||||
|
// ttsOps := addTTSFlags()
|
||||||
|
// flag.Parse()
|
||||||
|
|
||||||
|
// // parse configs
|
||||||
|
// configDir := getConfigDir(configFlag)
|
||||||
|
// config := readConfig(configDir)
|
||||||
|
|
||||||
|
// // initialize TTS
|
||||||
|
// tts, err := newTTS(ttsOps)
|
||||||
|
// logger.LogIfError(err, "Failed to initialize TTS")
|
||||||
|
|
||||||
|
// // Initialize virtual devices with event buffers
|
||||||
|
// vBuffersByName, vBuffersByDevice := initVirtualBuffers(config)
|
||||||
|
|
||||||
|
// // Initialize physical devices
|
||||||
|
// pDevices := initPhysicalDevices(config)
|
||||||
|
|
||||||
|
// // Load the rules
|
||||||
|
// rules, eventChannel, cancel, wg := loadRules(config, pDevices, getVirtualDevices(vBuffersByName))
|
||||||
|
|
||||||
|
// // initialize the mode variable
|
||||||
|
// mode := config.GetModes()[0]
|
||||||
|
|
||||||
|
// // initialize TTS phrases for modes
|
||||||
|
// for _, m := range config.GetModes() {
|
||||||
|
// tts.AddMessage(m)
|
||||||
|
// logger.LogDebugf("Added TTS message '%s'", m)
|
||||||
|
// }
|
||||||
|
|
||||||
|
// fmt.Println("Joyful Running! Press Ctrl+C to quit. Press Enter to reload rules.")
|
||||||
|
// if len(config.GetModes()) > 1 {
|
||||||
|
// logger.Logf("Initial mode set to '%s'", mode)
|
||||||
|
// }
|
||||||
|
|
||||||
|
// for {
|
||||||
|
// lastMode := mode
|
||||||
|
// // Get an event (blocks if necessary)
|
||||||
|
// channelEvent := <-eventChannel
|
||||||
|
|
||||||
|
// switch channelEvent.Type {
|
||||||
|
// case ChannelEventInput:
|
||||||
|
// switch channelEvent.Event.Type {
|
||||||
|
// case evdev.EV_SYN:
|
||||||
|
// // We've received a SYN_REPORT, so now we send all pending events; since SYN_REPORTs
|
||||||
|
// // might come from multiple input devices, we'll always flush, just to be sure.
|
||||||
|
// for _, buffer := range vBuffersByName {
|
||||||
|
// buffer.SendEvents()
|
||||||
|
// }
|
||||||
|
|
||||||
|
// case evdev.EV_KEY, evdev.EV_ABS:
|
||||||
|
// // We have a matchable event type. Check all the events
|
||||||
|
// for _, rule := range rules {
|
||||||
|
// device, outputEvent := rule.MatchEvent(channelEvent.Device, channelEvent.Event, &mode)
|
||||||
|
// if device == nil || outputEvent == nil {
|
||||||
|
// continue
|
||||||
|
// }
|
||||||
|
// vBuffersByDevice[device].AddEvent(outputEvent)
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
// case ChannelEventTimer:
|
||||||
|
// // Timer events give us the device and event to use directly
|
||||||
|
// vBuffersByDevice[channelEvent.Device].AddEvent(channelEvent.Event)
|
||||||
|
// // If we get a timer event, flush the output device buffer immediately
|
||||||
|
// vBuffersByDevice[channelEvent.Device].SendEvents()
|
||||||
|
|
||||||
|
// case ChannelEventReload:
|
||||||
|
// // stop existing channels
|
||||||
|
// fmt.Println("Reloading rules.")
|
||||||
|
// cancel()
|
||||||
|
// fmt.Println("Waiting for existing listeners to exit. Provide input from each of your devices.")
|
||||||
|
// wg.Wait()
|
||||||
|
// fmt.Println("Listeners exited. Parsing config.")
|
||||||
|
// config := readConfig(configDir) // reload the config
|
||||||
|
// rules, eventChannel, cancel, wg = loadRules(config, pDevices, getVirtualDevices(vBuffersByName))
|
||||||
|
// fmt.Println("Config re-loaded. Only rule changes applied. Device and Mode changes require restart.")
|
||||||
|
// }
|
||||||
|
|
||||||
|
// if lastMode != mode && tts != nil {
|
||||||
|
// tts.Say(mode)
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
// func loadRules(
|
||||||
|
// config *config.ConfigParser,
|
||||||
|
// pDevices map[string]*evdev.InputDevice,
|
||||||
|
// vDevices map[string]*evdev.InputDevice) ([]mappingrules.MappingRule, <-chan ChannelEvent, func(), *sync.WaitGroup) {
|
||||||
|
|
||||||
|
// var wg sync.WaitGroup
|
||||||
|
// eventChannel := make(chan ChannelEvent, 1000)
|
||||||
|
// ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
|
||||||
|
// // Initialize rules
|
||||||
|
// rules := config.BuildRules(pDevices, vDevices)
|
||||||
|
// logger.Logf("Created %d mapping rules.", len(rules))
|
||||||
|
|
||||||
|
// // start listening for events on devices and timers
|
||||||
|
// for _, device := range pDevices {
|
||||||
|
// wg.Add(1)
|
||||||
|
// go eventWatcher(device, eventChannel, ctx, &wg)
|
||||||
|
// }
|
||||||
|
|
||||||
|
// timerCount := 0
|
||||||
|
// for _, rule := range rules {
|
||||||
|
// if timedRule, ok := rule.(mappingrules.TimedEventEmitter); ok {
|
||||||
|
// wg.Add(1)
|
||||||
|
// go timerWatcher(timedRule, eventChannel, ctx, &wg)
|
||||||
|
// timerCount++
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// logger.Logf("Registered %d timers.", timerCount)
|
||||||
|
|
||||||
|
// go consoleWatcher(eventChannel)
|
||||||
|
|
||||||
|
// return rules, eventChannel, cancel, &wg
|
||||||
|
// }
|
Loading…
Add table
Add a link
Reference in a new issue