Compare commits

..

7 Commits

Author SHA1 Message Date
Tarasov Aleksandr 9f50809a99 fix(ci): add SDK extensions installation step (#79) 2026-04-25 16:57:01 +03:00
Tarasov Aleksandr 7dda4bc2b1 fix(ci): update Flatter container image to version 25.08 (#77) 2026-04-25 16:46:56 +03:00
Tarasov Aleksandr 1569955e12 chore(ci): Add flatter to host Flatpak repo on GitHub Pages (#76)
* chore(ci): Add flatter to host Flatpak repo on GitHub Pages

- Update release.yml to not upload .flatpak file to releases
- Create flatter.yml to automate building and hosting of Flatpak via GitHub pages using andyholmes/flatter
- Add nightly branch for main pushes and stable branch for releases
- Update README.md with the new Flatpak installation instructions

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

* Potential fix for pull request finding 'CodeQL / Workflow does not contain permissions'

Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>

---------

Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com>
Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
2026-04-25 16:38:34 +03:00
Tarasov Aleksandr 9adc6cfbda update dependencies and change version to 1.7.4 (#75)
* deps: cargo update

cc v1.2.60 -> v1.2.61
libc v0.2.185 -> v0.2.186
winnow v1.0.1 -> v1.0.2

* deps: update cargo-sources.json for flatpak

* change version to 1.7.4
2026-04-25 15:58:49 +03:00
Tarasov Aleksandr 76b1d4f345 fix(gui): footer and hotkeys table are no longer clipped because of long filenames (#74)
* fix: truncate file button text in draw function so footer is no clipped

* fix(gui): fix hotkeys table clipping with egui_extras::TableBuilder

fully reworked hotkeys page

* deps: update flatpak cargo-sources.json
2026-04-25 15:44:50 +03:00
Tarasov Aleksandr 10f9937dc3 tests: parse_command set_volume edge cases (#73)
Add unit tests for parse_command in src/utils/commands.rs to ensure
robust handling of set_volume edge cases including missing or
invalid volume and id arguments.

Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com>
2026-04-25 14:37:55 +03:00
Tarasov Aleksandr 498c09eb50 fix(gui): remove unwrap() calls in input handling to prevent potential panics (#72)
Replaced `.chars().next().unwrap()` with `.chars().next().is_some_and(...)` in `chord_from_event` and `parse_chord` functions in `src/gui/input.rs`. This ensures that even if the string is empty, the application will not panic, adhering to the project's safety guidelines and resolving a potential security vulnerability.

Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com>
2026-04-25 14:36:46 +03:00
15 changed files with 674 additions and 190 deletions
+70
View File
@@ -0,0 +1,70 @@
name: Flatter
on:
push:
branches: [ main, master ]
release:
types: [ published ]
workflow_dispatch:
jobs:
flatter:
name: Flatter
runs-on: ubuntu-latest
permissions:
contents: read
container:
image: ghcr.io/andyholmes/flatter/freedesktop:25.08
options: --privileged
steps:
- name: Checkout
uses: actions/checkout@v4
- 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
else
echo "branch=nightly" >> $GITHUB_OUTPUT
fi
- name: Modify Manifest
run: |
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
uses: andyholmes/flatter@main
with:
files: packages/flatpak/ru.arabianq.pwsp.yaml
gpg-sign: ${{ steps.gpg.outputs.fingerprint }}
upload-bundles: false
upload-pages-artifact: true
deploy:
name: Deploy to GitHub Pages
runs-on: ubuntu-latest
needs: flatter
permissions:
pages: write
id-token: write
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
steps:
- name: GitHub Pages
id: deployment
uses: actions/deploy-pages@v4
-7
View File
@@ -174,10 +174,3 @@ jobs:
cache: true
branch: master
build-bundle: true
- name: Upload Flatpak to release
uses: softprops/action-gh-release@v2
with:
token: ${{ secrets.GITHUB_TOKEN }}
tag_name: ${{ needs.prepare.outputs.tag }}
files: ru.arabianq.pwsp.flatpak
Generated
+134 -10
View File
@@ -486,9 +486,9 @@ dependencies = [
[[package]]
name = "cc"
version = "1.2.60"
version = "1.2.61"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "43c5703da9466b66a946814e1adf53ea2c90f10063b86290cc9eb67ce3478a20"
checksum = "d16d90359e986641506914ba71350897565610e87ce0ad9e6f28569db3dd5c6d"
dependencies = [
"find-msvc-tools",
"jobserver",
@@ -1022,6 +1022,20 @@ dependencies = [
"web-time",
]
[[package]]
name = "egui_extras"
version = "0.34.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "62bfc6870c68d3f254e33aca8200095d422e09edacb0f365f79fe23a5ba10963"
dependencies = [
"ahash",
"egui",
"enum-map",
"log",
"mime_guess2",
"profiling",
]
[[package]]
name = "egui_glow"
version = "0.34.1"
@@ -1079,6 +1093,26 @@ version = "1.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "66b7e2430c6dff6a955451e2cfc438f09cea1965a9d6f87f7e3b90decc014099"
[[package]]
name = "enum-map"
version = "2.7.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6866f3bfdf8207509a033af1a75a7b08abda06bbaaeae6669323fd5a097df2e9"
dependencies = [
"enum-map-derive",
]
[[package]]
name = "enum-map-derive"
version = "0.17.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f282cfdfe92516eb26c2af8589c274c7c17681f5ecc03c18255fe741c6aa64eb"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "enumflags2"
version = "0.7.12"
@@ -1898,9 +1932,9 @@ checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2"
[[package]]
name = "libc"
version = "0.2.185"
version = "0.2.186"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "52ff2c0fe9bc6cb6b14a0592c2ff4fa9ceb83eea9db979b0487cd054946a2b8f"
checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66"
[[package]]
name = "libflate"
@@ -2060,6 +2094,24 @@ dependencies = [
"autocfg",
]
[[package]]
name = "mime"
version = "0.3.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a"
[[package]]
name = "mime_guess2"
version = "2.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1706dc14a2e140dec0a7a07109d9a3d5890b81e85bd6c60b906b249a77adf0ca"
dependencies = [
"mime",
"phf",
"phf_shared",
"unicase",
]
[[package]]
name = "minimal-lexical"
version = "0.2.1"
@@ -2721,6 +2773,50 @@ version = "2.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220"
[[package]]
name = "phf"
version = "0.11.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1fd6780a80ae0c52cc120a26a1a42c1ae51b247a253e4e06113d23d2c2edd078"
dependencies = [
"phf_macros",
"phf_shared",
]
[[package]]
name = "phf_generator"
version = "0.11.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3c80231409c20246a13fddb31776fb942c38553c51e871f8cbd687a4cfb5843d"
dependencies = [
"phf_shared",
"rand",
]
[[package]]
name = "phf_macros"
version = "0.11.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f84ac04429c13a7ff43785d75ad27569f2951ce0ffd30a3321230db2fc727216"
dependencies = [
"phf_generator",
"phf_shared",
"proc-macro2",
"quote",
"syn",
"unicase",
]
[[package]]
name = "phf_shared"
version = "0.11.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "67eabc2ef2a60eb7faa00097bd1ffdb5bd28e62bf39990626a582201b7a754e5"
dependencies = [
"siphasher",
"unicase",
]
[[package]]
name = "pin-project"
version = "1.1.11"
@@ -2913,7 +3009,7 @@ checksum = "3eb8486b569e12e2c32ad3e204dbaba5e4b5b216e9367044f25f1dba42341773"
[[package]]
name = "pwsp"
version = "1.7.3"
version = "1.7.4"
dependencies = [
"async-trait",
"clap",
@@ -2921,6 +3017,7 @@ dependencies = [
"eframe",
"egui",
"egui_dnd",
"egui_extras",
"egui_material_icons",
"evdev",
"itertools 0.14.0",
@@ -2981,6 +3078,21 @@ version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09"
[[package]]
name = "rand"
version = "0.8.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5ca0ecfa931c29007047d1bc58e623ab12e5590e8c7cc53200d5202b69266d8a"
dependencies = [
"rand_core",
]
[[package]]
name = "rand_core"
version = "0.6.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
[[package]]
name = "raw-window-handle"
version = "0.6.2"
@@ -3326,6 +3438,12 @@ version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c82d449ab1bccfeec125893c6875008206f038d4eb8a09e1e10caf86f44d574e"
[[package]]
name = "siphasher"
version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b2aa850e253778c88a04c3d7323b043aeda9d3e30d5971937c1855769763678e"
[[package]]
name = "skrifa"
version = "0.40.0"
@@ -3816,7 +3934,7 @@ dependencies = [
"toml_datetime",
"toml_parser",
"toml_writer",
"winnow 1.0.1",
"winnow 1.0.2",
]
[[package]]
@@ -3837,7 +3955,7 @@ dependencies = [
"indexmap",
"toml_datetime",
"toml_parser",
"winnow 1.0.1",
"winnow 1.0.2",
]
[[package]]
@@ -3846,7 +3964,7 @@ version = "1.1.2+spec-1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526"
dependencies = [
"winnow 1.0.1",
"winnow 1.0.2",
]
[[package]]
@@ -3907,6 +4025,12 @@ dependencies = [
"windows-sys 0.61.2",
]
[[package]]
name = "unicase"
version = "2.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dbc4bc3a9f746d862c45cb89d705aa10f187bb96c76001afab07a0d35ce60142"
[[package]]
name = "unicode-ident"
version = "1.0.24"
@@ -4824,9 +4948,9 @@ dependencies = [
[[package]]
name = "winnow"
version = "1.0.1"
version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09dac053f1cd375980747450bfc7250c264eaae0583872e845c0c7cd578872b5"
checksum = "2ee1708bef14716a11bae175f579062d4554d95be2c6829f518df847b7b3fdd0"
dependencies = [
"memchr",
]
+2 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "pwsp"
version = "1.7.3"
version = "1.7.4"
edition = "2024"
authors = ["arabian"]
description = "PWSP lets you play audio files through your microphone. Has both CLI and GUI clients."
@@ -50,6 +50,7 @@ eframe = { version = "0.34.1", default-features = false, features = [
"x11",
"wayland",
] }
egui_extras = "0.34.1"
egui_material_icons = "0.6.0"
egui_dnd = "0.15.0"
+22
View File
@@ -52,6 +52,28 @@ three main components:
You can download pre-built binaries and .deb packages from
the [releases page](https://github.com/arabianq/pipewire-soundpad/releases).
## **Flatpak**
You can install PWSP via Flatpak from our custom repository hosted on GitHub Pages.
Add the repository:
```bash
flatpak remote-add --user --if-not-exists arabianq-repo https://arabianq.github.io/pipewire-soundpad/index.flatpakrepo
```
Install the stable version:
```bash
flatpak install --user arabianq-repo ru.arabianq.pwsp//stable
```
Or install the nightly version (latest commit to `main`):
```bash
flatpak install --user arabianq-repo ru.arabianq.pwsp//nightly
```
## **Fedora Linux (and derivatives)**
If you're using Fedora, you can install PWSP from a dedicated repository using DNF.
+3 -3
View File
@@ -1,6 +1,6 @@
pkgbase = pwsp-bin
pkgdesc = Lets you play audio files through your microphone (Pre-built binaries)
pkgver = 1.7.3
pkgver = 1.7.4
pkgrel = 2
url = https://github.com/arabianq/pipewire-soundpad
arch = x86_64
@@ -9,8 +9,8 @@ depends = pipewire
depends = alsa-lib
provides = pwsp
conflicts = pwsp
source = pwsp-bin-1.7.3.zip :: https://github.com/arabianq/pipewire-soundpad/releases/download/v1.7.3/pwsp-v1.7.3-linux-x64.zip
source = pipewire-soundpad-1.7.3.tar.gz :: https://github.com/arabianq/pipewire-soundpad/archive/refs/tags/v1.7.3.tar.gz
source = pwsp-bin-1.7.4.zip :: https://github.com/arabianq/pipewire-soundpad/releases/download/v1.7.4/pwsp-v1.7.4-linux-x64.zip
source = pipewire-soundpad-1.7.4.tar.gz :: https://github.com/arabianq/pipewire-soundpad/archive/refs/tags/v1.7.4.tar.gz
sha256sums = SKIP
sha256sums = SKIP
+1 -1
View File
@@ -1,7 +1,7 @@
# Maintainer: Alexander Tarasov <a.tevg@ya.ru>
pkgname=pwsp-bin
_pkgname=pipewire-soundpad
pkgver=1.7.3
pkgver=1.7.4
pkgrel=2
pkgdesc="Lets you play audio files through your microphone (Pre-built binaries)"
arch=('x86_64')
+2 -2
View File
@@ -1,6 +1,6 @@
pkgbase = pwsp
pkgdesc = Lets you play audio files through your microphone
pkgver = 1.7.3
pkgver = 1.7.4
pkgrel = 1
url = https://github.com/arabianq/pipewire-soundpad
arch = any
@@ -10,7 +10,7 @@ pkgbase = pwsp
makedepends = cargo
makedepends = pipewire
makedepends = alsa-lib
source = https://github.com/arabianq/pipewire-soundpad/archive/refs/tags/v1.7.3.tar.gz
source = https://github.com/arabianq/pipewire-soundpad/archive/refs/tags/v1.7.4.tar.gz
sha256sums = SKIP
pkgname = pwsp
+1 -1
View File
@@ -1,7 +1,7 @@
# Maintainer: Alexander Tarasov <a.tevg@ya.ru>
pkgsubn=pwsp
pkgname=pwsp
pkgver=1.7.3
pkgver=1.7.4
pkgrel=1
pkgdesc="Lets you play audio files through your microphone"
arch=('any')
+184 -15
View File
@@ -613,14 +613,14 @@
{
"type": "archive",
"archive-type": "tar-gzip",
"url": "https://static.crates.io/crates/cc/cc-1.2.60.crate",
"sha256": "43c5703da9466b66a946814e1adf53ea2c90f10063b86290cc9eb67ce3478a20",
"dest": "cargo/vendor/cc-1.2.60"
"url": "https://static.crates.io/crates/cc/cc-1.2.61.crate",
"sha256": "d16d90359e986641506914ba71350897565610e87ce0ad9e6f28569db3dd5c6d",
"dest": "cargo/vendor/cc-1.2.61"
},
{
"type": "inline",
"contents": "{\"package\": \"43c5703da9466b66a946814e1adf53ea2c90f10063b86290cc9eb67ce3478a20\", \"files\": {}}",
"dest": "cargo/vendor/cc-1.2.60",
"contents": "{\"package\": \"d16d90359e986641506914ba71350897565610e87ce0ad9e6f28569db3dd5c6d\", \"files\": {}}",
"dest": "cargo/vendor/cc-1.2.61",
"dest-filename": ".cargo-checksum.json"
},
{
@@ -1273,6 +1273,19 @@
"dest": "cargo/vendor/egui_dnd-0.15.0",
"dest-filename": ".cargo-checksum.json"
},
{
"type": "archive",
"archive-type": "tar-gzip",
"url": "https://static.crates.io/crates/egui_extras/egui_extras-0.34.1.crate",
"sha256": "62bfc6870c68d3f254e33aca8200095d422e09edacb0f365f79fe23a5ba10963",
"dest": "cargo/vendor/egui_extras-0.34.1"
},
{
"type": "inline",
"contents": "{\"package\": \"62bfc6870c68d3f254e33aca8200095d422e09edacb0f365f79fe23a5ba10963\", \"files\": {}}",
"dest": "cargo/vendor/egui_extras-0.34.1",
"dest-filename": ".cargo-checksum.json"
},
{
"type": "archive",
"archive-type": "tar-gzip",
@@ -1351,6 +1364,32 @@
"dest": "cargo/vendor/endi-1.1.1",
"dest-filename": ".cargo-checksum.json"
},
{
"type": "archive",
"archive-type": "tar-gzip",
"url": "https://static.crates.io/crates/enum-map/enum-map-2.7.3.crate",
"sha256": "6866f3bfdf8207509a033af1a75a7b08abda06bbaaeae6669323fd5a097df2e9",
"dest": "cargo/vendor/enum-map-2.7.3"
},
{
"type": "inline",
"contents": "{\"package\": \"6866f3bfdf8207509a033af1a75a7b08abda06bbaaeae6669323fd5a097df2e9\", \"files\": {}}",
"dest": "cargo/vendor/enum-map-2.7.3",
"dest-filename": ".cargo-checksum.json"
},
{
"type": "archive",
"archive-type": "tar-gzip",
"url": "https://static.crates.io/crates/enum-map-derive/enum-map-derive-0.17.0.crate",
"sha256": "f282cfdfe92516eb26c2af8589c274c7c17681f5ecc03c18255fe741c6aa64eb",
"dest": "cargo/vendor/enum-map-derive-0.17.0"
},
{
"type": "inline",
"contents": "{\"package\": \"f282cfdfe92516eb26c2af8589c274c7c17681f5ecc03c18255fe741c6aa64eb\", \"files\": {}}",
"dest": "cargo/vendor/enum-map-derive-0.17.0",
"dest-filename": ".cargo-checksum.json"
},
{
"type": "archive",
"archive-type": "tar-gzip",
@@ -2433,14 +2472,14 @@
{
"type": "archive",
"archive-type": "tar-gzip",
"url": "https://static.crates.io/crates/libc/libc-0.2.185.crate",
"sha256": "52ff2c0fe9bc6cb6b14a0592c2ff4fa9ceb83eea9db979b0487cd054946a2b8f",
"dest": "cargo/vendor/libc-0.2.185"
"url": "https://static.crates.io/crates/libc/libc-0.2.186.crate",
"sha256": "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66",
"dest": "cargo/vendor/libc-0.2.186"
},
{
"type": "inline",
"contents": "{\"package\": \"52ff2c0fe9bc6cb6b14a0592c2ff4fa9ceb83eea9db979b0487cd054946a2b8f\", \"files\": {}}",
"dest": "cargo/vendor/libc-0.2.185",
"contents": "{\"package\": \"68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66\", \"files\": {}}",
"dest": "cargo/vendor/libc-0.2.186",
"dest-filename": ".cargo-checksum.json"
},
{
@@ -2677,6 +2716,32 @@
"dest": "cargo/vendor/memoffset-0.9.1",
"dest-filename": ".cargo-checksum.json"
},
{
"type": "archive",
"archive-type": "tar-gzip",
"url": "https://static.crates.io/crates/mime/mime-0.3.17.crate",
"sha256": "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a",
"dest": "cargo/vendor/mime-0.3.17"
},
{
"type": "inline",
"contents": "{\"package\": \"6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a\", \"files\": {}}",
"dest": "cargo/vendor/mime-0.3.17",
"dest-filename": ".cargo-checksum.json"
},
{
"type": "archive",
"archive-type": "tar-gzip",
"url": "https://static.crates.io/crates/mime_guess2/mime_guess2-2.3.1.crate",
"sha256": "1706dc14a2e140dec0a7a07109d9a3d5890b81e85bd6c60b906b249a77adf0ca",
"dest": "cargo/vendor/mime_guess2-2.3.1"
},
{
"type": "inline",
"contents": "{\"package\": \"1706dc14a2e140dec0a7a07109d9a3d5890b81e85bd6c60b906b249a77adf0ca\", \"files\": {}}",
"dest": "cargo/vendor/mime_guess2-2.3.1",
"dest-filename": ".cargo-checksum.json"
},
{
"type": "archive",
"archive-type": "tar-gzip",
@@ -3457,6 +3522,58 @@
"dest": "cargo/vendor/percent-encoding-2.3.2",
"dest-filename": ".cargo-checksum.json"
},
{
"type": "archive",
"archive-type": "tar-gzip",
"url": "https://static.crates.io/crates/phf/phf-0.11.3.crate",
"sha256": "1fd6780a80ae0c52cc120a26a1a42c1ae51b247a253e4e06113d23d2c2edd078",
"dest": "cargo/vendor/phf-0.11.3"
},
{
"type": "inline",
"contents": "{\"package\": \"1fd6780a80ae0c52cc120a26a1a42c1ae51b247a253e4e06113d23d2c2edd078\", \"files\": {}}",
"dest": "cargo/vendor/phf-0.11.3",
"dest-filename": ".cargo-checksum.json"
},
{
"type": "archive",
"archive-type": "tar-gzip",
"url": "https://static.crates.io/crates/phf_generator/phf_generator-0.11.3.crate",
"sha256": "3c80231409c20246a13fddb31776fb942c38553c51e871f8cbd687a4cfb5843d",
"dest": "cargo/vendor/phf_generator-0.11.3"
},
{
"type": "inline",
"contents": "{\"package\": \"3c80231409c20246a13fddb31776fb942c38553c51e871f8cbd687a4cfb5843d\", \"files\": {}}",
"dest": "cargo/vendor/phf_generator-0.11.3",
"dest-filename": ".cargo-checksum.json"
},
{
"type": "archive",
"archive-type": "tar-gzip",
"url": "https://static.crates.io/crates/phf_macros/phf_macros-0.11.3.crate",
"sha256": "f84ac04429c13a7ff43785d75ad27569f2951ce0ffd30a3321230db2fc727216",
"dest": "cargo/vendor/phf_macros-0.11.3"
},
{
"type": "inline",
"contents": "{\"package\": \"f84ac04429c13a7ff43785d75ad27569f2951ce0ffd30a3321230db2fc727216\", \"files\": {}}",
"dest": "cargo/vendor/phf_macros-0.11.3",
"dest-filename": ".cargo-checksum.json"
},
{
"type": "archive",
"archive-type": "tar-gzip",
"url": "https://static.crates.io/crates/phf_shared/phf_shared-0.11.3.crate",
"sha256": "67eabc2ef2a60eb7faa00097bd1ffdb5bd28e62bf39990626a582201b7a754e5",
"dest": "cargo/vendor/phf_shared-0.11.3"
},
{
"type": "inline",
"contents": "{\"package\": \"67eabc2ef2a60eb7faa00097bd1ffdb5bd28e62bf39990626a582201b7a754e5\", \"files\": {}}",
"dest": "cargo/vendor/phf_shared-0.11.3",
"dest-filename": ".cargo-checksum.json"
},
{
"type": "archive",
"archive-type": "tar-gzip",
@@ -3808,6 +3925,32 @@
"dest": "cargo/vendor/radium-0.7.0",
"dest-filename": ".cargo-checksum.json"
},
{
"type": "archive",
"archive-type": "tar-gzip",
"url": "https://static.crates.io/crates/rand/rand-0.8.6.crate",
"sha256": "5ca0ecfa931c29007047d1bc58e623ab12e5590e8c7cc53200d5202b69266d8a",
"dest": "cargo/vendor/rand-0.8.6"
},
{
"type": "inline",
"contents": "{\"package\": \"5ca0ecfa931c29007047d1bc58e623ab12e5590e8c7cc53200d5202b69266d8a\", \"files\": {}}",
"dest": "cargo/vendor/rand-0.8.6",
"dest-filename": ".cargo-checksum.json"
},
{
"type": "archive",
"archive-type": "tar-gzip",
"url": "https://static.crates.io/crates/rand_core/rand_core-0.6.4.crate",
"sha256": "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c",
"dest": "cargo/vendor/rand_core-0.6.4"
},
{
"type": "inline",
"contents": "{\"package\": \"ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c\", \"files\": {}}",
"dest": "cargo/vendor/rand_core-0.6.4",
"dest-filename": ".cargo-checksum.json"
},
{
"type": "archive",
"archive-type": "tar-gzip",
@@ -4302,6 +4445,19 @@
"dest": "cargo/vendor/simple-easing-1.0.2",
"dest-filename": ".cargo-checksum.json"
},
{
"type": "archive",
"archive-type": "tar-gzip",
"url": "https://static.crates.io/crates/siphasher/siphasher-1.0.2.crate",
"sha256": "b2aa850e253778c88a04c3d7323b043aeda9d3e30d5971937c1855769763678e",
"dest": "cargo/vendor/siphasher-1.0.2"
},
{
"type": "inline",
"contents": "{\"package\": \"b2aa850e253778c88a04c3d7323b043aeda9d3e30d5971937c1855769763678e\", \"files\": {}}",
"dest": "cargo/vendor/siphasher-1.0.2",
"dest-filename": ".cargo-checksum.json"
},
{
"type": "archive",
"archive-type": "tar-gzip",
@@ -4978,6 +5134,19 @@
"dest": "cargo/vendor/uds_windows-1.2.1",
"dest-filename": ".cargo-checksum.json"
},
{
"type": "archive",
"archive-type": "tar-gzip",
"url": "https://static.crates.io/crates/unicase/unicase-2.9.0.crate",
"sha256": "dbc4bc3a9f746d862c45cb89d705aa10f187bb96c76001afab07a0d35ce60142",
"dest": "cargo/vendor/unicase-2.9.0"
},
{
"type": "inline",
"contents": "{\"package\": \"dbc4bc3a9f746d862c45cb89d705aa10f187bb96c76001afab07a0d35ce60142\", \"files\": {}}",
"dest": "cargo/vendor/unicase-2.9.0",
"dest-filename": ".cargo-checksum.json"
},
{
"type": "archive",
"archive-type": "tar-gzip",
@@ -6138,14 +6307,14 @@
{
"type": "archive",
"archive-type": "tar-gzip",
"url": "https://static.crates.io/crates/winnow/winnow-1.0.1.crate",
"sha256": "09dac053f1cd375980747450bfc7250c264eaae0583872e845c0c7cd578872b5",
"dest": "cargo/vendor/winnow-1.0.1"
"url": "https://static.crates.io/crates/winnow/winnow-1.0.2.crate",
"sha256": "2ee1708bef14716a11bae175f579062d4554d95be2c6829f518df847b7b3fdd0",
"dest": "cargo/vendor/winnow-1.0.2"
},
{
"type": "inline",
"contents": "{\"package\": \"09dac053f1cd375980747450bfc7250c264eaae0583872e845c0c7cd578872b5\", \"files\": {}}",
"dest": "cargo/vendor/winnow-1.0.1",
"contents": "{\"package\": \"2ee1708bef14716a11bae175f579062d4554d95be2c6829f518df847b7b3fdd0\", \"files\": {}}",
"dest": "cargo/vendor/winnow-1.0.2",
"dest-filename": ".cargo-checksum.json"
},
{
@@ -25,7 +25,7 @@
<name>arabian</name>
</developer>
<releases>
<release version="1.7.3" date="2026-04-20" />
<release version="1.7.4" date="2026-04-25" />
</releases>
<content_rating type="oars-1.1" />
</component>
+1 -1
View File
@@ -4,7 +4,7 @@
%global cargo_install_lib 0
Name: pwsp
Version: 1.7.3
Version: 1.7.4
Release: %autorelease
Summary: Lets you play audio files through your microphone
+166 -146
View File
@@ -1,9 +1,10 @@
use crate::gui::SoundpadGui;
use egui::{
Align, AtomExt, Button, CollapsingHeader, Color32, ComboBox, CursorIcon, FontFamily, Grid,
Label, Layout, RichText, ScrollArea, Sense, Slider, TextEdit, Ui, Vec2,
Align, AtomExt, Button, CollapsingHeader, Color32, ComboBox, CursorIcon, FontFamily, Label,
Layout, RichText, ScrollArea, Sense, Slider, TextEdit, Ui, Vec2,
};
use egui_dnd::dnd;
use egui_extras::{Column, TableBuilder};
use egui_material_icons::icons::*;
use pwsp::types::socket::Request;
use pwsp::types::{audio_player::TrackInfo, gui::AppState};
@@ -133,26 +134,24 @@ impl SoundpadGui {
}
pub fn draw_hotkeys(&mut self, ui: &mut Ui) {
let area_size = ui.available_size();
ui.vertical(|ui| {
ui.set_min_width(area_size.x);
ui.set_min_height(area_size.y);
ui.spacing_mut().item_spacing.y = 5.0;
// Header
ui.horizontal_top(|ui| {
// --- Header ---
ui.horizontal(|ui| {
let back_button = Button::new(ICON_ARROW_BACK).frame(false);
if ui.add(back_button).clicked() {
self.app_state.show_hotkeys = false;
}
ui.add_space(ui.available_width() / 2.0 - 40.0);
ui.label(RichText::new("Hotkeys").color(Color32::WHITE).monospace());
ui.vertical_centered(|ui| {
ui.label(RichText::new("Hotkeys").color(Color32::WHITE).monospace());
});
});
ui.separator();
// Search and Add Command
// --- Search and Add Command ---
ui.horizontal(|ui| {
ui.menu_button(format!("{} Add Command", ICON_ADD.codepoint), |ui| {
let mut selected_cmd = None;
@@ -185,10 +184,10 @@ impl SoundpadGui {
ui.add_space(10.0);
ui.add_sized(
[ui.available_width(), 22.0],
ui.add(
TextEdit::singleline(&mut self.app_state.hotkey_search_query)
.hint_text("Search hotkeys..."),
.hint_text("Search hotkeys...")
.desired_width(f32::INFINITY),
);
});
@@ -200,165 +199,186 @@ impl SoundpadGui {
conflicts.into_iter().flat_map(|(a, b)| [a, b]).collect();
let search = self.app_state.hotkey_search_query.to_lowercase();
// Slots table
let mut action: Option<HotkeyAction> = None;
let area_size = ui.available_size();
ScrollArea::vertical().show(ui, |ui| {
ui.set_min_width(area_size.x);
Grid::new("hotkeys_grid")
.striped(true)
.num_columns(4)
.max_col_width(area_size.x)
.min_col_width(area_size.x / 4.0)
.spacing([40.0, 10.0])
.show(ui, |ui| {
// Table header
let slots: Vec<_> = self
.app_state
.hotkey_config
.slots
.iter()
.filter(|s| {
if search.is_empty() {
return true;
}
s.slot.to_lowercase().contains(&search)
|| format!("{:?}", s.action).to_lowercase().contains(&search)
|| s.key_chord
.as_deref()
.unwrap_or("")
.to_lowercase()
.contains(&search)
})
.cloned()
.collect();
let available_width = ui.available_width();
let col_width = (available_width / 4.0).max(80.0);
TableBuilder::new(ui)
.striped(true)
.column(Column::exact(col_width).clip(true)) // Slot
.column(Column::exact(col_width).clip(true)) // Sound / Action name
.column(Column::exact(col_width).clip(true)) // Key Chord
.column(Column::exact(col_width).clip(true)) // Actions
.header(30.0, |mut header| {
header.col(|ui| {
ui.label(
RichText::new("Slot")
.strong()
.monospace()
.color(Color32::LIGHT_GRAY),
);
});
header.col(|ui| {
ui.label(
RichText::new("Sound")
.strong()
.monospace()
.color(Color32::LIGHT_GRAY),
);
});
header.col(|ui| {
ui.label(
RichText::new("Key Chord")
.strong()
.monospace()
.color(Color32::LIGHT_GRAY),
);
});
header.col(|ui| {
ui.label(
RichText::new("Actions")
.strong()
.monospace()
.color(Color32::LIGHT_GRAY),
);
ui.end_row();
let slots: Vec<_> = self
.app_state
.hotkey_config
.slots
.iter()
.filter(|s| {
if search.is_empty() {
return true;
}
s.slot.to_lowercase().contains(&search)
|| format!("{:?}", s.action).to_lowercase().contains(&search)
|| s.key_chord
.as_deref()
.unwrap_or("")
.to_lowercase()
.contains(&search)
})
.cloned()
.collect();
for slot in &slots {
ui.horizontal(|ui| {
// Conflict badge
if conflict_slots.contains(slot.slot.as_str()) {
ui.label(
RichText::new(ICON_WARNING.codepoint)
.color(Color32::from_rgb(255, 165, 0)),
)
.on_hover_text("Key chord conflict");
}
// Slot name
let slot_text = RichText::new(&slot.slot).monospace();
ui.label(slot_text);
});
// Action description
let action_name = match slot.action.name.as_str() {
"play" => {
if let Some(file_path_str) = slot.action.args.get("file_path") {
Path::new(file_path_str)
.file_name()
.unwrap_or_default()
.to_string_lossy()
.to_string()
} else {
"Play".to_string()
}
}
"toggle_pause" => "Toggle Pause".to_string(),
"pause" => "Pause Playback".to_string(),
"resume" => "Resume Playback".to_string(),
"stop" => "Stop Playback".to_string(),
"toggle_loop" => "Toggle Loop".to_string(),
other => other.to_string(),
};
ui.add(Label::new(RichText::new(action_name).monospace()).truncate());
// Key chord
let chord_text = slot.key_chord.as_deref().unwrap_or("(none)");
ui.label(RichText::new(chord_text).monospace().color(
if slot.key_chord.is_some() {
Color32::from_rgb(100, 200, 100)
} else {
Color32::GRAY
},
));
ui.horizontal(|ui| {
// Delete button
if ui
.add(Button::new(ICON_DELETE).frame(false))
.on_hover_text("Remove slot")
.clicked()
{
action = Some(HotkeyAction::Remove(slot.slot.clone()));
}
// Set key chord button
if ui
.add(Button::new(ICON_KEYBOARD).frame(false))
.on_hover_text("Set key chord")
.clicked()
{
action = Some(HotkeyAction::Capture(slot.slot.clone()));
}
// Clear key chord
if slot.key_chord.is_some()
&& ui
.add(Button::new(ICON_BACKSPACE).frame(false))
.on_hover_text("Clear key chord")
.clicked()
{
action = Some(HotkeyAction::ClearChord(slot.slot.clone()));
}
// Play button
if ui
.add(Button::new(ICON_PLAY_ARROW).frame(false))
.on_hover_text("Play")
.clicked()
{
action = Some(HotkeyAction::Play(slot.slot.clone()));
}
});
ui.end_row();
}
if slots.is_empty() {
ui.label("No hotkey slots configured.");
ui.label("");
ui.label("");
ui.label("");
ui.end_row();
}
});
});
})
.body(|mut body| {
if slots.is_empty() {
body.row(30.0, |mut row| {
row.col(|_| {});
row.col(|ui| {
ui.label(
RichText::new("No hotkey slots configured.")
.color(Color32::GRAY),
);
});
row.col(|_| {});
row.col(|_| {});
});
return;
}
for slot in &slots {
body.row(30.0, |mut row| {
// Column 1: Slot
row.col(|ui| {
ui.horizontal(|ui| {
if conflict_slots.contains(slot.slot.as_str()) {
ui.label(
RichText::new(ICON_WARNING.codepoint)
.color(Color32::from_rgb(255, 165, 0)),
)
.on_hover_text("Key chord conflict");
}
ui.add(
Label::new(RichText::new(&slot.slot).monospace())
.truncate(),
);
});
});
// Column 2: Sound / Action name
row.col(|ui| {
let action_name = match slot.action.name.as_str() {
"play" => {
if let Some(file_path_str) =
slot.action.args.get("file_path")
{
Path::new(file_path_str)
.file_name()
.unwrap_or_default()
.to_string_lossy()
.to_string()
} else {
"Play".to_string()
}
}
"toggle_pause" => "Toggle Pause".to_string(),
"pause" => "Pause Playback".to_string(),
"resume" => "Resume Playback".to_string(),
"stop" => "Stop Playback".to_string(),
"toggle_loop" => "Toggle Loop".to_string(),
other => other.to_string(),
};
ui.add(
Label::new(RichText::new(action_name).monospace()).truncate(),
);
});
// Column 3: Key Chord
row.col(|ui| {
let chord_text = slot.key_chord.as_deref().unwrap_or("(none)");
ui.add(
Label::new(RichText::new(chord_text).monospace().color(
if slot.key_chord.is_some() {
Color32::from_rgb(100, 200, 100)
} else {
Color32::GRAY
},
))
.truncate(),
);
});
// Column 4: Actions
row.col(|ui| {
ui.horizontal(|ui| {
if ui
.add(Button::new(ICON_DELETE).frame(false))
.on_hover_text("Remove slot")
.clicked()
{
action = Some(HotkeyAction::Remove(slot.slot.clone()));
}
if ui
.add(Button::new(ICON_KEYBOARD).frame(false))
.on_hover_text("Set key chord")
.clicked()
{
action = Some(HotkeyAction::Capture(slot.slot.clone()));
}
if slot.key_chord.is_some()
&& ui
.add(Button::new(ICON_BACKSPACE).frame(false))
.on_hover_text("Clear key chord")
.clicked()
{
action = Some(HotkeyAction::ClearChord(slot.slot.clone()));
}
if ui
.add(Button::new(ICON_PLAY_ARROW).frame(false))
.on_hover_text("Play")
.clicked()
{
action = Some(HotkeyAction::Play(slot.slot.clone()));
}
});
});
});
}
});
if let Some(action) = action {
match action {
@@ -731,7 +751,7 @@ impl SoundpadGui {
file_button_text = file_button_text.color(Color32::WHITE);
}
let file_button = Button::new(file_button_text).frame(false);
let file_button = Button::new(file_button_text).frame(false).truncate();
let file_button_response = ui.add(file_button);
if file_button_response.clicked() {
ui.input(|i| {
+2 -2
View File
@@ -9,7 +9,7 @@ use std::path::PathBuf;
fn chord_from_event(modifiers: &Modifiers, key: &Key) -> Option<String> {
let key_name = key.name();
let is_valid = (key_name.len() == 1
&& key_name.chars().next().unwrap().is_ascii_alphanumeric())
&& key_name.chars().next().is_some_and(|c| c.is_ascii_alphanumeric()))
|| (key_name.starts_with('F')
&& key_name.len() > 1
&& key_name[1..].chars().all(|c| c.is_ascii_digit()));
@@ -60,7 +60,7 @@ pub fn parse_chord(chord: &str) -> Option<(Modifiers, Key)> {
let key_name = parts[parts.len() - 1];
let is_valid = (key_name.len() == 1
&& key_name.chars().next().unwrap().is_ascii_alphanumeric())
&& key_name.chars().next().is_some_and(|c| c.is_ascii_alphanumeric()))
|| (key_name.starts_with('F')
&& key_name.len() > 1
&& key_name[1..].chars().all(|c| c.is_ascii_digit()));
+85
View File
@@ -122,3 +122,88 @@ pub fn parse_command(request: &Request) -> Option<Box<dyn Executable + Send>> {
_ => None,
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::types::socket::Request;
use std::collections::HashMap;
#[test]
fn test_parse_set_volume_valid() {
let mut args = HashMap::new();
args.insert("volume".to_string(), "0.5".to_string());
args.insert("id".to_string(), "1".to_string());
let request = Request {
name: "set_volume".to_string(),
args,
};
let cmd = parse_command(&request);
assert!(cmd.is_some());
}
#[test]
fn test_parse_set_volume_missing_volume() {
let mut args = HashMap::new();
args.insert("id".to_string(), "1".to_string());
let request = Request {
name: "set_volume".to_string(),
args,
};
let cmd = parse_command(&request);
assert!(cmd.is_some());
}
#[test]
fn test_parse_set_volume_invalid_volume() {
let mut args = HashMap::new();
args.insert("volume".to_string(), "not-a-float".to_string());
let request = Request {
name: "set_volume".to_string(),
args,
};
let cmd = parse_command(&request);
assert!(cmd.is_some());
}
#[test]
fn test_parse_set_volume_missing_id() {
let mut args = HashMap::new();
args.insert("volume".to_string(), "0.5".to_string());
let request = Request {
name: "set_volume".to_string(),
args,
};
let cmd = parse_command(&request);
assert!(cmd.is_some());
}
#[test]
fn test_parse_set_volume_invalid_id() {
let mut args = HashMap::new();
args.insert("id".to_string(), "not-an-int".to_string());
args.insert("volume".to_string(), "0.5".to_string());
let request = Request {
name: "set_volume".to_string(),
args,
};
let cmd = parse_command(&request);
assert!(cmd.is_some());
}
#[test]
fn test_parse_set_volume_empty_args() {
let request = Request {
name: "set_volume".to_string(),
args: HashMap::new(),
};
let cmd = parse_command(&request);
assert!(cmd.is_some());
}
}