Compare commits

...

21 Commits

Author SHA1 Message Date
arabianq 0060d0bdee deps: update cargo-sources.json 2026-06-02 22:38:27 +03:00
arabianq f91a49cb70 change version to 1.11.0 2026-06-02 22:36:40 +03:00
arabianq 8d513ff65b deps: cargo update 2026-06-02 22:35:26 +03:00
arabianq 226dfd91ff scripts: move generate-sources.sh into scripts/ 2026-06-02 22:34:57 +03:00
arabianq 344ea60fa5 scripts: add script to automatically update pwsp version 2026-06-02 22:34:24 +03:00
arabianq 8411cb3528 fix deb packaging 2026-06-02 22:21:23 +03:00
arabianq ad8f22a359 ci: add arm64 support 2026-06-02 21:59:46 +03:00
arabianq 4ec49d822b parallel deb packaging 2026-06-02 21:54:29 +03:00
arabianq ec2fa2a478 ci: better github actions 2026-06-02 21:44:56 +03:00
Tarasov Aleksandr e91465365d feat: better testing (#131)
* add tests

* update github actions to include testing step

* optimization
2026-06-02 21:37:22 +03:00
Tarasov Aleksandr 0476329798 Refactor to Cargo Workspace (#129)
* Refactor project into a Cargo workspace with distinct packages

- Created a root `Cargo.toml` defining a workspace.
- Moved `src/types` and `src/utils` into a new `pwsp-lib` crate for shared logic.
- Split binaries into their own crates: `pwsp-daemon`, `pwsp-cli`, and `pwsp-gui`.
- Shifted all dependencies into `[workspace.dependencies]` for centralized version management.
- Updated import paths across all crates (e.g. from `pwsp::` to `pwsp_lib::`).
- Updated build scripts, GitHub actions, Flatpak manifest, and AUR PKGBUILD to support the new workspace structure.
- Ensured no core application logic was altered.

Co-authored-by: arabianq <55220741+arabianq@users.noreply.github.com>

* Fix cargo-deb build process in GitHub actions for workspace architecture

Co-authored-by: arabianq <55220741+arabianq@users.noreply.github.com>

* Fix cargo-deb asset discovery by using exact target/release paths

Co-authored-by: arabianq <55220741+arabianq@users.noreply.github.com>

* refactor deps in Cargo.toml files

* fix incorrect assets path

---------

Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com>
2026-06-02 21:12:44 +03:00
arabianq 6c59137639 fix icon 2026-06-01 23:37:46 +03:00
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
57 changed files with 1693 additions and 678 deletions
+94 -17
View File
@@ -13,7 +13,15 @@ on:
jobs:
linux-build:
runs-on: ubuntu-latest
strategy:
matrix:
include:
- arch: x64
runner: ubuntu-latest
- arch: arm64
runner: ubuntu-24.04-arm
fail-fast: false
runs-on: ${{ matrix.runner }}
steps:
- name: Install apt deps (jq/zip + dev-libs)
@@ -33,35 +41,44 @@ jobs:
fetch-depth: 0
- name: Setup Rust toolchain
uses: actions-rs/toolchain@v1
uses: actions-rust-lang/setup-rust-toolchain@v1
with:
toolchain: 1.94.1
toolchain: 1.96.0
- name: Run tests
run: cargo test --locked
- name: Build all binaries (debug-speed compilation into target/release)
env:
CARGO_PROFILE_RELEASE_OPT_LEVEL: 0
CARGO_PROFILE_RELEASE_DEBUG: "true"
CARGO_PROFILE_RELEASE_STRIP: "false"
CARGO_PROFILE_RELEASE_LTO: "false"
CARGO_PROFILE_RELEASE_CODEGEN_UNITS: 256
run: cargo build --release --locked
- name: Extract all binary names
id: cargo-meta
run: |
set -euo pipefail
BIN_NAMES=$(cargo metadata --no-deps --format-version 1 \
| jq -r '.packages[0].targets[] | select(.kind[] | contains("bin")) | .name')
| jq -r '.packages[].targets[] | select(.kind[] | contains("bin")) | .name')
echo "bin_names<<EOF" >> $GITHUB_OUTPUT
echo "$BIN_NAMES" >> $GITHUB_OUTPUT
echo "EOF" >> $GITHUB_OUTPUT
- name: Build all binaries
run: cargo build --locked
- name: Package all binaries into one archive
shell: bash
run: |
set -euo pipefail
COMMIT_SHA="${{ github.sha }}"
ARCHIVE_NAME="pwsp-${COMMIT_SHA}-linux-x64.zip"
ARCHIVE_NAME="pwsp-${COMMIT_SHA}-linux-${{ matrix.arch }}.zip"
echo "Creating archive: $ARCHIVE_NAME"
FILES=()
while IFS= read -r BIN; do
[ -z "$BIN" ] && continue
FILES+=("target/debug/$BIN")
FILES+=("target/release/$BIN")
done <<< "${{ steps.cargo-meta.outputs.bin_names }}"
if [ "${#FILES[@]}" -eq 0 ]; then
@@ -82,28 +99,87 @@ jobs:
- name: Upload archive as artifact
uses: actions/upload-artifact@v4
with:
name: archive
name: archive-${{ matrix.arch }}
path: pwsp-*.zip
retention-days: 7
- name: Install cargo-deb and create .deb
deb-build:
strategy:
matrix:
include:
- arch: x64
runner: ubuntu-latest
- arch: arm64
runner: ubuntu-24.04-arm
fail-fast: false
runs-on: ${{ matrix.runner }}
steps:
- name: Install apt deps (dev-libs)
run: |
sudo apt-get update
sudo apt-get install -y \
libpipewire-0.3-dev \
libclang-dev \
libasound2-dev \
libdbus-1-dev \
pkg-config
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Setup Rust toolchain
uses: actions-rust-lang/setup-rust-toolchain@v1
with:
toolchain: 1.96.0
- name: Build all binaries (debug-speed compilation into target/release)
env:
CARGO_PROFILE_RELEASE_OPT_LEVEL: 0
CARGO_PROFILE_RELEASE_DEBUG: "true"
CARGO_PROFILE_RELEASE_STRIP: "false"
CARGO_PROFILE_RELEASE_LTO: "false"
CARGO_PROFILE_RELEASE_CODEGEN_UNITS: 256
run: cargo build --release --locked
- name: Cache cargo-deb
id: cache-cargo-deb
uses: actions/cache@v4
with:
path: ~/.cargo/bin/cargo-deb
key: ${{ runner.os }}-${{ runner.arch }}-cargo-deb-v1
- name: Install cargo-deb
if: steps.cache-cargo-deb.outputs.cache-hit != 'true'
run: cargo install --locked cargo-deb
- name: Create .deb package (debug binaries from target/release)
shell: bash
run: |
set -euo pipefail
cargo install --locked cargo-deb
export PATH="$HOME/.cargo/bin:$PATH"
cargo-deb
cargo-deb -p pwsp-gui --no-build --no-strip
- name: Upload .deb(s) as artifacts
uses: actions/upload-artifact@v4
with:
name: deb-packages
name: deb-packages-${{ matrix.arch }}
path: target/debian/*.deb
retention-days: 7
flatpak-build:
runs-on: ubuntu-latest
if: github.event_name == 'pull_request' || github.event_name == 'workflow_dispatch'
strategy:
matrix:
include:
- arch: x86_64
runner: ubuntu-latest
- arch: aarch64
runner: ubuntu-24.04-arm
fail-fast: false
runs-on: ${{ matrix.runner }}
container:
image: ghcr.io/flathub-infra/flatpak-github-actions:freedesktop-25.08
options: --privileged
@@ -114,8 +190,9 @@ jobs:
- name: Build Flatpak
uses: flatpak/flatpak-github-actions/flatpak-builder@v6
with:
bundle: ru.arabianq.pwsp.flatpak
bundle: ru.arabianq.pwsp_${{ matrix.arch }}.flatpak
manifest-path: packages/flatpak/ru.arabianq.pwsp.yaml
cache: true
branch: master
build-bundle: true
arch: ${{ matrix.arch }}
+59 -4
View File
@@ -21,8 +21,8 @@ on:
default: "stable"
jobs:
flatter:
name: Flatter
flatter-x64:
name: Flatter (x86_64)
runs-on: ubuntu-latest
permissions:
contents: read
@@ -60,7 +60,61 @@ jobs:
echo "default-branch: ${{ steps.set_branch.outputs.branch }}" >> packages/flatpak/ru.arabianq.pwsp.yaml
- name: Install SDK Extensions
run: flatpak install -y flathub org.freedesktop.Sdk.Extension.rust-stable//25.08
run:
flatpak install -y flathub org.freedesktop.Sdk.Extension.rust-stable//25.08
org.freedesktop.Sdk.Extension.llvm20//25.08
- name: Build Flatpak
uses: andyholmes/flatter@main
with:
files: packages/flatpak/ru.arabianq.pwsp.yaml
gpg-sign: ${{ steps.gpg.outputs.fingerprint }}
upload-bundles: false
upload-pages-artifact: false
arch: x86_64
flatter-arm64:
name: Flatter (aarch64)
needs: flatter-x64
runs-on: ubuntu-24.04-arm
permissions:
contents: read
container:
image: ghcr.io/andyholmes/flatter/freedesktop:25.08
options: --privileged
steps:
- name: Checkout
uses: actions/checkout@v4
with:
ref: ${{ inputs.tag_name || github.ref }}
- name: Setup GPG
id: gpg
uses: crazy-max/ghaction-import-gpg@v6
with:
gpg_private_key: ${{ secrets.GPG_PRIVATE_KEY }}
passphrase: ${{ secrets.GPG_PASSPHRASE }}
- name: Set Default Branch
id: set_branch
run: |
if [ "${{ github.event_name }}" == "release" ]; then
echo "branch=stable" >> $GITHUB_OUTPUT
elif [ "${{ github.event_name }}" == "workflow_dispatch" ]; then
echo "branch=${{ inputs.build_branch }}" >> $GITHUB_OUTPUT
else
echo "branch=nightly" >> $GITHUB_OUTPUT
fi
- name: Modify Manifest
run: |
echo "branch: ${{ steps.set_branch.outputs.branch }}" >> packages/flatpak/ru.arabianq.pwsp.yaml
echo "default-branch: ${{ steps.set_branch.outputs.branch }}" >> packages/flatpak/ru.arabianq.pwsp.yaml
- name: Install SDK Extensions
run:
flatpak install -y flathub org.freedesktop.Sdk.Extension.rust-stable//25.08
org.freedesktop.Sdk.Extension.llvm20//25.08
- name: Build Flatpak
@@ -70,11 +124,12 @@ jobs:
gpg-sign: ${{ steps.gpg.outputs.fingerprint }}
upload-bundles: false
upload-pages-artifact: true
arch: aarch64
deploy:
name: Deploy to GitHub Pages
runs-on: ubuntu-latest
needs: flatter
needs: flatter-arm64
permissions:
pages: write
id-token: write
+105 -37
View File
@@ -64,7 +64,15 @@ jobs:
linux-release:
needs: prepare
runs-on: ubuntu-latest
strategy:
matrix:
include:
- arch: x64
runner: ubuntu-latest
- arch: arm64
runner: ubuntu-24.04-arm
fail-fast: false
runs-on: ${{ matrix.runner }}
steps:
- name: Install apt deps (jq/zip + dev-libs)
@@ -85,16 +93,16 @@ jobs:
fetch-depth: 0
- name: Setup Rust toolchain
uses: actions-rs/toolchain@v1
uses: actions-rust-lang/setup-rust-toolchain@v1
with:
toolchain: 1.94.1
toolchain: 1.96.0
- name: Extract all binary names
id: cargo-meta
run: |
set -euo pipefail
BIN_NAMES=$(cargo metadata --no-deps --format-version 1 \
| jq -r '.packages[0].targets[] | select(.kind[] | contains("bin")) | .name')
| jq -r '.packages[].targets[] | select(.kind[] | contains("bin")) | .name')
echo "bin_names<<EOF" >> $GITHUB_OUTPUT
echo "$BIN_NAMES" >> $GITHUB_OUTPUT
echo "EOF" >> $GITHUB_OUTPUT
@@ -107,7 +115,7 @@ jobs:
run: |
set -euo pipefail
TAG="${{ needs.prepare.outputs.tag }}"
ARCHIVE_NAME="pwsp-${TAG}-linux-x64.zip"
ARCHIVE_NAME="pwsp-${TAG}-linux-${{ matrix.arch }}.zip"
echo "Creating archive: $ARCHIVE_NAME"
FILES=()
@@ -131,48 +139,108 @@ jobs:
zip -j "$ARCHIVE_NAME" "${FILES[@]}"
- name: Upload release archive
uses: softprops/action-gh-release@v2
- name: Upload zip archive
uses: actions/upload-artifact@v4
with:
token: ${{ secrets.GITHUB_TOKEN }}
tag_name: ${{ needs.prepare.outputs.tag }}
files: |
pwsp-*.zip
name: zip-archive-${{ matrix.arch }}
path: pwsp-*.zip
retention-days: 1
- name: Install cargo-deb and create .deb
deb-release:
needs: prepare
strategy:
matrix:
include:
- arch: x64
runner: ubuntu-latest
- arch: arm64
runner: ubuntu-24.04-arm
fail-fast: false
runs-on: ${{ matrix.runner }}
steps:
- name: Install apt deps (dev-libs)
run: |
sudo apt-get update
sudo apt-get install -y \
libpipewire-0.3-dev \
libclang-dev \
libasound2-dev \
libdbus-1-dev \
pkg-config
- name: Checkout code at tag
uses: actions/checkout@v4
with:
ref: ${{ needs.prepare.outputs.tag }}
fetch-depth: 0
- name: Setup Rust toolchain
uses: actions-rust-lang/setup-rust-toolchain@v1
with:
toolchain: 1.96.0
- name: Build all release binaries
run: cargo build --release --locked
- name: Cache cargo-deb
id: cache-cargo-deb
uses: actions/cache@v4
with:
path: ~/.cargo/bin/cargo-deb
key: ${{ runner.os }}-${{ runner.arch }}-cargo-deb-v1
- name: Install cargo-deb
if: steps.cache-cargo-deb.outputs.cache-hit != 'true'
run: cargo install --locked cargo-deb
- name: Create .deb package (release binaries)
shell: bash
run: |
set -euo pipefail
cargo install --locked cargo-deb
export PATH="$HOME/.cargo/bin:$PATH"
cargo-deb -p pwsp-gui --no-build
cargo-deb
- name: Upload deb package
uses: actions/upload-artifact@v4
with:
name: deb-package-${{ matrix.arch }}
path: target/debian/*.deb
retention-days: 1
- name: Upload .deb(s) to release
publish-release:
needs: [prepare, linux-release, deb-release]
runs-on: ubuntu-latest
steps:
- name: Download zip archive (x64)
uses: actions/download-artifact@v4
with:
name: zip-archive-x64
path: ./dist
- name: Download zip archive (arm64)
uses: actions/download-artifact@v4
with:
name: zip-archive-arm64
path: ./dist
- name: Download deb package (x64)
uses: actions/download-artifact@v4
with:
name: deb-package-x64
path: ./dist
- name: Download deb package (arm64)
uses: actions/download-artifact@v4
with:
name: deb-package-arm64
path: ./dist
- name: Upload artifacts to Release
uses: softprops/action-gh-release@v2
with:
token: ${{ secrets.GITHUB_TOKEN }}
tag_name: ${{ needs.prepare.outputs.tag }}
files: |
target/debian/*.deb
flatpak-release:
needs: prepare
runs-on: ubuntu-latest
container:
image: ghcr.io/flathub-infra/flatpak-github-actions:freedesktop-25.08
options: --privileged
steps:
- uses: actions/checkout@v4
with:
ref: ${{ needs.prepare.outputs.tag }}
- name: Build Flatpak
uses: flatpak/flatpak-github-actions/flatpak-builder@v6
with:
bundle: ru.arabianq.pwsp.flatpak
manifest-path: packages/flatpak/ru.arabianq.pwsp.yaml
cache: true
branch: master
build-bundle: true
./dist/pwsp-*.zip
./dist/*.deb
Generated
+156 -117
View File
@@ -58,7 +58,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "812947049edcd670a82cd5c73c3661d2e58468577ba8489de58e1a73c04cbd5d"
dependencies = [
"alsa-sys",
"bitflags 2.11.1",
"bitflags 2.12.1",
"cfg-if",
"libc",
]
@@ -80,7 +80,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0f2a1bb052857d5dd49572219344a7332b31b76405648eabac5bc68978251bcd"
dependencies = [
"android-properties",
"bitflags 2.11.1",
"bitflags 2.12.1",
"cc",
"jni",
"libc",
@@ -345,7 +345,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "993776b509cfb49c750f11b8f07a46fa23e0a1386ffc01fb1e7d343efc387895"
dependencies = [
"annotate-snippets",
"bitflags 2.11.1",
"bitflags 2.12.1",
"cexpr",
"clang-sys",
"itertools 0.13.0",
@@ -353,7 +353,7 @@ dependencies = [
"quote",
"regex",
"rustc-hash 2.1.2",
"shlex",
"shlex 1.3.0",
"syn",
]
@@ -380,9 +380,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
[[package]]
name = "bitflags"
version = "2.11.1"
version = "2.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3"
checksum = "84d7ced0ae9557296835c32bf1b1e02b44c746701f898460fb000d7eaa84f00a"
[[package]]
name = "bitvec"
@@ -482,7 +482,7 @@ version = "0.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b99da2f8558ca23c71f4fd15dc57c906239752dd27ff3c00a1d56b685b7cbfec"
dependencies = [
"bitflags 2.11.1",
"bitflags 2.12.1",
"log",
"polling",
"rustix 0.38.44",
@@ -496,7 +496,7 @@ version = "0.14.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4dbf9978365bac10f54d1d4b04f7ce4427e51f71d61f2fe15e3fed5166474df7"
dependencies = [
"bitflags 2.11.1",
"bitflags 2.12.1",
"polling",
"rustix 1.1.4",
"slab",
@@ -529,14 +529,14 @@ dependencies = [
[[package]]
name = "cc"
version = "1.2.62"
version = "1.2.63"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a1dce859f0832a7d088c4f1119888ab94ef4b5d6795d1ce05afb7fe159d79f98"
checksum = "556e016178bb5662a08681bbe0f00f8e17631781a4dfc8c45e466e4b185ec27f"
dependencies = [
"find-msvc-tools",
"jobserver",
"libc",
"shlex",
"shlex 2.0.1",
]
[[package]]
@@ -550,9 +550,9 @@ dependencies = [
[[package]]
name = "cfg-expr"
version = "0.20.7"
version = "0.20.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3c6b04e07d8080154ed4ac03546d9a2b303cc2fe1901ba0b35b301516e289368"
checksum = "fb693542bcafa528e198be0ebd9d3632ca5b7c93dbe7237460e199910835997c"
dependencies = [
"smallvec",
"target-lexicon",
@@ -765,7 +765,7 @@ version = "0.14.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7d5d7dca3ebcf65a035582c9ad4385371a9d9ee6537474d2a278f4e1e475bb58"
dependencies = [
"bitflags 2.11.1",
"bitflags 2.12.1",
"libc",
"objc2-audio-toolbox",
"objc2-core-audio",
@@ -776,7 +776,7 @@ dependencies = [
[[package]]
name = "cpal"
version = "0.18.0"
source = "git+https://github.com/RustAudio/cpal#d379d9d704cd564b206fb64ec235ff44bcb7b5d2"
source = "git+https://github.com/RustAudio/cpal#d2a268839bf9ada7c2477c8a5f9483c28aa12d0c"
dependencies = [
"alsa",
"block2 0.6.2",
@@ -893,7 +893,7 @@ version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e0e367e4e7da84520dedcac1901e4da967309406d1e51017ae1abfb97adbd38"
dependencies = [
"bitflags 2.11.1",
"bitflags 2.12.1",
"block2 0.6.2",
"libc",
"objc2 0.6.4",
@@ -999,7 +999,7 @@ checksum = "42112be0ae157289312b92b3dfaf20e911b5a3c4c65d4aab0e7c47fbc0ce16e3"
dependencies = [
"accesskit",
"ahash",
"bitflags 2.11.1",
"bitflags 2.12.1",
"emath",
"epaint",
"log",
@@ -1596,7 +1596,7 @@ version = "0.32.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "12124de845cacfebedff80e877bb37b5b75c34c5a4c89e47e1cdd67fb6041325"
dependencies = [
"bitflags 2.11.1",
"bitflags 2.12.1",
"cfg_aliases",
"cgl",
"dispatch2",
@@ -1788,9 +1788,9 @@ checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87"
[[package]]
name = "hyper"
version = "1.10.0"
version = "1.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eb92f162bf56536459fc83c79b974bb12837acfed43d6bc370a7916d0ae15ecc"
checksum = "55281c53a1894c864990125767da440a4e630446785086f52523b20033b74498"
dependencies = [
"atomic-waker",
"bytes",
@@ -2232,10 +2232,10 @@ version = "0.1.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f02ab6bace2054fb888a3c16f990117b579d14a3088e472d63c6011fa185c9d3"
dependencies = [
"bitflags 2.11.1",
"bitflags 2.12.1",
"libc",
"plain",
"redox_syscall 0.8.0",
"redox_syscall 0.8.1",
]
[[package]]
@@ -2244,7 +2244,7 @@ version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2909f3be29d674e7f10604aff18d1bbe1bb03c4cd61c8a8ba19c0b1d162f7d4e"
dependencies = [
"bitflags 2.11.1",
"bitflags 2.12.1",
"cc",
"cookie-factory",
"libc",
@@ -2306,9 +2306,9 @@ dependencies = [
[[package]]
name = "log"
version = "0.4.30"
version = "0.4.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "616ec5685824bcc94416c6d4a7a446eea774a31efd7062c8480ba6fd06d7a6e5"
checksum = "113b30b4cd05f7c06868fdb2854f66a7b9fece9a48425351cd532e810d74024f"
[[package]]
name = "lru-slab"
@@ -2382,9 +2382,9 @@ dependencies = [
[[package]]
name = "mio"
version = "1.2.0"
version = "1.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "50b7e5b27aa02a74bac8c3f23f448f8d87ff11f92d3aac1a6ed369ee08cc56c1"
checksum = "02bd0af71c67b473010cbbc60715ee815645a4dc942899111f494b4b737d6fda"
dependencies = [
"libc",
"wasi",
@@ -2409,7 +2409,7 @@ checksum = "0dd91265cc2454558f659b3b4b9640f0ddb8cc6521277f166b8a8c181c898079"
dependencies = [
"arrayvec",
"bit-set",
"bitflags 2.11.1",
"bitflags 2.12.1",
"cfg-if",
"cfg_aliases",
"codespan-reporting",
@@ -2432,7 +2432,7 @@ version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c3f42e7bbe13d351b6bead8286a43aac9534b82bd3cc43e47037f012ebfd62d4"
dependencies = [
"bitflags 2.11.1",
"bitflags 2.12.1",
"jni-sys 0.3.1",
"log",
"ndk-sys",
@@ -2462,7 +2462,7 @@ version = "0.29.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46"
dependencies = [
"bitflags 2.11.1",
"bitflags 2.12.1",
"cfg-if",
"cfg_aliases",
"libc",
@@ -2615,7 +2615,7 @@ version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e4e89ad9e3d7d297152b17d39ed92cd50ca8063a89a9fa569046d41568891eff"
dependencies = [
"bitflags 2.11.1",
"bitflags 2.12.1",
"block2 0.5.1",
"libc",
"objc2 0.5.2",
@@ -2631,7 +2631,7 @@ version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d49e936b501e5c5bf01fda3a9452ff86dc3ea98ad5f283e1455153142d97518c"
dependencies = [
"bitflags 2.11.1",
"bitflags 2.12.1",
"block2 0.6.2",
"objc2 0.6.4",
"objc2-core-foundation",
@@ -2645,7 +2645,7 @@ version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6948501a91121d6399b79abaa33a8aa4ea7857fe019f341b8c23ad6e81b79b08"
dependencies = [
"bitflags 2.11.1",
"bitflags 2.12.1",
"libc",
"objc2 0.6.4",
"objc2-core-audio",
@@ -2660,7 +2660,7 @@ version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "13a380031deed8e99db00065c45937da434ca987c034e13b87e4441f9e4090be"
dependencies = [
"bitflags 2.11.1",
"bitflags 2.12.1",
"objc2 0.6.4",
"objc2-foundation 0.3.2",
]
@@ -2671,7 +2671,7 @@ version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "74dd3b56391c7a0596a295029734d3c1c5e7e510a4cb30245f8221ccea96b009"
dependencies = [
"bitflags 2.11.1",
"bitflags 2.12.1",
"block2 0.5.1",
"objc2 0.5.2",
"objc2-core-location",
@@ -2708,7 +2708,7 @@ version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5a89f2ec274a0cf4a32642b2991e8b351a404d290da87bb6a9a9d8632490bd1c"
dependencies = [
"bitflags 2.11.1",
"bitflags 2.12.1",
"objc2 0.6.4",
]
@@ -2718,7 +2718,7 @@ version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "617fbf49e071c178c0b24c080767db52958f716d9eabdf0890523aeae54773ef"
dependencies = [
"bitflags 2.11.1",
"bitflags 2.12.1",
"block2 0.5.1",
"objc2 0.5.2",
"objc2-foundation 0.2.2",
@@ -2730,7 +2730,7 @@ version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536"
dependencies = [
"bitflags 2.11.1",
"bitflags 2.12.1",
"block2 0.6.2",
"dispatch2",
"libc",
@@ -2743,7 +2743,7 @@ version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e022c9d066895efa1345f8e33e584b9f958da2fd4cd116792e15e07e4720a807"
dependencies = [
"bitflags 2.11.1",
"bitflags 2.12.1",
"dispatch2",
"objc2 0.6.4",
"objc2-core-foundation",
@@ -2786,7 +2786,7 @@ version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0ee638a5da3799329310ad4cfa62fbf045d5f56e3ef5ba4149e7452dcf89d5a8"
dependencies = [
"bitflags 2.11.1",
"bitflags 2.12.1",
"block2 0.5.1",
"dispatch",
"libc",
@@ -2799,7 +2799,7 @@ version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272"
dependencies = [
"bitflags 2.11.1",
"bitflags 2.12.1",
"block2 0.6.2",
"libc",
"objc2 0.6.4",
@@ -2812,7 +2812,7 @@ version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "180788110936d59bab6bd83b6060ffdfffb3b922ba1396b312ae795e1de9d81d"
dependencies = [
"bitflags 2.11.1",
"bitflags 2.12.1",
"objc2 0.6.4",
"objc2-core-foundation",
]
@@ -2835,7 +2835,7 @@ version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dd0cba1276f6023976a406a14ffa85e1fdd19df6b0f737b063b95f6c8c7aadd6"
dependencies = [
"bitflags 2.11.1",
"bitflags 2.12.1",
"block2 0.5.1",
"objc2 0.5.2",
"objc2-foundation 0.2.2",
@@ -2847,7 +2847,7 @@ version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e42bee7bff906b14b167da2bac5efe6b6a07e6f7c0a21a7308d40c960242dc7a"
dependencies = [
"bitflags 2.11.1",
"bitflags 2.12.1",
"block2 0.5.1",
"objc2 0.5.2",
"objc2-foundation 0.2.2",
@@ -2870,7 +2870,7 @@ version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b8bb46798b20cd6b91cbd113524c490f1686f4c4e8f49502431415f3512e2b6f"
dependencies = [
"bitflags 2.11.1",
"bitflags 2.12.1",
"block2 0.5.1",
"objc2 0.5.2",
"objc2-cloud-kit",
@@ -2891,7 +2891,7 @@ version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d87d638e33c06f577498cbcc50491496a3ed4246998a7fbba7ccb98b1e7eab22"
dependencies = [
"bitflags 2.11.1",
"bitflags 2.12.1",
"objc2 0.6.4",
"objc2-core-foundation",
"objc2-foundation 0.3.2",
@@ -2914,7 +2914,7 @@ version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "76cfcbf642358e8689af64cee815d139339f3ed8ad05103ed5eaf73db8d84cb3"
dependencies = [
"bitflags 2.11.1",
"bitflags 2.12.1",
"block2 0.5.1",
"objc2 0.5.2",
"objc2-core-location",
@@ -3116,7 +3116,7 @@ version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8585aba8a52ad74ccc633b8e293c1dc4277976bd5d510b925533f34fd6685f38"
dependencies = [
"bitflags 2.11.1",
"bitflags 2.12.1",
"libc",
"libspa",
"libspa-sys",
@@ -3153,7 +3153,7 @@ version = "0.18.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "60769b8b31b2a9f263dae2776c37b1b28ae246943cf719eb6946a1db05128a61"
dependencies = [
"bitflags 2.11.1",
"bitflags 2.12.1",
"crc32fast",
"fdeflate",
"flate2",
@@ -3279,28 +3279,45 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3d595e54a326bc53c1c197b32d295e14b169e3cfeaa8dc82b529f947fba6bcf5"
[[package]]
name = "pwsp"
version = "1.9.1"
name = "pwsp-cli"
version = "1.11.0"
dependencies = [
"anyhow",
"async-trait",
"clap",
"dirs",
"pwsp-lib",
"serde_json",
"tokio",
]
[[package]]
name = "pwsp-daemon"
version = "1.11.0"
dependencies = [
"anyhow",
"clap",
"pipewire",
"pwsp-lib",
"serde_json",
"tokio",
]
[[package]]
name = "pwsp-gui"
version = "1.11.0"
dependencies = [
"anyhow",
"eframe",
"egui",
"egui_dnd",
"egui_extras",
"egui_material_icons",
"evdev",
"itertools 0.14.0",
"opener",
"percent-encoding",
"pipewire",
"pwsp-lib",
"reqwest",
"rfd",
"rodio",
"rust-i18n",
"rustix 1.1.4",
"serde",
"serde_json",
"sys-locale",
@@ -3308,6 +3325,25 @@ dependencies = [
"tokio",
]
[[package]]
name = "pwsp-lib"
version = "1.11.0"
dependencies = [
"anyhow",
"async-trait",
"dirs",
"egui",
"evdev",
"itertools 0.14.0",
"pipewire",
"reqwest",
"rodio",
"rustix 1.1.4",
"serde",
"serde_json",
"tokio",
]
[[package]]
name = "pxfm"
version = "0.1.29"
@@ -3507,16 +3543,16 @@ version = "0.5.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d"
dependencies = [
"bitflags 2.11.1",
"bitflags 2.12.1",
]
[[package]]
name = "redox_syscall"
version = "0.8.0"
version = "0.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7c7591fa2c6b601dfcfe5f043f65a1c39fcdf50efefcd7f1572e538c1f4b398d"
checksum = "5b44b894f2a6e36457d665d1e08c3866add6ed5e70050c1b4ba8a8ddedb02ce7"
dependencies = [
"bitflags 2.11.1",
"bitflags 2.12.1",
]
[[package]]
@@ -3650,7 +3686,7 @@ checksum = "3582f63211428f83597b51b2ddb88e2a91a9d52d12831f9d08f5e624e8977422"
[[package]]
name = "rodio"
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 = [
"cpal",
"dasp_sample",
@@ -3668,9 +3704,9 @@ checksum = "6c20b6793b5c2fa6553b250154b78d6d0db37e72700ae35fad9387a46f487c97"
[[package]]
name = "rust-i18n"
version = "4.0.0"
version = "4.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "21031bf5e6f2c0ae745d831791c403608e99a8bd3776c7e5e5535acd70c3b7ba"
checksum = "55691a65892c33ee2de49c15ea5600c6f4a70e8eeb8e6c3cd96d2a231d230c40"
dependencies = [
"globwalk",
"regex",
@@ -3681,9 +3717,9 @@ dependencies = [
[[package]]
name = "rust-i18n-macro"
version = "4.0.0"
version = "4.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "51fe5295763b358606f7ca26a564e20f4469775a57ec1f09431249a33849ff52"
checksum = "30de488acadcf767d97cd48518a8da8ea9777b1c9a5beca4eab78bbf77d07309"
dependencies = [
"glob",
"proc-macro2",
@@ -3697,18 +3733,15 @@ dependencies = [
[[package]]
name = "rust-i18n-support"
version = "4.0.0"
version = "4.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "69bcc115c8eea2803aa3d85362e339776f4988a0349f2f475af572e497443f6f"
checksum = "aea0fef8a93c06326b66392c95a115120e609674cb2132d37d276a6b05b545b4"
dependencies = [
"arc-swap",
"base62",
"globwalk",
"itertools 0.11.0",
"lazy_static",
"normpath",
"proc-macro2",
"regex",
"serde",
"serde_json",
"serde_yaml",
@@ -3744,7 +3777,7 @@ version = "0.38.44"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154"
dependencies = [
"bitflags 2.11.1",
"bitflags 2.12.1",
"errno",
"libc",
"linux-raw-sys 0.4.15",
@@ -3757,7 +3790,7 @@ version = "1.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190"
dependencies = [
"bitflags 2.11.1",
"bitflags 2.12.1",
"errno",
"libc",
"linux-raw-sys 0.12.1",
@@ -3780,9 +3813,9 @@ dependencies = [
[[package]]
name = "rustls-native-certs"
version = "0.8.3"
version = "0.8.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "612460d5f7bea540c490b2b6395d8e34a953e52b491accd6c86c8164c5932a63"
checksum = "dab5152771c58876a2146916e53e35057e1a4dfa2b9df0f0305b07f611fdea4d"
dependencies = [
"openssl-probe",
"rustls-pki-types",
@@ -3887,7 +3920,7 @@ version = "3.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d"
dependencies = [
"bitflags 2.11.1",
"bitflags 2.12.1",
"core-foundation 0.10.1",
"core-foundation-sys",
"libc",
@@ -4007,6 +4040,12 @@ version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
[[package]]
name = "shlex"
version = "2.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba"
[[package]]
name = "signal-hook-registry"
version = "1.4.8"
@@ -4088,7 +4127,7 @@ version = "0.19.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3457dea1f0eb631b4034d61d4d8c32074caa6cd1ab2d59f2327bd8461e2c0016"
dependencies = [
"bitflags 2.11.1",
"bitflags 2.12.1",
"calloop 0.13.0",
"calloop-wayland-source 0.3.0",
"cursor-icon",
@@ -4113,7 +4152,7 @@ version = "0.20.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0512da38f5e2b31201a93524adb8d3136276fa4fe4aafab4e1f727a82b534cc0"
dependencies = [
"bitflags 2.11.1",
"bitflags 2.12.1",
"calloop 0.14.4",
"calloop-wayland-source 0.4.1",
"cursor-icon",
@@ -4156,9 +4195,9 @@ dependencies = [
[[package]]
name = "socket2"
version = "0.6.3"
version = "0.6.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e"
checksum = "52d1cfed4120b4d927bf7c0f86d2087a4a7d6027c906d9f9d525a80573b9be51"
dependencies = [
"libc",
"windows-sys 0.61.2",
@@ -4440,7 +4479,7 @@ version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a13f3d0daba03132c0aa9767f98351b3488edc2c100cda2d2ec2b04f3d8d3c8b"
dependencies = [
"bitflags 2.11.1",
"bitflags 2.12.1",
"core-foundation 0.9.4",
"system-configuration-sys",
]
@@ -4487,9 +4526,9 @@ checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369"
[[package]]
name = "target-lexicon"
version = "0.13.3"
version = "0.13.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "df7f62577c25e07834649fc3b39fafdc597c0a3527dc1c60129201ccfcbaa50c"
checksum = "adb6935a6f5c20170eeceb1a3835a49e12e19d792f6dd344ccc76a985ca5a6ca"
[[package]]
name = "tempfile"
@@ -4747,7 +4786,7 @@ version = "0.6.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4cfcf7e2740e6fc6d4d688b4ef00650406bb94adf4731e43c096c3a19fe40840"
dependencies = [
"bitflags 2.11.1",
"bitflags 2.12.1",
"bytes",
"futures-util",
"http",
@@ -4863,9 +4902,9 @@ checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
[[package]]
name = "unicode-segmentation"
version = "1.13.2"
version = "1.13.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9629274872b2bfaf8d66f5f15725007f635594914870f65218920345aa11aa8c"
checksum = "c6f5d3c3b1bf09027a88a6bc961fc00497d651009560b5463668dc81b0fa87a8"
[[package]]
name = "unicode-width"
@@ -4912,9 +4951,9 @@ checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be"
[[package]]
name = "uuid"
version = "1.23.1"
version = "1.23.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ddd74a9687298c6858e9b88ec8935ec45d22e8fd5e6394fa1bd4e99a87789c76"
checksum = "d258b83ceec21034727ecee8c382cfa6c3e133699b0742c64571814fb420c9f7"
dependencies = [
"js-sys",
"serde_core",
@@ -5085,7 +5124,7 @@ version = "0.244.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe"
dependencies = [
"bitflags 2.11.1",
"bitflags 2.12.1",
"hashbrown 0.15.5",
"indexmap",
"semver",
@@ -5111,7 +5150,7 @@ version = "0.31.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "645c7c96bb74690c3189b5c9cb4ca1627062bb23693a4fad9d8c3de958260144"
dependencies = [
"bitflags 2.11.1",
"bitflags 2.12.1",
"rustix 1.1.4",
"wayland-backend",
"wayland-scanner",
@@ -5123,7 +5162,7 @@ version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "625c5029dbd43d25e6aa9615e88b829a5cad13b2819c4ae129fdbb7c31ab4c7e"
dependencies = [
"bitflags 2.11.1",
"bitflags 2.12.1",
"cursor-icon",
"wayland-backend",
]
@@ -5145,7 +5184,7 @@ version = "0.32.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "563a85523cade2429938e790815fd7319062103b9f4a2dc806e9b53b95982d8f"
dependencies = [
"bitflags 2.11.1",
"bitflags 2.12.1",
"wayland-backend",
"wayland-client",
"wayland-scanner",
@@ -5157,7 +5196,7 @@ version = "20250721.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "40a1f863128dcaaec790d7b4b396cc9b9a7a079e878e18c47e6c2d2c5a8dcbb1"
dependencies = [
"bitflags 2.11.1",
"bitflags 2.12.1",
"wayland-backend",
"wayland-client",
"wayland-protocols",
@@ -5170,7 +5209,7 @@ version = "0.3.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6e9567599ef23e09b8dad6e429e5738d4509dfc46b3b21f32841a304d16b29c8"
dependencies = [
"bitflags 2.11.1",
"bitflags 2.12.1",
"wayland-backend",
"wayland-client",
"wayland-protocols",
@@ -5183,7 +5222,7 @@ version = "0.3.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2b6d8cf1eb2c1c31ed1f5643c88a6e53538129d4af80030c8cabd1f9fa884d91"
dependencies = [
"bitflags 2.11.1",
"bitflags 2.12.1",
"wayland-backend",
"wayland-client",
"wayland-protocols",
@@ -5196,7 +5235,7 @@ version = "0.3.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eb04e52f7836d7c7976c78ca0250d61e33873c34156a2a1fc9474828ec268234"
dependencies = [
"bitflags 2.11.1",
"bitflags 2.12.1",
"wayland-backend",
"wayland-client",
"wayland-protocols",
@@ -5284,7 +5323,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bb3feacc458f7bee8bc1737149b42b6c731aa461039a4264a67bb6681646b250"
dependencies = [
"arrayvec",
"bitflags 2.11.1",
"bitflags 2.12.1",
"bytemuck",
"cfg-if",
"cfg_aliases",
@@ -5314,7 +5353,7 @@ dependencies = [
"arrayvec",
"bit-set",
"bit-vec",
"bitflags 2.11.1",
"bitflags 2.12.1",
"bytemuck",
"cfg_aliases",
"document-features",
@@ -5351,7 +5390,7 @@ version = "29.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "31f8e1a9e7a8512f276f7c62e018c7fa8d60954303fed2e5750114332049193f"
dependencies = [
"bitflags 2.11.1",
"bitflags 2.12.1",
"cfg-if",
"cfg_aliases",
"libloading",
@@ -5382,7 +5421,7 @@ version = "29.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a9bcc31518a0e9735aefebedb5f7a9ef3ed1c42549c9f4c882fa9060ceaac639"
dependencies = [
"bitflags 2.11.1",
"bitflags 2.12.1",
"bytemuck",
"js-sys",
"log",
@@ -5694,7 +5733,7 @@ dependencies = [
"ahash",
"android-activity",
"atomic-waker",
"bitflags 2.11.1",
"bitflags 2.12.1",
"block2 0.5.1",
"bytemuck",
"calloop 0.13.0",
@@ -5818,7 +5857,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2"
dependencies = [
"anyhow",
"bitflags 2.11.1",
"bitflags 2.12.1",
"indexmap",
"log",
"serde",
@@ -5907,7 +5946,7 @@ version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d039de8032a9a8856a6be89cea3e5d12fdd82306ab7c94d74e6deab2460651c5"
dependencies = [
"bitflags 2.11.1",
"bitflags 2.12.1",
"dlib",
"log",
"once_cell",
@@ -5951,9 +5990,9 @@ dependencies = [
[[package]]
name = "zbus"
version = "5.15.0"
version = "5.16.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c3bcbf15c8708d7fc1be0c993622e0a5cbd5e8b52bfa40afa4c3e0cd8d724ac1"
checksum = "eee682d202a77e4a9f3b2c2bdf48a7b28af5c08c34ddf66f98c93e5e39464285"
dependencies = [
"async-broadcast",
"async-executor",
@@ -5986,9 +6025,9 @@ dependencies = [
[[package]]
name = "zbus_macros"
version = "5.15.0"
version = "5.16.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "51fa5406ad9175a8c825a931f8cf347116b531b3634fcb0b627c290f1f2516ff"
checksum = "adf1bd45a81a103745b1757754762a26e8cd01e4532e4d6c8ec431624b80d1d6"
dependencies = [
"proc-macro-crate",
"proc-macro2",
@@ -6012,18 +6051,18 @@ dependencies = [
[[package]]
name = "zerocopy"
version = "0.8.49"
version = "0.8.50"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bce33a6288fa3f072a8c2c7d0f2fdbb90e28298f0135c1f99b96c3db2efcc60b"
checksum = "3b065d4f0e55f82fae73202e189638116a87c55ab6b8e6c2721e13dd9d854ad1"
dependencies = [
"zerocopy-derive",
]
[[package]]
name = "zerocopy-derive"
version = "0.8.49"
version = "0.8.50"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8fd425244944f4ab65ccff928e7323354c5a018c75838362fdce749dfad2ee1e"
checksum = "0b631b19d36a892ab55420c92dbc83ccd79274f25be714855d3074aa71cab639"
dependencies = [
"proc-macro2",
"quote",
@@ -6141,9 +6180,9 @@ dependencies = [
[[package]]
name = "zvariant"
version = "5.11.0"
version = "5.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1c1567a6ec68df868cbbfde844cfc6d81649fe5109a62b116b19fabd53e618ee"
checksum = "a192a0bde63360d77a7523c833d4b4ce6070a927e2c53246e4c540b1a3e27be0"
dependencies = [
"endi",
"enumflags2",
@@ -6156,9 +6195,9 @@ dependencies = [
[[package]]
name = "zvariant_derive"
version = "5.11.0"
version = "5.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c7d5b780599bbde114e39d9a0799577fad1ced5105d38515745f7b3099d8ceda"
checksum = "90bc6cde9c01c511074be97f7ccb6c19d0da89e3f8662e812e999dcfd4638737"
dependencies = [
"proc-macro-crate",
"proc-macro2",
@@ -6169,9 +6208,9 @@ dependencies = [
[[package]]
name = "zvariant_utils"
version = "3.3.1"
version = "3.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6d464f5733ffa07a3164d656f18533caace9d0638596721355d73256a410d691"
checksum = "1e8535915cfa75547e559d8c68e8139909a4aeee076831e4ef7fc59d8172c4d6"
dependencies = [
"proc-macro2",
"quote",
+18 -54
View File
@@ -1,17 +1,28 @@
[package]
name = "pwsp"
version = "1.9.1"
[workspace]
members = [
"pwsp-lib",
"pwsp-daemon",
"pwsp-cli",
"pwsp-gui"
]
resolver = "2"
[workspace.package]
version = "1.11.0"
edition = "2024"
authors = ["arabian"]
description = "PWSP lets you play audio files through your microphone. Has both CLI and GUI clients."
readme = "README.md"
homepage = "https://pwsp.arabianq.ru"
repository = "https://github.com/arabianq/pipewire-soundpad"
license = "MIT"
description = "PWSP lets you play audio files through your microphone. Has both CLI and GUI clients."
keywords = ["soundpad", "pipewire", "linux", "cli", "gui"]
[workspace.dependencies]
pwsp-lib = { path = "pwsp-lib" }
pwsp-daemon = { path = "pwsp-daemon" }
pwsp-cli = { path = "pwsp-cli" }
pwsp-gui = { path = "pwsp-gui" }
[dependencies]
tokio = { version = "1.52.3", features = ["full"] }
async-trait = "0.1.89"
@@ -31,9 +42,7 @@ dirs = "6.0.0"
itertools = "0.14.0"
evdev = { version = "0.13.2", features = ["tokio"] }
rfd = { version = "0.17.2", default-features = false, features = [
"xdg-portal",
] }
opener = { version = "0.8.4", features = ["reveal"] }
system-fonts = "0.1.1"
@@ -43,7 +52,7 @@ rustix = { version = "1.1.4", features = ["process"] }
rust-i18n = "4.0.0"
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-libopus",
"playback",
@@ -67,18 +76,6 @@ egui_dnd = "0.15.0"
reqwest = "0.13.4"
percent-encoding = "2.3.2"
[[bin]]
name = "pwsp-daemon"
path = "src/bin/daemon.rs"
[[bin]]
name = "pwsp-cli"
path = "src/bin/cli.rs"
[[bin]]
name = "pwsp-gui"
path = "src/main.rs"
[profile.release]
strip = true
lto = true
@@ -86,36 +83,3 @@ codegen-units = 1
opt-level = "z"
panic = "abort"
[package.metadata.deb]
assets = [
[
"target/release/pwsp-daemon",
"usr/bin/",
"755",
],
[
"target/release/pwsp-cli",
"usr/bin/",
"755",
],
[
"target/release/pwsp-gui",
"usr/bin/",
"755",
],
[
"assets/pwsp-gui.desktop",
"usr/share/applications/pwsp.desktop",
"644",
],
[
"assets/icon.png",
"usr/share/icons/hicolor/256x256/apps/pwsp.png",
"644",
],
[
"assets/pwsp-daemon.service",
"usr/lib/systemd/user/pwsp-daemon.service",
"644",
],
]
+1 -1
View File
@@ -1,7 +1,7 @@
<div align="center">
<h1>🎵 PipeWire Soundpad (PWSP)</h1>
<p><b>A simple, modern, and powerful soundboard for Linux, written in Rust.</b></p>
<img src="assets/screenshot.png" alt="PWSP Screenshot" width="700"/>
<img src="pwsp-gui/assets/screenshot.png" alt="PWSP Screenshot" width="700"/>
</div>
## 🌟 Overview
BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

+7 -4
View File
@@ -1,17 +1,20 @@
pkgbase = pwsp-bin
pkgdesc = Lets you play audio files through your microphone (Pre-built binaries)
pkgver = 1.9.1
pkgver = 1.11.0
pkgrel = 1
url = https://github.com/arabianq/pipewire-soundpad
arch = x86_64
arch = aarch64
license = MIT
depends = pipewire
depends = alsa-lib
provides = 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 = pipewire-soundpad-1.9.1.tar.gz :: https://github.com/arabianq/pipewire-soundpad/archive/refs/tags/v1.9.1.tar.gz
sha256sums = SKIP
source = pipewire-soundpad-1.11.0.tar.gz :: https://github.com/arabianq/pipewire-soundpad/archive/refs/tags/v1.11.0.tar.gz
sha256sums = SKIP
source_x86_64 = pwsp-1.11.0-x86_64.zip :: https://github.com/arabianq/pipewire-soundpad/releases/download/v1.11.0/pwsp-v1.11.0-linux-x64.zip
sha256sums_x86_64 = SKIP
source_aarch64 = pwsp-1.11.0-aarch64.zip :: https://github.com/arabianq/pipewire-soundpad/releases/download/v1.11.0/pwsp-v1.11.0-linux-arm64.zip
sha256sums_aarch64 = SKIP
pkgname = pwsp-bin
+8 -6
View File
@@ -1,21 +1,23 @@
# Maintainer: Alexander Tarasov <a.tevg@ya.ru>
pkgname=pwsp-bin
_pkgname=pipewire-soundpad
pkgver=1.9.1
pkgver=1.11.0
pkgrel=1
pkgdesc="Lets you play audio files through your microphone (Pre-built binaries)"
arch=('x86_64')
arch=('x86_64' 'aarch64')
url="https://github.com/arabianq/pipewire-soundpad"
license=('MIT')
depends=('pipewire' 'alsa-lib')
provides=('pwsp')
conflicts=('pwsp')
source=("${pkgname}-${pkgver}.zip::https://github.com/arabianq/$_pkgname/releases/download/v$pkgver/pwsp-v$pkgver-linux-x64.zip"
"${_pkgname}-${pkgver}.tar.gz::https://github.com/arabianq/$_pkgname/archive/refs/tags/v$pkgver.tar.gz")
source_x86_64=("pwsp-${pkgver}-x86_64.zip::https://github.com/arabianq/$_pkgname/releases/download/v$pkgver/pwsp-v$pkgver-linux-x64.zip")
source_aarch64=("pwsp-${pkgver}-aarch64.zip::https://github.com/arabianq/$_pkgname/releases/download/v$pkgver/pwsp-v$pkgver-linux-arm64.zip")
source=("${_pkgname}-${pkgver}.tar.gz::https://github.com/arabianq/$_pkgname/archive/refs/tags/v$pkgver.tar.gz")
sha256sums=('SKIP'
'SKIP')
sha256sums=('SKIP')
sha256sums_x86_64=('SKIP')
sha256sums_aarch64=('SKIP')
package() {
_srcsrc="${srcdir}/${_pkgname}-${pkgver}"
+5 -4
View File
@@ -1,9 +1,10 @@
pkgbase = pwsp
pkgdesc = Lets you play audio files through your microphone
pkgver = 1.9.1
pkgrel = 2
pkgver = 1.11.0
pkgrel = 1
url = https://github.com/arabianq/pipewire-soundpad
arch = any
arch = x86_64
arch = aarch64
license = MIT
makedepends = clang
makedepends = rust
@@ -11,7 +12,7 @@ pkgbase = pwsp
makedepends = cmake
makedepends = pipewire
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.11.0.tar.gz
sha256sums = SKIP
pkgname = pwsp
+6 -6
View File
@@ -1,10 +1,10 @@
# Maintainer: Alexander Tarasov <a.tevg@ya.ru>
pkgsubn=pwsp
pkgname=pwsp
pkgver=1.9.1
pkgrel=2
pkgver=1.11.0
pkgrel=1
pkgdesc="Lets you play audio files through your microphone"
arch=('any')
arch=('x86_64' 'aarch64')
url="https://github.com/arabianq/pipewire-soundpad"
license=('MIT')
makedepends=(clang rust cargo cmake pipewire alsa-lib)
@@ -40,8 +40,8 @@ package() {
install -Dm755 "target/release/pwsp-daemon" "${pkgdir}/usr/bin/pwsp-daemon"
install -Dm755 "target/release/pwsp-gui" "${pkgdir}/usr/bin/pwsp-gui"
install -Dm644 "assets/pwsp-gui.desktop" "${pkgdir}/usr/share/applications/pwsp-gui.desktop"
install -Dm644 "assets/icon.png" "${pkgdir}/usr/share/icons/hicolor/256x256/apps/pwsp.png"
install -Dm644 "pwsp-gui/assets/pwsp-gui.desktop" "${pkgdir}/usr/share/applications/pwsp-gui.desktop"
install -Dm644 "pwsp-gui/assets/icon.png" "${pkgdir}/usr/share/icons/hicolor/256x256/apps/pwsp.png"
install -Dm644 "assets/pwsp-daemon.service" "${pkgdir}/usr/lib/systemd/user/pwsp-daemon.service"
install -Dm644 "pwsp-gui/assets/pwsp-daemon.service" "${pkgdir}/usr/lib/systemd/user/pwsp-daemon.service"
}
File diff suppressed because one or more lines are too long
@@ -15,7 +15,7 @@
<launchable type="desktop-id">ru.arabianq.pwsp.desktop</launchable>
<screenshots>
<screenshot type="default">
<image>https://raw.githubusercontent.com/arabianq/pipewire-soundpad/master/assets/screenshot.png</image>
<image>https://raw.githubusercontent.com/arabianq/pipewire-soundpad/main/pwsp-gui/assets/screenshot.png</image>
</screenshot>
</screenshots>
<url type="homepage">https://pwsp.arabianq.ru</url>
@@ -25,7 +25,7 @@
<name>arabian</name>
</developer>
<releases>
<release version="1.9.1" date="2026-05-22" />
<release version="1.11.0" date="2026-06-02" />
</releases>
<content_rating type="oars-1.1" />
</component>
+1 -1
View File
@@ -35,7 +35,7 @@ modules:
- install -Dm755 target/release/pwsp-cli /app/bin/pwsp-cli
- install -Dm755 target/release/pwsp-gui /app/bin/pwsp-gui
- install -Dm755 packages/flatpak/pwsp-wrapper.py /app/bin/pwsp-wrapper.py
- install -Dm644 assets/icon.png /app/share/icons/hicolor/256x256/apps/ru.arabianq.pwsp.png
- install -Dm644 pwsp-gui/assets/icon.png /app/share/icons/hicolor/256x256/apps/ru.arabianq.pwsp.png
- install -Dm644 packages/flatpak/ru.arabianq.pwsp.desktop /app/share/applications/ru.arabianq.pwsp.desktop
- install -Dm644 packages/flatpak/ru.arabianq.pwsp.metainfo.xml /app/share/metainfo/ru.arabianq.pwsp.metainfo.xml
sources:
+4 -4
View File
@@ -4,7 +4,7 @@
%global cargo_install_lib 0
Name: pwsp
Version: 1.9.1
Version: 1.11.0
Release: %autorelease
Summary: Lets you play audio files through your microphone
@@ -39,10 +39,10 @@ install -Dm755 target/release/pwsp-cli %{buildroot}%{_bindir}/pwsp-cli
install -Dm755 target/release/pwsp-daemon %{buildroot}%{_bindir}/pwsp-daemon
install -Dm755 target/release/pwsp-gui %{buildroot}%{_bindir}/pwsp-gui
install -Dm644 assets/pwsp-gui.desktop %{buildroot}%{_datadir}/applications/pwsp.desktop
install -Dm644 assets/icon.png %{buildroot}%{_datadir}/icons/hicolor/256x256/apps/pwsp.png
install -Dm644 pwsp-gui/assets/pwsp-gui.desktop %{buildroot}%{_datadir}/applications/pwsp.desktop
install -Dm644 pwsp-gui/assets/icon.png %{buildroot}%{_datadir}/icons/hicolor/256x256/apps/pwsp.png
install -Dm644 assets/pwsp-daemon.service %{buildroot}/usr/lib/systemd/user/pwsp-daemon.service
install -Dm644 pwsp-gui/assets/pwsp-daemon.service %{buildroot}/usr/lib/systemd/user/pwsp-daemon.service
%files
%license LICENSE
+18
View File
@@ -0,0 +1,18 @@
[package]
name = "pwsp-cli"
version.workspace = true
edition.workspace = true
authors.workspace = true
homepage.workspace = true
repository.workspace = true
license.workspace = true
[dependencies]
pwsp-lib.workspace = true
tokio.workspace = true
anyhow.workspace = true
clap.workspace = true
serde_json.workspace = true
+1 -1
View File
@@ -1,6 +1,6 @@
use anyhow::{Result, anyhow};
use clap::{Parser, Subcommand};
use pwsp::{
use pwsp_lib::{
types::socket::Request,
utils::daemon::{make_request, wait_for_daemon},
};
+20
View File
@@ -0,0 +1,20 @@
[package]
name = "pwsp-daemon"
version.workspace = true
edition.workspace = true
authors.workspace = true
homepage.workspace = true
repository.workspace = true
license.workspace = true
[dependencies]
pwsp-lib.workspace = true
tokio.workspace = true
serde_json.workspace = true
clap.workspace = true
anyhow.workspace = true
pipewire.workspace = true
+18 -9
View File
@@ -1,5 +1,5 @@
use anyhow::{Result, anyhow};
use pwsp::{
use pwsp_lib::{
types::socket::{MAX_MESSAGE_SIZE, Request, Response},
utils::{
commands::parse_command,
@@ -15,7 +15,7 @@ use std::os::unix::fs::PermissionsExt;
use std::{fs, time::Duration};
use tokio::{
io::{AsyncReadExt, AsyncWriteExt},
net::UnixListener,
net::{UnixListener, UnixStream},
time::sleep,
};
@@ -83,9 +83,15 @@ async fn main() -> Result<()> {
async fn commands_loop(listener: UnixListener) -> Result<()> {
loop {
let (mut stream, _addr) = listener.accept().await?;
let (stream, _addr) = listener.accept().await?;
tokio::spawn(async move {
handle_connection(stream).await;
});
}
}
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() {
@@ -103,8 +109,14 @@ async fn commands_loop(listener: UnixListener) -> Result<()> {
return;
}
let mut buffer = vec![0u8; request_len];
if stream.read_exact(&mut buffer).await.is_err() {
let mut buffer = Vec::new();
if (&mut stream)
.take(request_len as u64)
.read_to_end(&mut buffer)
.await
.is_err()
|| buffer.len() != request_len
{
eprintln!("Failed to read message from client!");
return;
}
@@ -112,8 +124,7 @@ async fn commands_loop(listener: UnixListener) -> Result<()> {
let request: Request = match serde_json::from_slice(&buffer) {
Ok(req) => req,
Err(err) => {
let response =
Response::new(false, format!("Failed to parse request: {}", err));
let response = Response::new(false, format!("Failed to parse request: {}", err));
let response_data = match serde_json::to_vec(&response) {
Ok(data) => data,
Err(_) => return, // Should not happen with this simple Response
@@ -159,8 +170,6 @@ async fn commands_loop(listener: UnixListener) -> Result<()> {
if response.status && response.message.eq("killed") {
std::process::exit(0);
}
});
}
}
async fn player_loop() {
+69
View File
@@ -0,0 +1,69 @@
[package]
name = "pwsp-gui"
version.workspace = true
edition.workspace = true
authors.workspace = true
homepage.workspace = true
repository.workspace = true
license.workspace = true
description.workspace = true
[dependencies]
pwsp-lib.workspace = true
tokio.workspace = true
opener.workspace = true
rfd.workspace = true
itertools.workspace = true
anyhow.workspace = true
serde.workspace = true
serde_json.workspace = true
egui.workspace = true
eframe.workspace = true
egui_extras.workspace = true
egui_material_icons.workspace = true
egui_dnd.workspace = true
system-fonts.workspace = true
rust-i18n.workspace = true
sys-locale.workspace = true
reqwest.workspace = true
percent-encoding.workspace = true
[package.metadata.deb]
assets = [
[
"target/release/pwsp-daemon",
"usr/bin/",
"755",
],
[
"target/release/pwsp-cli",
"usr/bin/",
"755",
],
[
"target/release/pwsp-gui",
"usr/bin/",
"755",
],
[
"assets/pwsp-gui.desktop",
"usr/share/applications/pwsp.desktop",
"644",
],
[
"assets/icon.png",
"usr/share/icons/hicolor/256x256/apps/pwsp.png",
"644",
],
[
"assets/pwsp-daemon.service",
"usr/lib/systemd/user/pwsp-daemon.service",
"644",
],
]
Binary file not shown.

After

Width:  |  Height:  |  Size: 79 KiB

Before

Width:  |  Height:  |  Size: 94 KiB

After

Width:  |  Height:  |  Size: 94 KiB

+69 -2
View File
@@ -1,7 +1,7 @@
use crate::gui::SoundpadGui;
use egui::{Context, Id, Key, Modifiers};
use pwsp::types::socket::Request;
use pwsp::utils::gui::make_request_async;
use pwsp_lib::types::socket::Request;
use pwsp_lib::utils::gui::make_request_async;
/// Convert an egui Key + Modifiers to a normalized chord string like "Ctrl+Shift+A".
fn chord_from_event(modifiers: &Modifiers, key: &Key) -> Option<String> {
@@ -217,3 +217,70 @@ impl SoundpadGui {
// });
}
}
#[cfg(test)]
mod tests {
use super::*;
use egui::{Key, Modifiers};
#[test]
fn test_chord_from_event() {
// Valid modifier + key
let mut mods = Modifiers::NONE;
mods.ctrl = true;
let chord = chord_from_event(&mods, &Key::A);
assert_eq!(chord, Some("Ctrl+A".to_string()));
// Multiple modifiers
mods.shift = true;
let chord = chord_from_event(&mods, &Key::F1);
assert_eq!(chord, Some("Ctrl+Shift+F1".to_string()));
// Missing modifiers (requires at least one modifier)
let no_mods = Modifiers::NONE;
let chord = chord_from_event(&no_mods, &Key::A);
assert_eq!(chord, None);
// Invalid keys (e.g. Escape or Enter shouldn't be accepted by chord_from_event)
mods.shift = false;
let chord = chord_from_event(&mods, &Key::Escape);
assert_eq!(chord, None);
}
#[test]
fn test_parse_chord() {
// Valid Ctrl+A
let res = parse_chord("Ctrl+A");
assert!(res.is_some());
let (mods, key) = res.unwrap();
assert!(mods.ctrl);
assert!(!mods.alt);
assert!(!mods.shift);
assert_eq!(key, Key::A);
// Valid Ctrl+Shift+F12
let res = parse_chord("Ctrl+Shift+F12");
assert!(res.is_some());
let (mods, key) = res.unwrap();
assert!(mods.ctrl);
assert!(mods.shift);
assert!(!mods.alt);
assert_eq!(key, Key::F12);
// Valid Ctrl+Alt+Shift+Super+B
let res = parse_chord("Ctrl+Alt+Shift+Super+B");
assert!(res.is_some());
let (mods, key) = res.unwrap();
assert!(mods.ctrl);
assert!(mods.alt);
assert!(mods.shift);
assert!(mods.command); // Super maps to command in egui Modifiers
assert_eq!(key, Key::B);
// Invalid keys/chords
assert!(parse_chord("").is_none());
assert!(parse_chord("Ctrl+").is_none());
assert!(parse_chord("Ctrl+Escape").is_none());
assert!(parse_chord("Invalid+A").is_none());
}
}
+43 -1
View File
@@ -6,7 +6,7 @@ use anyhow::{Result, anyhow};
use eframe::{HardwareAcceleration, NativeOptions, icon_data::from_png_bytes, run_native};
use egui::{Context, FontData, FontDefinitions, FontFamily, FontTweak, Vec2, ViewportBuilder};
use itertools::Itertools;
use pwsp::{
use pwsp_lib::{
types::{
audio_player::PlayerState,
config::GuiConfig,
@@ -294,3 +294,45 @@ pub async fn run() -> Result<()> {
Err(e) => Err(anyhow!(e.to_string())),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_get_filtered_files() {
let mut gui = SoundpadGui {
app_state: AppState::default(),
config: GuiConfig::default(),
audio_player_state: AudioPlayerState::default(),
audio_player_state_shared: Arc::new(Mutex::new(AudioPlayerState::default())),
};
// Create some dummy paths
// We will mock path properties using standard Rust PathBuf
let dir_a = PathBuf::from("a_dir");
let file_b = PathBuf::from("b_file.mp3");
let file_c = PathBuf::from("c_file.wav");
let file_txt = PathBuf::from("invalid.txt");
gui.app_state.listed_files.insert(dir_a.clone());
gui.app_state.listed_files.insert(file_b.clone());
gui.app_state.listed_files.insert(file_c.clone());
gui.app_state.listed_files.insert(file_txt.clone());
// Note: is_dir() check in get_filtered_files relies on physical filesystem properties.
// On the real OS filesystem, these paths don't exist, so they are treated as files.
// Unsupported extensions (like .txt) will be filtered out.
// So we expect only file_b and file_c, sorted alphabetically.
let filtered = gui.get_filtered_files();
assert_eq!(filtered.len(), 2);
assert_eq!(filtered[0], file_b);
assert_eq!(filtered[1], file_c);
// Test search query
gui.app_state.search_query = "c_fi".to_string();
let filtered_search = gui.get_filtered_files();
assert_eq!(filtered_search.len(), 1);
assert_eq!(filtered_search[0], file_c);
}
}
@@ -1,7 +1,7 @@
use crate::gui::SoundpadGui;
use eframe::{App, Frame as EFrame};
use egui::{CentralPanel, Context, ThemePreference};
use pwsp::{
use pwsp_lib::{
types::{config::PreferredTheme, socket::Request},
utils::{daemon::get_daemon_config, gui::make_request_async},
};
@@ -5,7 +5,7 @@ use egui::{
};
use egui_dnd::dnd;
use egui_material_icons::icons::*;
use pwsp::types::{gui::AppState, gui::AudioPlayerState};
use pwsp_lib::types::{gui::AppState, gui::AudioPlayerState};
use rust_i18n::t;
use std::{cmp::Ordering, path::Path, path::PathBuf};
+92
View File
@@ -0,0 +1,92 @@
use crate::gui::SoundpadGui;
use egui::{AtomExt, Button, ComboBox, Label, RichText, Slider, Ui, Vec2};
use egui_material_icons::icons::*;
use rust_i18n::t;
use std::time::Instant;
impl SoundpadGui {
pub fn draw_footer(&mut self, ui: &mut Ui) {
ui.add_space(5.0);
ui.horizontal(|ui| {
self.draw_mic_selection(ui);
self.draw_master_volume(ui);
ui.add_space(ui.available_width() - 18.0 * 2.0 - ui.spacing().item_spacing.x * 2.0);
self.draw_hotkeys_button(ui);
self.draw_settings_button(ui);
});
}
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;
}
}
}
@@ -1,8 +1,8 @@
use crate::gui::SoundpadGui;
use egui::{Button, CollapsingHeader, FontFamily, Label, RichText, Slider, Ui};
use egui_material_icons::icons::*;
use pwsp::types::{audio_player::TrackInfo, gui::AppState};
use pwsp::utils::gui::format_time_pair;
use pwsp_lib::types::{audio_player::TrackInfo, gui::AppState};
use pwsp_lib::utils::gui::format_time_pair;
use std::time::Instant;
pub(crate) enum TrackAction {
@@ -91,7 +91,7 @@ impl SoundpadGui {
fn draw_position_control(
ui: &mut Ui,
ui_state: &mut pwsp::types::gui::TrackUiState,
ui_state: &mut pwsp_lib::types::gui::TrackUiState,
track: &TrackInfo,
default_slider_width: f32,
) {
@@ -117,7 +117,7 @@ impl SoundpadGui {
fn draw_volume_control(
ui: &mut Ui,
ui_state: &mut pwsp::types::gui::TrackUiState,
ui_state: &mut pwsp_lib::types::gui::TrackUiState,
track: &TrackInfo,
default_slider_width: f32,
) {
@@ -2,8 +2,8 @@ use crate::gui::SoundpadGui;
use egui::{Button, Color32, Label, RichText, TextEdit, Ui};
use egui_extras::{Column, TableBuilder};
use egui_material_icons::icons::*;
use pwsp::types::socket::Request;
use pwsp::utils::gui::make_request_async;
use pwsp_lib::types::socket::Request;
use pwsp_lib::utils::gui::make_request_async;
use rust_i18n::t;
use std::path::Path;
@@ -30,3 +30,26 @@ impl SoundpadGui {
self.draw_footer(ui);
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_get_volume_icon() {
assert_eq!(SoundpadGui::get_volume_icon(0.8), ICON_VOLUME_UP.codepoint);
assert_eq!(SoundpadGui::get_volume_icon(0.0), ICON_VOLUME_OFF.codepoint);
assert_eq!(
SoundpadGui::get_volume_icon(-0.1),
ICON_VOLUME_OFF.codepoint
);
assert_eq!(
SoundpadGui::get_volume_icon(0.2),
ICON_VOLUME_MUTE.codepoint
);
assert_eq!(
SoundpadGui::get_volume_icon(0.5),
ICON_VOLUME_DOWN.codepoint
);
}
}
@@ -1,7 +1,7 @@
use crate::gui::SoundpadGui;
use egui::{Align, Button, Color32, ComboBox, Layout, RichText, Ui};
use egui_material_icons::icons::ICON_ARROW_BACK;
use pwsp::types::config::PreferredTheme;
use pwsp_lib::types::config::PreferredTheme;
use rust_i18n::t;
impl SoundpadGui {
+1 -1
View File
@@ -1,7 +1,7 @@
mod gui;
use anyhow::{Context, Result};
use pwsp::utils::gui::ensure_pwsp_audio_dir;
use pwsp_lib::utils::gui::ensure_pwsp_audio_dir;
use rust_i18n::i18n;
use std::{env, path::PathBuf};
+28
View File
@@ -0,0 +1,28 @@
[package]
name = "pwsp-lib"
version.workspace = true
edition.workspace = true
authors.workspace = true
homepage.workspace = true
repository.workspace = true
license.workspace = true
[dependencies]
tokio.workspace = true
async-trait.workspace = true
serde.workspace = true
serde_json.workspace = true
dirs.workspace = true
itertools.workspace = true
evdev.workspace = true
anyhow.workspace = true
rustix.workspace = true
rodio.workspace = true
pipewire.workspace = true
egui.workspace = true
reqwest.workspace = true
View File
@@ -349,7 +349,11 @@ impl AudioPlayer {
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);
sink.set_volume(self.volume); // Default volume is 1.0 * master
sink.append(source);
@@ -218,3 +218,82 @@ impl HotkeyConfig {
.collect()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_gui_config_default() {
let config = GuiConfig::default();
assert_eq!(config.scale_factor, 1.0);
assert_eq!(config.left_panel_width, 280.0);
assert!(!config.save_volume);
assert_eq!(config.preferred_theme, PreferredTheme::System);
}
#[test]
fn test_hotkey_config_operations() {
let mut config = HotkeyConfig::default();
assert!(config.slots.is_empty());
let req = Request::ping();
config.set_slot("slot1".to_string(), req.clone());
assert_eq!(config.slots.len(), 1);
assert_eq!(config.slots[0].slot, "slot1");
assert_eq!(config.slots[0].action, req);
assert!(config.slots[0].key_chord.is_none());
// Test find_slot
let found = config.find_slot("slot1");
assert!(found.is_some());
assert_eq!(found.unwrap().slot, "slot1");
// Test set_key_chord
let updated = config.set_key_chord("slot1", Some("Ctrl+A".to_string()));
assert!(updated);
assert_eq!(config.slots[0].key_chord.as_deref(), Some("Ctrl+A"));
// Test set_key_chord for non-existent slot
let updated_non_existent = config.set_key_chord("slot2", Some("Ctrl+B".to_string()));
assert!(!updated_non_existent);
// Test find_slot_mut
let found_mut = config.find_slot_mut("slot1");
assert!(found_mut.is_some());
found_mut.unwrap().key_chord = Some("Ctrl+B".to_string());
assert_eq!(config.slots[0].key_chord.as_deref(), Some("Ctrl+B"));
// Test slots_for_chord
let slots = config.slots_for_chord("Ctrl+B");
assert_eq!(slots.len(), 1);
assert_eq!(slots[0].slot, "slot1");
let empty_slots = config.slots_for_chord("Ctrl+A");
assert!(empty_slots.is_empty());
// Test remove_slot
let removed = config.remove_slot("slot1");
assert!(removed);
assert!(config.slots.is_empty());
let removed_non_existent = config.remove_slot("slot1");
assert!(!removed_non_existent);
}
#[test]
fn test_hotkey_config_conflicts() {
let mut config = HotkeyConfig::default();
config.set_slot("slot1".to_string(), Request::ping());
config.set_slot("slot2".to_string(), Request::ping());
config.set_slot("slot3".to_string(), Request::ping());
config.set_key_chord("slot1", Some("Ctrl+A".to_string()));
config.set_key_chord("slot2", Some("Ctrl+A".to_string())); // Conflict with slot1
config.set_key_chord("slot3", Some("Ctrl+B".to_string()));
let conflicts = config.find_conflicts();
assert_eq!(conflicts.len(), 1);
assert!(conflicts.contains(&("slot1", "slot2")) || conflicts.contains(&("slot2", "slot1")));
}
}
+155
View File
@@ -0,0 +1,155 @@
#[derive(Debug)]
pub struct Terminate {}
#[derive(Debug, Clone, Ord, PartialOrd, Eq, PartialEq)]
pub struct Port {
pub node_id: u32,
pub port_id: u32,
pub name: String,
}
#[derive(Debug, Clone, Ord, PartialOrd, Eq, PartialEq)]
pub enum DeviceType {
Input,
Output,
}
#[derive(Debug, Clone, Ord, PartialOrd, Eq, PartialEq)]
pub struct AudioDevice {
pub id: u32,
pub nick: String,
pub name: String,
pub device_type: DeviceType,
pub input_fl: Option<Port>,
pub input_fr: Option<Port>,
pub output_fl: 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);
}
_ => {}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_audio_device_new() {
let device = AudioDevice::new(
1,
Some("NickName"),
Some("Description"),
Some("Name"),
DeviceType::Input,
);
assert_eq!(device.id, 1);
assert_eq!(device.nick, "NickName");
assert_eq!(device.name, "Name");
assert_eq!(device.device_type, DeviceType::Input);
// Fallbacks for nick
let device_no_nick =
AudioDevice::new(2, None, Some("Desc"), Some("Name"), DeviceType::Output);
assert_eq!(device_no_nick.nick, "Desc");
let device_no_desc = AudioDevice::new(3, None, None, Some("Name"), DeviceType::Output);
assert_eq!(device_no_desc.nick, "Name");
}
#[test]
fn test_audio_device_add_port() {
let mut device = AudioDevice::new(1, None, None, Some("device-name"), DeviceType::Input);
let port_fl = Port {
node_id: 1,
port_id: 10,
name: "input_FL".to_string(),
};
let port_fr = Port {
node_id: 1,
port_id: 11,
name: "input_FR".to_string(),
};
device.add_port(port_fl.clone());
device.add_port(port_fr.clone());
assert_eq!(device.input_fl, Some(port_fl));
assert_eq!(device.input_fr, Some(port_fr));
// Test output ports
let port_out_fl = Port {
node_id: 1,
port_id: 12,
name: "output_FL".to_string(),
};
let port_out_fr = Port {
node_id: 1,
port_id: 13,
name: "capture_FR".to_string(),
};
device.add_port(port_out_fl.clone());
device.add_port(port_out_fr.clone());
assert_eq!(device.output_fl, Some(port_out_fl));
assert_eq!(device.output_fr, Some(port_out_fr));
// Test MONO ports
let mut device_mono =
AudioDevice::new(2, None, None, Some("mono-device"), DeviceType::Input);
let port_mono = Port {
node_id: 2,
port_id: 20,
name: "input_MONO".to_string(),
};
device_mono.add_port(port_mono.clone());
assert_eq!(device_mono.input_fl, Some(port_mono.clone()));
assert_eq!(device_mono.input_fr, Some(port_mono));
}
}
@@ -238,3 +238,88 @@ impl Response {
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_response_new() {
let res = Response::new(true, "success-msg");
assert!(res.status);
assert_eq!(res.message, "success-msg");
}
#[test]
fn test_request_constructors() {
// test ping
let req_ping = Request::ping();
assert_eq!(req_ping.name, "ping");
assert!(req_ping.args.is_empty());
// test kill
let req_kill = Request::kill();
assert_eq!(req_kill.name, "kill");
// test pause (with and without id)
let req_pause_no_id = Request::pause(None);
assert_eq!(req_pause_no_id.name, "pause");
assert!(req_pause_no_id.args.is_empty());
let req_pause_with_id = Request::pause(Some(42));
assert_eq!(req_pause_with_id.name, "pause");
assert_eq!(
req_pause_with_id.args.get("id").map(|s| s.as_str()),
Some("42")
);
// test play
let req_play = Request::play("/path/to/sound.mp3", true);
assert_eq!(req_play.name, "play");
assert_eq!(
req_play.args.get("file_path").map(|s| s.as_str()),
Some("/path/to/sound.mp3")
);
assert_eq!(
req_play.args.get("concurrent").map(|s| s.as_str()),
Some("true")
);
// test set_volume
let req_volume = Request::set_volume(0.8, Some(10));
assert_eq!(req_volume.name, "set_volume");
assert_eq!(
req_volume.args.get("volume").map(|s| s.as_str()),
Some("0.8")
);
assert_eq!(req_volume.args.get("id").map(|s| s.as_str()), Some("10"));
// test set_hotkey_action_and_key
let action = Request::ping();
let req_hotkey_action_and_key =
Request::set_hotkey_action_and_key("slot1", &action, "Ctrl+P");
assert_eq!(req_hotkey_action_and_key.name, "set_hotkey_action_and_key");
assert_eq!(
req_hotkey_action_and_key
.args
.get("slot")
.map(|s| s.as_str()),
Some("slot1")
);
assert_eq!(
req_hotkey_action_and_key
.args
.get("key_chord")
.map(|s| s.as_str()),
Some("Ctrl+P")
);
let action_json = serde_json::to_string(&action).unwrap();
assert_eq!(
req_hotkey_action_and_key
.args
.get("action")
.map(|s| s.as_str()),
Some(action_json.as_str())
);
}
}
@@ -199,3 +199,79 @@ pub async fn start_global_hotkey_listener() {
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_modifier_state() {
let mut state = ModifierState::new();
assert!(!state.any_active());
// Press Ctrl
state.update(KeyCode::KEY_LEFTCTRL, true);
assert!(state.ctrl);
assert!(state.any_active());
// Release Ctrl
state.update(KeyCode::KEY_LEFTCTRL, false);
assert!(!state.ctrl);
assert!(!state.any_active());
// Press multiple modifiers
state.update(KeyCode::KEY_RIGHTALT, true);
state.update(KeyCode::KEY_LEFTSHIFT, true);
assert!(state.alt);
assert!(state.shift);
assert!(state.any_active());
// Update a non-modifier key
state.update(KeyCode::KEY_A, true);
// Modifier states should remain unchanged
assert!(state.alt);
assert!(state.shift);
}
#[test]
fn test_is_modifier() {
assert!(ModifierState::is_modifier(KeyCode::KEY_LEFTCTRL));
assert!(ModifierState::is_modifier(KeyCode::KEY_RIGHTCTRL));
assert!(ModifierState::is_modifier(KeyCode::KEY_LEFTALT));
assert!(ModifierState::is_modifier(KeyCode::KEY_RIGHTALT));
assert!(ModifierState::is_modifier(KeyCode::KEY_LEFTSHIFT));
assert!(ModifierState::is_modifier(KeyCode::KEY_RIGHTSHIFT));
assert!(ModifierState::is_modifier(KeyCode::KEY_LEFTMETA));
assert!(ModifierState::is_modifier(KeyCode::KEY_RIGHTMETA));
assert!(!ModifierState::is_modifier(KeyCode::KEY_A));
assert!(!ModifierState::is_modifier(KeyCode::KEY_1));
}
#[test]
fn test_evdev_key_name() {
assert_eq!(evdev_key_name(KeyCode::KEY_A), Some("A"));
assert_eq!(evdev_key_name(KeyCode::KEY_Z), Some("Z"));
assert_eq!(evdev_key_name(KeyCode::KEY_0), Some("0"));
assert_eq!(evdev_key_name(KeyCode::KEY_F1), Some("F1"));
assert_eq!(evdev_key_name(KeyCode::KEY_F12), Some("F12"));
assert_eq!(evdev_key_name(KeyCode::KEY_ENTER), None);
}
#[test]
fn test_build_chord() {
let mut modifiers = ModifierState::new();
assert_eq!(build_chord(&modifiers, "A"), "A");
modifiers.ctrl = true;
assert_eq!(build_chord(&modifiers, "A"), "Ctrl+A");
modifiers.shift = true;
assert_eq!(build_chord(&modifiers, "B"), "Ctrl+Shift+B");
modifiers.alt = true;
modifiers.meta = true;
assert_eq!(build_chord(&modifiers, "F5"), "Ctrl+Alt+Shift+Super+F5");
}
}
@@ -139,3 +139,16 @@ pub fn start_app_state_thread(audio_player_state_shared: Arc<Mutex<AudioPlayerSt
}
});
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_format_time_pair() {
assert_eq!(format_time_pair(0.0, 0.0), "00:00/00:00");
assert_eq!(format_time_pair(5.4, 10.0), "00:05/00:10");
assert_eq!(format_time_pair(59.9, 125.1), "01:00/02:05");
assert_eq!(format_time_pair(3600.0, 7205.0), "60:00/120:05");
}
}
@@ -20,51 +20,40 @@ pub fn setup_pipewire_context() -> Result<(MainLoopRc, ContextRc), String> {
fn parse_global_object(
global_object: &GlobalObject<&DictRef>,
) -> (Option<AudioDevice>, Option<Port>) {
// Only objects with props can be devices/ports
if let Some(props) = global_object.props {
// Only objects with media.class can be devices
let props = match global_object.props {
Some(p) => p,
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
return if media_class.starts_with("Audio/Source") {
let input_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::Input,
input_fl: None,
input_fr: None,
output_fl: None,
output_fr: None,
};
(Some(input_device), None)
if media_class.starts_with("Audio/Source") {
let input_device = AudioDevice::new(
node_id,
node_nick,
node_description,
node_name,
DeviceType::Input,
);
return (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,
let output_device = AudioDevice::new(
node_id,
node_nick,
node_description,
node_name,
DeviceType::Output,
);
return (Some(output_device), None);
}
return (None, None);
}
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()
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()),
@@ -76,10 +65,9 @@ fn parse_global_object(
port_id,
name: port_name.to_string(),
};
return (None, Some(port));
}
}
(None, None)
}
@@ -188,47 +176,14 @@ pub async fn get_all_devices() -> Result<(Vec<AudioDevice>, Vec<AudioDevice>)> {
let node_id = port.node_id;
if let Some(input_device) = input_devices.get_mut(&node_id) {
match port.name.as_str() {
"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);
}
_ => {}
}
input_device.add_port(port);
} else if let Some(output_device) = output_devices.get_mut(&node_id) {
match port.name.as_str() {
"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)
}
_ => {}
}
output_device.add_port(port);
}
}
let mut input_devices: Vec<AudioDevice> = input_devices.values().cloned().collect();
let mut output_devices: Vec<AudioDevice> =
output_devices.values().cloned().collect();
let mut input_devices: Vec<AudioDevice> = input_devices.into_values().collect();
let mut output_devices: Vec<AudioDevice> = output_devices.into_values().collect();
input_devices.sort_by_key(|a| a.id);
output_devices.sort_by_key(|a| a.id);
+175
View File
@@ -0,0 +1,175 @@
#!/usr/bin/env python3
import sys
import os
import re
import subprocess
import shutil
from datetime import datetime
# Helper to print errors and exit
def fatal(msg):
print(f"Error: {msg}", file=sys.stderr)
sys.exit(1)
# Get the root directory of the project
root_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
os.chdir(root_dir)
# Read current version from Cargo.toml
cargo_toml_path = "Cargo.toml"
if not os.path.exists(cargo_toml_path):
fatal("Cargo.toml not found in the root directory.")
with open(cargo_toml_path, "r", encoding="utf-8") as f:
cargo_toml_content = f.read()
# We want to match version in [workspace.package]
# First, let's find the [workspace.package] section
workspace_package_match = re.search(
r"\[workspace\.package\](.*?)(?=\n\[|$)", cargo_toml_content, re.DOTALL
)
if not workspace_package_match:
fatal("Could not find [workspace.package] section in Cargo.toml.")
workspace_package_sec = workspace_package_match.group(1)
version_match = re.search(r'version\s*=\s*"([^"]+)"', workspace_package_sec)
if not version_match:
fatal("Could not find version in [workspace.package] in Cargo.toml.")
current_version = version_match.group(1)
print(f"Current version detected: {current_version}")
# Get new version
if len(sys.argv) < 2:
try:
new_version = input(f"Enter new version: ").strip()
except (KeyboardInterrupt, EOFError):
print()
sys.exit(0)
if not new_version:
fatal("No version provided.")
else:
new_version = sys.argv[1].strip()
if not re.match(r"^\d+\.\d+\.\d+(-[a-zA-Z0-9.]+)?$", new_version):
fatal(f"Invalid version format: '{new_version}'. Should be like '1.10.1'.")
# 1. Update Cargo.toml
print("Updating Cargo.toml...")
def replace_version_in_workspace(match):
section_content = match.group(1)
updated_section_content = re.sub(
r'(version\s*=\s*")[^"]+(")', rf"\g<1>{new_version}\g<2>", section_content
)
return f"[workspace.package]{updated_section_content}"
new_cargo_toml = re.sub(
r"\[workspace\.package\](.*?)(?=\n\[|$)",
replace_version_in_workspace,
cargo_toml_content,
flags=re.DOTALL,
)
with open(cargo_toml_path, "w", encoding="utf-8") as f:
f.write(new_cargo_toml)
# Update Cargo.lock using cargo
print("Updating Cargo.lock using cargo generate-lockfile...")
try:
subprocess.run(["cargo", "generate-lockfile"], check=True)
except Exception as e:
print(f"Warning: Failed to update Cargo.lock using cargo: {e}")
# 2. Update packages/aur/bin/PKGBUILD
pkgbuild_bin_path = "packages/aur/bin/PKGBUILD"
if os.path.exists(pkgbuild_bin_path):
print(f"Updating {pkgbuild_bin_path}...")
with open(pkgbuild_bin_path, "r", encoding="utf-8") as f:
content = f.read()
content = re.sub(r"pkgver=[^\n]+", f"pkgver={new_version}", content)
content = re.sub(r"pkgrel=[^\n]+", "pkgrel=1", content)
with open(pkgbuild_bin_path, "w", encoding="utf-8") as f:
f.write(content)
# 3. Update packages/aur/standart/PKGBUILD
pkgbuild_std_path = "packages/aur/standart/PKGBUILD"
if os.path.exists(pkgbuild_std_path):
print(f"Updating {pkgbuild_std_path}...")
with open(pkgbuild_std_path, "r", encoding="utf-8") as f:
content = f.read()
content = re.sub(r"pkgver=[^\n]+", f"pkgver={new_version}", content)
content = re.sub(r"pkgrel=[^\n]+", "pkgrel=1", content)
with open(pkgbuild_std_path, "w", encoding="utf-8") as f:
f.write(content)
# Update AUR .SRCINFO files
def update_srcinfo(directory, pkgbuild_path, srcinfo_path):
if not os.path.exists(srcinfo_path):
return
print(f"Updating {srcinfo_path}...")
if shutil.which("makepkg"):
try:
print(f"Running makepkg --printsrcinfo in {directory}...")
result = subprocess.run(
["makepkg", "--printsrcinfo"],
cwd=directory,
capture_output=True,
text=True,
check=True,
)
with open(srcinfo_path, "w", encoding="utf-8") as f:
f.write(result.stdout)
return
except Exception as e:
print(
f"Warning: makepkg failed in {directory}: {e}. Falling back to text replacement."
)
# Text replacement fallback
with open(srcinfo_path, "r", encoding="utf-8") as f:
content = f.read()
content = re.sub(r"pkgver\s*=\s*[^\n]+", f"pkgver = {new_version}", content)
content = re.sub(r"pkgrel\s*=\s*[^\n]+", "pkgrel = 1", content)
content = content.replace(current_version, new_version)
with open(srcinfo_path, "w", encoding="utf-8") as f:
f.write(content)
update_srcinfo("packages/aur/bin", pkgbuild_bin_path, "packages/aur/bin/.SRCINFO")
update_srcinfo(
"packages/aur/standart", pkgbuild_std_path, "packages/aur/standart/.SRCINFO"
)
# 4. Update packages/flatpak/ru.arabianq.pwsp.metainfo.xml
flatpak_xml_path = "packages/flatpak/ru.arabianq.pwsp.metainfo.xml"
if os.path.exists(flatpak_xml_path):
print(f"Updating {flatpak_xml_path}...")
with open(flatpak_xml_path, "r", encoding="utf-8") as f:
content = f.read()
today_str = datetime.today().strftime("%Y-%m-%d")
content = re.sub(
r'<release\s+version="[^"]+"\s+date="[^"]+"\s*/?>',
f'<release version="{new_version}" date="{today_str}" />',
content,
)
with open(flatpak_xml_path, "w", encoding="utf-8") as f:
f.write(content)
# 5. Update packages/rpm/pwsp.spec
rpm_spec_path = "packages/rpm/pwsp.spec"
if os.path.exists(rpm_spec_path):
print(f"Updating {rpm_spec_path}...")
with open(rpm_spec_path, "r", encoding="utf-8") as f:
content = f.read()
content = re.sub(r"Version:\s*[^\n]+", f"Version: {new_version}", content)
with open(rpm_spec_path, "w", encoding="utf-8") as f:
f.write(content)
print(f"Successfully updated all versions to {new_version}!")
-86
View File
@@ -1,86 +0,0 @@
use crate::gui::SoundpadGui;
use egui::{AtomExt, Button, ComboBox, Label, RichText, Slider, Ui, Vec2};
use egui_material_icons::icons::*;
use rust_i18n::t;
use std::time::Instant;
impl SoundpadGui {
pub fn draw_footer(&mut self, ui: &mut Ui) {
ui.add_space(5.0);
ui.horizontal(|ui| {
// ---------- Microphone selection ----------
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);
}
// --------------------------------
// ---------- 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);
// ---------- Hotkeys button ----------
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)");
// --------------------------------
// ---------- 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;
}
// --------------------------------
});
}
}
-31
View File
@@ -1,31 +0,0 @@
#[derive(Debug)]
pub struct Terminate {}
#[derive(Debug, Clone, Ord, PartialOrd, Eq, PartialEq)]
pub struct Port {
pub node_id: u32,
pub port_id: u32,
pub name: String,
}
#[derive(Debug, Clone, Ord, PartialOrd, Eq, PartialEq)]
pub enum DeviceType {
Input,
Output,
}
#[derive(Debug, Clone, Ord, PartialOrd, Eq, PartialEq)]
pub struct AudioDevice {
pub id: u32,
pub nick: String,
pub name: String,
pub device_type: DeviceType,
pub input_fl: Option<Port>,
pub input_fr: Option<Port>,
pub output_fl: Option<Port>,
pub output_fr: Option<Port>,
}