diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 33b817fb..892fa419 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -33,9 +33,9 @@ jobs: ${{ runner.os }}-${{ matrix.target }}-build-${{ env.cache-name }}- - name: Install Rust - uses: actions-rs/toolchain@v1 + uses: dtolnay/rust-toolchain@master with: - toolchain: stable + toolchain: nightly target: ${{ matrix.target }} - name: Install Dependencies @@ -68,16 +68,13 @@ jobs: ${{ runner.os }}-x86_64-unknown-linux-gnu-build-${{ env.cache-name }}- - name: Install Rust - uses: actions-rs/toolchain@v1 + uses: dtolnay/rust-toolchain@master with: - toolchain: stable + toolchain: nightly + components: clippy - name: Run tests - uses: actions-rs/cargo@v1 - with: - command: clippy - # "-- -D warnings" will make the job fail if their are clippy warnings - args: --workspace --no-deps -- -D warnings -W clippy::print_stdout + run: cargo clippy --tests --workspace --no-deps -- -D warnings rustfmt: runs-on: ubuntu-latest @@ -86,17 +83,14 @@ jobs: uses: actions/checkout@v3 - name: Install Rust - uses: actions-rs/toolchain@v1 + uses: dtolnay/rust-toolchain@master with: profile: minimal - toolchain: stable + toolchain: nightly components: rustfmt - name: Run formatter - uses: actions-rs/cargo@v1 - with: - command: fmt - args: --all --check + run: cargo fmt --all --check rustdoc: runs-on: ubuntu-latest @@ -119,14 +113,12 @@ jobs: ${{ runner.os }}-x86_64-unknown-linux-gnu-build-${{ env.cache-name }}- - name: Install Rust - uses: actions-rs/toolchain@v1 + uses: dtolnay/rust-toolchain@master with: - toolchain: stable + toolchain: nightly - name: Generate Documentation - uses: actions-rs/cargo@v1 - with: - command: doc + run: cargo doc --workspace --document-private-items - name: Deploy Documentation uses: peaceiris/actions-gh-pages@v3 @@ -159,7 +151,10 @@ jobs: restore-keys: | ${{ runner.os }}-${{ matrix.target }}-build-${{ env.cache-name }}- - name: Install Rust - uses: dtolnay/rust-toolchain@stable + uses: dtolnay/rust-toolchain@master + with: + toolchain: nightly + components: llvm-tools - name: cargo install llvm-cov uses: taiki-e/install-action@cargo-llvm-cov - name: cargo generate lockfile @@ -199,7 +194,7 @@ jobs: - name: Install Rust uses: dtolnay/rust-toolchain@master with: - toolchain: stable + toolchain: nightly - name: Install Cargo MSRV Verifier run: cargo install cargo-msrv --force - name: Check MSRV Compliance diff --git a/Cargo.lock b/Cargo.lock index bb657954..191047da 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4,9 +4,9 @@ version = 3 [[package]] name = "addr2line" -version = "0.21.0" +version = "0.22.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a30b2e23b9e17a9f90641c7ab1549cd9b44f296d3ccbf309d2863cfe398a0cb" +checksum = "6e4503c46a5c0c7844e948c9a4d6acd9f50cccb4de1c48eb9e291ea17470c678" dependencies = [ "gimli", ] @@ -82,9 +82,9 @@ dependencies = [ [[package]] name = "anstyle-query" -version = "1.0.3" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a64c907d4e79225ac72e2a354c9ce84d50ebb4586dee56c82b3ee73004f537f5" +checksum = "ad186efb764318d35165f1758e7dcef3b10628e26d41a44bc5550652e6804391" dependencies = [ "windows-sys 0.52.0", ] @@ -99,14 +99,20 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "anyhow" +version = "1.0.86" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3d1d046238990b9cf5bcde22a3fb3584ee5cf65fb2765f454ed428c7a0063da" + [[package]] name = "async-broadcast" -version = "0.7.0" +version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "258b52a1aa741b9f09783b2d86cf0aeeb617bbf847f6933340a39644227acbdb" +checksum = "20cd0e2e25ea8e5f7e9df04578dc6cf5c83577fd09b1a46aaf5c85e1c33f2a7e" dependencies = [ - "event-listener 5.3.0", - "event-listener-strategy 0.5.2", + "event-listener 5.3.1", + "event-listener-strategy", "futures-core", "pin-project-lite", ] @@ -129,16 +135,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "89b47800b0be77592da0afd425cc03468052844aff33b84e33cc696f64e77b6a" dependencies = [ "concurrent-queue", - "event-listener-strategy 0.5.2", + "event-listener-strategy", "futures-core", "pin-project-lite", ] [[package]] name = "async-executor" -version = "1.11.0" +version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b10202063978b3351199d68f8b22c4e47e4b1b822f8d43fd862d5ea8c006b29a" +checksum = "c8828ec6e544c02b0d6691d21ed9f9218d0384a82542855073c2a3f58304aaf0" dependencies = [ "async-task", "concurrent-queue", @@ -153,7 +159,7 @@ version = "2.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ebcd09b382f40fcd159c2d695175b2ae620ffa5f3bd6f664131efff4e8b9e04a" dependencies = [ - "async-lock 3.3.0", + "async-lock 3.4.0", "blocking", "futures-lite 2.3.0", ] @@ -166,8 +172,8 @@ checksum = "05b1b633a2115cd122d73b955eadd9916c18c8f510ec9cd1686404c60ad1c29c" dependencies = [ "async-channel 2.3.1", "async-executor", - "async-io 2.3.2", - "async-lock 3.3.0", + "async-io 2.3.3", + "async-lock 3.4.0", "blocking", "futures-lite 2.3.0", "once_cell", @@ -195,17 +201,17 @@ dependencies = [ [[package]] name = "async-io" -version = "2.3.2" +version = "2.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dcccb0f599cfa2f8ace422d3555572f47424da5648a4382a9dd0310ff8210884" +checksum = "0d6baa8f0178795da0e71bc42c9e5d13261aac7ee549853162e66a241ba17964" dependencies = [ - "async-lock 3.3.0", + "async-lock 3.4.0", "cfg-if", "concurrent-queue", "futures-io", "futures-lite 2.3.0", "parking", - "polling 3.7.0", + "polling 3.7.1", "rustix 0.38.34", "slab", "tracing", @@ -223,29 +229,29 @@ dependencies = [ [[package]] name = "async-lock" -version = "3.3.0" +version = "3.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d034b430882f8381900d3fe6f0aaa3ad94f2cb4ac519b429692a1bc2dda4ae7b" +checksum = "ff6e472cdea888a4bd64f342f09b3f50e1886d32afe8df3d663c01140b811b18" dependencies = [ - "event-listener 4.0.3", - "event-listener-strategy 0.4.0", + "event-listener 5.3.1", + "event-listener-strategy", "pin-project-lite", ] [[package]] name = "async-process" -version = "2.2.2" +version = "2.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a53fc6301894e04a92cb2584fedde80cb25ba8e02d9dc39d4a87d036e22f397d" +checksum = "f7eda79bbd84e29c2b308d1dc099d7de8dcc7035e48f4bf5dc4a531a44ff5e2a" dependencies = [ "async-channel 2.3.1", - "async-io 2.3.2", - "async-lock 3.3.0", + "async-io 2.3.3", + "async-lock 3.4.0", "async-signal", "async-task", "blocking", "cfg-if", - "event-listener 5.3.0", + "event-listener 5.3.1", "futures-lite 2.3.0", "rustix 0.38.34", "tracing", @@ -265,12 +271,12 @@ dependencies = [ [[package]] name = "async-signal" -version = "0.2.6" +version = "0.2.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "afe66191c335039c7bb78f99dc7520b0cbb166b3a1cb33a03f53d8a1c6f2afda" +checksum = "794f185324c2f00e771cd9f1ae8b5ac68be2ca7abb129a87afd6e86d228bc54d" dependencies = [ - "async-io 2.3.2", - "async-lock 3.3.0", + "async-io 2.3.3", + "async-lock 3.4.0", "atomic-waker", "cfg-if", "futures-core", @@ -364,8 +370,7 @@ checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" [[package]] name = "atspi" version = "0.22.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be534b16650e35237bb1ed189ba2aab86ce65e88cc84c66f4935ba38575cecbf" +source = "git+https://github.com/odilia-app/atspi/?branch=main#bfa3bc4e09bde61ae4bc41b52ec18b39a7840211" dependencies = [ "atspi-common", "atspi-connection", @@ -375,8 +380,7 @@ dependencies = [ [[package]] name = "atspi-common" version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1909ed2dc01d0a17505d89311d192518507e8a056a48148e3598fef5e7bb6ba7" +source = "git+https://github.com/odilia-app/atspi/?branch=main#bfa3bc4e09bde61ae4bc41b52ec18b39a7840211" dependencies = [ "enumflags2", "serde", @@ -391,8 +395,7 @@ dependencies = [ [[package]] name = "atspi-connection" version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "430c5960624a4baaa511c9c0fcc2218e3b58f5dbcc47e6190cafee344b873333" +source = "git+https://github.com/odilia-app/atspi/?branch=main#bfa3bc4e09bde61ae4bc41b52ec18b39a7840211" dependencies = [ "atspi-common", "atspi-proxies", @@ -403,8 +406,7 @@ dependencies = [ [[package]] name = "atspi-proxies" version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a5e6c5de3e524cf967569722446bcd458d5032348554d9a17d7d72b041ab7496" +source = "git+https://github.com/odilia-app/atspi/?branch=main#bfa3bc4e09bde61ae4bc41b52ec18b39a7840211" dependencies = [ "atspi-common", "serde", @@ -429,11 +431,56 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0" +[[package]] +name = "axum" +version = "0.6.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b829e4e32b91e643de6eafe82b1d90675f5874230191a4ffbc1b336dec4d6bf" +dependencies = [ + "async-trait", + "axum-core", + "bitflags 1.3.2", + "bytes", + "futures-util", + "http", + "http-body", + "hyper", + "itoa", + "matchit", + "memchr", + "mime", + "percent-encoding", + "pin-project-lite", + "rustversion", + "serde", + "sync_wrapper", + "tower", + "tower-layer", + "tower-service", +] + +[[package]] +name = "axum-core" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "759fa577a247914fd3f7f76d62972792636412fbfd634cd452f6a385a74d2d2c" +dependencies = [ + "async-trait", + "bytes", + "futures-util", + "http", + "http-body", + "mime", + "rustversion", + "tower-layer", + "tower-service", +] + [[package]] name = "backtrace" -version = "0.3.71" +version = "0.3.73" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26b05800d2e817c8b3b4b54abd461726265fa9789ae34330622f2db9ee696f9d" +checksum = "5cc23269a4f8976d0a4d2e7109211a419fe30e8d88d677cd60b6bc79c5732e0a" dependencies = [ "addr2line", "cc", @@ -444,6 +491,12 @@ dependencies = [ "rustc-demangle", ] +[[package]] +name = "base64" +version = "0.21.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" + [[package]] name = "bitflags" version = "1.3.2" @@ -456,6 +509,18 @@ version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf4b9d6a944f767f8e5e0db018570623c85f3d925ac718db4e06d0187adb21c1" +[[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 = "block" version = "0.1.6" @@ -473,12 +538,11 @@ dependencies = [ [[package]] name = "blocking" -version = "1.6.0" +version = "1.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "495f7104e962b7356f0aeb34247aca1fe7d2e783b346582db7f2904cb5717e88" +checksum = "703f41c54fc768e63e091340b424302bb1c29ef4aa0c7f10fe849dfb114d29ea" dependencies = [ "async-channel 2.3.1", - "async-lock 3.3.0", "async-task", "futures-io", "futures-lite 2.3.0", @@ -517,9 +581,9 @@ checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" [[package]] name = "cc" -version = "1.0.98" +version = "1.0.99" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41c270e7540d725e65ac7f1b212ac8ce349719624d7bcff99f8e2e488e8cf03f" +checksum = "96c51067fd44124faa7f870b4b1c969379ad32b2ba805aa959430ceaa384f695" [[package]] name = "cfg-if" @@ -583,9 +647,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.4" +version = "4.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90bc066a67923782aa8515dbaea16946c5bcc5addbd668bb80af688e53e548a0" +checksum = "5db83dced34638ad474f39f250d7fea9598bdd239eaced1bdf45d597da0f433f" dependencies = [ "clap_builder", "clap_derive", @@ -593,21 +657,21 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.2" +version = "4.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae129e2e766ae0ec03484e609954119f123cc1fe650337e155d03b022f24f7b4" +checksum = "f7e204572485eb3fbf28f871612191521df159bc3e15a9f5064c66dba3a8c05f" dependencies = [ "anstream", "anstyle", - "clap_lex 0.7.0", + "clap_lex 0.7.1", "strsim", ] [[package]] name = "clap_derive" -version = "4.5.4" +version = "4.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "528131438037fd55894f62d6e9f068b8f45ac57ffa77517819645d10aed04f64" +checksum = "c780290ccf4fb26629baa7a1081e68ced113f1d3ec302fa5948f1c381ebf06c6" dependencies = [ "heck 0.5.0", "proc-macro2", @@ -626,9 +690,9 @@ dependencies = [ [[package]] name = "clap_lex" -version = "0.7.0" +version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "98cc8fbded0c607b7ba9dd60cd98df59af97e84d24e49c8557331cfc26d301ce" +checksum = "4b82cf0babdbd58558212896d1a4272303a57bdb245c2bf1147185fb45640e70" [[package]] name = "colorchoice" @@ -645,6 +709,44 @@ dependencies = [ "crossbeam-utils", ] +[[package]] +name = "console-api" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a257c22cd7e487dd4a13d413beabc512c5052f0bc048db0da6a84c3d8a6142fd" +dependencies = [ + "futures-core", + "prost", + "prost-types", + "tonic", + "tracing-core", +] + +[[package]] +name = "console-subscriber" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "31c4cc54bae66f7d9188996404abdf7fdfa23034ef8e43478c8810828abad758" +dependencies = [ + "console-api", + "crossbeam-channel", + "crossbeam-utils", + "futures-task", + "hdrhistogram", + "humantime", + "prost", + "prost-types", + "serde", + "serde_json", + "thread_local", + "tokio", + "tokio-stream", + "tonic", + "tracing", + "tracing-core", + "tracing-subscriber", +] + [[package]] name = "core-foundation-sys" version = "0.8.6" @@ -660,6 +762,15 @@ dependencies = [ "libc", ] +[[package]] +name = "crc32fast" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a97769d94ddab943e4510d138150169a2758b5ef3eb191a9ee688de3e23ef7b3" +dependencies = [ + "cfg-if", +] + [[package]] name = "criterion" version = "0.4.0" @@ -698,6 +809,15 @@ dependencies = [ "itertools 0.10.5", ] +[[package]] +name = "crossbeam-channel" +version = "0.5.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33480d6946193aa8033910124896ca395333cae7e2d1113d1fef6c3272217df2" +dependencies = [ + "crossbeam-utils", +] + [[package]] name = "crossbeam-deque" version = "0.8.5" @@ -741,11 +861,12 @@ dependencies = [ [[package]] name = "dashmap" -version = "5.5.3" +version = "6.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "978747c1d849a7d2ee5e8adc0159961c48fb7e5db2f06af6723b80123bb53856" +checksum = "804c8821570c3f8b70230c2ba75ffa5c0f9a4189b9a432b6656c536712acae28" dependencies = [ "cfg-if", + "crossbeam-utils", "hashbrown 0.14.5", "lock_api", "once_cell", @@ -761,6 +882,17 @@ dependencies = [ "powerfmt", ] +[[package]] +name = "derived-deref" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "805ef2023ccd65425743a91ecd11fc020979a0b01921db3104fb606d18a7b43e" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.66", +] + [[package]] name = "digest" version = "0.10.7" @@ -824,11 +956,23 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a3d8a32ae18130a3c84dd492d4215c3d913c3b07c6b63c2eb3eb7ff1101ab7bf" +[[package]] +name = "enum_dispatch" +version = "0.3.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa18ce2bc66555b3218614519ac839ddb759a7d6720732f979ef8d13be147ecd" +dependencies = [ + "once_cell", + "proc-macro2", + "quote", + "syn 2.0.66", +] + [[package]] name = "enumflags2" -version = "0.7.9" +version = "0.7.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3278c9d5fb675e0a51dabcf4c0d355f692b064171535ba72361be1528a9d8e8d" +checksum = "d232db7f5956f3f14313dc2f87985c58bd2c695ce124c8cdd984e08e15ac133d" dependencies = [ "enumflags2_derive", "serde", @@ -836,9 +980,9 @@ dependencies = [ [[package]] name = "enumflags2_derive" -version = "0.7.9" +version = "0.7.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c785274071b1b420972453b306eeca06acf4633829db4223b58a2a8c5953bc4" +checksum = "de0d48a183585823424a4ce1aa132d174a6a81bd540895822eb4c8373a8e49e8" dependencies = [ "proc-macro2", "quote", @@ -869,43 +1013,22 @@ checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0" [[package]] name = "event-listener" -version = "4.0.3" +version = "5.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67b215c49b2b248c855fb73579eb1f4f26c38ffdc12973e20e07b91d78d5646e" +checksum = "6032be9bd27023a771701cc49f9f053c751055f71efb2e0ae5c15809093675ba" dependencies = [ "concurrent-queue", "parking", "pin-project-lite", ] -[[package]] -name = "event-listener" -version = "5.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d9944b8ca13534cdfb2800775f8dd4902ff3fc75a50101466decadfdf322a24" -dependencies = [ - "concurrent-queue", - "parking", - "pin-project-lite", -] - -[[package]] -name = "event-listener-strategy" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "958e4d70b6d5e81971bebec42271ec641e7ff4e170a6fa605f2b8a8b65cb97d3" -dependencies = [ - "event-listener 4.0.3", - "pin-project-lite", -] - [[package]] name = "event-listener-strategy" version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0f214dc438f977e6d4e3500aaa277f5ad94ca83fbbd9b1a15713ce2344ccc5a1" dependencies = [ - "event-listener 5.3.0", + "event-listener 5.3.1", "pin-project-lite", ] @@ -948,6 +1071,28 @@ dependencies = [ "version_check", ] +[[package]] +name = "flate2" +version = "1.0.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f54427cfd1c7829e2a139fcefea601bf088ebca651d2bf53ebc600eac295dae" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "funty" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" + [[package]] name = "futures" version = "0.3.30" @@ -963,6 +1108,17 @@ dependencies = [ "futures-util", ] +[[package]] +name = "futures-buffered" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02dcae03ee5afa5ea17b1aebc793806b8ddfc6dc500e0b8e8e1eb30b9dad22c0" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", +] + [[package]] name = "futures-channel" version = "0.3.30" @@ -973,6 +1129,21 @@ dependencies = [ "futures-sink", ] +[[package]] +name = "futures-concurrency" +version = "7.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b14ac911e85d57c5ea6eef76d7b4d4a3177ecd15f4bea2e61927e9e3823e19f" +dependencies = [ + "bitvec", + "futures-buffered", + "futures-core", + "futures-lite 1.13.0", + "pin-project", + "slab", + "smallvec", +] + [[package]] name = "futures-core" version = "0.3.30" @@ -1097,9 +1268,9 @@ dependencies = [ [[package]] name = "gimli" -version = "0.28.1" +version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4271d37baee1b8c7e4b708028c57d816cf9d2434acb33a549475f78c181f6253" +checksum = "40ecd4077b5ae9fd2e9e169b102c6c330d0605168eb0e8bf79952b256dbefffd" [[package]] name = "gloo-timers" @@ -1113,6 +1284,25 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "h2" +version = "0.3.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81fe527a889e1532da5c525686d96d4c2e74cdd345badf8dfef9f6b39dd5f5e8" +dependencies = [ + "bytes", + "fnv", + "futures-core", + "futures-sink", + "futures-util", + "http", + "indexmap 2.2.6", + "slab", + "tokio", + "tokio-util", + "tracing", +] + [[package]] name = "half" version = "2.4.1" @@ -1139,6 +1329,19 @@ dependencies = [ "allocator-api2", ] +[[package]] +name = "hdrhistogram" +version = "7.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "765c9198f173dd59ce26ff9f95ef0aafd0a0fe01fb9d72841bc5066a4c06511d" +dependencies = [ + "base64", + "byteorder", + "flate2", + "nom", + "num-traits", +] + [[package]] name = "heck" version = "0.4.1" @@ -1172,6 +1375,82 @@ version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" +[[package]] +name = "http" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "601cbb57e577e2f5ef5be8e7b83f0f63994f25aa94d673e54a92d5c516d101f1" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + +[[package]] +name = "http-body" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ceab25649e9960c0311ea418d17bee82c0dcec1bd053b5f9a66e265a693bed2" +dependencies = [ + "bytes", + "http", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0e7a4dd27b9476dc40cb050d3632d3bba3a70ddbff012285f7f8559a1e7e545" + +[[package]] +name = "httpdate" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" + +[[package]] +name = "humantime" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" + +[[package]] +name = "hyper" +version = "0.14.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f361cde2f109281a220d4307746cdfd5ee3f410da58a70377762396775634b33" +dependencies = [ + "bytes", + "futures-channel", + "futures-core", + "futures-util", + "h2", + "http", + "http-body", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "socket2 0.5.7", + "tokio", + "tower-service", + "tracing", + "want", +] + +[[package]] +name = "hyper-timeout" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbb958482e8c7be4bc3cf272a766a2b0bf1a6755e7a6ae777f017a31d11b13b1" +dependencies = [ + "hyper", + "pin-project-lite", + "tokio", + "tokio-io-timeout", +] + [[package]] name = "indenter" version = "0.3.3" @@ -1356,11 +1635,17 @@ dependencies = [ "regex-automata 0.1.10", ] +[[package]] +name = "matchit" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e7465ac9959cc2b1404e8e2367b43684a6d13790fe23056cc8c6c5a6b7bcb94" + [[package]] name = "memchr" -version = "2.7.2" +version = "2.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c8640c5d730cb13ebd907d8d04b52f55ac9a2eec55b440c8892f40d56c76c1d" +checksum = "6d0d8b92cd8358e8d229c11df9358decae64d137c5be540952c5ca7b25aea768" [[package]] name = "memoffset" @@ -1380,6 +1665,18 @@ dependencies = [ "autocfg", ] +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + [[package]] name = "miniz_oxide" version = "0.7.3" @@ -1426,6 +1723,16 @@ dependencies = [ "memoffset 0.9.1", ] +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + [[package]] name = "notify-rust" version = "4.11.0" @@ -1514,9 +1821,9 @@ dependencies = [ [[package]] name = "object" -version = "0.32.2" +version = "0.36.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a6a622008b6e321afc04970976f62ee297fdbaa6f95318ca343e3eebb9648441" +checksum = "576dfe1fc8f9df304abb159d767a29d0476f7750fbf8aa7ad07816004a207434" dependencies = [ "memchr", ] @@ -1530,23 +1837,32 @@ dependencies = [ "atspi-connection", "atspi-proxies", "circular-queue", - "clap 4.5.4", + "clap 4.5.7", + "console-subscriber", + "derived-deref", "eyre", "figment", "futures", + "futures-concurrency", + "futures-lite 2.3.0", "lazy_static", "odilia-cache", "odilia-common", "odilia-input", "odilia-notify", "odilia-tts", + "pin-project", + "refinement", "serde_json", "serde_plain", + "ssip", "ssip-client-async", + "static_assertions", "tokio", "tokio-test", "tokio-util", "toml", + "tower", "tracing", "tracing-error", "tracing-journald", @@ -1586,12 +1902,18 @@ version = "0.3.0" dependencies = [ "atspi", "atspi-common", + "atspi-proxies", "bitflags 1.3.2", + "enum_dispatch", "figment", "serde", "serde_plain", "smartstring", + "ssip", + "strum 0.26.2", "thiserror", + "tokio", + "tracing", "xdg", "zbus", ] @@ -1632,6 +1954,7 @@ name = "odilia-tts" version = "0.1.4" dependencies = [ "eyre", + "ssip", "ssip-client-async", "tokio", "tokio-util", @@ -1680,9 +2003,9 @@ checksum = "bb813b8af86854136c6922af0598d719255ecb2179515e6e7730d468f05c9cae" [[package]] name = "parking_lot" -version = "0.12.2" +version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e4af0ca4f6caed20e900d564c242b8e5d4903fdacf31d3daf527b66fe6f42fb" +checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" dependencies = [ "lock_api", "parking_lot_core", @@ -1724,6 +2047,32 @@ dependencies = [ "syn 2.0.66", ] +[[package]] +name = "percent-encoding" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" + +[[package]] +name = "pin-project" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6bf43b791c5b9e34c3d182969b4abb522f9343702850a2e57f460d00d09b4b3" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f38a4412a78282e09a2cf38d195ea5420d15ba0602cb375210efbc877243965" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.66", +] + [[package]] name = "pin-project-lite" version = "0.2.14" @@ -1738,9 +2087,9 @@ checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" [[package]] name = "piper" -version = "0.2.2" +version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "464db0c665917b13ebb5d453ccdec4add5658ee1adc7affc7677615356a8afaf" +checksum = "ae1d5c74c9876f070d3e8fd503d748c7d974c3e48da8f41350fa5222ef9b4391" dependencies = [ "atomic-waker", "fastrand 2.1.0", @@ -1793,9 +2142,9 @@ dependencies = [ [[package]] name = "polling" -version = "3.7.0" +version = "3.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "645493cf344456ef24219d02a768cf1fb92ddf8c92161679ae3d91b91a637be3" +checksum = "5e6a007746f34ed64099e88783b0ae369eaa3da6392868ba262e2af9b8fbaea1" dependencies = [ "cfg-if", "concurrent-queue", @@ -1829,9 +2178,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.83" +version = "1.0.85" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b33eb56c327dec362a9e55b3ad14f9d2f0904fb5a5b03b513ab5465399e9f43" +checksum = "22244ce15aa966053a896d1accb3a6e68469b97c7f33f284b99f0d576879fc23" dependencies = [ "unicode-ident", ] @@ -1849,6 +2198,38 @@ dependencies = [ "yansi", ] +[[package]] +name = "prost" +version = "0.12.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "deb1435c188b76130da55f17a466d252ff7b1418b2ad3e037d127b94e3411f29" +dependencies = [ + "bytes", + "prost-derive", +] + +[[package]] +name = "prost-derive" +version = "0.12.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81bddcdb20abf9501610992b6759a4c888aef7d1a7247ef75e2404275ac24af1" +dependencies = [ + "anyhow", + "itertools 0.12.1", + "proc-macro2", + "quote", + "syn 2.0.66", +] + +[[package]] +name = "prost-types" +version = "0.12.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9091c90b0a32608e984ff2fa4091273cbdd755d54935c51d520887f4a1dbd5b0" +dependencies = [ + "prost", +] + [[package]] name = "quick-xml" version = "0.30.0" @@ -1877,6 +2258,12 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "radium" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09" + [[package]] name = "rand" version = "0.8.5" @@ -1947,16 +2334,22 @@ dependencies = [ "thiserror", ] +[[package]] +name = "refinement" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41c1f648652916ca373423bde65c39208b194d83b0d25936b619b21b11958506" + [[package]] name = "regex" -version = "1.10.4" +version = "1.10.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c117dbdfde9c8308975b6a18d71f3f385c89461f7b3fb054288ecf2a2058ba4c" +checksum = "b91213439dad192326a0d7c6ee3955910425f441d7038e0d6933b0aec5c4517f" dependencies = [ "aho-corasick", "memchr", - "regex-automata 0.4.6", - "regex-syntax 0.8.3", + "regex-automata 0.4.7", + "regex-syntax 0.8.4", ] [[package]] @@ -1970,13 +2363,13 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.6" +version = "0.4.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "86b83b8b9847f9bf95ef68afb0b8e6cdb80f498442f5179a29fad448fcc1eaea" +checksum = "38caf58cc5ef2fed281f89292ef23f6365465ed9a41b7a7754eb4e26496c92df" dependencies = [ "aho-corasick", "memchr", - "regex-syntax 0.8.3", + "regex-syntax 0.8.4", ] [[package]] @@ -1987,9 +2380,9 @@ checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" [[package]] name = "regex-syntax" -version = "0.8.3" +version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "adad44e29e4c806119491a7f06f03de4d1af22c3a680dd47f1e6e179439d1f56" +checksum = "7a66a03ae7c801facd77a29370b4faec201768915ac14a721ba36f20bc9c209b" [[package]] name = "rustc-demangle" @@ -2053,18 +2446,18 @@ checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" [[package]] name = "serde" -version = "1.0.202" +version = "1.0.203" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "226b61a0d411b2ba5ff6d7f73a476ac4f8bb900373459cd00fab8512828ba395" +checksum = "7253ab4de971e72fb7be983802300c30b5a7f0c2e56fab8abfc6a214307c0094" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.202" +version = "1.0.203" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6048858004bcff69094cd972ed40a32500f153bd3be9f716b2eed2e8217c4838" +checksum = "500cbc0ebeb6f46627f50f3f5811ccf6bf00643be300b4c3eabc0ef55dc5b5ba" dependencies = [ "proc-macro2", "quote", @@ -2188,30 +2581,29 @@ dependencies = [ ] [[package]] -name = "ssip-client-async" -version = "0.12.0" +name = "ssip" +version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d20c8da60d51225982d675776f18b55c6bd41763b01c03f431190e0ad99c9dbf" +checksum = "5fc609dd23978fbb3684835bbf22d075c6ba36bcada9f0e6884a94b0842d2593" dependencies = [ - "async-std", - "dirs", - "log", - "ssip-common", - "strum", - "strum_macros", + "strum_macros 0.24.3", "thiserror", - "tokio", ] [[package]] -name = "ssip-common" -version = "0.1.0" +name = "ssip-client-async" +version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7193e9edf18417738259bf590224732d68c1eac3a486dd9ce101b8f9a443e7e4" +checksum = "bffa78d7a908c0617c2a18c9716efc75c6ee93946b01e684deb45c0cbc59f5fe" dependencies = [ - "strum", - "strum_macros", + "async-std", + "dirs", + "log", + "ssip", + "strum 0.24.1", + "strum_macros 0.24.3", "thiserror", + "tokio", ] [[package]] @@ -2232,6 +2624,15 @@ version = "0.24.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "063e6045c0e62079840579a7e47a355ae92f60eb74daaf156fb1e84ba164e63f" +[[package]] +name = "strum" +version = "0.26.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d8cec3501a5194c432b2b7976db6b7d10ec95c253208b45f83f7136aa985e29" +dependencies = [ + "strum_macros 0.26.4", +] + [[package]] name = "strum_macros" version = "0.24.3" @@ -2245,6 +2646,19 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "strum_macros" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c6bee85a5a24955dc440386795aa378cd9cf82acd5f764469152d2270e581be" +dependencies = [ + "heck 0.5.0", + "proc-macro2", + "quote", + "rustversion", + "syn 2.0.66", +] + [[package]] name = "syn" version = "1.0.109" @@ -2267,6 +2681,12 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "sync_wrapper" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160" + [[package]] name = "sysinfo" version = "0.26.9" @@ -2281,6 +2701,12 @@ dependencies = [ "winapi", ] +[[package]] +name = "tap" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" + [[package]] name = "tauri-winrt-notification" version = "0.2.1" @@ -2371,9 +2797,9 @@ dependencies = [ [[package]] name = "tokio" -version = "1.37.0" +version = "1.38.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1adbebffeca75fcfd058afa480fb6c0b81e165a0323f9c9d39c9697e37c46787" +checksum = "ba4f4a02a7a80d6f274636f0aa95c7e383b912d41fe721a31f29e29698585a4a" dependencies = [ "backtrace", "bytes", @@ -2388,11 +2814,21 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "tokio-io-timeout" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30b74022ada614a1b4834de765f9bb43877f910cc8ce4be40e89042c9223a8bf" +dependencies = [ + "pin-project-lite", + "tokio", +] + [[package]] name = "tokio-macros" -version = "2.2.0" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b8a1e28f2deaa14e508979454cb3a223b10b938b45af148bc0986de36f1923b" +checksum = "5f5ae998a069d4b5aba8ee9dad856af7d520c3699e6159b185c2acd48155d39a" dependencies = [ "proc-macro2", "quote", @@ -2440,14 +2876,14 @@ dependencies = [ [[package]] name = "toml" -version = "0.8.13" +version = "0.8.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4e43f8cc456c9704c851ae29c67e17ef65d2c30017c17a9765b89c382dc8bba" +checksum = "6f49eb2ab21d2f26bd6db7bf383edc527a7ebaee412d17af4d40fdccd442f335" dependencies = [ "serde", "serde_spanned", "toml_datetime", - "toml_edit 0.22.13", + "toml_edit 0.22.14", ] [[package]] @@ -2472,23 +2908,83 @@ dependencies = [ [[package]] name = "toml_edit" -version = "0.22.13" +version = "0.22.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c127785850e8c20836d49732ae6abfa47616e60bf9d9f57c43c250361a9db96c" +checksum = "f21c7aaf97f1bd9ca9d4f9e73b0a6c74bd5afef56f2bc931943a6e1c37e04e38" dependencies = [ "indexmap 2.2.6", "serde", "serde_spanned", "toml_datetime", - "winnow 0.6.8", + "winnow 0.6.13", ] +[[package]] +name = "tonic" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76c4eb7a4e9ef9d4763600161f12f5070b92a578e1b634db88a6887844c91a13" +dependencies = [ + "async-stream", + "async-trait", + "axum", + "base64", + "bytes", + "h2", + "http", + "http-body", + "hyper", + "hyper-timeout", + "percent-encoding", + "pin-project", + "prost", + "tokio", + "tokio-stream", + "tower", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tower" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8fa9be0de6cf49e536ce1851f987bd21a43b771b09473c3549a6c853db37c1c" +dependencies = [ + "futures-core", + "futures-util", + "indexmap 1.9.3", + "pin-project", + "pin-project-lite", + "rand", + "slab", + "tokio", + "tokio-util", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tower-layer" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c20c8dbed6283a09604c3e69b4b7eeb54e298b8a600d4d5ecb5ad39de609f1d0" + +[[package]] +name = "tower-service" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6bc1c9ce2b5135ac7f93c72918fc37feb872bdc6a5533a8b85eb4b86bfdae52" + [[package]] name = "tracing" version = "0.1.40" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef" dependencies = [ + "log", "pin-project-lite", "tracing-attributes", "tracing-core", @@ -2589,6 +3085,12 @@ dependencies = [ "tracing-subscriber", ] +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + [[package]] name = "typenum" version = "1.17.0" @@ -2623,9 +3125,9 @@ checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" [[package]] name = "utf8parse" -version = "0.2.1" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "valuable" @@ -2661,6 +3163,15 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + [[package]] name = "wasi" version = "0.11.0+wasi-snapshot-preview1" @@ -2820,9 +3331,9 @@ dependencies = [ [[package]] name = "windows-result" -version = "0.1.1" +version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "749f0da9cc72d82e600d8d2e44cadd0b9eedb9038f71a1c58556ac1c5791813b" +checksum = "5e383302e8ec8515204254685643de10811af0ed97ea37210dc26fb0032647f8" dependencies = [ "windows-targets 0.52.5", ] @@ -2986,13 +3497,22 @@ dependencies = [ [[package]] name = "winnow" -version = "0.6.8" +version = "0.6.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3c52e9c97a68071b23e836c9380edae937f17b9c4667bd021973efc689f618d" +checksum = "59b5e5f6c299a3c7890b876a2a587f3115162487e704907d9b6cd29473052ba1" dependencies = [ "memchr", ] +[[package]] +name = "wyz" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f360fc0b24296329c78fda852a1e9ae82de9cf7b27dae4b7f62f118f77b9ed" +dependencies = [ + "tap", +] + [[package]] name = "xdg" version = "2.5.2" @@ -3001,12 +3521,12 @@ checksum = "213b7324336b53d2414b2db8537e56544d981803139155afa84f76eeebb7a546" [[package]] name = "xdg-home" -version = "1.1.0" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21e5a325c3cb8398ad6cf859c1135b25dd29e186679cf2da7581d9679f63b38e" +checksum = "ca91dcf8f93db085f3a0a29358cd0b9d670915468f4290e8b85d118a34211ab8" dependencies = [ "libc", - "winapi", + "windows-sys 0.52.0", ] [[package]] @@ -3017,22 +3537,22 @@ checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049" [[package]] name = "zbus" -version = "4.2.2" +version = "4.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "989c3977a7aafa97b12b9a35d21cdcff9b0d2289762b14683f45d66b1ba6c48f" +checksum = "23915fcb26e7a9a9dc05fd93a9870d336d6d032cd7e8cebf1c5c37666489fdd5" dependencies = [ "async-broadcast", "async-executor", "async-fs", - "async-io 2.3.2", - "async-lock 3.3.0", + "async-io 2.3.3", + "async-lock 3.4.0", "async-process", "async-recursion", "async-task", "async-trait", "blocking", "enumflags2", - "event-listener 5.3.0", + "event-listener 5.3.1", "futures-core", "futures-sink", "futures-util", @@ -3080,9 +3600,9 @@ dependencies = [ [[package]] name = "zbus_macros" -version = "4.2.2" +version = "4.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6fe9de53245dcf426b7be226a4217dd5e339080e5d46e64a02d6e5dcbf90fca1" +checksum = "02bcca0b586d2f8589da32347b4784ba424c4891ed86aa5b50d5e88f6b2c4f5d" dependencies = [ "proc-macro-crate", "proc-macro2", diff --git a/Cargo.toml b/Cargo.toml index cfedceae..71d50ffc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -31,11 +31,12 @@ pre-release-hook = ["cargo", "fmt"] dependent-version = "upgrade" [workspace.dependencies] -atspi = { version = "0.22.0", default-features = false, features = ["tokio"] } -atspi-proxies = { version = "0.6.0", default-features = false, features = ["tokio"] } -atspi-common = { version = "0.6.0", default-features = false, features = ["tokio"] } -atspi-connection = { version = "0.6.0", default-features = false, features = ["tokio"] } -odilia-common = { version = "0.3.0", path = "./common" } +atspi = { git = "https://github.com/odilia-app/atspi/", branch = "main", default-features = false, features = ["tokio"] } +atspi-proxies = { git = "https://github.com/odilia-app/atspi/", branch = "main", default-features = false, features = ["tokio"] } +atspi-common = { git = "https://github.com/odilia-app/atspi/", branch = "main", default-features = false, features = ["tokio"] } +atspi-connection = { git = "https://github.com/odilia-app/atspi/", branch = "main", default-features = false, features = ["tokio"] } +futures-concurrency = { version = "7.6.1" } +odilia-common = { version = "0.3.0", path = "./common", features = ["tokio"] } odilia-cache = { version = "0.3.0", path = "./cache" } eyre = "0.6.8" nix = "0.26.2" @@ -49,7 +50,7 @@ tracing-log = "^0.1.3" tracing-subscriber = { version = "0.3.16", default-features = false, features = ["env-filter", "parking_lot"] } tracing-error = "^0.2.0" tracing-tree = "^0.2.2" -zbus = { version = "4.2", features = ["tokio"] } +zbus = { version = "4.3", features = ["tokio"] } serde_plain = "1.0.1" xdg = "2.5.2" diff --git a/cache/Cargo.toml b/cache/Cargo.toml index 9c70f14f..705e3f25 100644 --- a/cache/Cargo.toml +++ b/cache/Cargo.toml @@ -16,7 +16,7 @@ atspi.workspace = true atspi-proxies.workspace = true atspi-common.workspace = true odilia-common.workspace = true -dashmap = "5.4.0" +dashmap = { version = "6.0.1", features = ["inline"] } serde = "1.0.147" tokio.workspace = true tracing.workspace = true diff --git a/cache/benches/load_test.rs b/cache/benches/load_test.rs index 41a0f966..6c4762e4 100644 --- a/cache/benches/load_test.rs +++ b/cache/benches/load_test.rs @@ -5,11 +5,13 @@ use std::{ }; use atspi_connection::AccessibilityConnection; -use atspi_proxies::accessible::Accessible; use criterion::{criterion_group, criterion_main, BatchSize, BenchmarkId, Criterion}; -use odilia_cache::{AccessiblePrimitive, Cache, CacheItem}; +use odilia_cache::{Cache, CacheItem}; -use odilia_common::errors::{CacheError, OdiliaError}; +use odilia_common::{ + cache::AccessiblePrimitive, + errors::{CacheError, OdiliaError}, +}; use tokio::select; use tokio_test::block_on; @@ -59,7 +61,7 @@ async fn traverse_up(children: Vec) { for child in children { let mut item = child.clone(); loop { - item = match item.parent().await { + item = match item.parent() { Ok(item) => item, Err(OdiliaError::Cache(CacheError::NoItem)) => { // Missing item from cache; there's always exactly one. @@ -121,7 +123,7 @@ async fn reads_while_writing(cache: Cache, ids: Vec, items: fn cache_benchmark(c: &mut Criterion) { let rt = tokio::runtime::Runtime::new().unwrap(); - let a11y = block_on(AccessibilityConnection::open()).unwrap(); + let a11y = block_on(AccessibilityConnection::new()).unwrap(); let zbus_connection = a11y.connection(); let zbus_items: Vec = load_items!("./zbus_docs_cache_items.json"); diff --git a/cache/src/lib.rs b/cache/src/lib.rs index 7ec62a4d..f6f84d8a 100644 --- a/cache/src/lib.rs +++ b/cache/src/lib.rs @@ -18,26 +18,25 @@ pub use accessible_ext::AccessibleExt; use std::{ collections::HashMap, + fmt::Debug, + ops::Deref, sync::{Arc, RwLock, Weak}, }; use atspi_common::{ - object_ref::ObjectRef, ClipType, CoordType, EventProperties, Granularity, InterfaceSet, - RelationType, Role, StateSet, + ClipType, CoordType, EventProperties, Granularity, InterfaceSet, RelationType, Role, + StateSet, }; use atspi_proxies::{accessible::AccessibleProxy, text::TextProxy}; use dashmap::DashMap; use fxhash::FxBuildHasher; use odilia_common::{ - errors::{AccessiblePrimitiveConversionError, CacheError, OdiliaError}, + cache::AccessiblePrimitive, + errors::{CacheError, OdiliaError}, result::OdiliaResult, }; use serde::{Deserialize, Serialize}; -use zbus::{ - names::OwnedUniqueName, - zvariant::{ObjectPath, OwnedObjectPath}, - CacheProperties, ProxyBuilder, -}; +use zbus::CacheProperties; trait AllText { async fn get_all_text(&self) -> Result; @@ -53,121 +52,6 @@ type CacheKey = AccessiblePrimitive; type InnerCache = DashMap>, FxBuildHasher>; type ThreadSafeCache = Arc; -#[derive(Clone, Debug, PartialEq, Eq, Hash, Deserialize, Serialize)] -/// A struct which represents the bare minimum of an accessible for purposes of caching. -/// This makes some *possibly eronious* assumptions about what the sender is. -pub struct AccessiblePrimitive { - /// The accessible ID, which is an arbitrary string specified by the application. - /// It is guaranteed to be unique per application. - /// Examples: - /// * /org/a11y/atspi/accessible/1234 - /// * /org/a11y/atspi/accessible/null - /// * /org/a11y/atspi/accessible/root - /// * /org/Gnome/GTK/abab22-bbbb33-2bba2 - pub id: String, - /// Assuming that the sender is ":x.y", this stores the (x,y) portion of this sender. - /// Examples: - /// * :1.1 (the first window has opened) - /// * :2.5 (a second session exists, where at least 5 applications have been lauinched) - /// * :1.262 (many applications have been started on this bus) - pub sender: smartstring::alias::String, -} -impl AccessiblePrimitive { - /// Convert into an [`atspi_proxies::accessible::AccessibleProxy`]. Must be async because the creation of an async proxy requires async itself. - /// # Errors - /// Will return a [`zbus::Error`] in the case of an invalid destination, path, or failure to create a `Proxy` from those properties. - #[tracing::instrument(skip_all, level = "trace", ret, err)] - pub async fn into_accessible<'a>( - self, - conn: &zbus::Connection, - ) -> zbus::Result> { - let id = self.id; - let sender = self.sender.clone(); - let path: ObjectPath<'a> = id.try_into()?; - ProxyBuilder::new(conn) - .path(path)? - .destination(sender.as_str().to_owned())? - .cache_properties(CacheProperties::No) - .build() - .await - } - /// Convert into an [`atspi_proxies::text::TextProxy`]. Must be async because the creation of an async proxy requires async itself. - /// # Errors - /// Will return a [`zbus::Error`] in the case of an invalid destination, path, or failure to create a `Proxy` from those properties. - #[tracing::instrument(skip_all, level = "trace", ret, err)] - pub async fn into_text<'a>(self, conn: &zbus::Connection) -> zbus::Result> { - let id = self.id; - let sender = self.sender.clone(); - let path: ObjectPath<'a> = id.try_into()?; - ProxyBuilder::new(conn) - .path(path)? - .destination(sender.as_str().to_owned())? - .cache_properties(CacheProperties::No) - .build() - .await - } - /// Turns any `atspi::event` type into an `AccessiblePrimitive`, the basic type which is used for keys in the cache. - /// # Errors - /// The errors are self-explanitory variants of the [`odilia_common::errors::AccessiblePrimitiveConversionError`]. - #[tracing::instrument(skip_all, level = "trace", ret, err)] - pub fn from_event( - event: &T, - ) -> Result { - let sender = event.sender(); - let path = event.path(); - let id = path.to_string(); - Ok(Self { id, sender: sender.as_str().into() }) - } -} -impl From for AccessiblePrimitive { - fn from(atspi_accessible: ObjectRef) -> AccessiblePrimitive { - let tuple_converter = (atspi_accessible.name, atspi_accessible.path); - tuple_converter.into() - } -} - -impl From<(OwnedUniqueName, OwnedObjectPath)> for AccessiblePrimitive { - fn from(so: (OwnedUniqueName, OwnedObjectPath)) -> AccessiblePrimitive { - let accessible_id = so.1; - AccessiblePrimitive { id: accessible_id.to_string(), sender: so.0.as_str().into() } - } -} -impl From<(String, OwnedObjectPath)> for AccessiblePrimitive { - #[tracing::instrument(level = "trace", ret)] - fn from(so: (String, OwnedObjectPath)) -> AccessiblePrimitive { - let accessible_id = so.1; - AccessiblePrimitive { id: accessible_id.to_string(), sender: so.0.into() } - } -} -impl<'a> From<(String, ObjectPath<'a>)> for AccessiblePrimitive { - #[tracing::instrument(level = "trace", ret)] - fn from(so: (String, ObjectPath<'a>)) -> AccessiblePrimitive { - AccessiblePrimitive { id: so.1.to_string(), sender: so.0.into() } - } -} -impl<'a> TryFrom<&AccessibleProxy<'a>> for AccessiblePrimitive { - type Error = AccessiblePrimitiveConversionError; - - #[tracing::instrument(level = "trace", ret, err)] - fn try_from(accessible: &AccessibleProxy<'_>) -> Result { - let accessible = accessible.inner(); - let sender = accessible.destination().as_str().into(); - let id = accessible.path().as_str().into(); - Ok(AccessiblePrimitive { id, sender }) - } -} -impl<'a> TryFrom> for AccessiblePrimitive { - type Error = AccessiblePrimitiveConversionError; - - #[tracing::instrument(level = "trace", ret, err)] - fn try_from(accessible: AccessibleProxy<'_>) -> Result { - let accessible = accessible.inner(); - let sender = accessible.destination().as_str().into(); - let id = accessible.path().as_str().into(); - Ok(AccessiblePrimitive { id, sender }) - } -} - #[derive(Clone, Debug, Deserialize, Serialize)] /// A struct representing an accessible. To get any information from the cache other than the stored information like role, interfaces, and states, you will need to instantiate an [`atspi_proxies::accessible::AccessibleProxy`] or other `*Proxy` type from atspi to query further info. pub struct CacheItem { @@ -223,10 +107,10 @@ impl CacheItem { #[tracing::instrument(level = "trace", skip_all, ret, err)] pub async fn from_atspi_event( event: &T, - cache: Weak, + cache: Arc, connection: &zbus::Connection, ) -> OdiliaResult { - let a11y_prim = AccessiblePrimitive::from_event(event)?; + let a11y_prim = AccessiblePrimitive::from_event(event); accessible_to_cache_item(&a11y_prim.into_accessible(connection).await?, cache).await } /// Convert an [`atspi::CacheItem`] into a [`crate::CacheItem`]. @@ -431,28 +315,17 @@ impl CacheItem { &self, ) -> Result)>, OdiliaError> { let cache = strong_cache(&self.cache)?; - as_accessible(self) - .await? - .get_relation_set() - .await? - .into_iter() - .map(|(relation, object_pairs)| { - ( - relation, - object_pairs - .into_iter() - .map(|object_pair| { - cache.get(&object_pair.into()).ok_or( - OdiliaError::Cache( - CacheError::NoItem, - ), - ) - }) - .collect::, OdiliaError>>(), - ) - }) - .map(|(relation, result_selfs)| Ok((relation, result_selfs?))) - .collect::)>, OdiliaError>>() + let ipc_rs = as_accessible(self).await?.get_relation_set().await?; + let mut relations = Vec::new(); + for (relation, object_pairs) in ipc_rs { + let mut cache_keys = Vec::new(); + for object_pair in object_pairs { + let cached = cache.get_ipc(&object_pair.into()).await?; + cache_keys.push(cached); + } + relations.push((relation, cache_keys)); + } + Ok(relations) } /// See [`atspi_proxies::accessible::AccessibleProxy::get_child_at_index`] /// # Errors @@ -722,19 +595,25 @@ impl CacheItem { /// This contains (mostly) all accessibles in the entire accessibility tree, and /// they are referenced by their IDs. If you are having issues with incorrect or /// invalid accessibles trying to be accessed, this is code is probably the issue. -#[derive(Clone, Debug)] +#[derive(Clone)] pub struct Cache { pub by_id: ThreadSafeCache, pub connection: zbus::Connection, } +impl std::fmt::Debug for Cache { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str(&format!("Cache {{ by_id: ...{} items..., .. }}", self.by_id.len())) + } +} + // N.B.: we are using std RwLockes internally here, within the cache hashmap // entries. When adding async methods, take care not to hold these mutexes // across .await points. impl Cache { /// create a new, fresh cache #[must_use] - #[tracing::instrument(level = "debug", ret)] + #[tracing::instrument(level = "debug", ret, skip_all)] pub fn new(conn: zbus::Connection) -> Self { Self { by_id: Arc::new(DashMap::with_capacity_and_hasher( @@ -793,6 +672,18 @@ impl Cache { Some(self.by_id.get(id).as_deref()?.read().ok()?.clone()) } + /// Get a single item from the cache. This will also get the information from DBus if it does not + /// exist in the cache. + #[must_use] + #[tracing::instrument(level = "trace", ret)] + pub async fn get_ipc(&self, id: &CacheKey) -> Result { + if let Some(ci) = self.get(id) { + return Ok(ci); + } + let acc = id.clone().into_accessible(&self.connection).await?; + accessible_to_cache_item(&acc, self).await + } + /// get a many items from the cache; this only creates one read handle (note that this will copy all data you would like to access) #[must_use] #[tracing::instrument(level = "trace", ret)] @@ -865,7 +756,7 @@ impl Cache { pub async fn get_or_create( &self, accessible: &AccessibleProxy<'_>, - cache: Weak, + cache: Arc, ) -> OdiliaResult { // if the item already exists in the cache, return it let primitive = accessible.try_into()?; @@ -925,6 +816,11 @@ impl Cache { } Ok(()) } + pub async fn from_event(&self, ev: &T) -> OdiliaResult { + let a11y_prim = AccessiblePrimitive::from_event(ev); + accessible_to_cache_item(&a11y_prim.into_accessible(&self.connection).await?, self) + .await + } } /// Convert an [`atspi_proxies::accessible::AccessibleProxy`] into a [`crate::CacheItem`]. @@ -939,9 +835,9 @@ impl Cache { /// 2. Any of the function calls on the `accessible` fail. /// 3. Any `(String, OwnedObjectPath) -> AccessiblePrimitive` conversions fail. This *should* never happen, but technically it is possible. #[tracing::instrument(level = "trace", ret, err)] -pub async fn accessible_to_cache_item( +pub async fn accessible_to_cache_item + Debug>( accessible: &AccessibleProxy<'_>, - cache: Weak, + cache: C, ) -> OdiliaResult { let (app, parent, index, children_num, interfaces, role, states, children) = tokio::try_join!( accessible.get_application(), @@ -971,6 +867,6 @@ pub async fn accessible_to_cache_item( states, text, children: children.into_iter().map(|k| CacheRef::new(k.into())).collect(), - cache, + cache: Arc::downgrade(&Arc::new(cache.deref().clone())), }) } diff --git a/common/Cargo.toml b/common/Cargo.toml index f234421f..6cc82321 100644 --- a/common/Cargo.toml +++ b/common/Cargo.toml @@ -11,9 +11,15 @@ keywords = ["screen-reader", "accessibility", "a11y", "data-structures", "linux" categories = ["accessibility"] edition = "2021" +[features] +default = [] +tokio = ["dep:tokio"] +tracing = ["dep:tracing"] + [dependencies] atspi.workspace = true atspi-common.workspace = true +atspi-proxies.workspace = true bitflags = "1.3.2" serde = "1.0.147" smartstring = "1.0.1" @@ -21,4 +27,9 @@ thiserror = "1.0.37" zbus.workspace = true serde_plain.workspace = true figment = "0.10.15" -xdg.workspace=true +enum_dispatch = "0.3.13" +strum = { version = "0.26.2", features = ["derive"] } +tokio = { workspace = true, optional = true } +tracing = { workspace = true, optional = true } +ssip = "0.1.0" +xdg.workspace = true diff --git a/common/src/cache.rs b/common/src/cache.rs new file mode 100644 index 00000000..2f7f3d85 --- /dev/null +++ b/common/src/cache.rs @@ -0,0 +1,119 @@ +use crate::{errors::AccessiblePrimitiveConversionError, ObjectPath}; +use atspi::{EventProperties, ObjectRef}; +use atspi_proxies::{accessible::AccessibleProxy, text::TextProxy}; +use serde::{Deserialize, Serialize}; +use zbus::{names::OwnedUniqueName, zvariant::OwnedObjectPath, CacheProperties, ProxyBuilder}; + +#[derive(Clone, Debug, PartialEq, Eq, Hash, Deserialize, Serialize)] +/// A struct which represents the bare minimum of an accessible for purposes of caching. +/// This makes some *possibly eronious* assumptions about what the sender is. +pub struct AccessiblePrimitive { + /// The accessible ID, which is an arbitrary string specified by the application. + /// It is guaranteed to be unique per application. + /// Examples: + /// * /org/a11y/atspi/accessible/1234 + /// * /org/a11y/atspi/accessible/null + /// * /org/a11y/atspi/accessible/root + /// * /org/Gnome/GTK/abab22-bbbb33-2bba2 + pub id: String, + /// Assuming that the sender is ":x.y", this stores the (x,y) portion of this sender. + /// Examples: + /// * :1.1 (the first window has opened) + /// * :2.5 (a second session exists, where at least 5 applications have been lauinched) + /// * :1.262 (many applications have been started on this bus) + pub sender: smartstring::alias::String, +} + +impl AccessiblePrimitive { + /// Turns any `atspi::event` type into an `AccessiblePrimitive`, the basic type which is used for keys in the cache. + #[cfg_attr(feature = "tracing", tracing::instrument(skip_all, level = "trace", ret, err))] + pub fn from_event(event: &T) -> Self { + let sender = event.sender(); + let path = event.path(); + let id = path.to_string(); + Self { id, sender: sender.as_str().into() } + } + + /// Convert into an [`atspi_proxies::accessible::AccessibleProxy`]. Must be async because the creation of an async proxy requires async itself. + /// # Errors + /// Will return a [`zbus::Error`] in the case of an invalid destination, path, or failure to create a `Proxy` from those properties. + #[cfg_attr(feature = "tracing", tracing::instrument(skip_all, level = "trace", ret, err))] + pub async fn into_accessible<'a>( + self, + conn: &zbus::Connection, + ) -> zbus::Result> { + let id = self.id; + let sender = self.sender.clone(); + let path: ObjectPath<'a> = id.try_into()?; + ProxyBuilder::new(conn) + .path(path)? + .destination(sender.as_str().to_owned())? + .cache_properties(CacheProperties::No) + .build() + .await + } + /// Convert into an [`atspi_proxies::text::TextProxy`]. Must be async because the creation of an async proxy requires async itself. + /// # Errors + /// Will return a [`zbus::Error`] in the case of an invalid destination, path, or failure to create a `Proxy` from those properties. + #[cfg_attr(feature = "tracing", tracing::instrument(skip_all, level = "trace", ret, err))] + pub async fn into_text<'a>(self, conn: &zbus::Connection) -> zbus::Result> { + let id = self.id; + let sender = self.sender.clone(); + let path: ObjectPath<'a> = id.try_into()?; + ProxyBuilder::new(conn) + .path(path)? + .destination(sender.as_str().to_owned())? + .cache_properties(CacheProperties::No) + .build() + .await + } +} + +impl From for AccessiblePrimitive { + fn from(atspi_accessible: ObjectRef) -> AccessiblePrimitive { + let tuple_converter = (atspi_accessible.name, atspi_accessible.path); + tuple_converter.into() + } +} + +impl From<(OwnedUniqueName, OwnedObjectPath)> for AccessiblePrimitive { + fn from(so: (OwnedUniqueName, OwnedObjectPath)) -> AccessiblePrimitive { + let accessible_id = so.1; + AccessiblePrimitive { id: accessible_id.to_string(), sender: so.0.as_str().into() } + } +} +impl From<(String, OwnedObjectPath)> for AccessiblePrimitive { + #[cfg_attr(feature = "tracing", tracing::instrument(skip_all, level = "trace", ret))] + fn from(so: (String, OwnedObjectPath)) -> AccessiblePrimitive { + let accessible_id = so.1; + AccessiblePrimitive { id: accessible_id.to_string(), sender: so.0.into() } + } +} +impl<'a> From<(String, ObjectPath<'a>)> for AccessiblePrimitive { + #[cfg_attr(feature = "tracing", tracing::instrument(skip_all, level = "trace", ret))] + fn from(so: (String, ObjectPath<'a>)) -> AccessiblePrimitive { + AccessiblePrimitive { id: so.1.to_string(), sender: so.0.into() } + } +} +impl<'a> TryFrom<&AccessibleProxy<'a>> for AccessiblePrimitive { + type Error = AccessiblePrimitiveConversionError; + + #[cfg_attr(feature = "tracing", tracing::instrument(skip_all, level = "trace", ret, err))] + fn try_from(accessible: &AccessibleProxy<'_>) -> Result { + let accessible = accessible.inner(); + let sender = accessible.destination().as_str().into(); + let id = accessible.path().as_str().into(); + Ok(AccessiblePrimitive { id, sender }) + } +} +impl<'a> TryFrom> for AccessiblePrimitive { + type Error = AccessiblePrimitiveConversionError; + + #[cfg_attr(feature = "tracing", tracing::instrument(skip_all, level = "trace", ret, err))] + fn try_from(accessible: AccessibleProxy<'_>) -> Result { + let accessible = accessible.inner(); + let sender = accessible.destination().as_str().into(); + let id = accessible.path().as_str().into(); + Ok(AccessiblePrimitive { id, sender }) + } +} diff --git a/common/src/command.rs b/common/src/command.rs new file mode 100644 index 00000000..8b8ece51 --- /dev/null +++ b/common/src/command.rs @@ -0,0 +1,160 @@ +#![allow(clippy::module_name_repetitions)] + +use crate::cache::AccessiblePrimitive; +use crate::errors::OdiliaError; +use enum_dispatch::enum_dispatch; +use ssip::Priority; +use std::convert::Infallible; + +use strum::{Display, EnumDiscriminants}; + +pub trait TryIntoCommands { + type Error: Into; + /// Fallibly returns a [`Vec`] of [`OdiliaCommand`]s to run. + /// + /// # Errors + /// + /// When implemented, the function is allowed to fail with any type that can be converted into + /// [`OdiliaError`], but conversion should between these types should be done from the + /// implementers' side, liekly using `?`. + fn try_into_commands(self) -> Result, OdiliaError>; +} +impl TryIntoCommands for Result, OdiliaError> { + type Error = OdiliaError; + fn try_into_commands(self) -> Result, OdiliaError> { + self + } +} +impl TryIntoCommands for T { + type Error = Infallible; + fn try_into_commands(self) -> Result, OdiliaError> { + Ok(self.into_commands()) + } +} +impl> TryIntoCommands for Result { + type Error = E; + fn try_into_commands(self) -> Result, OdiliaError> { + match self { + Ok(ok) => Ok(ok.into_commands()), + Err(err) => Err(err.into()), + } + } +} + +pub trait IntoCommands { + fn into_commands(self) -> Vec; +} + +impl IntoCommands for CaretPos { + fn into_commands(self) -> Vec { + vec![self.into()] + } +} +impl IntoCommands for Focus { + fn into_commands(self) -> Vec { + vec![self.into()] + } +} +impl IntoCommands for (Priority, &str) { + fn into_commands(self) -> Vec { + vec![Speak(self.1.to_string(), self.0).into()] + } +} +impl IntoCommands for (Priority, String) { + fn into_commands(self) -> Vec { + vec![Speak(self.1, self.0).into()] + } +} +impl IntoCommands for () { + fn into_commands(self) -> Vec { + vec![] + } +} +impl IntoCommands for (T1,) +where + T1: IntoCommands, +{ + fn into_commands(self) -> Vec { + self.0.into_commands() + } +} +impl IntoCommands for (T1, T2) +where + T1: IntoCommands, + T2: IntoCommands, +{ + fn into_commands(self) -> Vec { + let mut ret = self.0.into_commands(); + ret.extend(self.1.into_commands()); + ret + } +} +impl IntoCommands for (T1, T2, T3) +where + T1: IntoCommands, + T2: IntoCommands, + T3: IntoCommands, +{ + fn into_commands(self) -> Vec { + let mut ret = self.0.into_commands(); + ret.extend(self.1.into_commands()); + ret.extend(self.2.into_commands()); + ret + } +} +impl IntoCommands for (T1, T2, T3, T4) +where + T1: IntoCommands, + T2: IntoCommands, + T3: IntoCommands, + T4: IntoCommands, +{ + fn into_commands(self) -> Vec { + let mut ret = self.0.into_commands(); + ret.extend(self.1.into_commands()); + ret.extend(self.2.into_commands()); + ret.extend(self.3.into_commands()); + ret + } +} + +pub trait CommandType { + const CTYPE: OdiliaCommandDiscriminants; +} +#[enum_dispatch] +pub trait CommandTypeDynamic { + fn ctype(&self) -> OdiliaCommandDiscriminants; +} +impl CommandTypeDynamic for T { + fn ctype(&self) -> OdiliaCommandDiscriminants { + T::CTYPE + } +} + +#[derive(Debug, Clone)] +pub struct CaretPos(pub usize); + +#[derive(Debug, Clone)] +pub struct Speak(pub String, pub Priority); + +#[derive(Debug, Clone)] +pub struct Focus(pub AccessiblePrimitive); + +impl CommandType for Speak { + const CTYPE: OdiliaCommandDiscriminants = OdiliaCommandDiscriminants::Speak; +} +impl CommandType for Focus { + const CTYPE: OdiliaCommandDiscriminants = OdiliaCommandDiscriminants::Focus; +} +impl CommandType for CaretPos { + const CTYPE: OdiliaCommandDiscriminants = OdiliaCommandDiscriminants::CaretPos; +} + +#[derive(Debug, Clone, EnumDiscriminants)] +#[strum_discriminants(derive(Ord, PartialOrd, Display))] +#[enum_dispatch(CommandTypeDynamic)] +pub enum OdiliaCommand { + Speak(Speak), + Focus(Focus), + CaretPos(CaretPos), +} diff --git a/common/src/errors.rs b/common/src/errors.rs index 271ecbba..8a7dd4fe 100644 --- a/common/src/errors.rs +++ b/common/src/errors.rs @@ -1,3 +1,4 @@ +use crate::command::OdiliaCommand; use atspi::AtspiError; use atspi_common::AtspiError as AtspiTypesError; use serde_plain::Error as SerdePlainError; @@ -14,13 +15,49 @@ pub enum OdiliaError { Zbus(zbus::Error), ZbusFdo(zbus::fdo::Error), Zvariant(zbus::zvariant::Error), + SendError(SendError), Cache(CacheError), InfallibleConversion(std::convert::Infallible), ConversionError(std::num::TryFromIntError), Config(ConfigError), PoisoningError, Generic(String), + Static(&'static str), + ServiceNotFound(String), + PredicateFailure(String), } + +impl From<&'static str> for OdiliaError { + fn from(s: &'static str) -> OdiliaError { + Self::Static(s) + } +} + +#[derive(Debug)] +pub enum SendError { + Atspi(atspi::Event), + Command(OdiliaCommand), + Ssip(ssip::Request), +} + +macro_rules! send_err_impl { + ($tokio_err:ty, $variant:path) => { + #[cfg(feature = "tokio")] + impl From<$tokio_err> for OdiliaError { + fn from(t_err: $tokio_err) -> OdiliaError { + OdiliaError::SendError($variant(t_err.0)) + } + } + }; +} + +send_err_impl!(tokio::sync::broadcast::error::SendError, SendError::Atspi); +send_err_impl!(tokio::sync::mpsc::error::SendError, SendError::Atspi); +send_err_impl!(tokio::sync::broadcast::error::SendError, SendError::Command); +send_err_impl!(tokio::sync::mpsc::error::SendError, SendError::Command); +send_err_impl!(tokio::sync::broadcast::error::SendError, SendError::Ssip); +send_err_impl!(tokio::sync::mpsc::error::SendError, SendError::Ssip); + #[derive(Debug)] pub enum ConfigError { Figment(figment::Error), diff --git a/common/src/lib.rs b/common/src/lib.rs index 61037eff..c54f3cf6 100644 --- a/common/src/lib.rs +++ b/common/src/lib.rs @@ -9,6 +9,8 @@ use zbus::{names::UniqueName, zvariant::ObjectPath}; +pub mod cache; +pub mod command; pub mod elements; pub mod errors; pub mod events; diff --git a/odilia-notify/src/urgency.rs b/odilia-notify/src/urgency.rs index 7924a37e..be16d2db 100644 --- a/odilia-notify/src/urgency.rs +++ b/odilia-notify/src/urgency.rs @@ -2,7 +2,7 @@ use serde::{Deserialize, Serialize}; use zbus::zvariant::{OwnedValue, Type, Value}; /// A priority/urgency level. -/// https://specifications.freedesktop.org/notification-spec/notification-spec-latest.html#urgency-levels +/// [See specification here](https://specifications.freedesktop.org/notification-spec/notification-spec-latest.html#urgency-levels) #[derive( Clone, Copy, diff --git a/odilia/Cargo.toml b/odilia/Cargo.toml index 0e30265a..b75c0e03 100644 --- a/odilia/Cargo.toml +++ b/odilia/Cargo.toml @@ -14,7 +14,7 @@ homepage = "https://odilia.app" keywords = ["screen-reader", "accessibility", "a11y", "tts", "linux"] categories = ["accessibility"] edition = "2021" -rust-version = "1.75" +rust-version = "1.81" publish = true [package.metadata.release] @@ -43,8 +43,8 @@ odilia-input = { path = "../input", version = "0.0.3" } odilia-tts = { path = "../tts", version = "0.1.4" } serde_json.workspace = true serde_plain.workspace = true -ssip-client-async.workspace = true -tokio.workspace = true +ssip-client-async = { version = "0.13.0", features = ["tokio"] } +tokio = { workspace = true, features = ["rt-multi-thread"] } tracing-error.workspace = true tracing-log.workspace = true tracing-subscriber.workspace = true @@ -58,9 +58,19 @@ tokio-util.workspace=true toml = "0.8.11" figment = { version = "0.10.14", features = ["env", "toml"] } tracing-journald = "0.3.0" +tower = { version = "0.4.13", features = ["util", "filter", "steer"] } +ssip = "0.1.0" +futures-lite = "2.3.0" +pin-project = "1.1.5" +static_assertions = "1.1.0" +futures-concurrency.workspace = true +console-subscriber = { version = "0.3.0", optional = true } +refinement = "0.5.0" +derived-deref = "2.1.0" [dev-dependencies] lazy_static = "1.4.0" tokio-test = "0.4.2" [features] +tokio-console = ["dep:console-subscriber"] diff --git a/odilia/src/commands.rs b/odilia/src/commands.rs new file mode 100644 index 00000000..d1e454c3 --- /dev/null +++ b/odilia/src/commands.rs @@ -0,0 +1,9 @@ +use crate::tower::Handler; +use odilia_common::command::OdiliaCommand as Command; +use odilia_common::errors::OdiliaError; +use std::future::Future; + +type Request = Command; +type Response = (); +type Error = OdiliaError; + diff --git a/odilia/src/events/cache.rs b/odilia/src/events/cache.rs index e8fe0690..5dc14319 100644 --- a/odilia/src/events/cache.rs +++ b/odilia/src/events/cache.rs @@ -2,7 +2,7 @@ use crate::ScreenReaderState; use atspi::events::{ AddAccessibleEvent, CacheEvents, LegacyAddAccessibleEvent, RemoveAccessibleEvent, }; -use odilia_cache::AccessiblePrimitive; +use odilia_common::cache::AccessiblePrimitive; #[tracing::instrument(level = "debug", skip(state), ret, err)] pub async fn dispatch(state: &ScreenReaderState, event: &CacheEvents) -> eyre::Result<()> { @@ -41,7 +41,7 @@ pub fn remove_accessible( state: &ScreenReaderState, event: &RemoveAccessibleEvent, ) -> eyre::Result<()> { - let accessible_prim: AccessiblePrimitive = AccessiblePrimitive::from_event(event)?; + let accessible_prim: AccessiblePrimitive = AccessiblePrimitive::from_event(event); state.cache.remove(&accessible_prim); Ok(()) } diff --git a/odilia/src/events/mod.rs b/odilia/src/events/mod.rs index e2ded987..0a8079ac 100644 --- a/odilia/src/events/mod.rs +++ b/odilia/src/events/mod.rs @@ -26,7 +26,7 @@ pub async fn structural_navigation( role: Role, ) -> OdiliaResult { tracing::debug!("Structural nav call begins!"); - let curr = match state.history_item(0).await { + let curr = match state.history_item(0) { Some(acc) => acc.into_accessible(state.atspi.connection()).await?, None => return Ok(false), }; @@ -36,7 +36,7 @@ pub async fn structural_navigation( let curr_prim = curr.try_into()?; let _: bool = comp.grab_focus().await?; comp.scroll_to(ScrollType::TopLeft).await?; - state.update_accessible(curr_prim).await; + state.update_accessible(curr_prim); let _: bool = texti.set_caret_offset(0).await?; let role = next.get_role().await?; let len = texti.character_count().await?; @@ -58,34 +58,35 @@ pub async fn sr_event( ) -> eyre::Result<()> { loop { tokio::select! { - sr_event = sr_events.recv() => { - tracing::debug!("SR Event received"); - match sr_event { - Some(ScreenReaderEvent::StructuralNavigation(dir, role)) => { - if let Err(e) = structural_navigation(&state, dir, role).await { - tracing::debug!(error = %e, "There was an error with the structural navigation call."); - } else { - tracing::debug!("Structural navigation successful!"); + sr_event = sr_events.recv() => { + tracing::debug!("SR Event received"); + match sr_event { + Some(ScreenReaderEvent::StructuralNavigation(dir, role)) => { + if let Err(e) = structural_navigation(&state, dir, role).await { + tracing::debug!(error = %e, "There was an error with the structural navigation call."); + } else { + tracing::debug!("Structural navigation successful!"); + } + }, + Some(ScreenReaderEvent::StopSpeech) => { + tracing::debug!("Stopping speech!"); + state.stop_speech().await; + }, + Some(ScreenReaderEvent::ChangeMode(new_sr_mode)) => { + tracing::debug!("Changing mode to {:?}", new_sr_mode); + if let Ok(mut sr_mode) = state.mode.lock() { + *sr_mode = new_sr_mode; + } } - }, - Some(ScreenReaderEvent::StopSpeech) => { - tracing::debug!("Stopping speech!"); - state.stop_speech().await; - }, - Some(ScreenReaderEvent::ChangeMode(new_sr_mode)) => { - tracing::debug!("Changing mode to {:?}", new_sr_mode); - let mut sr_mode = state.mode.lock().await; - *sr_mode = new_sr_mode; - } - _ => { continue; } - }; - continue; - } - () = shutdown.cancelled() => { - tracing::debug!("sr_event cancelled"); - break; + _ => { continue; } + }; + continue; + } + () = shutdown.cancelled() => { + tracing::debug!("sr_event cancelled"); + break; + } } - } } Ok(()) } @@ -175,7 +176,7 @@ async fn dispatch(state: &ScreenReaderState, event: Event) -> eyre::Result<()> { ); } } - state.event_history_update(event).await; + state.event_history_update(event); Ok(()) } diff --git a/odilia/src/events/object.rs b/odilia/src/events/object.rs index 61b28b40..c84ceac1 100644 --- a/odilia/src/events/object.rs +++ b/odilia/src/events/object.rs @@ -26,7 +26,7 @@ pub async fn dispatch(state: &ScreenReaderState, event: &ObjectEvents) -> eyre:: mod text_changed { use crate::state::ScreenReaderState; - use atspi_common::events::object::TextChangedEvent; + use atspi_common::{events::object::TextChangedEvent, Operation}; use odilia_cache::CacheItem; use odilia_common::{ errors::OdiliaError, @@ -160,15 +160,9 @@ mod text_changed { state: &ScreenReaderState, event: &TextChangedEvent, ) -> eyre::Result<()> { - match event.operation.as_str() { - "insert/system" => insert_or_delete(state, event, true).await?, - "insert" => insert_or_delete(state, event, true).await?, - "delete/system" => insert_or_delete(state, event, false).await?, - "delete" => insert_or_delete(state, event, false).await?, - _ => tracing::trace!( - "TextChangedEvent has invalid kind: {}", - event.operation - ), + match event.operation { + Operation::Insert => insert_or_delete(state, event, true).await?, + Operation::Delete => insert_or_delete(state, event, false).await?, }; Ok(()) } @@ -246,9 +240,9 @@ mod text_changed { mod children_changed { use crate::state::ScreenReaderState; - use atspi_common::events::object::ChildrenChangedEvent; - use odilia_cache::{AccessiblePrimitive, CacheItem}; - use odilia_common::result::OdiliaResult; + use atspi_common::{events::object::ChildrenChangedEvent, Operation}; + use odilia_cache::CacheItem; + use odilia_common::{cache::AccessiblePrimitive, result::OdiliaResult}; use std::sync::Arc; #[tracing::instrument(level = "debug", skip(state), err)] @@ -257,10 +251,9 @@ mod children_changed { event: &ChildrenChangedEvent, ) -> eyre::Result<()> { // Dispatch based on kind - match event.operation.as_str() { - "remove" | "remove/system" => remove(state, event)?, - "add" | "add/system" => add(state, event).await?, - kind => tracing::debug!(kind, "Ignoring event with unknown kind"), + match event.operation { + Operation::Insert => add(state, event).await?, + Operation::Delete => remove(state, event)?, } Ok(()) } @@ -272,10 +265,8 @@ mod children_changed { let accessible = get_child_primitive(event) .into_accessible(state.atspi.connection()) .await?; - let _: OdiliaResult = state - .cache - .get_or_create(&accessible, Arc::downgrade(&Arc::clone(&state.cache))) - .await; + let _: OdiliaResult = + state.cache.get_or_create(&accessible, Arc::clone(&state.cache)).await; tracing::debug!("Add a single item to cache."); Ok(()) } @@ -369,7 +360,7 @@ mod text_caret_moved { return Ok(false); } // Hopefully this shouldn't happen, but technically the caret may change before any other event happens. Since we already know that the caret position is 0, it may be a caret moved event - let last_accessible = match state.history_item(0).await { + let last_accessible = match state.history_item(0) { Some(acc) => state.get_or_create_cache_item(acc).await?, None => return Ok(true), }; @@ -398,7 +389,7 @@ mod text_caret_moved { let new_item = state.get_or_create_event_object_to_cache(event).await?; let new_prim = new_item.object.clone(); - let text = match state.history_item(0).await { + let text = match state.history_item(0) { Some(old_prim) => { let old_pos = state.previous_caret_position.load(Ordering::Relaxed); let old_item = @@ -419,7 +410,7 @@ mod text_caret_moved { } }; state.say(Priority::Text, text).await; - state.update_accessible(new_prim).await; + state.update_accessible(new_prim); Ok(()) } @@ -443,7 +434,7 @@ mod text_caret_moved { mod state_changed { use crate::state::ScreenReaderState; use atspi_common::{events::object::StateChangedEvent, State}; - use odilia_cache::AccessiblePrimitive; + use odilia_common::cache::AccessiblePrimitive; /// Update the state of an item in the cache using a `StateChanged` event and the `ScreenReaderState` as context. /// This writes to the value in-place, and does not clone any values. @@ -469,16 +460,16 @@ mod state_changed { state: &ScreenReaderState, event: &StateChangedEvent, ) -> eyre::Result<()> { - let state_value = event.enabled == 1; + let state_value = event.enabled; // update cache with state of item - let a11y_prim = AccessiblePrimitive::from_event(event)?; + let a11y_prim = AccessiblePrimitive::from_event(event); if update_state(state, &a11y_prim, event.state, state_value)? { tracing::trace!("Updating of the state was not successful! The item with id {:?} was not found in the cache.", a11y_prim.id); } else { tracing::trace!("Updated the state of accessible with ID {:?}, and state {:?} to {state_value}.", a11y_prim.id, event.state); } // enabled can only be 1 or 0, but is not a boolean over dbus - match (event.state, event.enabled == 1) { + match (event.state, event.enabled) { (State::Focused, true) => focused(state, event).await?, (state, enabled) => tracing::trace!( "Ignoring state_changed event with unknown kind: {:?}/{}", @@ -495,7 +486,7 @@ mod state_changed { event: &StateChangedEvent, ) -> eyre::Result<()> { let accessible = state.get_or_create_event_object_to_cache(event).await?; - if let Some(curr) = state.history_item(0).await { + if let Some(curr) = state.history_item(0) { if curr == accessible.object { return Ok(()); } @@ -506,7 +497,7 @@ mod state_changed { accessible.description(), accessible.get_relation_set(), )?; - state.update_accessible(accessible.object.clone()).await; + state.update_accessible(accessible.object.clone()); tracing::debug!( "Focus event received on: {:?} with role {}", accessible.object.id, @@ -520,7 +511,7 @@ mod state_changed { ) .await; - state.update_accessible(accessible.object).await; + state.update_accessible(accessible.object); Ok(()) } } @@ -531,7 +522,8 @@ mod tests { use atspi_common::{Interface, InterfaceSet, Role, State, StateSet}; use atspi_connection::AccessibilityConnection; use lazy_static::lazy_static; - use odilia_cache::{AccessiblePrimitive, Cache, CacheItem}; + use odilia_cache::{Cache, CacheItem}; + use odilia_common::cache::AccessiblePrimitive; use std::sync::Arc; use tokio_test::block_on; diff --git a/odilia/src/logging.rs b/odilia/src/logging.rs index c79e53c2..76a06305 100644 --- a/odilia/src/logging.rs +++ b/odilia/src/logging.rs @@ -9,6 +9,7 @@ use eyre::Context; use odilia_common::settings::{log::LoggingKind, ApplicationConfig}; use tracing_error::ErrorLayer; use tracing_subscriber::{prelude::*, EnvFilter}; +use tracing_tree::time::Uptime; use tracing_tree::HierarchicalLayer; /// Initialise the logging stack @@ -25,7 +26,8 @@ pub fn init(config: &ApplicationConfig) -> eyre::Result<()> { .with_span_retrace(true) .with_indent_lines(true) .with_ansi(false) - .with_wraparound(4); + .with_wraparound(4) + .with_timer(Uptime::default()); //this requires boxing because the types returned by this match block would be incompatible otherwise, since we return different layers, or modifications to a layer depending on what we get from the configuration. It is possible to do it otherwise, hopefully, but for now this would do let final_layer = match &config.log.logger { LoggingKind::File(path) => { @@ -39,7 +41,16 @@ pub fn init(config: &ApplicationConfig) -> eyre::Result<()> { .with_syslog_identifier("odilia".to_owned()) .boxed(), }; - tracing_subscriber::Registry::default() + #[cfg(feature = "tokio-console")] + let trace_sub = { + let console_layer = console_subscriber::spawn(); + tracing_subscriber::Registry::default() + .with(EnvFilter::from("tokio=trace,runtime=trace")) + .with(console_layer) + }; + #[cfg(not(feature = "tokio-console"))] + let trace_sub = { tracing_subscriber::Registry::default() }; + trace_sub .with(env_filter) .with(ErrorLayer::default()) .with(final_layer) diff --git a/odilia/src/main.rs b/odilia/src/main.rs index 632675fa..03ea3836 100644 --- a/odilia/src/main.rs +++ b/odilia/src/main.rs @@ -7,16 +7,26 @@ unsafe_code )] #![allow(clippy::multiple_crate_versions)] +#![feature(impl_trait_in_assoc_type)] mod cli; mod events; mod logging; mod state; +mod tower; use std::{fs, path::PathBuf, process::exit, sync::Arc, time::Duration}; use crate::cli::Args; +use crate::state::AccessibleHistory; +use crate::state::Command; +use crate::state::CurrentCaretPos; +use crate::state::LastCaretPos; +use crate::state::LastFocused; use crate::state::ScreenReaderState; +use crate::state::Speech; +use crate::tower::{CacheEvent, cache_event::ActiveAppEvent}; +use crate::tower::Handlers; use clap::Parser; use eyre::WrapErr; use figment::{ @@ -24,10 +34,15 @@ use figment::{ Figment, }; use futures::{future::FutureExt, StreamExt}; -use odilia_common::settings::ApplicationConfig; +use odilia_common::{ + command::{CaretPos, Focus, IntoCommands, OdiliaCommand, Speak, TryIntoCommands}, + errors::OdiliaError, + settings::ApplicationConfig, +}; use odilia_input::sr_event_receiver; use odilia_notify::listen_to_dbus_notifications; -use ssip_client_async::Priority; +use ssip::Priority; +use ssip::Request as SSIPRequest; use tokio::{ signal::unix::{signal, SignalKind}, sync::mpsc, @@ -78,8 +93,97 @@ async fn sigterm_signal_watcher( Ok(()) } -#[tracing::instrument] -#[tokio::main(flavor = "current_thread")] +use atspi::events::document::LoadCompleteEvent; +use atspi::events::object::TextCaretMovedEvent; +use atspi::Granularity; +use std::cmp::{max, min}; + +#[tracing::instrument(ret, err)] +async fn speak( + Command(Speak(text, priority)): Command, + Speech(ssip): Speech, +) -> Result<(), odilia_common::errors::OdiliaError> { + ssip.send(SSIPRequest::SetPriority(priority)).await?; + ssip.send(SSIPRequest::Speak).await?; + ssip.send(SSIPRequest::SendLines(Vec::from([text]))).await?; + Ok(()) +} + +#[tracing::instrument(ret)] +async fn doc_loaded(loaded: ActiveAppEvent) -> impl TryIntoCommands { + (Priority::Text, "Doc loaded") +} + +use crate::tower::state_changed::{Focused, Unfocused}; + +#[tracing::instrument(ret)] +async fn focused(state_changed: CacheEvent) -> impl TryIntoCommands { + Ok(vec![ + Focus(state_changed.item.object).into(), + Speak(state_changed.item.text, Priority::Text).into(), + ]) +} + +#[tracing::instrument(ret)] +async fn unfocused(state_changed: CacheEvent) -> impl TryIntoCommands { + Ok(vec![ + Focus(state_changed.item.object).into(), + Speak(state_changed.item.text, Priority::Text).into(), + ]) +} + +#[tracing::instrument(ret, err)] +async fn new_focused_item( + Command(Focus(new_focus)): Command, + AccessibleHistory(old_focus): AccessibleHistory, +) -> Result<(), OdiliaError> { + let _ = old_focus.lock()?.push(new_focus); + Ok(()) +} + +#[tracing::instrument(ret, err)] +async fn new_caret_pos( + Command(CaretPos(new_pos)): Command, + CurrentCaretPos(pos): CurrentCaretPos, +) -> Result<(), OdiliaError> { + pos.store(new_pos, core::sync::atomic::Ordering::Relaxed); + Ok(()) +} + +#[tracing::instrument(ret, err)] +async fn caret_moved( + caret_moved: CacheEvent, + LastCaretPos(last_pos): LastCaretPos, + LastFocused(last_focus): LastFocused, +) -> Result, OdiliaError> { + let mut commands: Vec = + vec![CaretPos(caret_moved.inner.position.try_into()?).into()]; + + if last_focus == caret_moved.item.object { + let start = min(caret_moved.inner.position.try_into()?, last_pos); + let end = max(caret_moved.inner.position.try_into()?, last_pos); + if let Some(text) = caret_moved.item.text.get(start..end) { + commands.extend((Priority::Text, text.to_string()).into_commands()); + } else { + return Err(OdiliaError::Generic(format!( + "Slide {}..{} could not be created from {}", + start, end, caret_moved.item.text + ))); + } + } else { + let (text, _, _) = caret_moved + .item + .get_string_at_offset( + caret_moved.inner.position.try_into()?, + Granularity::Line, + ) + .await?; + commands.extend((Priority::Text, text).into_commands()); + } + Ok(commands) +} + +#[tokio::main] async fn main() -> eyre::Result<()> { let args = Args::parse(); @@ -104,13 +208,12 @@ async fn main() -> eyre::Result<()> { tracing::error!("Could not set AT-SPI2 IsEnabled property because: {}", e); } let (sr_event_tx, sr_event_rx) = mpsc::channel(128); - // this channel must NEVER fill up; it will cause the thread receiving events to deadlock due to a zbus design choice. - // If you need to make it bigger, then make it bigger, but do NOT let it ever fill up. - let (atspi_event_tx, atspi_event_rx) = mpsc::channel(128); // this is the channel which handles all SSIP commands. If SSIP is not allowed to operate on a separate task, then waiting for the receiving message can block other long-running operations like structural navigation. // Although in the future, this may possibly be resolved through a proper cache, I think it still makes sense to separate SSIP's IO operations to a separate task. // Like the channel above, it is very important that this is *never* full, since it can cause deadlocking if the other task sending the request is working with zbus. let (ssip_req_tx, ssip_req_rx) = mpsc::channel::(128); + let (mut ev_tx, ev_rx) = + futures::channel::mpsc::channel::>(10_000); // Initialize state let state = Arc::new(ScreenReaderState::new(ssip_req_tx, config).await?); let ssip = odilia_tts::create_ssip_client().await?; @@ -133,15 +236,27 @@ async fn main() -> eyre::Result<()> { state.add_cache_match_rule(), )?; + // load handlers + let handlers = Handlers::new(state.clone()) + .command_listener(speak) + .command_listener(new_focused_item) + .command_listener(new_caret_pos) + .atspi_listener(doc_loaded) + .atspi_listener(caret_moved) + .atspi_listener(focused) + .atspi_listener(unfocused); + let ssip_event_receiver = odilia_tts::handle_ssip_commands(ssip, ssip_req_rx, token.clone()) .map(|r| r.wrap_err("Could no process SSIP request")); - let atspi_event_receiver = - events::receive(Arc::clone(&state), atspi_event_tx, token.clone()) - .map(|()| Ok::<_, eyre::Report>(())); - let atspi_event_processor = - events::process(Arc::clone(&state), atspi_event_rx, token.clone()) - .map(|()| Ok::<_, eyre::Report>(())); + /* + let atspi_event_receiver = + events::receive(Arc::clone(&state), atspi_event_tx, token.clone()) + .map(|()| Ok::<_, eyre::Report>(())); + let atspi_event_processor = + events::process(Arc::clone(&state), atspi_event_rx, token.clone()) + .map(|()| Ok::<_, eyre::Report>(())); + */ let odilia_event_receiver = sr_event_receiver(sr_event_tx, token.clone()) .map(|r| r.wrap_err("Could not process Odilia events")); let odilia_event_processor = @@ -149,13 +264,32 @@ async fn main() -> eyre::Result<()> { .map(|r| r.wrap_err("Could not process Odilia event")); let notification_task = notifications_monitor(Arc::clone(&state), token.clone()) .map(|r| r.wrap_err("Could not process signal shutdown.")); + let mut stream = state.atspi.event_stream(); + // There is a reason we are not reading from the event stream directly. + // This `MessageStream` can only store 64 events in its buffer. + // And, even if it could store more (it can via options), `zbus` specifically states that: + // > You must ensure a MessageStream is continuously polled or you will experience hangs. + // So, we continually poll it here, then receive it on the other end. + // Additioanlly, since sending is not async, but simply errors when there is an issue, this will + // help us avoid hangs. + let event_send_task = async move { + std::pin::pin!(&mut stream); + while let Some(ev) = stream.next().await { + if let Err(e) = ev_tx.try_send(ev) { + tracing::error!("Error sending event across channel! {e:?}"); + } + } + }; + let atspi_handlers_task = handlers.atspi_handler(ev_rx); - tracker.spawn(atspi_event_receiver); - tracker.spawn(atspi_event_processor); + //tracker.spawn(atspi_event_receiver); + //tracker.spawn(atspi_event_processor); tracker.spawn(odilia_event_receiver); tracker.spawn(odilia_event_processor); tracker.spawn(ssip_event_receiver); tracker.spawn(notification_task); + tracker.spawn(atspi_handlers_task); + tracker.spawn(event_send_task); tracker.close(); let _ = sigterm_signal_watcher(token, tracker) .await diff --git a/odilia/src/state.rs b/odilia/src/state.rs index 8237cac0..f41a8544 100644 --- a/odilia/src/state.rs +++ b/odilia/src/state.rs @@ -1,10 +1,15 @@ -use std::sync::atomic::AtomicUsize; +use std::{fmt::Debug, sync::atomic::AtomicUsize}; +use crate::tower::from_state::TryFromState; use circular_queue::CircularQueue; use eyre::WrapErr; +use futures::future::err; +use futures::future::ok; +use futures::future::Ready; use ssip_client_async::{MessageScope, Priority, PunctuationMode, Request as SSIPRequest}; -use tokio::sync::{mpsc::Sender, Mutex}; -use tracing::{debug, Instrument}; +use std::sync::Mutex; +use tokio::sync::mpsc::Sender; +use tracing::{debug, Instrument, Level}; use zbus::{fdo::DBusProxy, names::BusName, zvariant::ObjectPath, MatchRule, MessageType}; use atspi_common::{ @@ -14,9 +19,11 @@ use atspi_common::{ use atspi_connection::AccessibilityConnection; use atspi_proxies::{accessible::AccessibleProxy, cache::CacheProxy}; use odilia_cache::Convertable; -use odilia_cache::{AccessibleExt, AccessiblePrimitive, Cache, CacheItem}; +use odilia_cache::{AccessibleExt, Cache, CacheItem}; use odilia_common::{ - errors::CacheError, + cache::AccessiblePrimitive, + command::CommandType, + errors::{CacheError, OdiliaError}, modes::ScreenReaderMode, settings::{speech::PunctuationSpellingMode, ApplicationConfig}, types::TextSelectionArea, @@ -25,16 +32,113 @@ use odilia_common::{ use std::sync::Arc; #[allow(clippy::module_name_repetitions)] -pub struct ScreenReaderState { +pub(crate) struct ScreenReaderState { pub atspi: AccessibilityConnection, pub dbus: DBusProxy<'static>, pub ssip: Sender, - pub previous_caret_position: AtomicUsize, + pub previous_caret_position: Arc, pub mode: Mutex, - pub accessible_history: Mutex>, + pub accessible_history: Arc>>, pub event_history: Mutex>, pub cache: Arc, } +#[derive(Debug, Clone)] +pub struct AccessibleHistory(pub Arc>>); + +impl TryFromState, C> for AccessibleHistory { + type Error = OdiliaError; + type Future = Ready>; + fn try_from_state(state: Arc, _cmd: C) -> Self::Future { + ok(AccessibleHistory(Arc::clone(&state.accessible_history))) + } +} +impl TryFromState, C> for CurrentCaretPos { + type Error = OdiliaError; + type Future = Ready>; + fn try_from_state(state: Arc, _cmd: C) -> Self::Future { + ok(CurrentCaretPos(Arc::clone(&state.previous_caret_position))) + } +} + +#[derive(Debug, Clone)] +pub struct LastFocused(pub AccessiblePrimitive); +#[derive(Debug)] +pub struct CurrentCaretPos(pub Arc); +#[derive(Debug, Clone)] +pub struct LastCaretPos(pub usize); +pub struct Speech(pub Sender); +#[derive(Debug)] +pub struct Command(pub T) +where + T: CommandType; + +impl TryFromState, C> for Command +where + C: CommandType + Clone + Debug, +{ + type Error = OdiliaError; + type Future = Ready, Self::Error>>; + fn try_from_state(_state: Arc, cmd: C) -> Self::Future { + ok(Command(cmd)) + } +} + +impl TryFromState, C> for Speech +where + C: CommandType + Debug, +{ + type Error = OdiliaError; + type Future = Ready>; + fn try_from_state(state: Arc, _cmd: C) -> Self::Future { + ok(Speech(state.ssip.clone())) + } +} + +impl TryFromState, E> for LastCaretPos +where + E: Debug, +{ + type Error = OdiliaError; + type Future = Ready>; + fn try_from_state(state: Arc, _event: E) -> Self::Future { + ok(LastCaretPos( + state.previous_caret_position + .load(core::sync::atomic::Ordering::Relaxed), + )) + } +} + +impl TryFromState, E> for LastFocused +where + E: Debug, +{ + type Error = OdiliaError; + type Future = Ready>; + fn try_from_state(state: Arc, _event: E) -> Self::Future { + let span = tracing::span!(Level::INFO, "try_from_state"); + let _enter = span.enter(); + let Ok(ml) = state.accessible_history.lock() else { + let e = OdiliaError::Generic("Could not get a lock on the history mutex. This is usually due to memory corruption or degradation and is a fatal error.".to_string()); + tracing::error!("{e:?}"); + return err(e); + }; + let Some(last) = ml.iter().nth(0).cloned() else { + let e = OdiliaError::Generic( + "There are no previously focused items.".to_string(), + ); + tracing::error!("{e:?}"); + return err(e); + }; + ok(LastFocused(last)) + } +} + +enum ConfigType { + CliOverride, + XDGConfigHome, + Etc, + CreateDefault, +} impl ScreenReaderState { #[tracing::instrument(skip_all)] @@ -57,8 +161,8 @@ impl ScreenReaderState { tracing::debug!("Reading configuration"); - let previous_caret_position = AtomicUsize::new(0); - let accessible_history = Mutex::new(CircularQueue::with_capacity(16)); + let previous_caret_position = Arc::new(AtomicUsize::new(0)); + let accessible_history = Arc::new(Mutex::new(CircularQueue::with_capacity(16))); let event_history = Mutex::new(CircularQueue::with_capacity(16)); let cache = Arc::new(Cache::new(atspi.connection().clone())); ssip.send(SSIPRequest::SetPitch( @@ -151,11 +255,11 @@ impl ScreenReaderState { &self, event: &T, ) -> OdiliaResult { - let prim = AccessiblePrimitive::from_event(event)?; + let prim = AccessiblePrimitive::from_event(event); if self.cache.get(&prim).is_none() { self.cache.add(CacheItem::from_atspi_event( event, - Arc::downgrade(&Arc::clone(&self.cache)), + Arc::clone(&self.cache), self.atspi.connection(), ) .await?)?; @@ -212,7 +316,7 @@ impl ScreenReaderState { // TODO: add logic for punctuation Ok(text_selection) } - #[tracing::instrument(skip_all)] + #[tracing::instrument(skip_all, err)] pub async fn register_event( &self, ) -> OdiliaResult<()> { @@ -260,25 +364,27 @@ impl ScreenReaderState { } #[allow(dead_code)] - pub async fn event_history_item(&self, index: usize) -> Option { - let history = self.event_history.lock().await; + pub fn event_history_item(&self, index: usize) -> Option { + let history = self.event_history.lock().ok()?; history.iter().nth(index).cloned() } - pub async fn event_history_update(&self, event: Event) { - let mut history = self.event_history.lock().await; - history.push(event); + pub fn event_history_update(&self, event: Event) { + if let Ok(mut history) = self.event_history.lock() { + history.push(event); + } } - pub async fn history_item<'a>(&self, index: usize) -> Option { - let history = self.accessible_history.lock().await; + pub fn history_item(&self, index: usize) -> Option { + let history = self.accessible_history.lock().ok()?; history.iter().nth(index).cloned() } /// Adds a new accessible to the history. We only store 16 previous accessibles, but theoretically, it should be lower. - pub async fn update_accessible(&self, new_a11y: AccessiblePrimitive) { - let mut history = self.accessible_history.lock().await; - history.push(new_a11y); + pub fn update_accessible(&self, new_a11y: AccessiblePrimitive) { + if let Ok(mut history) = self.accessible_history.lock() { + history.push(new_a11y); + } } pub async fn build_cache<'a, T>(&self, dest: T) -> OdiliaResult> where @@ -304,7 +410,7 @@ impl ScreenReaderState { .build() .await?; self.cache - .get_or_create(&accessible_proxy, Arc::downgrade(&self.cache)) + .get_or_create(&accessible_proxy, Arc::clone(&self.cache)) .await } #[tracing::instrument(skip_all, ret, err)] diff --git a/odilia/src/tower/README.md b/odilia/src/tower/README.md new file mode 100644 index 00000000..7b4bc4da --- /dev/null +++ b/odilia/src/tower/README.md @@ -0,0 +1,45 @@ +# SRaaS (Screen Reader as a Service) + +This document describes in some moderate detail both _why_ we chose to use `tower` (the Rust service-based architecture) for the infrastructure of Odilia, and also _how_ to contribute to various parts of the system. + +## Why + +Think about screen readers, what do they even do? + +Well, for the most part, they receive events from the system (user input, accessibility events), and then produce output for the system (speak text, update braille display, synthesize user input events). +All of these are things which _should_ be performed asyncronously (i.e., concurrently) since it is all based around IO. + +Since `tower` is a generic service/layer system for dealing with these kinds of asyncronous handling at various levels, let's explore the current architecture of Odilia and what kind of services we have created to deal with the unique challenges that a screen reader has to face. +TODO + +``` +Current tree of services: +TryIntoCommands is a trait which means it can be converted into Result, Error> + +TryInto(Event) -> Result + CacheLayer(E) -> (E, Arc) + AsyncTryInto(E, Arc) -> CacheEvent + Handler(CacheEvent, State): + fn(CacheEvnet, ...impl async TryFromState) + -> O: TryIntoCommand + for cmd in O -> Resul, Result>: + run_command(cmd) + +Desired tree: + +TryInto(Event) -> Result + StateLayer(E), -> (E, Arc) + AsyncTryInto(E, Arc) -> (CacheEvemt, ...impl async TryFromState) + Handler(CacheEvemt, ...impl async TryFromState) -> O: TryIntoCommands + for cmd in O::try_into_commands(): + run_command(cmd) + +This is more generic since it can convert _any_ type which needs state, including the cache, or any other part, for whatever reason. +This also makes it more complicated because `CacheEvent` needs to actually pass `E` and state into a conversion function to work, whereas some types can be converted without passing any additional information into the state for lookup. + +It gets a little complicated here with variable argument lists. Let's hope we can find the solution. +``` + +## TODOs + +- [ ] Document all the various services and layers and why they are there. diff --git a/odilia/src/tower/async_try.rs b/odilia/src/tower/async_try.rs new file mode 100644 index 00000000..386b8f23 --- /dev/null +++ b/odilia/src/tower/async_try.rs @@ -0,0 +1,110 @@ +#![allow(clippy::module_name_repetitions)] + +use crate::tower::from_state::TryFromState; +use futures::TryFutureExt; +use odilia_common::errors::OdiliaError; +use std::{ + future::Future, + marker::PhantomData, + task::{Context, Poll}, +}; +use tower::{Layer, Service}; + +impl AsyncTryFrom<(S, T)> for U +where + U: TryFromState, +{ + type Error = U::Error; + type Future = U::Future; + fn try_from_async(value: (S, T)) -> Self::Future { + U::try_from_state(value.0, value.1) + } +} + +pub trait AsyncTryFrom: Sized { + type Error; + type Future: Future>; + + fn try_from_async(value: T) -> Self::Future; +} +pub trait AsyncTryInto: Sized { + type Error; + type Future: Future>; + + fn try_into_async(self) -> Self::Future; +} +impl> AsyncTryInto for T { + type Error = U::Error; + type Future = U::Future; + fn try_into_async(self: T) -> Self::Future { + U::try_from_async(self) + } +} + +pub struct AsyncTryIntoService, S, R, Fut1> { + inner: S, + _marker: PhantomData R>, +} +impl, S, R, Fut1> AsyncTryIntoService { + pub fn new(inner: S) -> Self { + AsyncTryIntoService { inner, _marker: PhantomData } + } +} +pub struct AsyncTryIntoLayer> { + _marker: PhantomData O>, +} +impl> Clone for AsyncTryIntoLayer { + fn clone(&self) -> Self { + AsyncTryIntoLayer { _marker: PhantomData } + } +} +impl> AsyncTryIntoLayer { + pub fn new() -> Self { + AsyncTryIntoLayer { _marker: PhantomData } + } +} + +impl, O, S, Fut1> Layer for AsyncTryIntoLayer +where + S: Service, +{ + type Service = AsyncTryIntoService>::Response, Fut1>; + fn layer(&self, inner: S) -> Self::Service { + AsyncTryIntoService::new(inner) + } +} + +impl, S, R, Fut1> Clone for AsyncTryIntoService +where + S: Clone, +{ + fn clone(&self) -> Self { + AsyncTryIntoService { inner: self.inner.clone(), _marker: PhantomData } + } +} + +impl, S, R, Fut1> Service for AsyncTryIntoService +where + I: AsyncTryInto, + E: Into, + E2: Into, + S: Service + Clone, + Fut1: Future>, +{ + type Response = R; + type Future = impl Future>; + type Error = OdiliaError; + fn poll_ready(&mut self, _ctx: &mut Context<'_>) -> Poll> { + Poll::Ready(Ok(())) + } + fn call(&mut self, req: I) -> Self::Future { + let clone = self.inner.clone(); + let mut inner = std::mem::replace(&mut self.inner, clone); + async move { + match req.try_into_async().await { + Ok(resp) => inner.call(resp).err_into().await, + Err(e) => Err(e.into()), + } + } + } +} diff --git a/odilia/src/tower/cache_event.rs b/odilia/src/tower/cache_event.rs new file mode 100644 index 00000000..ab996701 --- /dev/null +++ b/odilia/src/tower/cache_event.rs @@ -0,0 +1,132 @@ +use crate::{tower::from_state::TryFromState, OdiliaError, ScreenReaderState}; +use atspi_common::EventProperties; +use derived_deref::{Deref, DerefMut}; +use odilia_cache::CacheItem; +use odilia_common::cache::AccessiblePrimitive; +use refinement::Predicate; +use std::fmt::Debug; +use std::{future::Future, marker::PhantomData, sync::Arc}; +use zbus::{names::UniqueName, zvariant::ObjectPath}; + +pub type CacheEvent = CacheEventPredicate; +pub type ActiveAppEvent = CacheEventPredicate; + +#[derive(Debug, Clone, Deref, DerefMut)] +pub struct CacheEventInner { + #[target] + pub inner: E, + pub item: CacheItem, +} +impl CacheEventInner +where + E: EventProperties + Debug, +{ + fn new(inner: E, item: CacheItem) -> Self { + Self { inner, item } + } +} + +#[derive(Debug, Clone, Deref, DerefMut)] +pub struct CacheEventPredicate< + E: EventProperties + Debug, + P: Predicate<(E, Arc)>, +> { + #[target] + pub inner: E, + pub item: CacheItem, + _marker: PhantomData

