diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 7c9b727..bc68b83 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -45,6 +45,9 @@ jobs: with: toolchain: 1.96.0 + - name: Rust Cache + uses: swatinem/rust-cache@v2 + - name: Run tests run: cargo test --locked @@ -135,6 +138,9 @@ jobs: with: toolchain: 1.96.0 + - name: Rust Cache + uses: swatinem/rust-cache@v2 + - name: Build all binaries (debug-speed compilation into target/release) env: CARGO_PROFILE_RELEASE_OPT_LEVEL: 0 diff --git a/.github/workflows/flatter.yml b/.github/workflows/flatter.yml index b526122..2ab39b6 100644 --- a/.github/workflows/flatter.yml +++ b/.github/workflows/flatter.yml @@ -60,9 +60,9 @@ jobs: echo "default-branch: ${{ steps.set_branch.outputs.branch }}" >> packages/flatpak/ru.arabianq.pwsp.yaml - name: Install SDK Extensions - run: + run: | flatpak install -y flathub org.freedesktop.Sdk.Extension.rust-stable//25.08 - org.freedesktop.Sdk.Extension.llvm20//25.08 + flatpak install -y flathub org.freedesktop.Sdk.Extension.llvm20//25.08 - name: Build Flatpak uses: andyholmes/flatter@main @@ -72,6 +72,7 @@ jobs: upload-bundles: false upload-pages-artifact: false arch: x86_64 + cache: true flatter-arm64: name: Flatter (aarch64) @@ -113,9 +114,9 @@ jobs: echo "default-branch: ${{ steps.set_branch.outputs.branch }}" >> packages/flatpak/ru.arabianq.pwsp.yaml - name: Install SDK Extensions - run: + run: | flatpak install -y flathub org.freedesktop.Sdk.Extension.rust-stable//25.08 - org.freedesktop.Sdk.Extension.llvm20//25.08 + flatpak install -y flathub org.freedesktop.Sdk.Extension.llvm20//25.08 - name: Build Flatpak uses: andyholmes/flatter@main @@ -125,6 +126,7 @@ jobs: upload-bundles: false upload-pages-artifact: true arch: aarch64 + cache: true deploy: name: Deploy to GitHub Pages diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index b2a9b80..970c785 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -97,6 +97,9 @@ jobs: with: toolchain: 1.96.0 + - name: Rust Cache + uses: swatinem/rust-cache@v2 + - name: Extract all binary names id: cargo-meta run: | @@ -180,6 +183,9 @@ jobs: with: toolchain: 1.96.0 + - name: Rust Cache + uses: swatinem/rust-cache@v2 + - name: Build all release binaries run: cargo build --release --locked @@ -254,8 +260,8 @@ jobs: run: | mkdir -p ~/.config echo "$COPR_CONFIG" > ~/.config/copr - copr-cli buildscm --clone-url https://github.com/arabianq/pipewire-soundpad.git \ + copr-cli buildscm --nowait \ + --clone-url https://github.com/arabianq/pipewire-soundpad.git \ --commit ${{ needs.prepare.outputs.tag }} \ --spec packages/rpm/pwsp.spec \ arabianq/pwsp - diff --git a/Cargo.lock b/Cargo.lock index bd805d5..784e6ac 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -878,7 +878,7 @@ dependencies = [ "libc", "option-ext", "redox_users", - "windows-sys 0.61.2", + "windows-sys 0.59.0", ] [[package]] @@ -1227,7 +1227,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.61.2", + "windows-sys 0.52.0", ] [[package]] @@ -1337,12 +1337,6 @@ version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" -[[package]] -name = "foldhash" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" - [[package]] name = "foldhash" version = "0.2.0" @@ -1519,24 +1513,11 @@ dependencies = [ "cfg-if", "js-sys", "libc", - "r-efi 5.3.0", + "r-efi", "wasip2", "wasm-bindgen", ] -[[package]] -name = "getrandom" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" -dependencies = [ - "cfg-if", - "libc", - "r-efi 6.0.0", - "wasip2", - "wasip3", -] - [[package]] name = "gl_generator" version = "0.14.0" @@ -1687,15 +1668,6 @@ dependencies = [ "zerocopy", ] -[[package]] -name = "hashbrown" -version = "0.15.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" -dependencies = [ - "foldhash 0.1.5", -] - [[package]] name = "hashbrown" version = "0.16.1" @@ -1704,7 +1676,7 @@ checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" dependencies = [ "allocator-api2", "equivalent", - "foldhash 0.2.0", + "foldhash", ] [[package]] @@ -1929,12 +1901,6 @@ dependencies = [ "zerovec", ] -[[package]] -name = "id-arena" -version = "2.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" - [[package]] name = "idna" version = "1.1.0" @@ -2027,8 +1993,6 @@ checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" dependencies = [ "equivalent", "hashbrown 0.17.1", - "serde", - "serde_core", ] [[package]] @@ -2173,12 +2137,6 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" -[[package]] -name = "leb128fmt" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" - [[package]] name = "libc" version = "0.2.186" @@ -3221,16 +3179,6 @@ dependencies = [ "zerocopy", ] -[[package]] -name = "prettyplease" -version = "0.2.37" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" -dependencies = [ - "proc-macro2", - "syn", -] - [[package]] name = "proc-macro-crate" version = "3.5.0" @@ -3435,12 +3383,6 @@ version = "5.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" -[[package]] -name = "r-efi" -version = "6.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" - [[package]] name = "radium" version = "0.7.0" @@ -3793,7 +3735,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys 0.12.1", - "windows-sys 0.61.2", + "windows-sys 0.52.0", ] [[package]] @@ -3850,7 +3792,7 @@ dependencies = [ "security-framework", "security-framework-sys", "webpki-root-certs", - "windows-sys 0.61.2", + "windows-sys 0.52.0", ] [[package]] @@ -4199,7 +4141,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "52d1cfed4120b4d927bf7c0f86d2087a4a7d6027c906d9f9d525a80573b9be51" dependencies = [ "libc", - "windows-sys 0.61.2", + "windows-sys 0.60.2", ] [[package]] @@ -4536,10 +4478,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" dependencies = [ "fastrand", - "getrandom 0.4.2", + "getrandom 0.3.4", "once_cell", "rustix 1.1.4", - "windows-sys 0.61.2", + "windows-sys 0.52.0", ] [[package]] @@ -4884,7 +4826,7 @@ checksum = "f2f6fb2847f6742cd76af783a2a2c49e9375d0a111c7bef6f71cd9e738c72d6e" dependencies = [ "memoffset", "tempfile", - "windows-sys 0.61.2", + "windows-sys 0.60.2", ] [[package]] @@ -4911,12 +4853,6 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" -[[package]] -name = "unicode-xid" -version = "0.2.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" - [[package]] name = "unsafe-libyaml" version = "0.2.11" @@ -5028,16 +4964,7 @@ version = "1.0.4+wasi-0.2.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b67efb37e106e55ce722a510d6b5f9c17f083e5fc79afc2badeb12cc313d9487" dependencies = [ - "wit-bindgen 0.57.1", -] - -[[package]] -name = "wasip3" -version = "0.4.0+wasi-0.3.0-rc-2026-01-06" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" -dependencies = [ - "wit-bindgen 0.51.0", + "wit-bindgen", ] [[package]] @@ -5095,40 +5022,6 @@ dependencies = [ "unicode-ident", ] -[[package]] -name = "wasm-encoder" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" -dependencies = [ - "leb128fmt", - "wasmparser", -] - -[[package]] -name = "wasm-metadata" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" -dependencies = [ - "anyhow", - "indexmap", - "wasm-encoder", - "wasmparser", -] - -[[package]] -name = "wasmparser" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" -dependencies = [ - "bitflags 2.13.0", - "hashbrown 0.15.5", - "indexmap", - "semver", -] - [[package]] name = "wayland-backend" version = "0.3.15" @@ -5434,7 +5327,7 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.61.2", + "windows-sys 0.52.0", ] [[package]] @@ -5792,100 +5685,12 @@ dependencies = [ "memchr", ] -[[package]] -name = "wit-bindgen" -version = "0.51.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" -dependencies = [ - "wit-bindgen-rust-macro", -] - [[package]] name = "wit-bindgen" version = "0.57.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" -[[package]] -name = "wit-bindgen-core" -version = "0.51.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" -dependencies = [ - "anyhow", - "heck", - "wit-parser", -] - -[[package]] -name = "wit-bindgen-rust" -version = "0.51.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" -dependencies = [ - "anyhow", - "heck", - "indexmap", - "prettyplease", - "syn", - "wasm-metadata", - "wit-bindgen-core", - "wit-component", -] - -[[package]] -name = "wit-bindgen-rust-macro" -version = "0.51.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a" -dependencies = [ - "anyhow", - "prettyplease", - "proc-macro2", - "quote", - "syn", - "wit-bindgen-core", - "wit-bindgen-rust", -] - -[[package]] -name = "wit-component" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" -dependencies = [ - "anyhow", - "bitflags 2.13.0", - "indexmap", - "log", - "serde", - "serde_derive", - "serde_json", - "wasm-encoder", - "wasm-metadata", - "wasmparser", - "wit-parser", -] - -[[package]] -name = "wit-parser" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" -dependencies = [ - "anyhow", - "id-arena", - "indexmap", - "log", - "semver", - "serde", - "serde_derive", - "serde_json", - "unicode-xid", - "wasmparser", -] - [[package]] name = "writeable" version = "0.6.3" diff --git a/packages/flatpak/cargo-sources.json b/packages/flatpak/cargo-sources.json index 68328a1..62bc209 100644 --- a/packages/flatpak/cargo-sources.json +++ b/packages/flatpak/cargo-sources.json @@ -1732,19 +1732,6 @@ "dest": "cargo/vendor/fnv-1.0.7", "dest-filename": ".cargo-checksum.json" }, - { - "type": "archive", - "archive-type": "tar-gzip", - "url": "https://static.crates.io/crates/foldhash/foldhash-0.1.5.crate", - "sha256": "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2", - "dest": "cargo/vendor/foldhash-0.1.5" - }, - { - "type": "inline", - "contents": "{\"package\": \"d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2\", \"files\": {}}", - "dest": "cargo/vendor/foldhash-0.1.5", - "dest-filename": ".cargo-checksum.json" - }, { "type": "archive", "archive-type": "tar-gzip", @@ -2005,19 +1992,6 @@ "dest": "cargo/vendor/getrandom-0.3.4", "dest-filename": ".cargo-checksum.json" }, - { - "type": "archive", - "archive-type": "tar-gzip", - "url": "https://static.crates.io/crates/getrandom/getrandom-0.4.2.crate", - "sha256": "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555", - "dest": "cargo/vendor/getrandom-0.4.2" - }, - { - "type": "inline", - "contents": "{\"package\": \"0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555\", \"files\": {}}", - "dest": "cargo/vendor/getrandom-0.4.2", - "dest-filename": ".cargo-checksum.json" - }, { "type": "archive", "archive-type": "tar-gzip", @@ -2174,19 +2148,6 @@ "dest": "cargo/vendor/half-2.7.1", "dest-filename": ".cargo-checksum.json" }, - { - "type": "archive", - "archive-type": "tar-gzip", - "url": "https://static.crates.io/crates/hashbrown/hashbrown-0.15.5.crate", - "sha256": "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1", - "dest": "cargo/vendor/hashbrown-0.15.5" - }, - { - "type": "inline", - "contents": "{\"package\": \"9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1\", \"files\": {}}", - "dest": "cargo/vendor/hashbrown-0.15.5", - "dest-filename": ".cargo-checksum.json" - }, { "type": "archive", "archive-type": "tar-gzip", @@ -2460,19 +2421,6 @@ "dest": "cargo/vendor/icu_provider-2.2.0", "dest-filename": ".cargo-checksum.json" }, - { - "type": "archive", - "archive-type": "tar-gzip", - "url": "https://static.crates.io/crates/id-arena/id-arena-2.3.0.crate", - "sha256": "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954", - "dest": "cargo/vendor/id-arena-2.3.0" - }, - { - "type": "inline", - "contents": "{\"package\": \"3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954\", \"files\": {}}", - "dest": "cargo/vendor/id-arena-2.3.0", - "dest-filename": ".cargo-checksum.json" - }, { "type": "archive", "archive-type": "tar-gzip", @@ -2772,19 +2720,6 @@ "dest": "cargo/vendor/lazy_static-1.5.0", "dest-filename": ".cargo-checksum.json" }, - { - "type": "archive", - "archive-type": "tar-gzip", - "url": "https://static.crates.io/crates/leb128fmt/leb128fmt-0.1.0.crate", - "sha256": "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2", - "dest": "cargo/vendor/leb128fmt-0.1.0" - }, - { - "type": "inline", - "contents": "{\"package\": \"09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2\", \"files\": {}}", - "dest": "cargo/vendor/leb128fmt-0.1.0", - "dest-filename": ".cargo-checksum.json" - }, { "type": "archive", "archive-type": "tar-gzip", @@ -4124,19 +4059,6 @@ "dest": "cargo/vendor/ppv-lite86-0.2.21", "dest-filename": ".cargo-checksum.json" }, - { - "type": "archive", - "archive-type": "tar-gzip", - "url": "https://static.crates.io/crates/prettyplease/prettyplease-0.2.37.crate", - "sha256": "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b", - "dest": "cargo/vendor/prettyplease-0.2.37" - }, - { - "type": "inline", - "contents": "{\"package\": \"479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b\", \"files\": {}}", - "dest": "cargo/vendor/prettyplease-0.2.37", - "dest-filename": ".cargo-checksum.json" - }, { "type": "archive", "archive-type": "tar-gzip", @@ -4306,19 +4228,6 @@ "dest": "cargo/vendor/r-efi-5.3.0", "dest-filename": ".cargo-checksum.json" }, - { - "type": "archive", - "archive-type": "tar-gzip", - "url": "https://static.crates.io/crates/r-efi/r-efi-6.0.0.crate", - "sha256": "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf", - "dest": "cargo/vendor/r-efi-6.0.0" - }, - { - "type": "inline", - "contents": "{\"package\": \"f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf\", \"files\": {}}", - "dest": "cargo/vendor/r-efi-6.0.0", - "dest-filename": ".cargo-checksum.json" - }, { "type": "archive", "archive-type": "tar-gzip", @@ -6170,19 +6079,6 @@ "dest": "cargo/vendor/unicode-width-0.2.2", "dest-filename": ".cargo-checksum.json" }, - { - "type": "archive", - "archive-type": "tar-gzip", - "url": "https://static.crates.io/crates/unicode-xid/unicode-xid-0.2.6.crate", - "sha256": "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853", - "dest": "cargo/vendor/unicode-xid-0.2.6" - }, - { - "type": "inline", - "contents": "{\"package\": \"ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853\", \"files\": {}}", - "dest": "cargo/vendor/unicode-xid-0.2.6", - "dest-filename": ".cargo-checksum.json" - }, { "type": "archive", "archive-type": "tar-gzip", @@ -6355,22 +6251,9 @@ { "type": "archive", "archive-type": "tar-gzip", - "url": "https://static.crates.io/crates/wasip3/wasip3-0.4.0+wasi-0.3.0-rc-2026-01-06.crate", - "sha256": "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5", - "dest": "cargo/vendor/wasip3-0.4.0+wasi-0.3.0-rc-2026-01-06" - }, - { - "type": "inline", - "contents": "{\"package\": \"5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5\", \"files\": {}}", - "dest": "cargo/vendor/wasip3-0.4.0+wasi-0.3.0-rc-2026-01-06", - "dest-filename": ".cargo-checksum.json" - }, - { - "type": "archive", - "archive-type": "tar-gzip", - "url": "https://static.crates.io/crates/wasm-bindgen/wasm-bindgen-0.2.125.crate", - "sha256": "8ddb3f79143bced6de84270411622a2699cee572fc0875aeaf1e7867cf9fca1a", - "dest": "cargo/vendor/wasm-bindgen-0.2.125" + "url": "https://static.crates.io/crates/wasm-bindgen/wasm-bindgen-0.2.122.crate", + "sha256": "3ed04576f974d2b2fba0f38c51dbc5518011e38c36bf1143164be765528fd409", + "dest": "cargo/vendor/wasm-bindgen-0.2.122" }, { "type": "inline", @@ -6430,45 +6313,6 @@ "dest": "cargo/vendor/wasm-bindgen-shared-0.2.125", "dest-filename": ".cargo-checksum.json" }, - { - "type": "archive", - "archive-type": "tar-gzip", - "url": "https://static.crates.io/crates/wasm-encoder/wasm-encoder-0.244.0.crate", - "sha256": "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319", - "dest": "cargo/vendor/wasm-encoder-0.244.0" - }, - { - "type": "inline", - "contents": "{\"package\": \"990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319\", \"files\": {}}", - "dest": "cargo/vendor/wasm-encoder-0.244.0", - "dest-filename": ".cargo-checksum.json" - }, - { - "type": "archive", - "archive-type": "tar-gzip", - "url": "https://static.crates.io/crates/wasm-metadata/wasm-metadata-0.244.0.crate", - "sha256": "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909", - "dest": "cargo/vendor/wasm-metadata-0.244.0" - }, - { - "type": "inline", - "contents": "{\"package\": \"bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909\", \"files\": {}}", - "dest": "cargo/vendor/wasm-metadata-0.244.0", - "dest-filename": ".cargo-checksum.json" - }, - { - "type": "archive", - "archive-type": "tar-gzip", - "url": "https://static.crates.io/crates/wasmparser/wasmparser-0.244.0.crate", - "sha256": "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe", - "dest": "cargo/vendor/wasmparser-0.244.0" - }, - { - "type": "inline", - "contents": "{\"package\": \"47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe\", \"files\": {}}", - "dest": "cargo/vendor/wasmparser-0.244.0", - "dest-filename": ".cargo-checksum.json" - }, { "type": "archive", "archive-type": "tar-gzip", @@ -7249,19 +7093,6 @@ "dest": "cargo/vendor/winnow-1.0.3", "dest-filename": ".cargo-checksum.json" }, - { - "type": "archive", - "archive-type": "tar-gzip", - "url": "https://static.crates.io/crates/wit-bindgen/wit-bindgen-0.51.0.crate", - "sha256": "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5", - "dest": "cargo/vendor/wit-bindgen-0.51.0" - }, - { - "type": "inline", - "contents": "{\"package\": \"d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5\", \"files\": {}}", - "dest": "cargo/vendor/wit-bindgen-0.51.0", - "dest-filename": ".cargo-checksum.json" - }, { "type": "archive", "archive-type": "tar-gzip", @@ -7275,71 +7106,6 @@ "dest": "cargo/vendor/wit-bindgen-0.57.1", "dest-filename": ".cargo-checksum.json" }, - { - "type": "archive", - "archive-type": "tar-gzip", - "url": "https://static.crates.io/crates/wit-bindgen-core/wit-bindgen-core-0.51.0.crate", - "sha256": "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc", - "dest": "cargo/vendor/wit-bindgen-core-0.51.0" - }, - { - "type": "inline", - "contents": "{\"package\": \"ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc\", \"files\": {}}", - "dest": "cargo/vendor/wit-bindgen-core-0.51.0", - "dest-filename": ".cargo-checksum.json" - }, - { - "type": "archive", - "archive-type": "tar-gzip", - "url": "https://static.crates.io/crates/wit-bindgen-rust/wit-bindgen-rust-0.51.0.crate", - "sha256": "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21", - "dest": "cargo/vendor/wit-bindgen-rust-0.51.0" - }, - { - "type": "inline", - "contents": "{\"package\": \"b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21\", \"files\": {}}", - "dest": "cargo/vendor/wit-bindgen-rust-0.51.0", - "dest-filename": ".cargo-checksum.json" - }, - { - "type": "archive", - "archive-type": "tar-gzip", - "url": "https://static.crates.io/crates/wit-bindgen-rust-macro/wit-bindgen-rust-macro-0.51.0.crate", - "sha256": "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a", - "dest": "cargo/vendor/wit-bindgen-rust-macro-0.51.0" - }, - { - "type": "inline", - "contents": "{\"package\": \"0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a\", \"files\": {}}", - "dest": "cargo/vendor/wit-bindgen-rust-macro-0.51.0", - "dest-filename": ".cargo-checksum.json" - }, - { - "type": "archive", - "archive-type": "tar-gzip", - "url": "https://static.crates.io/crates/wit-component/wit-component-0.244.0.crate", - "sha256": "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2", - "dest": "cargo/vendor/wit-component-0.244.0" - }, - { - "type": "inline", - "contents": "{\"package\": \"9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2\", \"files\": {}}", - "dest": "cargo/vendor/wit-component-0.244.0", - "dest-filename": ".cargo-checksum.json" - }, - { - "type": "archive", - "archive-type": "tar-gzip", - "url": "https://static.crates.io/crates/wit-parser/wit-parser-0.244.0.crate", - "sha256": "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736", - "dest": "cargo/vendor/wit-parser-0.244.0" - }, - { - "type": "inline", - "contents": "{\"package\": \"ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736\", \"files\": {}}", - "dest": "cargo/vendor/wit-parser-0.244.0", - "dest-filename": ".cargo-checksum.json" - }, { "type": "archive", "archive-type": "tar-gzip", diff --git a/pwsp-daemon/src/main.rs b/pwsp-daemon/src/main.rs index 87cc27e..1129806 100644 --- a/pwsp-daemon/src/main.rs +++ b/pwsp-daemon/src/main.rs @@ -28,7 +28,10 @@ async fn main() -> Result<()> { } get_daemon_config(); // Initialize daemon config - create_virtual_mic()?; + + // Virtual mic object must be kept alive by some variable until daemon exits + let _virtual_mic = create_virtual_mic().await?; + if let Err(err) = get_audio_player().await { eprintln!("Failed to initialize audio player: {}", err); } // Initialize audio player diff --git a/pwsp-lib/src/types/audio_player.rs b/pwsp-lib/src/types/audio_player.rs index 0f74193..f03fd93 100644 --- a/pwsp-lib/src/types/audio_player.rs +++ b/pwsp-lib/src/types/audio_player.rs @@ -1,8 +1,8 @@ use crate::{ - types::pipewire::{DeviceType, Terminate}, + types::pipewire::DeviceType, utils::{ daemon::get_daemon_config, - pipewire::{create_link, get_device, link_player_to_virtual_mic}, + pipewire::{PwTerminator, create_link, get_device, link_player_to_virtual_mic}, }, }; use anyhow::{Result, anyhow}; @@ -58,8 +58,8 @@ pub struct AudioPlayer { pub tracks: HashMap, pub next_id: u32, - input_link_sender: Option>, - player_link_sender: Option>, + input_link_sender: Option, + player_link_sender: Option, pub input_device_name: Option, pub volume: f32, // Master volume @@ -108,24 +108,16 @@ impl AudioPlayer { } fn abort_link_thread(&mut self) { - if let Some(sender) = &self.input_link_sender { - if sender.send(Terminate {}).is_ok() { - println!("Sent terminate signal to input link thread"); - self.input_link_sender = None; - } else { - eprintln!("Failed to send terminate signal to input link thread"); - } + if self.input_link_sender.is_some() { + println!("Sent terminate signal to input link thread"); + self.input_link_sender = None; } } fn abort_player_link_thread(&mut self) { - if let Some(sender) = &self.player_link_sender { - if sender.send(Terminate {}).is_ok() { - println!("Sent terminate signal to player link thread"); - self.player_link_sender = None; - } else { - eprintln!("Failed to send terminate signal to player link thread"); - } + if self.player_link_sender.is_some() { + println!("Sent terminate signal to player link thread"); + self.player_link_sender = None; } } @@ -187,7 +179,7 @@ impl AudioPlayer { return Ok(()); }; - self.input_link_sender = Some(create_link(output_fl, output_fr, input_fl, input_fr)?); + self.input_link_sender = Some(create_link(output_fl, output_fr, input_fl, input_fr).await?); Ok(()) } diff --git a/pwsp-lib/src/utils/pipewire.rs b/pwsp-lib/src/utils/pipewire.rs index 289493d..db9f485 100644 --- a/pwsp-lib/src/utils/pipewire.rs +++ b/pwsp-lib/src/utils/pipewire.rs @@ -1,14 +1,224 @@ -use crate::types::pipewire::{AudioDevice, DeviceType, Port, Terminate}; +use crate::types::pipewire::{AudioDevice, DeviceType, Port}; use anyhow::{Result, anyhow}; use pipewire::{ context::ContextRc, link::Link, main_loop::MainLoopRc, properties::properties, registry::GlobalObject, spa::utils::dict::DictRef, }; -use std::{collections::HashMap, thread}; -use tokio::{ - sync::mpsc, - time::{Duration, timeout}, -}; +use std::{cell::RefCell, collections::HashMap, rc::Rc, sync::OnceLock, thread}; +use tokio::sync::oneshot; + +pub enum PwCommand { + GetDevices { + resp: oneshot::Sender<(Vec, Vec)>, + }, + CreateVirtualMic { + resp: oneshot::Sender>, + }, + CreateLink { + output_fl: Port, + output_fr: Port, + input_fl: Port, + input_fr: Port, + resp: oneshot::Sender>, + }, + DestroyObject { + id: u32, + }, +} + +struct AppState { + input_devices: HashMap, + output_devices: HashMap, + ports: HashMap, + proxies: HashMap>, + proxy_id_counter: u32, + ready_tx: Option>, +} + +pub struct PipewireManager { + pub sender: pipewire::channel::Sender, +} + +static MANAGER: OnceLock = OnceLock::new(); + +pub fn get_manager() -> &'static PipewireManager { + MANAGER.get_or_init(|| { + let (pw_sender, pw_receiver) = pipewire::channel::channel::(); + let (ready_tx, ready_rx) = std::sync::mpsc::channel(); + + thread::spawn(move || { + let (main_loop, context) = setup_pipewire_context().expect("Failed to setup pipewire"); + + // Leak main_loop and context so their borrows can be 'static + let main_loop = Box::leak(Box::new(main_loop)); + let context = Box::leak(Box::new(context)); + + // Leak to fix lifetime issues since this thread lives forever + let core = Box::leak(Box::new( + context + .connect(None) + .expect("Failed to connect to pipewire"), + )); + let registry = Box::leak(Box::new( + core.get_registry().expect("Failed to get registry"), + )); + + let state = Rc::new(RefCell::new(AppState { + input_devices: HashMap::new(), + output_devices: HashMap::new(), + ports: HashMap::new(), + proxies: HashMap::new(), + proxy_id_counter: 10000, + ready_tx: Some(ready_tx), + })); + + let state_for_registry_add = state.clone(); + let state_for_registry_remove = state.clone(); + + let _listener = registry + .add_listener_local() + .global(move |global| { + let (device, port) = parse_global_object(global); + let mut s = state_for_registry_add.borrow_mut(); + if let Some(device) = device { + match device.device_type { + DeviceType::Input => { + s.input_devices.insert(device.id, device); + } + DeviceType::Output => { + s.output_devices.insert(device.id, device); + } + } + } else if let Some(port) = port { + let node_id = port.node_id; + s.ports.insert(port.port_id, port.clone()); + if let Some(d) = s.input_devices.get_mut(&node_id) { + d.add_port(port.clone()); + } else if let Some(d) = s.output_devices.get_mut(&node_id) { + d.add_port(port); + } + } + }) + .global_remove(move |id| { + let mut s = state_for_registry_remove.borrow_mut(); + s.input_devices.remove(&id); + s.output_devices.remove(&id); + s.ports.retain(|_, port| port.node_id != id); + s.ports.remove(&id); + }) + .register(); + + // sync to signal ready + let state_for_sync = state.clone(); + let _core_listener = core + .add_listener_local() + .done(move |id, _seq| { + if id == 0 { + let mut s = state_for_sync.borrow_mut(); + if let Some(tx) = s.ready_tx.take() { + let _ = tx.send(()); + } + } + }) + .register(); + + let _pending = core.sync(0).expect("sync failed"); + + let state_for_cmd = state.clone(); + let _receiver = pw_receiver.attach(main_loop.loop_(), move |cmd| { + let mut s = state_for_cmd.borrow_mut(); + match cmd { + PwCommand::GetDevices { resp } => { + let mut inputs: Vec = + s.input_devices.values().cloned().collect(); + let mut outputs: Vec = + s.output_devices.values().cloned().collect(); + inputs.sort_by_key(|a| a.id); + outputs.sort_by_key(|a| a.id); + let _ = resp.send((inputs, outputs)); + } + PwCommand::CreateVirtualMic { resp } => { + let props = properties!( + "factory.name" => "support.null-audio-sink", + "node.name" => "pwsp-virtual-mic", + "node.description" => "PWSP Virtual Mic", + "media.class" => "Audio/Source/Virtual", + "audio.position" => "[ FL FR ]", + "audio.channels" => "2", + "object.linger" => "false", + ); + match core.create_object::("adapter", &props) { + Ok(node) => { + s.proxy_id_counter += 1; + let id = s.proxy_id_counter; + s.proxies.insert(id, Box::new(node)); + let _ = resp.send(Ok(id)); + } + Err(e) => { + let _ = resp.send(Err(e.to_string())); + } + } + } + PwCommand::CreateLink { + output_fl, + output_fr, + input_fl, + input_fr, + resp, + } => { + let props_fl = properties! { + "link.output.node" => format!("{}", output_fl.node_id).as_str(), + "link.output.port" => format!("{}", output_fl.port_id).as_str(), + "link.input.node" => format!("{}", input_fl.node_id).as_str(), + "link.input.port" => format!("{}", input_fl.port_id).as_str(), + }; + let props_fr = properties! { + "link.output.node" => format!("{}", output_fr.node_id).as_str(), + "link.output.port" => format!("{}", output_fr.port_id).as_str(), + "link.input.node" => format!("{}", input_fr.node_id).as_str(), + "link.input.port" => format!("{}", input_fr.port_id).as_str(), + }; + + let link_fl = match core.create_object::("link-factory", &props_fl) { + Ok(link) => link, + Err(e) => { + let _ = resp.send(Err(e.to_string())); + return; + } + }; + let link_fr = match core.create_object::("link-factory", &props_fr) { + Ok(link) => link, + Err(e) => { + let _ = resp.send(Err(e.to_string())); + return; + } + }; + + s.proxy_id_counter += 1; + let id_fl = s.proxy_id_counter; + s.proxies.insert(id_fl, Box::new(link_fl)); + + s.proxy_id_counter += 1; + let id_fr = s.proxy_id_counter; + s.proxies.insert(id_fr, Box::new(link_fr)); + + let _ = resp.send(Ok((id_fl, id_fr))); + } + PwCommand::DestroyObject { id } => { + s.proxies.remove(&id); + } + } + }); + + main_loop.run(); + }); + + // Wait for the pipewire thread to be fully up and processed initial events + let _ = ready_rx.recv(); + + PipewireManager { sender: pw_sender } + }) +} pub fn setup_pipewire_context() -> Result<(MainLoopRc, ContextRc), String> { pipewire::init(); @@ -71,127 +281,17 @@ fn parse_global_object( (None, None) } -async fn pw_get_global_objects_thread( - main_sender: mpsc::Sender<(Option, Option)>, - pw_receiver: pipewire::channel::Receiver, - init_sender: tokio::sync::oneshot::Sender>, -) { - let (main_loop, context) = match setup_pipewire_context() { - Ok(res) => res, - Err(e) => { - let _ = init_sender.send(Err(e)); - return; - } - }; - - // Stop main loop on Terminate message - let _receiver = pw_receiver.attach(main_loop.loop_(), { - let _main_loop = main_loop.clone(); - move |_| _main_loop.quit() - }); - - let core = match context.connect(None) { - Ok(core) => core, - Err(e) => { - let _ = init_sender.send(Err(format!("Failed to connect to pipewire context: {}", e))); - return; - } - }; - - let registry = match core.get_registry() { - Ok(registry) => registry, - Err(e) => { - let _ = init_sender.send(Err(format!( - "Failed to get registry from pipewire context: {}", - e - ))); - return; - } - }; - - let _listener = registry - .add_listener_local() - .global(move |global| { - // Try to parse every global object pipewire finds - let (device, port) = parse_global_object(global); - - // Send message to the main thread - let sender_clone = main_sender.clone(); - tokio::task::spawn(async move { - sender_clone.send((device, port)).await.ok(); - }); - }) - .register(); - - // Signal successful initialization - if init_sender.send(Ok(())).is_err() { - return; - } - - main_loop.run(); -} - pub async fn get_all_devices() -> Result<(Vec, Vec)> { - // Channels to communicate with pipewire thread - let (main_sender, mut main_receiver) = mpsc::channel(10); - let (pw_sender, pw_receiver) = pipewire::channel::channel(); - let (init_sender, init_receiver) = tokio::sync::oneshot::channel(); - - // Spawn pipewire thread in background - let _pw_thread = tokio::spawn(async move { - pw_get_global_objects_thread(main_sender, pw_receiver, init_sender).await - }); - - // Wait for initialization to complete - if let Err(e) = init_receiver.await { - return Err(anyhow!(e)); - } - - let mut input_devices: HashMap = HashMap::new(); - let mut output_devices: HashMap = HashMap::new(); - let mut ports: Vec = vec![]; - - loop { - // If we don't receive a message in 100ms, we can assume that pipewire thread is finished - match timeout(Duration::from_millis(100), main_receiver.recv()).await { - Ok(Some((device, port))) => { - if let Some(device) = device { - match device.device_type { - DeviceType::Input => { - input_devices.insert(device.id, device); - } - DeviceType::Output => { - output_devices.insert(device.id, device); - } - } - } else if let Some(port) = port { - ports.push(port); - } - } - Ok(None) | Err(_) => { - // Pipewire thread is finished and we can collect our devices - let _ = pw_sender.send(Terminate {}); - - for port in ports { - let node_id = port.node_id; - - if let Some(input_device) = input_devices.get_mut(&node_id) { - input_device.add_port(port); - } else if let Some(output_device) = output_devices.get_mut(&node_id) { - output_device.add_port(port); - } - } - - let mut input_devices: Vec = input_devices.into_values().collect(); - let mut output_devices: Vec = output_devices.into_values().collect(); - - input_devices.sort_by_key(|a| a.id); - output_devices.sort_by_key(|a| a.id); - - return Ok((input_devices, output_devices)); - } - } - } + let (tx, rx) = oneshot::channel(); + let manager = get_manager(); + manager + .sender + .send(PwCommand::GetDevices { resp: tx }) + .map_err(|_| anyhow!("Failed to send GetDevices to manager"))?; + let res = rx + .await + .map_err(|e| anyhow!("Failed to receive response: {}", e))?; + Ok(res) } pub async fn get_device(device_name: &str) -> Result { @@ -209,65 +309,36 @@ pub async fn get_device(device_name: &str) -> Result { .ok_or_else(|| anyhow!("Device not found")) } -pub fn create_virtual_mic() -> Result> { - let (pw_sender, pw_receiver) = pipewire::channel::channel::(); - let (init_sender, init_receiver) = std::sync::mpsc::sync_channel(0); - - let _pw_thread = thread::spawn(move || { - let (main_loop, context) = match setup_pipewire_context() { - Ok(res) => res, - Err(e) => { - let _ = init_sender.send(Err(e)); - return; - } - }; - let core = match context.connect(None) { - Ok(core) => core, - Err(e) => { - let _ = - init_sender.send(Err(format!("Failed to connect to pipewire context: {}", e))); - return; - } - }; - - let props = properties!( - "factory.name" => "support.null-audio-sink", - "node.name" => "pwsp-virtual-mic", - "node.description" => "PWSP Virtual Mic", - "media.class" => "Audio/Source/Virtual", - "audio.position" => "[ FL FR ]", - "audio.channels" => "2", - "object.linger" => "false", // Destroy the node on app exit - ); - - let _node = match core.create_object::("adapter", &props) { - Ok(node) => node, - Err(e) => { - let _ = init_sender.send(Err(format!("Failed to create virtual mic: {}", e))); - return; - } - }; - - let _receiver = pw_receiver.attach(main_loop.loop_(), { - let _main_loop = main_loop.clone(); - move |_| _main_loop.quit() - }); - - println!("Virtual mic created"); - if init_sender.send(Ok(())).is_err() { - return; - } - main_loop.run(); - }); - - if let Err(e) = init_receiver.recv()? { - return Err(anyhow!(e)); - } - - Ok(pw_sender) +pub struct PwTerminator { + ids: Vec, } -pub async fn link_player_to_virtual_mic() -> Result> { +impl Drop for PwTerminator { + fn drop(&mut self) { + let manager = get_manager(); + for id in &self.ids { + let _ = manager.sender.send(PwCommand::DestroyObject { id: *id }); + } + } +} + +pub async fn create_virtual_mic() -> Result { + let (tx, rx) = oneshot::channel(); + let manager = get_manager(); + manager + .sender + .send(PwCommand::CreateVirtualMic { resp: tx }) + .map_err(|_| anyhow!("Failed to send CreateVirtualMic to manager"))?; + + let res = rx + .await + .map_err(|e| anyhow!("Failed to receive response: {}", e))?; + + let id = res.map_err(|e| anyhow!(e))?; + Ok(PwTerminator { ids: vec![id] }) +} + +pub async fn link_player_to_virtual_mic() -> Result { let pwsp_daemon_output = match get_device("pwsp-daemon").await { Ok(device) => device, Err(_) => { @@ -303,81 +374,34 @@ pub async fn link_player_to_virtual_mic() -> Result return Err(anyhow!("Failed to get pwsp-virtual-mic input_fr")), }; - create_link(output_fl, output_fr, input_fl, input_fr) + create_link(output_fl, output_fr, input_fl, input_fr).await } -pub fn create_link( +pub async fn create_link( output_fl: Port, output_fr: Port, input_fl: Port, input_fr: Port, -) -> Result> { - let (pw_sender, pw_receiver) = pipewire::channel::channel::(); - let (init_sender, init_receiver) = std::sync::mpsc::sync_channel(0); +) -> Result { + let (tx, rx) = oneshot::channel(); + let manager = get_manager(); + manager + .sender + .send(PwCommand::CreateLink { + output_fl, + output_fr, + input_fl, + input_fr, + resp: tx, + }) + .map_err(|_| anyhow!("Failed to send CreateLink to manager"))?; - let _pw_thread = thread::spawn(move || { - let (main_loop, context) = match setup_pipewire_context() { - Ok(res) => res, - Err(e) => { - let _ = init_sender.send(Err(e)); - return; - } - }; - let core = match context.connect(None) { - Ok(core) => core, - Err(e) => { - let _ = - init_sender.send(Err(format!("Failed to connect to pipewire context: {}", e))); - return; - } - }; + let res = rx + .await + .map_err(|e| anyhow!("Failed to receive response: {}", e))?; - let props_fl = properties! { - "link.output.node" => format!("{}", output_fl.node_id).as_str(), - "link.output.port" => format!("{}", output_fl.port_id).as_str(), - "link.input.node" => format!("{}", input_fl.node_id).as_str(), - "link.input.port" => format!("{}", input_fl.port_id).as_str(), - }; - let props_fr = properties! { - "link.output.node" => format!("{}", output_fr.node_id).as_str(), - "link.output.port" => format!("{}", output_fr.port_id).as_str(), - "link.input.node" => format!("{}", input_fr.node_id).as_str(), - "link.input.port" => format!("{}", input_fr.port_id).as_str(), - }; - - let _link_fl = match core.create_object::("link-factory", &props_fl) { - Ok(link) => link, - Err(e) => { - let _ = init_sender.send(Err(format!("Failed to create link FL: {}", e))); - return; - } - }; - let _link_fr = match core.create_object::("link-factory", &props_fr) { - Ok(link) => link, - Err(e) => { - let _ = init_sender.send(Err(format!("Failed to create link FR: {}", e))); - return; - } - }; - - let _receiver = pw_receiver.attach(main_loop.loop_(), { - let _main_loop = main_loop.clone(); - move |_| _main_loop.quit() - }); - - println!( - "Link created: FL: {}-{} FR: {}-{}", - output_fl.node_id, input_fl.node_id, output_fr.node_id, input_fr.node_id - ); - if init_sender.send(Ok(())).is_err() { - return; - } - main_loop.run(); - }); - - if let Err(e) = init_receiver.recv()? { - return Err(anyhow!(e)); - } - - Ok(pw_sender) + let (id_fl, id_fr) = res.map_err(|e| anyhow!(e))?; + Ok(PwTerminator { + ids: vec![id_fl, id_fr], + }) }