Compare commits

..

9 Commits

Author SHA1 Message Date
arabianq 3693a678ea new icon 2026-06-01 23:20:11 +03:00
arabianq 5511d23c3e deps: update cargo-sources.json 2026-06-01 23:05:02 +03:00
arabianq 818cd8b50d deps: cargo update 2026-06-01 23:04:42 +03:00
arabianq 6f7d631e28 deps: update rodio 2026-06-01 23:03:21 +03:00
arabianq 18904052c7 change version to 1.10.0 2026-06-01 23:01:32 +03:00
Tarasov Aleksandr 6841d8d1c3 refactor(gui): break down monolithic draw_footer into helper methods (#127)
Split the long continuous block in `draw_footer` into smaller,
modular methods (`draw_mic_selection`, `draw_master_volume`,
`draw_hotkeys_button`, `draw_settings_button`) for better
readability and maintainability.

Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com>
2026-06-01 22:56:37 +03:00
Tarasov Aleksandr 105be87222 refactor(daemon): Refactor commands_loop to use handle_connection (#126)
- Extracted the main token processing loop body in `commands_loop` into `handle_connection` to resolve deep nesting and improve code readability.
- Improved request reading logic by using `(&mut stream).take(request_len as u64).read_to_end(&mut buffer)` to strictly bound allocation to `request_len` and prevent initialization overhead.
- Passed `cargo fmt` and `cargo clippy`.

Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com>
2026-06-01 22:54:27 +03:00
Tarasov Aleksandr 0f8abbc443 refactor(daemon): Refactor src/utils/pipewire.rs to flatten deep conditionals and consolidate logic. (#124)
Moved redundant struct initialization into `AudioDevice::new` and unified port mapping assignments in an `add_port` method. This removes nesting using early returns and eliminates an unnecessary clone on the hashmap conversion step.

Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com>
2026-06-01 22:51:58 +03:00
Tarasov Aleksandr 54011e7ff1 fix(daemon): Replace unwrap with safe Option handling in audio_player (#125)
The `play` method in `src/types/audio_player.rs` previously used `.unwrap()`
directly on `self.stream_handle.as_ref()`. This posed a security/stability risk
where if `stream_handle` was uninitialized or became `None` unexpectedly
despite the prior `ensure_stream()` call, it would cause the thread to panic
and potentially crash the application.

This commit replaces the `.unwrap()` call with `.ok_or_else` to safely handle
the `None` case, returning an `anyhow` error instead of panicking, adhering to
the project's no-panic policy.

Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com>
2026-06-01 22:48:08 +03:00
15 changed files with 431 additions and 398 deletions
Generated
+52 -49
View File
@@ -353,7 +353,7 @@ dependencies = [
"quote", "quote",
"regex", "regex",
"rustc-hash 2.1.2", "rustc-hash 2.1.2",
"shlex", "shlex 1.3.0",
"syn", "syn",
] ]
@@ -529,14 +529,14 @@ dependencies = [
[[package]] [[package]]
name = "cc" name = "cc"
version = "1.2.62" version = "1.2.63"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a1dce859f0832a7d088c4f1119888ab94ef4b5d6795d1ce05afb7fe159d79f98" checksum = "556e016178bb5662a08681bbe0f00f8e17631781a4dfc8c45e466e4b185ec27f"
dependencies = [ dependencies = [
"find-msvc-tools", "find-msvc-tools",
"jobserver", "jobserver",
"libc", "libc",
"shlex", "shlex 2.0.1",
] ]
[[package]] [[package]]
@@ -550,9 +550,9 @@ dependencies = [
[[package]] [[package]]
name = "cfg-expr" name = "cfg-expr"
version = "0.20.7" version = "0.20.8"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3c6b04e07d8080154ed4ac03546d9a2b303cc2fe1901ba0b35b301516e289368" checksum = "fb693542bcafa528e198be0ebd9d3632ca5b7c93dbe7237460e199910835997c"
dependencies = [ dependencies = [
"smallvec", "smallvec",
"target-lexicon", "target-lexicon",
@@ -776,7 +776,7 @@ dependencies = [
[[package]] [[package]]
name = "cpal" name = "cpal"
version = "0.18.0" version = "0.18.0"
source = "git+https://github.com/RustAudio/cpal#d379d9d704cd564b206fb64ec235ff44bcb7b5d2" source = "git+https://github.com/RustAudio/cpal#2e7e09c1e8ab3d742c23f929eb6b0d93888e63cc"
dependencies = [ dependencies = [
"alsa", "alsa",
"block2 0.6.2", "block2 0.6.2",
@@ -1788,9 +1788,9 @@ checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87"
[[package]] [[package]]
name = "hyper" name = "hyper"
version = "1.10.0" version = "1.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eb92f162bf56536459fc83c79b974bb12837acfed43d6bc370a7916d0ae15ecc" checksum = "55281c53a1894c864990125767da440a4e630446785086f52523b20033b74498"
dependencies = [ dependencies = [
"atomic-waker", "atomic-waker",
"bytes", "bytes",
@@ -2235,7 +2235,7 @@ dependencies = [
"bitflags 2.11.1", "bitflags 2.11.1",
"libc", "libc",
"plain", "plain",
"redox_syscall 0.8.0", "redox_syscall 0.8.1",
] ]
[[package]] [[package]]
@@ -2382,9 +2382,9 @@ dependencies = [
[[package]] [[package]]
name = "mio" name = "mio"
version = "1.2.0" version = "1.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "50b7e5b27aa02a74bac8c3f23f448f8d87ff11f92d3aac1a6ed369ee08cc56c1" checksum = "02bd0af71c67b473010cbbc60715ee815645a4dc942899111f494b4b737d6fda"
dependencies = [ dependencies = [
"libc", "libc",
"wasi", "wasi",
@@ -3280,7 +3280,7 @@ checksum = "3d595e54a326bc53c1c197b32d295e14b169e3cfeaa8dc82b529f947fba6bcf5"
[[package]] [[package]]
name = "pwsp" name = "pwsp"
version = "1.9.1" version = "1.10.0"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"async-trait", "async-trait",
@@ -3512,9 +3512,9 @@ dependencies = [
[[package]] [[package]]
name = "redox_syscall" name = "redox_syscall"
version = "0.8.0" version = "0.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7c7591fa2c6b601dfcfe5f043f65a1c39fcdf50efefcd7f1572e538c1f4b398d" checksum = "5b44b894f2a6e36457d665d1e08c3866add6ed5e70050c1b4ba8a8ddedb02ce7"
dependencies = [ dependencies = [
"bitflags 2.11.1", "bitflags 2.11.1",
] ]
@@ -3650,7 +3650,7 @@ checksum = "3582f63211428f83597b51b2ddb88e2a91a9d52d12831f9d08f5e624e8977422"
[[package]] [[package]]
name = "rodio" name = "rodio"
version = "0.22.2" version = "0.22.2"
source = "git+https://github.com/arabianq/rodio.git?rev=33afba2a2d97bb730eb6537f09d2b3815bff0f33#33afba2a2d97bb730eb6537f09d2b3815bff0f33" source = "git+https://github.com/arabianq/rodio.git?rev=a634dd471e9d59196e19bf01323fb45f2f899821#a634dd471e9d59196e19bf01323fb45f2f899821"
dependencies = [ dependencies = [
"cpal", "cpal",
"dasp_sample", "dasp_sample",
@@ -3668,9 +3668,9 @@ checksum = "6c20b6793b5c2fa6553b250154b78d6d0db37e72700ae35fad9387a46f487c97"
[[package]] [[package]]
name = "rust-i18n" name = "rust-i18n"
version = "4.0.0" version = "4.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "21031bf5e6f2c0ae745d831791c403608e99a8bd3776c7e5e5535acd70c3b7ba" checksum = "55691a65892c33ee2de49c15ea5600c6f4a70e8eeb8e6c3cd96d2a231d230c40"
dependencies = [ dependencies = [
"globwalk", "globwalk",
"regex", "regex",
@@ -3681,9 +3681,9 @@ dependencies = [
[[package]] [[package]]
name = "rust-i18n-macro" name = "rust-i18n-macro"
version = "4.0.0" version = "4.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "51fe5295763b358606f7ca26a564e20f4469775a57ec1f09431249a33849ff52" checksum = "30de488acadcf767d97cd48518a8da8ea9777b1c9a5beca4eab78bbf77d07309"
dependencies = [ dependencies = [
"glob", "glob",
"proc-macro2", "proc-macro2",
@@ -3697,18 +3697,15 @@ dependencies = [
[[package]] [[package]]
name = "rust-i18n-support" name = "rust-i18n-support"
version = "4.0.0" version = "4.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "69bcc115c8eea2803aa3d85362e339776f4988a0349f2f475af572e497443f6f" checksum = "aea0fef8a93c06326b66392c95a115120e609674cb2132d37d276a6b05b545b4"
dependencies = [ dependencies = [
"arc-swap", "arc-swap",
"base62", "base62",
"globwalk", "globwalk",
"itertools 0.11.0", "itertools 0.11.0",
"lazy_static",
"normpath", "normpath",
"proc-macro2",
"regex",
"serde", "serde",
"serde_json", "serde_json",
"serde_yaml", "serde_yaml",
@@ -3780,9 +3777,9 @@ dependencies = [
[[package]] [[package]]
name = "rustls-native-certs" name = "rustls-native-certs"
version = "0.8.3" version = "0.8.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "612460d5f7bea540c490b2b6395d8e34a953e52b491accd6c86c8164c5932a63" checksum = "dab5152771c58876a2146916e53e35057e1a4dfa2b9df0f0305b07f611fdea4d"
dependencies = [ dependencies = [
"openssl-probe", "openssl-probe",
"rustls-pki-types", "rustls-pki-types",
@@ -4007,6 +4004,12 @@ version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
[[package]]
name = "shlex"
version = "2.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba"
[[package]] [[package]]
name = "signal-hook-registry" name = "signal-hook-registry"
version = "1.4.8" version = "1.4.8"
@@ -4156,9 +4159,9 @@ dependencies = [
[[package]] [[package]]
name = "socket2" name = "socket2"
version = "0.6.3" version = "0.6.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e" checksum = "52d1cfed4120b4d927bf7c0f86d2087a4a7d6027c906d9f9d525a80573b9be51"
dependencies = [ dependencies = [
"libc", "libc",
"windows-sys 0.61.2", "windows-sys 0.61.2",
@@ -4487,9 +4490,9 @@ checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369"
[[package]] [[package]]
name = "target-lexicon" name = "target-lexicon"
version = "0.13.3" version = "0.13.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "df7f62577c25e07834649fc3b39fafdc597c0a3527dc1c60129201ccfcbaa50c" checksum = "adb6935a6f5c20170eeceb1a3835a49e12e19d792f6dd344ccc76a985ca5a6ca"
[[package]] [[package]]
name = "tempfile" name = "tempfile"
@@ -4863,9 +4866,9 @@ checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
[[package]] [[package]]
name = "unicode-segmentation" name = "unicode-segmentation"
version = "1.13.2" version = "1.13.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9629274872b2bfaf8d66f5f15725007f635594914870f65218920345aa11aa8c" checksum = "c6f5d3c3b1bf09027a88a6bc961fc00497d651009560b5463668dc81b0fa87a8"
[[package]] [[package]]
name = "unicode-width" name = "unicode-width"
@@ -4912,9 +4915,9 @@ checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be"
[[package]] [[package]]
name = "uuid" name = "uuid"
version = "1.23.1" version = "1.23.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ddd74a9687298c6858e9b88ec8935ec45d22e8fd5e6394fa1bd4e99a87789c76" checksum = "d258b83ceec21034727ecee8c382cfa6c3e133699b0742c64571814fb420c9f7"
dependencies = [ dependencies = [
"js-sys", "js-sys",
"serde_core", "serde_core",
@@ -5951,9 +5954,9 @@ dependencies = [
[[package]] [[package]]
name = "zbus" name = "zbus"
version = "5.15.0" version = "5.16.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c3bcbf15c8708d7fc1be0c993622e0a5cbd5e8b52bfa40afa4c3e0cd8d724ac1" checksum = "eee682d202a77e4a9f3b2c2bdf48a7b28af5c08c34ddf66f98c93e5e39464285"
dependencies = [ dependencies = [
"async-broadcast", "async-broadcast",
"async-executor", "async-executor",
@@ -5986,9 +5989,9 @@ dependencies = [
[[package]] [[package]]
name = "zbus_macros" name = "zbus_macros"
version = "5.15.0" version = "5.16.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "51fa5406ad9175a8c825a931f8cf347116b531b3634fcb0b627c290f1f2516ff" checksum = "adf1bd45a81a103745b1757754762a26e8cd01e4532e4d6c8ec431624b80d1d6"
dependencies = [ dependencies = [
"proc-macro-crate", "proc-macro-crate",
"proc-macro2", "proc-macro2",
@@ -6012,18 +6015,18 @@ dependencies = [
[[package]] [[package]]
name = "zerocopy" name = "zerocopy"
version = "0.8.49" version = "0.8.50"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bce33a6288fa3f072a8c2c7d0f2fdbb90e28298f0135c1f99b96c3db2efcc60b" checksum = "3b065d4f0e55f82fae73202e189638116a87c55ab6b8e6c2721e13dd9d854ad1"
dependencies = [ dependencies = [
"zerocopy-derive", "zerocopy-derive",
] ]
[[package]] [[package]]
name = "zerocopy-derive" name = "zerocopy-derive"
version = "0.8.49" version = "0.8.50"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8fd425244944f4ab65ccff928e7323354c5a018c75838362fdce749dfad2ee1e" checksum = "0b631b19d36a892ab55420c92dbc83ccd79274f25be714855d3074aa71cab639"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@@ -6141,9 +6144,9 @@ dependencies = [
[[package]] [[package]]
name = "zvariant" name = "zvariant"
version = "5.11.0" version = "5.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1c1567a6ec68df868cbbfde844cfc6d81649fe5109a62b116b19fabd53e618ee" checksum = "a192a0bde63360d77a7523c833d4b4ce6070a927e2c53246e4c540b1a3e27be0"
dependencies = [ dependencies = [
"endi", "endi",
"enumflags2", "enumflags2",
@@ -6156,9 +6159,9 @@ dependencies = [
[[package]] [[package]]
name = "zvariant_derive" name = "zvariant_derive"
version = "5.11.0" version = "5.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c7d5b780599bbde114e39d9a0799577fad1ced5105d38515745f7b3099d8ceda" checksum = "90bc6cde9c01c511074be97f7ccb6c19d0da89e3f8662e812e999dcfd4638737"
dependencies = [ dependencies = [
"proc-macro-crate", "proc-macro-crate",
"proc-macro2", "proc-macro2",
@@ -6169,9 +6172,9 @@ dependencies = [
[[package]] [[package]]
name = "zvariant_utils" name = "zvariant_utils"
version = "3.3.1" version = "3.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6d464f5733ffa07a3164d656f18533caace9d0638596721355d73256a410d691" checksum = "1e8535915cfa75547e559d8c68e8139909a4aeee076831e4ef7fc59d8172c4d6"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
+2 -2
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "pwsp" name = "pwsp"
version = "1.9.1" version = "1.10.0"
edition = "2024" edition = "2024"
authors = ["arabian"] authors = ["arabian"]
description = "PWSP lets you play audio files through your microphone. Has both CLI and GUI clients." description = "PWSP lets you play audio files through your microphone. Has both CLI and GUI clients."
@@ -43,7 +43,7 @@ rustix = { version = "1.1.4", features = ["process"] }
rust-i18n = "4.0.0" rust-i18n = "4.0.0"
sys-locale = "0.3.2" sys-locale = "0.3.2"
rodio = { git = "https://github.com/arabianq/rodio.git", rev = "33afba2a2d97bb730eb6537f09d2b3815bff0f33", default-features = false, features = [ rodio = { git = "https://github.com/arabianq/rodio.git", rev = "a634dd471e9d59196e19bf01323fb45f2f899821", default-features = false, features = [
"symphonia-all", "symphonia-all",
"symphonia-libopus", "symphonia-libopus",
"playback", "playback",
BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 82 KiB

+3 -3
View File
@@ -1,6 +1,6 @@
pkgbase = pwsp-bin pkgbase = pwsp-bin
pkgdesc = Lets you play audio files through your microphone (Pre-built binaries) pkgdesc = Lets you play audio files through your microphone (Pre-built binaries)
pkgver = 1.9.1 pkgver = 1.10.0
pkgrel = 1 pkgrel = 1
url = https://github.com/arabianq/pipewire-soundpad url = https://github.com/arabianq/pipewire-soundpad
arch = x86_64 arch = x86_64
@@ -9,8 +9,8 @@ depends = pipewire
depends = alsa-lib depends = alsa-lib
provides = pwsp provides = pwsp
conflicts = pwsp conflicts = pwsp
source = pwsp-bin-1.9.1.zip :: https://github.com/arabianq/pipewire-soundpad/releases/download/v1.9.1/pwsp-v1.9.1-linux-x64.zip source = pwsp-bin-1.10.0.zip :: https://github.com/arabianq/pipewire-soundpad/releases/download/v1.10.0/pwsp-v1.10.0-linux-x64.zip
source = pipewire-soundpad-1.9.1.tar.gz :: https://github.com/arabianq/pipewire-soundpad/archive/refs/tags/v1.9.1.tar.gz source = pipewire-soundpad-1.10.0.tar.gz :: https://github.com/arabianq/pipewire-soundpad/archive/refs/tags/v1.10.0.tar.gz
sha256sums = SKIP sha256sums = SKIP
sha256sums = SKIP sha256sums = SKIP
+1 -1
View File
@@ -1,7 +1,7 @@
# Maintainer: Alexander Tarasov <a.tevg@ya.ru> # Maintainer: Alexander Tarasov <a.tevg@ya.ru>
pkgname=pwsp-bin pkgname=pwsp-bin
_pkgname=pipewire-soundpad _pkgname=pipewire-soundpad
pkgver=1.9.1 pkgver=1.10.0
pkgrel=1 pkgrel=1
pkgdesc="Lets you play audio files through your microphone (Pre-built binaries)" pkgdesc="Lets you play audio files through your microphone (Pre-built binaries)"
arch=('x86_64') arch=('x86_64')
+3 -3
View File
@@ -1,7 +1,7 @@
pkgbase = pwsp pkgbase = pwsp
pkgdesc = Lets you play audio files through your microphone pkgdesc = Lets you play audio files through your microphone
pkgver = 1.9.1 pkgver = 1.10.0
pkgrel = 2 pkgrel = 1
url = https://github.com/arabianq/pipewire-soundpad url = https://github.com/arabianq/pipewire-soundpad
arch = any arch = any
license = MIT license = MIT
@@ -11,7 +11,7 @@ pkgbase = pwsp
makedepends = cmake makedepends = cmake
makedepends = pipewire makedepends = pipewire
makedepends = alsa-lib makedepends = alsa-lib
source = https://github.com/arabianq/pipewire-soundpad/archive/refs/tags/v1.9.1.tar.gz source = https://github.com/arabianq/pipewire-soundpad/archive/refs/tags/v1.10.0.tar.gz
sha256sums = SKIP sha256sums = SKIP
pkgname = pwsp pkgname = pwsp
+2 -2
View File
@@ -1,8 +1,8 @@
# Maintainer: Alexander Tarasov <a.tevg@ya.ru> # Maintainer: Alexander Tarasov <a.tevg@ya.ru>
pkgsubn=pwsp pkgsubn=pwsp
pkgname=pwsp pkgname=pwsp
pkgver=1.9.1 pkgver=1.10.0
pkgrel=2 pkgrel=1
pkgdesc="Lets you play audio files through your microphone" pkgdesc="Lets you play audio files through your microphone"
arch=('any') arch=('any')
url="https://github.com/arabianq/pipewire-soundpad" url="https://github.com/arabianq/pipewire-soundpad"
File diff suppressed because one or more lines are too long
@@ -25,7 +25,7 @@
<name>arabian</name> <name>arabian</name>
</developer> </developer>
<releases> <releases>
<release version="1.9.1" date="2026-05-22" /> <release version="1.10.0" date="2026-06-01" />
</releases> </releases>
<content_rating type="oars-1.1" /> <content_rating type="oars-1.1" />
</component> </component>
+1 -1
View File
@@ -4,7 +4,7 @@
%global cargo_install_lib 0 %global cargo_install_lib 0
Name: pwsp Name: pwsp
Version: 1.9.1 Version: 1.10.0
Release: %autorelease Release: %autorelease
Summary: Lets you play audio files through your microphone Summary: Lets you play audio files through your microphone
+74 -65
View File
@@ -15,7 +15,7 @@ use std::os::unix::fs::PermissionsExt;
use std::{fs, time::Duration}; use std::{fs, time::Duration};
use tokio::{ use tokio::{
io::{AsyncReadExt, AsyncWriteExt}, io::{AsyncReadExt, AsyncWriteExt},
net::UnixListener, net::{UnixListener, UnixStream},
time::sleep, time::sleep,
}; };
@@ -83,83 +83,92 @@ async fn main() -> Result<()> {
async fn commands_loop(listener: UnixListener) -> Result<()> { async fn commands_loop(listener: UnixListener) -> Result<()> {
loop { loop {
let (mut stream, _addr) = listener.accept().await?; let (stream, _addr) = listener.accept().await?;
tokio::spawn(async move { tokio::spawn(async move {
// ---------- Read request (start) ---------- handle_connection(stream).await;
let mut len_bytes = [0u8; 4]; });
if stream.read_exact(&mut len_bytes).await.is_err() { }
eprintln!("Failed to read message length from client!"); }
return;
}
let request_len = u32::from_le_bytes(len_bytes) as usize; async fn handle_connection(mut stream: UnixStream) {
// ---------- Read request (start) ----------
let mut len_bytes = [0u8; 4];
if stream.read_exact(&mut len_bytes).await.is_err() {
eprintln!("Failed to read message length from client!");
return;
}
if request_len > MAX_MESSAGE_SIZE { let request_len = u32::from_le_bytes(len_bytes) as usize;
eprintln!(
"Failed to read message from client: request too large ({} bytes)!",
request_len
);
return;
}
let mut buffer = vec![0u8; request_len]; if request_len > MAX_MESSAGE_SIZE {
if stream.read_exact(&mut buffer).await.is_err() { eprintln!(
eprintln!("Failed to read message from client!"); "Failed to read message from client: request too large ({} bytes)!",
return; request_len
} );
return;
}
let request: Request = match serde_json::from_slice(&buffer) { let mut buffer = Vec::new();
Ok(req) => req, if (&mut stream)
Err(err) => { .take(request_len as u64)
let response = .read_to_end(&mut buffer)
Response::new(false, format!("Failed to parse request: {}", err)); .await
let response_data = match serde_json::to_vec(&response) { .is_err()
Ok(data) => data, || buffer.len() != request_len
Err(_) => return, // Should not happen with this simple Response {
}; eprintln!("Failed to read message from client!");
let response_len = response_data.len() as u32; return;
let _ = stream.write_all(&response_len.to_le_bytes()).await; }
let _ = stream.write_all(&response_data).await;
return;
}
};
// ---------- Read request (end) ----------
// ---------- Generate response (start) ---------- let request: Request = match serde_json::from_slice(&buffer) {
let command = parse_command(&request); Ok(req) => req,
let response: Response; Err(err) => {
if let Some(command) = command { let response = Response::new(false, format!("Failed to parse request: {}", err));
response = command.execute().await;
} else {
response = Response::new(false, "Unknown command");
}
// ---------- Generate response (end) ----------
// ---------- Send response (start) ----------
let response_data = match serde_json::to_vec(&response) { let response_data = match serde_json::to_vec(&response) {
Ok(data) => data, Ok(data) => data,
Err(err) => { Err(_) => return, // Should not happen with this simple Response
eprintln!("Failed to serialize response: {}", err);
return;
}
}; };
let response_len = response_data.len() as u32; let response_len = response_data.len() as u32;
let _ = stream.write_all(&response_len.to_le_bytes()).await;
let _ = stream.write_all(&response_data).await;
return;
}
};
// ---------- Read request (end) ----------
if stream.write_all(&response_len.to_le_bytes()).await.is_err() { // ---------- Generate response (start) ----------
eprintln!("Failed to write response length to client!"); let command = parse_command(&request);
return; let response: Response;
} if let Some(command) = command {
if stream.write_all(&response_data).await.is_err() { response = command.execute().await;
eprintln!("Failed to write response to client!"); } else {
return; response = Response::new(false, "Unknown command");
} }
// ---------- Send response (end) ---------- // ---------- Generate response (end) ----------
if response.status && response.message.eq("killed") { // ---------- Send response (start) ----------
std::process::exit(0); let response_data = match serde_json::to_vec(&response) {
} Ok(data) => data,
}); Err(err) => {
eprintln!("Failed to serialize response: {}", err);
return;
}
};
let response_len = response_data.len() as u32;
if stream.write_all(&response_len.to_le_bytes()).await.is_err() {
eprintln!("Failed to write response length to client!");
return;
}
if stream.write_all(&response_data).await.is_err() {
eprintln!("Failed to write response to client!");
return;
}
// ---------- Send response (end) ----------
if response.status && response.message.eq("killed") {
std::process::exit(0);
} }
} }
+76 -70
View File
@@ -8,79 +8,85 @@ impl SoundpadGui {
pub fn draw_footer(&mut self, ui: &mut Ui) { pub fn draw_footer(&mut self, ui: &mut Ui) {
ui.add_space(5.0); ui.add_space(5.0);
ui.horizontal(|ui| { ui.horizontal(|ui| {
// ---------- Microphone selection ---------- self.draw_mic_selection(ui);
let mics = &self.audio_player_state.all_inputs_sorted; self.draw_master_volume(ui);
let mut selected_input = self.audio_player_state.current_input.to_owned();
let prev_input = selected_input.to_owned();
ComboBox::from_label(t!("gui.choose_mic_select"))
.height(30.0)
.selected_text(
self.audio_player_state
.all_inputs
.get(&selected_input)
.unwrap_or(&String::new()),
)
.show_ui(ui, |ui| {
for (name, nick) in mics {
ui.selectable_value(&mut selected_input, name.clone(), nick);
}
});
if selected_input != prev_input {
self.set_input(selected_input);
}
// --------------------------------
// ---------- Master Volume Slider ----------
let volume_icon = Self::get_volume_icon(self.audio_player_state.volume);
let volume_label = Label::new(RichText::new(volume_icon).size(18.0));
ui.add_sized([18.0, 18.0], volume_label)
.on_hover_text(format!(
"Master Volume: {:.0}%",
self.audio_player_state.volume * 100.0
));
let should_update_volume = !self.app_state.volume_dragged
&& self
.app_state
.ignore_volume_update_until
.map(|t| Instant::now() > t)
.unwrap_or(true);
if should_update_volume {
self.app_state.volume_slider_value = self.audio_player_state.volume;
}
let volume_slider = Slider::new(&mut self.app_state.volume_slider_value, 0.0..=1.0)
.show_value(false)
.step_by(0.01);
let volume_slider_response = ui.add_sized([150.0, 18.0], volume_slider);
if volume_slider_response.drag_stopped() {
self.app_state.volume_dragged = true;
}
// ------------------------------------------
ui.add_space(ui.available_width() - 18.0 * 2.0 - ui.spacing().item_spacing.x * 2.0); ui.add_space(ui.available_width() - 18.0 * 2.0 - ui.spacing().item_spacing.x * 2.0);
// ---------- Hotkeys button ---------- self.draw_hotkeys_button(ui);
let hotkeys_button = self.draw_settings_button(ui);
Button::new(ICON_KEYBOARD.atom_size(Vec2::new(18.0, 18.0))).frame(false);
let hotkeys_button_response = ui.add_sized([18.0, 18.0], hotkeys_button);
if hotkeys_button_response.clicked() {
self.app_state.show_hotkeys = true;
}
hotkeys_button_response.on_hover_text("Hotkeys (H)");
// --------------------------------
// ---------- Settings button ----------
let settings_button =
Button::new(ICON_SETTINGS.atom_size(Vec2::new(18.0, 18.0))).frame(false);
let settings_button_response = ui.add_sized([18.0, 18.0], settings_button);
if settings_button_response.clicked() {
self.app_state.show_settings = true;
}
// --------------------------------
}); });
} }
fn draw_mic_selection(&mut self, ui: &mut Ui) {
let mics = &self.audio_player_state.all_inputs_sorted;
let mut selected_input = self.audio_player_state.current_input.to_owned();
let prev_input = selected_input.to_owned();
ComboBox::from_label(t!("gui.choose_mic_select"))
.height(30.0)
.selected_text(
self.audio_player_state
.all_inputs
.get(&selected_input)
.unwrap_or(&String::new()),
)
.show_ui(ui, |ui| {
for (name, nick) in mics {
ui.selectable_value(&mut selected_input, name.clone(), nick);
}
});
if selected_input != prev_input {
self.set_input(selected_input);
}
}
fn draw_master_volume(&mut self, ui: &mut Ui) {
let volume_icon = Self::get_volume_icon(self.audio_player_state.volume);
let volume_label = Label::new(RichText::new(volume_icon).size(18.0));
ui.add_sized([18.0, 18.0], volume_label)
.on_hover_text(format!(
"Master Volume: {:.0}%",
self.audio_player_state.volume * 100.0
));
let should_update_volume = !self.app_state.volume_dragged
&& self
.app_state
.ignore_volume_update_until
.map(|t| Instant::now() > t)
.unwrap_or(true);
if should_update_volume {
self.app_state.volume_slider_value = self.audio_player_state.volume;
}
let volume_slider = Slider::new(&mut self.app_state.volume_slider_value, 0.0..=1.0)
.show_value(false)
.step_by(0.01);
let volume_slider_response = ui.add_sized([150.0, 18.0], volume_slider);
if volume_slider_response.drag_stopped() {
self.app_state.volume_dragged = true;
}
}
fn draw_hotkeys_button(&mut self, ui: &mut Ui) {
let hotkeys_button =
Button::new(ICON_KEYBOARD.atom_size(Vec2::new(18.0, 18.0))).frame(false);
let hotkeys_button_response = ui.add_sized([18.0, 18.0], hotkeys_button);
if hotkeys_button_response.clicked() {
self.app_state.show_hotkeys = true;
}
hotkeys_button_response.on_hover_text("Hotkeys (H)");
}
fn draw_settings_button(&mut self, ui: &mut Ui) {
let settings_button =
Button::new(ICON_SETTINGS.atom_size(Vec2::new(18.0, 18.0))).frame(false);
let settings_button_response = ui.add_sized([18.0, 18.0], settings_button);
if settings_button_response.clicked() {
self.app_state.show_settings = true;
}
}
} }
+5 -1
View File
@@ -349,7 +349,11 @@ impl AudioPlayer {
let duration = source.total_duration().map(|d| d.as_secs_f32()); let duration = source.total_duration().map(|d| d.as_secs_f32());
let mixer = self.stream_handle.as_ref().unwrap().mixer(); let mixer = self
.stream_handle
.as_ref()
.ok_or_else(|| anyhow::anyhow!("stream_handle is unexpectedly missing"))?
.mixer();
let sink = Player::connect_new(mixer); let sink = Player::connect_new(mixer);
sink.set_volume(self.volume); // Default volume is 1.0 * master sink.set_volume(self.volume); // Default volume is 1.0 * master
sink.append(source); sink.append(source);
+43
View File
@@ -29,3 +29,46 @@ pub struct AudioDevice {
pub output_fl: Option<Port>, pub output_fl: Option<Port>,
pub output_fr: Option<Port>, pub output_fr: Option<Port>,
} }
impl AudioDevice {
pub fn new(
id: u32,
nick: Option<&str>,
description: Option<&str>,
name: Option<&str>,
device_type: DeviceType,
) -> Self {
Self {
id,
nick: nick
.or(description)
.or(name)
.unwrap_or_default()
.to_string(),
name: name.unwrap_or_default().to_string(),
device_type,
input_fl: None,
input_fr: None,
output_fl: None,
output_fr: None,
}
}
pub fn add_port(&mut self, port: Port) {
match port.name.as_str() {
"input_FL" => self.input_fl = Some(port),
"input_FR" => self.input_fr = Some(port),
"output_FL" | "capture_FL" => self.output_fl = Some(port),
"output_FR" | "capture_FR" => self.output_fr = Some(port),
"input_MONO" => {
self.input_fl = Some(port.clone());
self.input_fr = Some(port);
}
"output_MONO" | "capture_MONO" => {
self.output_fl = Some(port.clone());
self.output_fr = Some(port);
}
_ => {}
}
}
}
+47 -92
View File
@@ -20,66 +20,54 @@ pub fn setup_pipewire_context() -> Result<(MainLoopRc, ContextRc), String> {
fn parse_global_object( fn parse_global_object(
global_object: &GlobalObject<&DictRef>, global_object: &GlobalObject<&DictRef>,
) -> (Option<AudioDevice>, Option<Port>) { ) -> (Option<AudioDevice>, Option<Port>) {
// Only objects with props can be devices/ports let props = match global_object.props {
if let Some(props) = global_object.props { Some(p) => p,
// Only objects with media.class can be devices None => return (None, None),
if let Some(media_class) = props.get("media.class") { };
let node_id = global_object.id;
let node_nick = props.get("node.nick");
let node_name = props.get("node.name");
let node_description = props.get("node.description");
// Check if the device is an input or output if let Some(media_class) = props.get("media.class") {
return if media_class.starts_with("Audio/Source") { let node_id = global_object.id;
let input_device = AudioDevice { let node_nick = props.get("node.nick");
id: node_id, let node_name = props.get("node.name");
nick: node_nick let node_description = props.get("node.description");
.unwrap_or(node_description.unwrap_or(node_name.unwrap_or_default()))
.to_string(),
name: node_name.unwrap_or_default().to_string(),
device_type: DeviceType::Input,
input_fl: None, if media_class.starts_with("Audio/Source") {
input_fr: None, let input_device = AudioDevice::new(
output_fl: None,
output_fr: None,
};
(Some(input_device), None)
} else if media_class.starts_with("Stream/Output/Audio") {
let output_device = AudioDevice {
id: node_id,
nick: node_nick
.unwrap_or(node_description.unwrap_or(node_name.unwrap_or_default()))
.to_string(),
name: node_name.unwrap_or_default().to_string(),
device_type: DeviceType::Output,
input_fl: None,
input_fr: None,
output_fl: None,
output_fr: None,
};
(Some(output_device), None)
} else {
(None, None)
};
// Check if the object is a port
} else if props.get("port.direction").is_some()
&& let (Some(node_id), Some(port_id), Some(port_name)) = (
props.get("node.id").and_then(|id| id.parse::<u32>().ok()),
props.get("port.id").and_then(|id| id.parse::<u32>().ok()),
props.get("port.name"),
)
{
let port = Port {
node_id, node_id,
port_id, node_nick,
name: port_name.to_string(), node_description,
}; node_name,
DeviceType::Input,
return (None, Some(port)); );
return (Some(input_device), None);
} else if media_class.starts_with("Stream/Output/Audio") {
let output_device = AudioDevice::new(
node_id,
node_nick,
node_description,
node_name,
DeviceType::Output,
);
return (Some(output_device), None);
} }
return (None, None);
} }
if props.get("port.direction").is_some()
&& let (Some(node_id), Some(port_id), Some(port_name)) = (
props.get("node.id").and_then(|id| id.parse::<u32>().ok()),
props.get("port.id").and_then(|id| id.parse::<u32>().ok()),
props.get("port.name"),
)
{
let port = Port {
node_id,
port_id,
name: port_name.to_string(),
};
return (None, Some(port));
}
(None, None) (None, None)
} }
@@ -188,47 +176,14 @@ pub async fn get_all_devices() -> Result<(Vec<AudioDevice>, Vec<AudioDevice>)> {
let node_id = port.node_id; let node_id = port.node_id;
if let Some(input_device) = input_devices.get_mut(&node_id) { if let Some(input_device) = input_devices.get_mut(&node_id) {
match port.name.as_str() { input_device.add_port(port);
"input_FL" => input_device.input_fl = Some(port),
"input_FR" => input_device.input_fr = Some(port),
"output_FL" => input_device.output_fl = Some(port),
"output_FR" => input_device.output_fr = Some(port),
"capture_FL" => input_device.output_fl = Some(port),
"capture_FR" => input_device.output_fr = Some(port),
"input_MONO" => {
input_device.input_fl = Some(port.clone());
input_device.input_fr = Some(port)
}
"capture_MONO" => {
input_device.output_fl = Some(port.clone());
input_device.output_fr = Some(port);
}
_ => {}
}
} else if let Some(output_device) = output_devices.get_mut(&node_id) { } else if let Some(output_device) = output_devices.get_mut(&node_id) {
match port.name.as_str() { output_device.add_port(port);
"input_FL" => output_device.input_fl = Some(port),
"input_FR" => output_device.input_fr = Some(port),
"output_FL" => output_device.output_fl = Some(port),
"output_FR" => output_device.output_fr = Some(port),
"capture_FL" => output_device.output_fl = Some(port),
"capture_FR" => output_device.output_fr = Some(port),
"output_MONO" => {
output_device.output_fl = Some(port.clone());
output_device.output_fr = Some(port)
}
"capture_MONO" => {
output_device.output_fl = Some(port.clone());
output_device.output_fr = Some(port)
}
_ => {}
}
} }
} }
let mut input_devices: Vec<AudioDevice> = input_devices.values().cloned().collect(); let mut input_devices: Vec<AudioDevice> = input_devices.into_values().collect();
let mut output_devices: Vec<AudioDevice> = let mut output_devices: Vec<AudioDevice> = output_devices.into_values().collect();
output_devices.values().cloned().collect();
input_devices.sort_by_key(|a| a.id); input_devices.sort_by_key(|a| a.id);
output_devices.sort_by_key(|a| a.id); output_devices.sort_by_key(|a| a.id);