, +} +impl CacheEventPredicate +where + E: EventProperties + Debug + Clone, + P: Predicate<(E, Arc)>, +{ + pub fn from_cache_event( + ce: CacheEventInner, + state: Arc, + ) -> Option { + if P::test(&(ce.inner.clone(), state)) { + return Some(Self { inner: ce.inner, item: ce.item, _marker: PhantomData }); + } + None + } +} + +#[derive(Debug)] +pub struct Always; +impl Predicate<(E, Arc)> for Always { + fn test(_: &(E, Arc)) -> bool { + true + } +} + +#[derive(Debug)] +pub struct ActiveApplication; +impl Predicate<(E, Arc)> for ActiveApplication +where + E: EventProperties, +{ + fn test((ev, state): &(E, Arc)) -> bool { + let Some(last_focused) = state.history_item(0) else { + return false; + }; + last_focused == ev.object_ref().into() + } +} + +impl TryFromState, E> for CacheEventInner +where + E: EventProperties + Debug + Clone, +{ + type Error = OdiliaError; + type Future = impl Future>; + #[tracing::instrument(skip(state), ret)] + fn try_from_state(state: Arc, event: E) -> Self::Future { + async move { + let a11y = AccessiblePrimitive::from_event(&event); + let proxy = a11y.into_accessible(state.connection()).await?; + let cache_item = + state.cache.get_or_create(&proxy, Arc::clone(&state.cache)).await?; + Ok(CacheEventInner::new(event, cache_item)) + } + } +} + +impl TryFromState, E> for CacheEventPredicate +where + E: EventProperties + Debug + Clone, + P: Predicate<(E, Arc)> + Debug, +{ + type Error = OdiliaError; + type Future = impl Future>; + #[tracing::instrument(skip(state), ret)] + fn try_from_state(state: Arc, event: E) -> Self::Future { + async move { + let a11y = AccessiblePrimitive::from_event(&event); + let proxy = a11y.into_accessible(state.connection()).await?; + let cache_item = + state.cache.get_or_create(&proxy, Arc::clone(&state.cache)).await?; + let cache_event = CacheEventInner::new(event.clone(), cache_item); + CacheEventPredicate::from_cache_event(cache_event, state).ok_or( + OdiliaError::PredicateFailure(format!( + "Predicate cache event {} failed for event {:?}", + std::any::type_name::

(), + event + )), + ) + } + } +} + +impl EventProperties for CacheEventPredicate +where + E: EventProperties + Debug, + P: Predicate<(E, Arc)>, +{ + fn path(&self) -> ObjectPath<'_> { + self.inner.path() + } + fn sender(&self) -> UniqueName<'_> { + self.inner.sender() + } +} diff --git a/odilia/src/tower/choice.rs b/odilia/src/tower/choice.rs new file mode 100644 index 00000000..96f0e57e --- /dev/null +++ b/odilia/src/tower/choice.rs @@ -0,0 +1,126 @@ +use atspi_common::{BusProperties, Event, EventTypeProperties}; +use futures::future::err; +use futures::future::Either; +use futures::TryFutureExt; +use odilia_common::{ + command::{ + CommandType, CommandTypeDynamic, OdiliaCommand as Command, + OdiliaCommandDiscriminants as CommandDiscriminants, + }, + errors::OdiliaError, +}; +use std::collections::{btree_map::Entry, BTreeMap}; +use std::fmt::Debug; +use std::future::Future; +use std::marker::PhantomData; +use std::task::{Context, Poll}; +use tower::Service; + +pub trait Chooser { + fn identifier(&self) -> K; +} +pub trait ChooserStatic { + fn identifier() -> K; +} + +#[allow(clippy::module_name_repetitions)] +pub struct ChoiceService +where + S: Service, + Req: Chooser, +{ + services: BTreeMap, + _marker: PhantomData, +} + +impl Clone for ChoiceService +where + K: Clone, + S: Clone + Service, + Req: Chooser, +{ + fn clone(&self) -> Self { + ChoiceService { services: self.services.clone(), _marker: PhantomData } + } +} + +impl ChoiceService +where + S: Service, + Req: Chooser, +{ + pub fn new() -> Self { + ChoiceService { services: BTreeMap::new(), _marker: PhantomData } + } + pub fn insert(&mut self, k: K, s: S) + where + K: Ord, + { + self.services.insert(k, s); + } + pub fn entry(&mut self, k: K) -> Entry + where + K: Ord, + { + self.services.entry(k) + } +} + +impl Service for ChoiceService +where + S: Service + Clone, + Req: Chooser, + K: Ord + Debug, + OdiliaError: From, +{ + type Response = S::Response; + type Error = OdiliaError; + type Future = impl Future>; + fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll> { + for (_k, svc) in &mut self.services.iter_mut() { + let _ = svc.poll_ready(cx)?; + } + Poll::Ready(Ok(())) + } + fn call(&mut self, req: Req) -> Self::Future { + let k = req.identifier(); + + let mut svc = if let Some(orig_svc) = self.services.get_mut(&k) { + let clone = orig_svc.clone(); + std::mem::replace(orig_svc, clone) + } else { + return Either::Left(err(OdiliaError::ServiceNotFound( + format!("A service with key {k:?} could not be found in a list with keys of {:?}", self.services.keys()) + ))); + }; + Either::Right(svc.call(req).err_into()) + } +} + +impl ChooserStatic<(&'static str, &'static str)> for E +where + E: BusProperties, +{ + fn identifier() -> (&'static str, &'static str) { + (E::DBUS_INTERFACE, E::DBUS_MEMBER) + } +} +impl ChooserStatic for C +where + C: CommandType, +{ + fn identifier() -> CommandDiscriminants { + C::CTYPE + } +} + +impl Chooser<(&'static str, &'static str)> for Event { + fn identifier(&self) -> (&'static str, &'static str) { + (self.interface(), self.member()) + } +} +impl Chooser for Command { + fn identifier(&self) -> CommandDiscriminants { + self.ctype() + } +} diff --git a/odilia/src/tower/from_state.rs b/odilia/src/tower/from_state.rs new file mode 100644 index 00000000..322a0c7d --- /dev/null +++ b/odilia/src/tower/from_state.rs @@ -0,0 +1,67 @@ +#![allow(clippy::module_name_repetitions)] + +use futures::FutureExt; +use futures_concurrency::future::Join; + +use odilia_common::errors::OdiliaError; +use std::fmt::Debug; +use std::future::Future; + +pub trait TryFromState: Sized { + type Error; + type Future: Future>; + fn try_from_state(state: S, data: T) -> Self::Future; +} + +impl TryFromState for (U1,) +where + U1: TryFromState, + OdiliaError: From, + T: Debug, +{ + type Error = OdiliaError; + type Future = impl Future>; + #[tracing::instrument(skip(state))] + fn try_from_state(state: S, data: T) -> Self::Future { + (U1::try_from_state(state, data),).join().map(|(u1,)| Ok((u1?,))) + } +} +impl TryFromState for (U1, U2) +where + U1: TryFromState, + U2: TryFromState, + OdiliaError: From + From, + S: Clone, + T: Clone + Debug, +{ + type Error = OdiliaError; + type Future = impl Future>; + #[tracing::instrument(skip(state))] + fn try_from_state(state: S, data: T) -> Self::Future { + (U1::try_from_state(state.clone(), data.clone()), U2::try_from_state(state, data)) + .join() + .map(|(u1, u2)| Ok((u1?, u2?))) + } +} +impl TryFromState for (U1, U2, U3) +where + U1: TryFromState, + U2: TryFromState, + U3: TryFromState, + OdiliaError: From + From + From, + S: Clone, + T: Clone + Debug, +{ + type Error = OdiliaError; + type Future = impl Future>; + #[tracing::instrument(skip(state))] + fn try_from_state(state: S, data: T) -> Self::Future { + ( + U1::try_from_state(state.clone(), data.clone()), + U2::try_from_state(state.clone(), data.clone()), + U3::try_from_state(state, data), + ) + .join() + .map(|(u1, u2, u3)| Ok((u1?, u2?, u3?))) + } +} diff --git a/odilia/src/tower/handler.rs b/odilia/src/tower/handler.rs new file mode 100644 index 00000000..f4611a8c --- /dev/null +++ b/odilia/src/tower/handler.rs @@ -0,0 +1,85 @@ +#![allow(clippy::module_name_repetitions)] + +use futures::FutureExt; +use std::{ + convert::Infallible, + future::Future, + marker::PhantomData, + task::{Context, Poll}, +}; +use tower::Service; + +pub trait Handler { + type Response; + type Future: Future; + fn into_service(self) -> HandlerService + where + Self: Sized, + { + HandlerService::new(self) + } + fn call(self, params: T) -> Self::Future; +} + +macro_rules! impl_handler { + ($($type:ident,)+) => { + #[allow(non_snake_case)] + impl Handler<($($type,)+)> for F + where + F: FnOnce($($type,)+) -> Fut + Send, + Fut: Future + Send, + $($type: Send,)+ { + type Response = R; + type Future = impl Future; + fn call(self, params: ($($type,)+)) -> Self::Future { + let ($($type,)+) = params; + self($($type,)+) + } + } +} +} +impl_handler!(T1,); +impl_handler!(T1, T2,); +impl_handler!(T1, T2, T3,); +impl_handler!(T1, T2, T3, T4,); +impl_handler!(T1, T2, T3, T4, T5,); +impl_handler!(T1, T2, T3, T4, T5, T6,); +impl_handler!(T1, T2, T3, T4, T5, T6, T7,); + +#[allow(clippy::type_complexity)] +pub struct HandlerService { + handler: H, + _marker: PhantomData, +} +impl Clone for HandlerService +where + H: Clone, +{ + fn clone(&self) -> Self { + HandlerService { handler: self.handler.clone(), _marker: PhantomData } + } +} +impl HandlerService { + fn new(handler: H) -> Self + where + H: Handler, + { + HandlerService { handler, _marker: PhantomData } + } +} + +impl Service for HandlerService +where + H: Handler + Clone, +{ + type Response = H::Response; + type Future = impl Future>; + type Error = Infallible; + + fn poll_ready(&mut self, _ctx: &mut Context<'_>) -> Poll> { + Poll::Ready(Ok(())) + } + fn call(&mut self, params: T) -> Self::Future { + self.handler.clone().call(params).map(Ok) + } +} diff --git a/odilia/src/tower/handlers.rs b/odilia/src/tower/handlers.rs new file mode 100644 index 00000000..283556c4 --- /dev/null +++ b/odilia/src/tower/handlers.rs @@ -0,0 +1,144 @@ +#![allow(dead_code)] + +use crate::state::ScreenReaderState; +use crate::tower::{ + choice::{ChoiceService, ChooserStatic}, + from_state::TryFromState, + service_set::ServiceSet, + Handler, ServiceExt as OdiliaServiceExt, +}; +use atspi::AtspiError; +use atspi::BusProperties; +use atspi::Event; +use atspi::EventProperties; +use atspi::EventTypeProperties; +use odilia_common::errors::OdiliaError; +use std::fmt::Debug; +use std::sync::Arc; + +use futures::{Stream, StreamExt}; + +use tower::util::BoxCloneService; +use tower::Service; +use tower::ServiceExt; + +use tokio::sync::mpsc::Receiver; + +use odilia_common::command::{ + CommandType, OdiliaCommand as Command, OdiliaCommandDiscriminants as CommandDiscriminants, + TryIntoCommands, +}; + +type Response = Vec; +type Request = Event; +type Error = OdiliaError; + +type AtspiHandler = BoxCloneService; +type CommandHandler = BoxCloneService; + +pub struct Handlers { + state: Arc, + atspi: ChoiceService<(&'static str, &'static str), ServiceSet, Event>, + command: ChoiceService, Command>, +} + +impl Handlers { + pub fn new(state: Arc) -> Self { + Handlers { state, atspi: ChoiceService::new(), command: ChoiceService::new() } + } + pub async fn command_handler(mut self, mut commands: Receiver) { + loop { + let maybe_cmd = commands.recv().await; + let Some(cmd) = maybe_cmd else { + tracing::error!("Error cmd: {maybe_cmd:?}"); + continue; + }; + // NOTE: Why not use join_all(...) ? + // Because this drives the futures concurrently, and we want ordered handlers. + // Otherwise, we cannot guarentee that the caching functions get run first. + // we could move caching to a separate, ordered system, then parallelize the other functions, + // if we determine this is a performance problem. + if let Err(e) = self.command.call(cmd).await { + tracing::error!("{e:?}"); + } + } + } + #[tracing::instrument(skip_all)] + pub async fn atspi_handler(mut self, mut events: R) + where + R: Stream> + Unpin, + { + std::pin::pin!(&mut events); + loop { + let maybe_ev = events.next().await; + let Some(Ok(ev)) = maybe_ev else { + tracing::error!("Error in processing {maybe_ev:?}"); + continue; + }; + if let Err(e) = self.atspi.call(ev).await { + tracing::error!("{e:?}"); + } + } + } + pub fn command_listener(mut self, handler: H) -> Self + where + H: Handler + Send + Clone + 'static, + >::Future: Send, + C: CommandType + ChooserStatic + Send + 'static, + Command: TryInto, + OdiliaError: From<>::Error> + + From<, C>>::Error>, + R: Into> + Send + 'static, + T: TryFromState, C> + Send + 'static, + , C>>::Future: Send, + , C>>::Error: Send, + { + let bs = handler + .into_service() + .unwrap_map(Into::into) + .request_async_try_from() + .with_state(Arc::clone(&self.state)) + .request_try_from() + .boxed_clone(); + self.command.entry(C::identifier()).or_default().push(bs); + Self { state: self.state, atspi: self.atspi, command: self.command } + } + pub fn atspi_listener(mut self, handler: H) -> Self + where + H: Handler + Send + Clone + 'static, + >::Future: Send, + E: EventTypeProperties + + Debug + + BusProperties + + TryFrom + + EventProperties + + ChooserStatic<(&'static str, &'static str)> + + Clone + + Send + + 'static, + OdiliaError: From<>::Error> + + From<, E>>::Error>, + R: TryIntoCommands + 'static, + T: TryFromState, E> + Send + 'static, + , E>>::Error: Send + 'static, + , E>>::Future: Send, + { + let bs = handler + .into_service() + .unwrap_map(TryIntoCommands::try_into_commands) + .request_async_try_from() + .with_state(Arc::clone(&self.state)) + .request_try_from() + .iter_into(self.command.clone()) + .map_result( + |res: Result>>, OdiliaError>| { + res?.into_iter() + .flatten() + .collect::>() + }, + ) + .boxed_clone(); + self.atspi.entry(E::identifier()).or_default().push(bs); + Self { state: self.state, atspi: self.atspi, command: self.command } + } +} diff --git a/odilia/src/tower/iter_svc.rs b/odilia/src/tower/iter_svc.rs new file mode 100644 index 00000000..a4ade66b --- /dev/null +++ b/odilia/src/tower/iter_svc.rs @@ -0,0 +1,66 @@ +use std::future::Future; +use std::marker::PhantomData; +use std::task::Context; +use std::task::Poll; +use tower::Service; + +#[allow(clippy::type_complexity)] +pub struct IterService { + inner: S1, + outer: S2, + _marker: PhantomData Result<(Iter, I), E>>, +} +impl Clone for IterService +where + S1: Clone, + S2: Clone, +{ + fn clone(&self) -> Self { + IterService { + inner: self.inner.clone(), + outer: self.outer.clone(), + _marker: PhantomData, + } + } +} +impl IterService +where + S1: Service, + Iter: IntoIterator, + S2: Service, +{ + pub fn new(inner: S1, outer: S2) -> Self { + IterService { inner, outer, _marker: PhantomData } + } +} + +impl Service for IterService +where + S1: Service + Clone, + Iter: IntoIterator, + S2: Service + Clone, + E: From + From, +{ + type Response = Vec; + type Error = E; + type Future = impl Future>; + fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll> { + let _ = self.inner.poll_ready(cx).map_err(Into::::into)?; + self.outer.poll_ready(cx).map_err(Into::into) + } + fn call(&mut self, input: Req) -> Self::Future { + let clone_outer = self.outer.clone(); + let mut outer = std::mem::replace(&mut self.outer, clone_outer); + let clone_inner = self.inner.clone(); + let mut inner = std::mem::replace(&mut self.inner, clone_inner); + async move { + let iter = inner.call(input).await?; + let mut results = vec![]; + for item in iter { + let result = outer.call(item).await?; + results.push(result); + } + Ok(results) + } + } +} diff --git a/odilia/src/tower/mod.rs b/odilia/src/tower/mod.rs new file mode 100644 index 00000000..52b5335d --- /dev/null +++ b/odilia/src/tower/mod.rs @@ -0,0 +1,18 @@ +pub mod async_try; +pub mod cache_event; +pub use cache_event::CacheEvent; +pub mod choice; +pub mod from_state; +pub mod handler; +pub mod iter_svc; +pub mod service_ext; +pub mod service_set; +pub mod state_changed; +pub mod state_svc; +pub mod sync_try; +pub mod unwrap_svc; +pub use handler::Handler; +pub use service_ext::ServiceExt; + +pub mod handlers; +pub use handlers::*; diff --git a/odilia/src/tower/service_ext.rs b/odilia/src/tower/service_ext.rs new file mode 100644 index 00000000..6a5a2e3e --- /dev/null +++ b/odilia/src/tower/service_ext.rs @@ -0,0 +1,52 @@ +use crate::tower::{ + async_try::{AsyncTryInto, AsyncTryIntoLayer, AsyncTryIntoService}, + iter_svc::IterService, + state_svc::{StateLayer, StateService}, + sync_try::{TryIntoLayer, TryIntoService}, + unwrap_svc::UnwrapService, +}; +use std::{convert::Infallible, sync::Arc}; +use tower::{Layer, Service}; + +pub trait ServiceExt: Service { + fn request_try_from(self) -> TryIntoService + where + Self: Sized, + I: TryInto, + Self: Service, + { + TryIntoLayer::new().layer(self) + } + fn request_async_try_from( + self, + ) -> AsyncTryIntoService + where + I: AsyncTryInto, + Self: Service + Clone, + { + AsyncTryIntoLayer::new().layer(self) + } + fn with_state(self, s: Arc) -> StateService + where + Self: Sized, + { + StateLayer::new(s).layer(self) + } + fn unwrap_map(self, f: F) -> UnwrapService + where + Self: Service + Sized, + F: FnOnce(>::Response) -> Result, + { + UnwrapService::new(self, f) + } + fn iter_into(self, s: S) -> IterService + where + Self: Service + Sized, + Iter: IntoIterator, + S: Service, + { + IterService::new(self, s) + } +} + +impl ServiceExt for T where T: Service {} diff --git a/odilia/src/tower/service_set.rs b/odilia/src/tower/service_set.rs new file mode 100644 index 00000000..29bb764d --- /dev/null +++ b/odilia/src/tower/service_set.rs @@ -0,0 +1,56 @@ +use std::future::Future; +use std::task::{Context, Poll}; +use tower::Service; + +/// A series of services which are executed in the order they are placed in the [`ServiceSet::new`] +/// initializer. +/// Useful when creating a set of handler functions that need to be run without concurrency. +/// +/// Note that although calling the [`ServiceSet::call`] function seems to return a +/// `Result, S::Error>`, the outer error is gaurenteed never to be +/// returned and can safely be unwrapped _from the caller function_. +#[derive(Clone)] +pub struct ServiceSet { + services: Vec, +} +impl Default for ServiceSet { + fn default() -> Self { + ServiceSet { services: vec![] } + } +} +impl ServiceSet { + pub fn new>(services: I) -> Self { + ServiceSet { services: services.into_iter().collect() } + } + pub fn push(&mut self, svc: S) { + self.services.push(svc); + } +} + +impl Service for ServiceSet +where + S: Service + Clone, + Req: Clone, +{ + type Response = Vec>; + type Error = S::Error; + type Future = impl Future>; + fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll> { + for svc in &mut self.services { + let _ = svc.poll_ready(cx)?; + } + Poll::Ready(Ok(())) + } + fn call(&mut self, req: Req) -> Self::Future { + let clone = self.services.clone(); + let services = std::mem::replace(&mut self.services, clone); + async move { + let mut results = vec![]; + for mut svc in services { + let result = svc.call(req.clone()).await; + results.push(result); + } + Ok(results) + } + } +} diff --git a/odilia/src/tower/state_changed.rs b/odilia/src/tower/state_changed.rs new file mode 100644 index 00000000..f832d88a --- /dev/null +++ b/odilia/src/tower/state_changed.rs @@ -0,0 +1,270 @@ +use atspi_common::{ + events::object::StateChangedEvent, AtspiError, EventProperties, State as AtspiState, +}; +use derived_deref::{Deref, DerefMut}; +use refinement::Predicate; +use std::marker::PhantomData; +use zbus::{names::UniqueName, zvariant::ObjectPath}; + +pub type Focused = StateChanged; +pub type Unfocused = StateChanged; + +#[derive(Debug, Default, Clone, Deref, DerefMut)] +pub struct StateChanged { + #[target] + ev: StateChangedEvent, + _marker: PhantomData<(S, E)>, +} +impl EventProperties for StateChanged { + fn sender(&self) -> UniqueName<'_> { + self.ev.sender() + } + fn path(&self) -> ObjectPath<'_> { + self.ev.path() + } +} +impl atspi::BusProperties for StateChanged +where + StateChanged: TryFrom, +{ + const DBUS_MEMBER: &'static str = StateChangedEvent::DBUS_MEMBER; + const DBUS_INTERFACE: &'static str = StateChangedEvent::DBUS_INTERFACE; + const MATCH_RULE_STRING: &'static str = StateChangedEvent::MATCH_RULE_STRING; + const REGISTRY_EVENT_STRING: &'static str = StateChangedEvent::REGISTRY_EVENT_STRING; + type Body = ::Body; + fn from_message_parts(or: atspi::ObjectRef, bdy: Self::Body) -> Result { + let ev = StateChangedEvent::from_message_parts(or, bdy)?; + // TODO: we do not have an appropriate event type here; this should really be an OdiliaError. + // We may want to consider adding a type Error in the BusProperties impl. + Self::try_from(ev).map_err(|_| AtspiError::InterfaceMatch(String::new())) + } + fn body(&self) -> Self::Body { + self.ev.body() + } +} + +impl TryFrom for StateChanged +where + S: Predicate, + E: Predicate, +{ + type Error = crate::OdiliaError; + fn try_from(ev: atspi::Event) -> Result { + let state_changed_ev: StateChangedEvent = ev.try_into()?; + StateChanged::::try_from(state_changed_ev) + } +} + +impl TryFrom for StateChanged +where + S: Predicate, + E: Predicate, +{ + type Error = crate::OdiliaError; + fn try_from(ev: StateChangedEvent) -> Result { + if >::test(&ev) { + Ok(Self { ev, _marker: PhantomData }) + } else { + Err(crate::OdiliaError::PredicateFailure(format!("The type {ev:?} is not compatible with the predicate requirements state = {:?} and enabled = {:?}", std::any::type_name::(), std::any::type_name::()))) + } + } +} + +impl Predicate for StateChanged +where + S: Predicate, + E: Predicate, +{ + fn test(ev: &StateChangedEvent) -> bool { + >::test(&ev.state) + && >::test(&ev.enabled) + } +} + +#[allow(unused)] +#[derive(Debug, Clone, Copy)] +pub struct True; +#[allow(unused)] +#[derive(Debug, Clone, Copy)] +pub struct False; + +impl Predicate for True { + fn test(b: &bool) -> bool { + *b + } +} +impl Predicate for False { + fn test(b: &bool) -> bool { + !*b + } +} + +macro_rules! impl_refinement_type { + ($enum:ty, $variant:expr, $name:ident) => { + #[allow(unused)] + #[derive(Debug, Clone, Copy)] + pub struct $name; + impl Predicate<$enum> for $name { + fn test(outer: &$enum) -> bool { + &$variant == outer + } + } + }; +} +#[allow(unused)] +pub struct AnyState; + +impl Predicate for AnyState { + fn test(outer: &AtspiState) -> bool { + match *outer { + AtspiState::Invalid => >::test(outer), + AtspiState::Active => >::test(outer), + AtspiState::Armed => >::test(outer), + AtspiState::Busy => >::test(outer), + AtspiState::Checked => >::test(outer), + AtspiState::Collapsed => { + >::test(outer) + } + AtspiState::Defunct => >::test(outer), + AtspiState::Editable => { + >::test(outer) + } + AtspiState::Enabled => >::test(outer), + AtspiState::Expandable => { + >::test(outer) + } + AtspiState::Expanded => { + >::test(outer) + } + AtspiState::Focusable => { + >::test(outer) + } + AtspiState::Focused => >::test(outer), + AtspiState::HasTooltip => { + >::test(outer) + } + AtspiState::Horizontal => { + >::test(outer) + } + AtspiState::Iconified => { + >::test(outer) + } + AtspiState::Modal => >::test(outer), + AtspiState::MultiLine => { + >::test(outer) + } + AtspiState::Multiselectable => { + >::test(outer) + } + AtspiState::Opaque => >::test(outer), + AtspiState::Pressed => >::test(outer), + AtspiState::Resizable => { + >::test(outer) + } + AtspiState::Selectable => { + >::test(outer) + } + AtspiState::Selected => { + >::test(outer) + } + AtspiState::Sensitive => { + >::test(outer) + } + AtspiState::Showing => >::test(outer), + AtspiState::SingleLine => { + >::test(outer) + } + AtspiState::Stale => >::test(outer), + AtspiState::Transient => { + >::test(outer) + } + AtspiState::Vertical => { + >::test(outer) + } + AtspiState::Visible => >::test(outer), + AtspiState::ManagesDescendants => { + >::test(outer) + } + AtspiState::Indeterminate => { + >::test(outer) + } + AtspiState::Required => { + >::test(outer) + } + AtspiState::Truncated => { + >::test(outer) + } + AtspiState::Animated => { + >::test(outer) + } + AtspiState::InvalidEntry => { + >::test(outer) + } + AtspiState::SupportsAutocompletion => { + >::test(outer) + } + AtspiState::SelectableText => { + >::test(outer) + } + AtspiState::IsDefault => { + >::test(outer) + } + AtspiState::Visited => >::test(outer), + AtspiState::Checkable => { + >::test(outer) + } + AtspiState::HasPopup => { + >::test(outer) + } + AtspiState::ReadOnly => { + >::test(outer) + } + _ => todo!(), + } + } +} + +impl_refinement_type!(AtspiState, AtspiState::Invalid, StateInvalid); +impl_refinement_type!(AtspiState, AtspiState::Active, StateActive); +impl_refinement_type!(AtspiState, AtspiState::Armed, StateArmed); +impl_refinement_type!(AtspiState, AtspiState::Busy, StateBusy); +impl_refinement_type!(AtspiState, AtspiState::Checked, StateChecked); +impl_refinement_type!(AtspiState, AtspiState::Collapsed, StateCollapsed); +impl_refinement_type!(AtspiState, AtspiState::Defunct, StateDefunct); +impl_refinement_type!(AtspiState, AtspiState::Editable, StateEditable); +impl_refinement_type!(AtspiState, AtspiState::Enabled, StateEnabled); +impl_refinement_type!(AtspiState, AtspiState::Expandable, StateExpandable); +impl_refinement_type!(AtspiState, AtspiState::Expanded, StateExpanded); +impl_refinement_type!(AtspiState, AtspiState::Focusable, StateFocusable); +impl_refinement_type!(AtspiState, AtspiState::Focused, StateFocused); +impl_refinement_type!(AtspiState, AtspiState::HasTooltip, StateHasTooltip); +impl_refinement_type!(AtspiState, AtspiState::Horizontal, StateHorizontal); +impl_refinement_type!(AtspiState, AtspiState::Iconified, StateIconified); +impl_refinement_type!(AtspiState, AtspiState::Modal, StateModal); +impl_refinement_type!(AtspiState, AtspiState::MultiLine, StateMultiLine); +impl_refinement_type!(AtspiState, AtspiState::Multiselectable, StateMultiselectable); +impl_refinement_type!(AtspiState, AtspiState::Opaque, StateOpaque); +impl_refinement_type!(AtspiState, AtspiState::Pressed, StatePressed); +impl_refinement_type!(AtspiState, AtspiState::Resizable, StateResizable); +impl_refinement_type!(AtspiState, AtspiState::Selectable, StateSelectable); +impl_refinement_type!(AtspiState, AtspiState::Selected, StateSelected); +impl_refinement_type!(AtspiState, AtspiState::Sensitive, StateSensitive); +impl_refinement_type!(AtspiState, AtspiState::Showing, StateShowing); +impl_refinement_type!(AtspiState, AtspiState::SingleLine, StateSingleLine); +impl_refinement_type!(AtspiState, AtspiState::Stale, StateStale); +impl_refinement_type!(AtspiState, AtspiState::Transient, StateTransient); +impl_refinement_type!(AtspiState, AtspiState::Vertical, StateVertical); +impl_refinement_type!(AtspiState, AtspiState::Visible, StateVisible); +impl_refinement_type!(AtspiState, AtspiState::ManagesDescendants, StateManagesDescendants); +impl_refinement_type!(AtspiState, AtspiState::Indeterminate, StateIndeterminate); +impl_refinement_type!(AtspiState, AtspiState::Required, StateRequired); +impl_refinement_type!(AtspiState, AtspiState::Truncated, StateTruncated); +impl_refinement_type!(AtspiState, AtspiState::Animated, StateAnimated); +impl_refinement_type!(AtspiState, AtspiState::InvalidEntry, StateInvalidEntry); +impl_refinement_type!(AtspiState, AtspiState::SupportsAutocompletion, StateSupportsAutocompletion); +impl_refinement_type!(AtspiState, AtspiState::SelectableText, StateSelectableText); +impl_refinement_type!(AtspiState, AtspiState::IsDefault, StateIsDefault); +impl_refinement_type!(AtspiState, AtspiState::Visited, StateVisited); +impl_refinement_type!(AtspiState, AtspiState::Checkable, StateCheckable); +impl_refinement_type!(AtspiState, AtspiState::HasPopup, StateHasPopup); +impl_refinement_type!(AtspiState, AtspiState::ReadOnly, StateReadOnly); diff --git a/odilia/src/tower/state_svc.rs b/odilia/src/tower/state_svc.rs new file mode 100644 index 00000000..c3d3db22 --- /dev/null +++ b/odilia/src/tower/state_svc.rs @@ -0,0 +1,49 @@ +use std::{ + sync::Arc, + task::{Context, Poll}, +}; +use tower::{Layer, Service}; + +pub struct StateLayer { + state: Arc, +} +impl StateLayer { + pub fn new(state: Arc) -> Self { + StateLayer { state } + } +} + +pub struct StateService { + inner: Srv, + state: Arc, +} +impl Clone for StateService +where + Srv: Clone, +{ + fn clone(&self) -> Self { + StateService { inner: self.inner.clone(), state: Arc::clone(&self.state) } + } +} + +impl Service for StateService +where + Srv: Service<(Arc, I)>, +{ + type Error = Srv::Error; + type Response = Srv::Response; + type Future = Srv::Future; + fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll> { + self.inner.poll_ready(cx) + } + fn call(&mut self, input: I) -> Self::Future { + self.inner.call((Arc::clone(&self.state), input)) + } +} + +impl Layer for StateLayer { + type Service = StateService; + fn layer(&self, inner: Srv) -> Self::Service { + StateService { inner, state: Arc::clone(&self.state) } + } +} diff --git a/odilia/src/tower/sync_try.rs b/odilia/src/tower/sync_try.rs new file mode 100644 index 00000000..892bb2e5 --- /dev/null +++ b/odilia/src/tower/sync_try.rs @@ -0,0 +1,65 @@ +use futures::future::{err, Either, Ready}; +use std::{ + future::Future, + marker::PhantomData, + task::{Context, Poll}, +}; +use tower::{Layer, Service}; + +pub struct TryIntoService, S, R, Fut1> { + inner: S, + _marker: PhantomData R>, +} +impl, S, R, Fut1> TryIntoService { + pub fn new(inner: S) -> Self { + TryIntoService { inner, _marker: PhantomData } + } +} +pub struct TryIntoLayer> { + _marker: PhantomData O>, +} +impl> TryIntoLayer { + pub fn new() -> Self { + TryIntoLayer { _marker: PhantomData } + } +} + +impl, O, S, Fut1> Layer for TryIntoLayer +where + S: Service, +{ + type Service = TryIntoService>::Response, Fut1>; + fn layer(&self, inner: S) -> Self::Service { + TryIntoService::new(inner) + } +} + +impl, S, R, Fut1> Clone for TryIntoService +where + S: Clone, +{ + fn clone(&self) -> Self { + TryIntoService { inner: self.inner.clone(), _marker: PhantomData } + } +} + +impl, S, R, Fut1> Service for TryIntoService +where + I: TryInto, + E: From<>::Error>, + S: Service, + Fut1: Future>, +{ + type Response = R; + type Future = Either>>; + type Error = E; + fn poll_ready(&mut self, _ctx: &mut Context<'_>) -> Poll> { + Poll::Ready(Ok(())) + } + fn call(&mut self, req: I) -> Self::Future { + match req.try_into() { + Ok(o) => Either::Left(self.inner.call(o)), + Err(e) => Either::Right(err(e.into())), + } + } +} diff --git a/odilia/src/tower/unwrap_svc.rs b/odilia/src/tower/unwrap_svc.rs new file mode 100644 index 00000000..9a902b63 --- /dev/null +++ b/odilia/src/tower/unwrap_svc.rs @@ -0,0 +1,47 @@ +use futures::FutureExt; +use std::convert::Infallible; +use std::future::Future; +use std::marker::PhantomData; +use std::task::{Context, Poll}; +use tower::Service; + +#[allow(clippy::type_complexity)] +pub struct UnwrapService { + inner: S, + f: F, + _marker: PhantomData Result>, +} +impl UnwrapService +where + S: Service, +{ + pub fn new(inner: S, f: F) -> Self { + UnwrapService { inner, f, _marker: PhantomData } + } +} +impl Clone for UnwrapService +where + S: Clone, + F: Clone, +{ + fn clone(&self) -> Self { + UnwrapService { inner: self.inner.clone(), f: self.f.clone(), _marker: PhantomData } + } +} + +impl Service for UnwrapService +where + S: Service, + E: From, + F: FnOnce(S::Response) -> Result + Clone, +{ + type Error = E; + type Response = R; + type Future = impl Future>; + fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll> { + self.inner.poll_ready(cx).map_err(Into::into) + } + fn call(&mut self, req: Req) -> Self::Future { + self.inner.call(req).map(|res| res.unwrap()).map(self.f.clone()) + } +} diff --git a/rust-toolchain.toml b/rust-toolchain.toml new file mode 100644 index 00000000..af52e536 --- /dev/null +++ b/rust-toolchain.toml @@ -0,0 +1,2 @@ +[toolchain] +channel = "nightly-2024-08-08" diff --git a/scripts/tokio_console_run.sh b/scripts/tokio_console_run.sh new file mode 100755 index 00000000..0e0f1918 --- /dev/null +++ b/scripts/tokio_console_run.sh @@ -0,0 +1,2 @@ +#!/bin/sh +RUSTFLAGS="--cfg tokio_unstable" RUST_LOG="trace" cargo run --features tokio-console diff --git a/tts/Cargo.toml b/tts/Cargo.toml index bd7fce12..0cfa7b9b 100644 --- a/tts/Cargo.toml +++ b/tts/Cargo.toml @@ -11,8 +11,9 @@ categories = ["accessibility", "api-bindings"] edition = "2021" [dependencies] -ssip-client-async.workspace = true +ssip-client-async = { version = "0.13.0", features = ["tokio"] } tokio.workspace = true tokio-util.workspace=true tracing.workspace = true eyre.workspace = true +ssip = "0.1.0"