Compare commits

...
62 Commits
Author SHA1 Message Date
arabianq 1616d80ee9 change version to 1.14.0 2026-07-24 23:58:39 +03:00
arabianq d97d4c60d2 cargo update 2026-07-24 23:57:38 +03:00
Tarasov AleksandrandGitHub 0c3d7cbd35 feat(gui): better search with automatic tree expansion, directories scanning and caching 2026-07-24 23:49:10 +03:00
arabianq 6d7abdbf53 change version to 1.13.0 2026-07-24 05:52:27 +03:00
arabianq 41bb035b2c cargo update 2026-07-24 05:43:52 +03:00
arabianq 3873bf4270 deps: update rodio 2026-07-24 05:43:02 +03:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2b60b47cce chore(deps): bump clap from 4.6.3 to 4.6.4 in the clap-stack group (#176)
* chore(deps): bump clap from 4.6.3 to 4.6.4 in the clap-stack group

Bumps the clap-stack group with 1 update: [clap](https://github.com/clap-rs/clap).


Updates `clap` from 4.6.3 to 4.6.4
- [Release notes](https://github.com/clap-rs/clap/releases)
- [Changelog](https://github.com/clap-rs/clap/blob/master/CHANGELOG.md)
- [Commits](https://github.com/clap-rs/clap/compare/clap_complete-v4.6.3...clap_complete-v4.6.4)

---
updated-dependencies:
- dependency-name: clap
  dependency-version: 4.6.4
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: clap-stack
...

Signed-off-by: dependabot[bot] <support@github.com>

* chore(flatpak): automatically update cargo-sources.json

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-24 05:42:53 +03:00
Tarasov AleksandrandGitHub 65ae8589db feat(lib, daemon, cli, gui): add volume multiplier (#175)
* initial implementation

* rename DaemonConfig.volume_multiplier to default_volume_multiplier

* add volume_multiplier to the FullState

* add pwsp-gui integration
2026-07-24 05:33:51 +03:00
Tarasov AleksandrandGitHub a33d023950 feat(daemon, cli): global DaemonConfig and an ability to get/save it using cli (#174)
* use one global Arc<DaemonConfig>

* implement commands

* replace Arc<DaemonConfig> with Arc<Mutex<DaemonConfig>> and fix pwsp-gui

* add pwsp-cli support

* replace serde_json::to_string_pretty with to_string

* refactor

* implement UpdateDaemonConfigCommand and use it in pwsp-gui so it now uses real in-memory config, not the saved one

* implement utils::gui get_daemon_config and update_daemon_config to simplify code
2026-07-24 04:49:51 +03:00
arabianq 5c73bc1bea fix cagro sources 2026-07-20 21:26:08 +03:00
arabianq 42e305869a change version to 1.12.5 2026-07-20 21:20:02 +03:00
arabianq cb7f4f7f6b update cargo-sources.json 2026-07-20 21:18:43 +03:00
arabianq 62821904af cargo update 2026-07-20 21:16:56 +03:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>Tarasov Aleksandrarabianq
3a218283db chore(deps): bump the serde-stack group with 2 updates (#169)
* chore(deps): bump the serde-stack group with 2 updates

Bumps the serde-stack group with 2 updates: [serde](https://github.com/serde-rs/serde) and [serde_json](https://github.com/serde-rs/json).


Updates `serde` from 1.0.228 to 1.0.229
- [Release notes](https://github.com/serde-rs/serde/releases)
- [Commits](https://github.com/serde-rs/serde/compare/v1.0.228...v1.0.229)

Updates `serde_json` from 1.0.150 to 1.0.151
- [Release notes](https://github.com/serde-rs/json/releases)
- [Commits](https://github.com/serde-rs/json/compare/v1.0.150...v1.0.151)

---
updated-dependencies:
- dependency-name: serde
  dependency-version: 1.0.229
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: serde-stack
- dependency-name: serde_json
  dependency-version: 1.0.151
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: serde-stack
...

Signed-off-by: dependabot[bot] <support@github.com>

* chore(flatpak): automatically update cargo-sources.json

* fix

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Tarasov Aleksandr <55220741+arabianq@users.noreply.github.com>
Co-authored-by: arabian <a.tevg@ya.ru>
2026-07-20 20:58:10 +03:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>Tarasov Aleksandr
02fdfb6ca8 chore(deps): bump rust-i18n from 4.2.0 to 4.2.1 (#172)
* chore(deps): bump rust-i18n from 4.2.0 to 4.2.1

Bumps [rust-i18n](https://github.com/longbridge/rust-i18n) from 4.2.0 to 4.2.1.
- [Release notes](https://github.com/longbridge/rust-i18n/releases)
- [Commits](https://github.com/longbridge/rust-i18n/compare/v4.2.0...v4.2.1)

---
updated-dependencies:
- dependency-name: rust-i18n
  dependency-version: 4.2.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>

* chore(flatpak): automatically update cargo-sources.json

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Tarasov Aleksandr <55220741+arabianq@users.noreply.github.com>
2026-07-20 19:33:48 +03:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
a027838d0e chore(deps): bump tokio from 1.52.3 to 1.53.0 in the tokio-stack group (#170)
* chore(deps): bump tokio from 1.52.3 to 1.53.0 in the tokio-stack group

Bumps the tokio-stack group with 1 update: [tokio](https://github.com/tokio-rs/tokio).


Updates `tokio` from 1.52.3 to 1.53.0
- [Release notes](https://github.com/tokio-rs/tokio/releases)
- [Commits](https://github.com/tokio-rs/tokio/compare/tokio-1.52.3...tokio-1.53.0)

---
updated-dependencies:
- dependency-name: tokio
  dependency-version: 1.53.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: tokio-stack
...

Signed-off-by: dependabot[bot] <support@github.com>

* chore(flatpak): automatically update cargo-sources.json

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-20 19:22:03 +03:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
1828d903a1 chore(deps): bump clap from 4.6.1 to 4.6.3 in the clap-stack group (#171)
* chore(deps): bump clap from 4.6.1 to 4.6.3 in the clap-stack group

Bumps the clap-stack group with 1 update: [clap](https://github.com/clap-rs/clap).


Updates `clap` from 4.6.1 to 4.6.3
- [Release notes](https://github.com/clap-rs/clap/releases)
- [Changelog](https://github.com/clap-rs/clap/blob/master/CHANGELOG.md)
- [Commits](https://github.com/clap-rs/clap/compare/clap_complete-v4.6.1...clap_complete-v4.6.3)

---
updated-dependencies:
- dependency-name: clap
  dependency-version: 4.6.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: clap-stack
...

Signed-off-by: dependabot[bot] <support@github.com>

* chore(flatpak): automatically update cargo-sources.json

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-20 19:21:48 +03:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
83ca7a5e34 chore(deps): bump async-trait from 0.1.89 to 0.1.91 (#173)
* chore(deps): bump async-trait from 0.1.89 to 0.1.91

Bumps [async-trait](https://github.com/dtolnay/async-trait) from 0.1.89 to 0.1.91.
- [Release notes](https://github.com/dtolnay/async-trait/releases)
- [Commits](https://github.com/dtolnay/async-trait/compare/0.1.89...0.1.91)

---
updated-dependencies:
- dependency-name: async-trait
  dependency-version: 0.1.91
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>

* chore(flatpak): automatically update cargo-sources.json

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-20 19:18:56 +03:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
f2aca5e34a chore(deps): bump actions/setup-python from 6 to 7 (#168)
Bumps [actions/setup-python](https://github.com/actions/setup-python) from 6 to 7.
- [Release notes](https://github.com/actions/setup-python/releases)
- [Commits](https://github.com/actions/setup-python/compare/v6...v7)

---
updated-dependencies:
- dependency-name: actions/setup-python
  dependency-version: '7'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-20 19:13:41 +03:00
6130a99c4b fix(lib): don't create '~' in $HOME (#166)
* fix(lib): don't create '~' in $HOME

* throw an error wen fail to get home directory

---------

Co-authored-by: arabian <a.tevg@ya.ru>
2026-07-20 19:02:10 +03:00
tonocoandGitHub 322b93efa1 fix(scripts): check workspace_package_match and version_match type before passing (#167) 2026-07-19 17:22:46 +03:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>arabianq
683f4127a0 chore(deps): bump rust-i18n from 4.1.0 to 4.2.0 (#163)
* chore(deps): bump rust-i18n from 4.1.0 to 4.2.0

Bumps [rust-i18n](https://github.com/longbridge/rust-i18n) from 4.1.0 to 4.2.0.
- [Release notes](https://github.com/longbridge/rust-i18n/releases)
- [Commits](https://github.com/longbridge/rust-i18n/compare/v4.1.0...v4.2.0)

---
updated-dependencies:
- dependency-name: rust-i18n
  dependency-version: 4.2.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>

* update cargo-sources.json

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: arabian <a.tevg@ya.ru>
2026-07-15 16:21:05 +03:00
arabianq c873166e21 fix(dependabot): install tomlkit 2026-07-14 16:43:57 +03:00
Tarasov AleksandrandGitHub ec4e873071 refactor: better systemd service (#161)
* move `pwsp-daemon.service` from `pwsp-gui/assets` to pwsp-daemon/assets + autorestart service + better logging + wait for pipewire until start

* adapt all packages

* improve systemd service calling for .deb package
2026-07-07 21:29:26 +03:00
arabianq a91252d7fb change version to 1.12.4 2026-07-07 19:31:00 +03:00
arabianq 239170df45 cargo update 2026-07-07 19:30:15 +03:00
arabianq 5fef42a2e2 cargo fmt 2026-07-07 19:29:48 +03:00
Tarasov AleksandrGitHubgoogle-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com>
4f8f663f12 fix(pwsp-gui): fix arbitrary file overwrite via path traversal (#159)
* Fix path traversal vulnerability in audio download logic

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

* std::path::Path -> Path

* add random id to the filename if it's unknown

---------

Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com>
2026-07-07 19:29:12 +03:00
Tarasov AleksandrGitHubgoogle-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com>
6ccc55a1d0 perf(pwsp-gui): optimize gui file filtering performance (#160)
Avoid unnecessary string cloning and allocation in `pwsp-gui/src/gui/mod.rs` when checking if a file matches the search query.

Specifically, we replaced `.to_string_lossy().to_string()` with `.to_string_lossy()` because `.to_lowercase()` directly works on `Cow<str>`, avoiding an intermediate String allocation.

Measured improvement was verified using an isolated benchmark demonstrating roughly 10-15% reduction in execution time.

Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com>
2026-07-07 19:00:16 +03:00
Tarasov AleksandrGitHubgoogle-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com>
d266a3f073 fix(pwsp-lib): fixed unhandled panic on missing config directory (#157)
Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com>
2026-07-07 18:58:43 +03:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
d2993592be chore(deps): bump actions/upload-artifact from 4 to 7 (#153)
Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 4 to 7.
- [Release notes](https://github.com/actions/upload-artifact/releases)
- [Commits](https://github.com/actions/upload-artifact/compare/v4...v7)

---
updated-dependencies:
- dependency-name: actions/upload-artifact
  dependency-version: '7'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-07 16:56:24 +03:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
1a53ab62dc chore(deps): bump peaceiris/actions-gh-pages from 3 to 4 (#154)
Bumps [peaceiris/actions-gh-pages](https://github.com/peaceiris/actions-gh-pages) from 3 to 4.
- [Release notes](https://github.com/peaceiris/actions-gh-pages/releases)
- [Changelog](https://github.com/peaceiris/actions-gh-pages/blob/main/CHANGELOG.md)
- [Commits](https://github.com/peaceiris/actions-gh-pages/compare/v3...v4)

---
updated-dependencies:
- dependency-name: peaceiris/actions-gh-pages
  dependency-version: '4'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-07 16:06:50 +03:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
51fcd94a61 chore(deps): bump actions/checkout from 4 to 7 (#155)
Bumps [actions/checkout](https://github.com/actions/checkout) from 4 to 7.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v4...v7)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: '7'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-07 15:59:53 +03:00
arabianq 7f6a122816 ci(flatpak): use /run/ccache for flatpak builds 2026-07-07 15:50:06 +03:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
7ca01ed8cb chore(deps): bump stefanzweifel/git-auto-commit-action from 5 to 7 (#156)
Bumps [stefanzweifel/git-auto-commit-action](https://github.com/stefanzweifel/git-auto-commit-action) from 5 to 7.
- [Release notes](https://github.com/stefanzweifel/git-auto-commit-action/releases)
- [Changelog](https://github.com/stefanzweifel/git-auto-commit-action/blob/master/CHANGELOG.md)
- [Commits](https://github.com/stefanzweifel/git-auto-commit-action/compare/v5...v7)

---
updated-dependencies:
- dependency-name: stefanzweifel/git-auto-commit-action
  dependency-version: '7'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-07 15:41:31 +03:00
arabianq 07321afb60 docs(README): change pwsp-gui to pwsp 2026-07-05 22:41:49 +03:00
arabianq 8534e516ad docs(README): change arabianq-repo to pwsp for flatpak repo name 2026-07-05 22:41:15 +03:00
arabianq 16ce233f79 packages(flatapak): fix gpg key for flatpak repo 2026-07-05 22:36:20 +03:00
arabianq c2f4658871 packages(flatpak): add flatpakrepo file generation 2026-07-05 22:22:50 +03:00
arabianq 701bdebc88 packages(aur, rpm, deb): add openssl-devel as build dependency 2026-07-05 21:17:55 +03:00
arabianq d4ae78c7b0 ci(rpm): arabianq/pwsp-git -> arabianq/pwsp 2026-07-05 21:04:07 +03:00
arabianq 9e1d50e30c ci(rpm): add workflow for pwsp-git.rpm 2026-07-05 21:01:05 +03:00
arabianq 85b6c17bd1 deps(flatpak): update cargro-sources.json 2026-07-05 21:00:14 +03:00
arabianq cfe478b02d ci(rpm): replaced rustls with native-tls (OpenSSL) so it can build in copr 2026-07-05 20:56:18 +03:00
arabianq 0b23b3ed51 packages(deb): change package.metadata.deb.name to pwsp 2026-07-05 20:55:27 +03:00
arabianq 728d14006c change version to 1.12.3 2026-07-05 20:20:46 +03:00
arabianq f8bb84bbcf docs(readme): fix screenshot 2026-07-05 19:35:56 +03:00
Tarasov AleksandrandGitHub 0dbcc771f5 docs: new readme (#152) 2026-07-05 19:33:20 +03:00
arabianq b3a786a76c deps(flatpak): update cargo-sources.json 2026-07-05 19:11:08 +03:00
arabianq 972e303bff deps: cargo update 2026-07-05 19:07:46 +03:00
arabianq c406704ce5 fix(ci, flatter): fix building nightly repo 2026-07-05 18:56:32 +03:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
fd4a35e0e1 chore(deps): bump crazy-max/ghaction-import-gpg from 6 to 7 (#147)
Bumps [crazy-max/ghaction-import-gpg](https://github.com/crazy-max/ghaction-import-gpg) from 6 to 7.
- [Release notes](https://github.com/crazy-max/ghaction-import-gpg/releases)
- [Commits](https://github.com/crazy-max/ghaction-import-gpg/compare/v6...v7)

---
updated-dependencies:
- dependency-name: crazy-max/ghaction-import-gpg
  dependency-version: '7'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-05 18:44:26 +03:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
c793d09006 chore(deps): bump actions/setup-python from 5 to 6 (#148)
Bumps [actions/setup-python](https://github.com/actions/setup-python) from 5 to 6.
- [Release notes](https://github.com/actions/setup-python/releases)
- [Commits](https://github.com/actions/setup-python/compare/v5...v6)

---
updated-dependencies:
- dependency-name: actions/setup-python
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-05 18:44:13 +03:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
e4dd6d8b56 chore(deps): bump actions/cache from 4 to 6 (#149)
Bumps [actions/cache](https://github.com/actions/cache) from 4 to 6.
- [Release notes](https://github.com/actions/cache/releases)
- [Changelog](https://github.com/actions/cache/blob/main/RELEASES.md)
- [Commits](https://github.com/actions/cache/compare/v4...v6)

---
updated-dependencies:
- dependency-name: actions/cache
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-05 18:43:58 +03:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
457416779a chore(deps): bump softprops/action-gh-release from 2 to 3 (#150)
Bumps [softprops/action-gh-release](https://github.com/softprops/action-gh-release) from 2 to 3.
- [Release notes](https://github.com/softprops/action-gh-release/releases)
- [Changelog](https://github.com/softprops/action-gh-release/blob/master/CHANGELOG.md)
- [Commits](https://github.com/softprops/action-gh-release/compare/v2...v3)

---
updated-dependencies:
- dependency-name: softprops/action-gh-release
  dependency-version: '3'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-05 18:43:42 +03:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
b0ac5490cc chore(deps): bump actions/download-artifact from 4 to 8 (#151)
Bumps [actions/download-artifact](https://github.com/actions/download-artifact) from 4 to 8.
- [Release notes](https://github.com/actions/download-artifact/releases)
- [Commits](https://github.com/actions/download-artifact/compare/v4...v8)

---
updated-dependencies:
- dependency-name: actions/download-artifact
  dependency-version: '8'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-05 18:43:27 +03:00
arabianq a728671b01 fix(ci, flatter): fixed building nightly repo 2026-07-05 18:31:38 +03:00
arabianq 08153e4a1c fix(ci, flatter): fixed building nightly repo 2026-07-05 18:22:03 +03:00
arabianq c4699002fd fix(ci, deb): add secion and priority fields to the package.metadata.deb 2026-07-05 18:09:52 +03:00
arabianq dbcbf36df0 ci(deb): add nightly builds 2026-07-05 18:03:22 +03:00
arabianq 873ed9a6db fix github pages 2026-07-05 17:58:38 +03:00
Tarasov AleksandrandGitHub 601a5ce336 ci: refactor and optimize GitHub Actions workflows (#146)
* ci: refactor and optimize GitHub Actions workflows\n\n- **Consolidate build processes (`build.yml`, `release.yml`)**: Merged the standalone `.zip` (linux-build) and `.deb` (deb-build) jobs into a single matrix job (`build-and-package` / `build-release`).
  This prevents compiling the Rust binaries twice per architecture, effectively cutting the build time in half and saving CI runner minutes.
    - **Parallelize and fix Flatpak deployment (`flatter.yml`)**:
      - Converted `x86_64` and `aarch64` flatpak jobs to run concurrently via a matrix strategy, removing the sequential bottleneck.
      - Resolved an issue where GitHub Pages would only host the `aarch64` repository. Introduced a new `prepare-repo` job that downloads the `.flatpak` bundles from both architectures, merges them into a
  single OSTree repository locally, signs it with GPG, and deploys the unified multi-arch repository to GitHub Pages.
      - Configured `flatter.yml` to automatically attach the offline `.flatpak` bundles to GitHub Releases when triggered by a release event.
    - **Toolchain and cleanup**: Switched hardcoded Rust toolchain version (`1.96.0`) to `stable` across all workflows to prevent manual bumps in the future. Simplified bash scripts and removed redundant
  dependency installation steps.

* Extract Copr trigger into a separate job: Copr build now starts immediately in parallel with GitHub Actions builds, eliminating unnecessary waiting time.

* use ubuntu 22.04 to build binaries instead of ubuntu-latest

* fix cache poisoning

* another attemp to fix cache poisoning

* disable cache in setup-rust-toolchain

* use ubuntu-latest

* optimize dependencies in debug profile

* remove --release flag from build.yml and CARGO env vars

* fix flatter names collision

* better flatter key changing

* auto formatting

* add auto updating cargo-sources for dependabot pull requests

* add groups to dependabot.yml

* add github-actions to dependabot

* add ppa deb repo

* add concurrency: gh-pages-deploy
2026-07-05 17:49:38 +03:00
42 changed files with 2763 additions and 2046 deletions
+23
View File
@@ -1,6 +1,29 @@
version: 2 version: 2
updates: updates:
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "weekly"
- package-ecosystem: "cargo" - package-ecosystem: "cargo"
directory: "/" directory: "/"
schedule: schedule:
interval: "weekly" interval: "weekly"
groups:
egui-stack:
patterns:
- "egui*"
- "eframe"
serde-stack:
patterns:
- "serde*"
tokio-stack:
patterns:
- "tokio*"
- "reqwest"
clap-stack:
patterns:
- "clap*"
+126 -73
View File
@@ -12,7 +12,7 @@ on:
workflow_dispatch: workflow_dispatch:
jobs: jobs:
linux-build: build-and-package:
strategy: strategy:
matrix: matrix:
include: include:
@@ -33,32 +33,30 @@ jobs:
libclang-dev \ libclang-dev \
libasound2-dev \ libasound2-dev \
libdbus-1-dev \ libdbus-1-dev \
libssl-dev \
pkg-config pkg-config
- name: Checkout code - name: Checkout code
uses: actions/checkout@v4 uses: actions/checkout@v7
with: with:
fetch-depth: 0 fetch-depth: 0
- name: Setup Rust toolchain - name: Setup Rust toolchain
uses: actions-rust-lang/setup-rust-toolchain@v1 uses: actions-rust-lang/setup-rust-toolchain@v1
with: with:
toolchain: 1.96.0 toolchain: stable
cache: false
- name: Rust Cache - name: Rust Cache
uses: swatinem/rust-cache@v2 uses: swatinem/rust-cache@v2
with:
prefix-key: "v3-${{ matrix.runner }}"
- name: Run tests - name: Run tests
run: cargo test --locked run: cargo test --locked
- name: Build all binaries (debug-speed compilation into target/release) - name: Build all binaries
env: run: cargo build --locked
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 - name: Extract all binary names
id: cargo-meta id: cargo-meta
@@ -70,7 +68,7 @@ jobs:
echo "$BIN_NAMES" >> $GITHUB_OUTPUT echo "$BIN_NAMES" >> $GITHUB_OUTPUT
echo "EOF" >> $GITHUB_OUTPUT echo "EOF" >> $GITHUB_OUTPUT
- name: Package all binaries into one archive - name: Package ZIP archive
shell: bash shell: bash
run: | run: |
set -euo pipefail set -euo pipefail
@@ -81,7 +79,7 @@ jobs:
FILES=() FILES=()
while IFS= read -r BIN; do while IFS= read -r BIN; do
[ -z "$BIN" ] && continue [ -z "$BIN" ] && continue
FILES+=("target/release/$BIN") FILES+=("target/debug/$BIN")
done <<< "${{ steps.cargo-meta.outputs.bin_names }}" done <<< "${{ steps.cargo-meta.outputs.bin_names }}"
if [ "${#FILES[@]}" -eq 0 ]; then if [ "${#FILES[@]}" -eq 0 ]; then
@@ -89,70 +87,18 @@ jobs:
exit 1 exit 1
fi fi
for f in "${FILES[@]}"; do
if [ ! -f "$f" ]; then
echo "Error: expected binary not found: $f" >&2
exit 1
fi
echo "Will add: $f"
done
zip -j "$ARCHIVE_NAME" "${FILES[@]}" zip -j "$ARCHIVE_NAME" "${FILES[@]}"
- name: Upload archive as artifact - name: Upload ZIP artifact
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v7
with: with:
name: archive-${{ matrix.arch }} name: archive-${{ matrix.arch }}
path: pwsp-*.zip path: pwsp-*.zip
retention-days: 7 retention-days: 7
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: Rust Cache
uses: swatinem/rust-cache@v2
- 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 - name: Cache cargo-deb
id: cache-cargo-deb id: cache-cargo-deb
uses: actions/cache@v4 uses: actions/cache@v6
with: with:
path: ~/.cargo/bin/cargo-deb path: ~/.cargo/bin/cargo-deb
key: ${{ runner.os }}-${{ runner.arch }}-cargo-deb-v1 key: ${{ runner.os }}-${{ runner.arch }}-cargo-deb-v1
@@ -161,20 +107,108 @@ jobs:
if: steps.cache-cargo-deb.outputs.cache-hit != 'true' if: steps.cache-cargo-deb.outputs.cache-hit != 'true'
run: cargo install --locked cargo-deb run: cargo install --locked cargo-deb
- name: Create .deb package (debug binaries from target/release) - name: Create .deb package
shell: bash shell: bash
run: | run: |
set -euo pipefail set -euo pipefail
export PATH="$HOME/.cargo/bin:$PATH" export PATH="$HOME/.cargo/bin:$PATH"
cargo-deb -p pwsp-gui --no-build --no-strip if [ "${{ github.event_name }}" == "push" ] && [ "${{ github.ref }}" == "refs/heads/main" ]; then
BASE_VERSION=$(grep '^version =' Cargo.toml | head -1 | cut -d'"' -f2)
GIT_REV=$(git rev-parse --short HEAD)
DATE=$(date +%Y%m%d)
DEB_VER="${BASE_VERSION}+git${DATE}.${GIT_REV}"
echo "Building nightly deb version: $DEB_VER"
cargo-deb -p pwsp-gui --no-build --no-strip --profile dev --deb-version "$DEB_VER"
else
cargo-deb -p pwsp-gui --no-build --no-strip --profile dev
fi
- name: Upload .deb(s) as artifacts - name: Upload .deb artifact
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v7
with: with:
name: deb-packages-${{ matrix.arch }} name: deb-packages-${{ matrix.arch }}
path: target/debian/*.deb path: target/debian/*.deb
retention-days: 7 retention-days: 7
deploy-nightly-apt:
needs: build-and-package
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
concurrency: gh-pages-deploy
permissions:
contents: write
steps:
- name: Checkout code
uses: actions/checkout@v7
- name: Setup GPG
id: gpg
uses: crazy-max/ghaction-import-gpg@v7
with:
gpg_private_key: ${{ secrets.GPG_PRIVATE_KEY }}
passphrase: ${{ secrets.GPG_PASSPHRASE }}
- name: Download DEB artifacts
uses: actions/download-artifact@v8
with:
pattern: deb-packages-*
path: debs
merge-multiple: true
- name: Checkout existing gh-pages
uses: actions/checkout@v7
with:
ref: gh-pages
path: gh-pages-branch
continue-on-error: true
- name: Generate Nightly APT Repository
run: |
sudo apt-get update
sudo apt-get install -y reprepro
mkdir -p apt-repo
if [ -d "gh-pages-branch/apt" ]; then
cp -r gh-pages-branch/apt/* apt-repo/ || true
fi
mkdir -p apt-repo/conf
cat <<EOF > apt-repo/conf/distributions
Origin: arabianq
Label: PipeWire Soundpad
Codename: stable
Architectures: amd64 arm64
Components: main
Description: APT Repository for PWSP
SignWith: ${{ steps.gpg.outputs.fingerprint }}
Origin: arabianq
Label: PipeWire Soundpad
Codename: nightly
Architectures: amd64 arm64
Components: main
Description: Nightly APT Repository for PWSP
SignWith: ${{ steps.gpg.outputs.fingerprint }}
EOF
gpg --armor --export ${{ steps.gpg.outputs.fingerprint }} > apt-repo/pubkey.gpg
cd apt-repo
for deb in ../debs/*.deb; do
if [ -f "$deb" ]; then
echo "Adding $deb to nightly APT repository..."
reprepro includedeb nightly "$deb"
fi
done
- name: Deploy APT Repo to GitHub Pages
uses: peaceiris/actions-gh-pages@v4
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
publish_dir: ./apt-repo
destination_dir: apt
keep_files: true
flatpak-build: flatpak-build:
if: github.event_name == 'pull_request' || github.event_name == 'workflow_dispatch' if: github.event_name == 'pull_request' || github.event_name == 'workflow_dispatch'
strategy: strategy:
@@ -191,7 +225,7 @@ jobs:
options: --privileged options: --privileged
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v7
- name: Build Flatpak - name: Build Flatpak
uses: flatpak/flatpak-github-actions/flatpak-builder@v6 uses: flatpak/flatpak-github-actions/flatpak-builder@v6
@@ -202,3 +236,22 @@ jobs:
branch: master branch: master
build-bundle: true build-bundle: true
arch: ${{ matrix.arch }} arch: ${{ matrix.arch }}
trigger-copr-nightly:
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
steps:
- name: Install copr-cli
run: pip install copr-cli
- name: Trigger Copr Build
env:
COPR_CONFIG: ${{ secrets.COPR_CONFIG }}
run: |
mkdir -p ~/.config
echo "$COPR_CONFIG" > ~/.config/copr
copr-cli buildscm --nowait \
--clone-url https://github.com/arabianq/pipewire-soundpad.git \
--commit ${{ github.sha }} \
--spec packages/rpm/pwsp-git.spec \
arabianq/pwsp
+36
View File
@@ -0,0 +1,36 @@
name: Dependabot Flatpak Cargo Sources
on:
pull_request:
types: [opened, synchronize, reopened]
paths:
- "Cargo.lock"
jobs:
update-cargo-sources:
if: github.actor == 'dependabot[bot]'
runs-on: ubuntu-latest
permissions:
contents: write # Needed to push back to the branch
steps:
- name: Checkout code
uses: actions/checkout@v7
with:
ref: ${{ github.head_ref }}
- name: Setup Python
uses: actions/setup-python@v7
with:
python-version: "3.x"
- name: Install dependencies
run: pip install aiohttp tomlkit
- name: Generate cargo-sources.json
run: bash ./scripts/generate-sources.sh
- name: Commit and push changes
uses: stefanzweifel/git-auto-commit-action@v7
with:
commit_message: "chore(flatpak): automatically update cargo-sources.json"
file_pattern: "packages/flatpak/cargo-sources.json"
+96 -55
View File
@@ -21,9 +21,17 @@ on:
default: "stable" default: "stable"
jobs: jobs:
flatter-x64: build-flatpak:
name: Flatter (x86_64) name: Build Flatpak (${{ matrix.arch }})
runs-on: ubuntu-latest strategy:
matrix:
include:
- arch: x86_64
runner: ubuntu-latest
- arch: aarch64
runner: ubuntu-24.04-arm
fail-fast: false
runs-on: ${{ matrix.runner }}
permissions: permissions:
contents: read contents: read
container: container:
@@ -32,13 +40,13 @@ jobs:
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v7
with: with:
ref: ${{ inputs.tag_name || github.ref }} ref: ${{ inputs.tag_name || github.ref }}
- name: Setup GPG - name: Setup GPG
id: gpg id: gpg
uses: crazy-max/ghaction-import-gpg@v6 uses: crazy-max/ghaction-import-gpg@v7
with: with:
gpg_private_key: ${{ secrets.GPG_PRIVATE_KEY }} gpg_private_key: ${{ secrets.GPG_PRIVATE_KEY }}
passphrase: ${{ secrets.GPG_PASSPHRASE }} passphrase: ${{ secrets.GPG_PASSPHRASE }}
@@ -56,6 +64,8 @@ jobs:
- name: Modify Manifest - name: Modify Manifest
run: | run: |
sed -i '/^branch:/d' packages/flatpak/ru.arabianq.pwsp.yaml
sed -i '/^default-branch:/d' packages/flatpak/ru.arabianq.pwsp.yaml
echo "branch: ${{ steps.set_branch.outputs.branch }}" >> packages/flatpak/ru.arabianq.pwsp.yaml 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 echo "default-branch: ${{ steps.set_branch.outputs.branch }}" >> packages/flatpak/ru.arabianq.pwsp.yaml
@@ -69,77 +79,108 @@ jobs:
with: with:
files: packages/flatpak/ru.arabianq.pwsp.yaml files: packages/flatpak/ru.arabianq.pwsp.yaml
gpg-sign: ${{ steps.gpg.outputs.fingerprint }} gpg-sign: ${{ steps.gpg.outputs.fingerprint }}
upload-bundles: false upload-bundles: true
upload-pages-artifact: false upload-pages-artifact: false
arch: x86_64 arch: ${{ matrix.arch }}
cache: true cache: true
flatter-arm64: prepare-repo:
name: Flatter (aarch64) name: Prepare OSTree Repo
needs: flatter-x64 needs: build-flatpak
runs-on: ubuntu-24.04-arm runs-on: ubuntu-latest
permissions: concurrency: gh-pages-deploy
contents: read
container: container:
image: ghcr.io/andyholmes/flatter/freedesktop:25.08 image: ghcr.io/andyholmes/flatter/freedesktop:25.08
options: --privileged options: --privileged
permissions:
contents: write # Needed to upload files to GitHub Release
steps: steps:
- name: Checkout - name: Download Flatpak Bundles
uses: actions/checkout@v4 uses: actions/download-artifact@v8
with: with:
ref: ${{ inputs.tag_name || github.ref }} path: bundles
pattern: ru.arabianq.pwsp-*
- name: Checkout existing gh-pages
uses: actions/checkout@v7
with:
ref: gh-pages
path: gh-pages-branch
continue-on-error: true
- name: Setup GPG - name: Setup GPG
id: gpg id: gpg
uses: crazy-max/ghaction-import-gpg@v6 uses: crazy-max/ghaction-import-gpg@v7
with: with:
gpg_private_key: ${{ secrets.GPG_PRIVATE_KEY }} gpg_private_key: ${{ secrets.GPG_PRIVATE_KEY }}
passphrase: ${{ secrets.GPG_PASSPHRASE }} passphrase: ${{ secrets.GPG_PASSPHRASE }}
- name: Set Default Branch - name: Update OSTree Repo and Prepare Release Bundles
id: set_branch
run: | run: |
if [ "${{ github.event_name }}" == "release" ]; then mkdir -p release-bundles
echo "branch=stable" >> $GITHUB_OUTPUT
elif [ "${{ github.event_name }}" == "workflow_dispatch" ]; then # Inherit existing OSTree repository to preserve previous branches (e.g. stable vs nightly)
echo "branch=${{ inputs.build_branch }}" >> $GITHUB_OUTPUT # Only inherit if objects/ exists, otherwise it's a corrupted/empty repo due to Git ignoring empty folders
if [ -d "gh-pages-branch/objects" ]; then
echo "Found existing OSTree repository. Copying..."
cp -r gh-pages-branch pages-repo
# Clean up the .git folder so it doesn't interfere
rm -rf pages-repo/.git
# Recreate empty OSTree directories that Git ignored when pushing previously
mkdir -p pages-repo/refs/heads pages-repo/refs/mirrors pages-repo/refs/remotes \
pages-repo/objects pages-repo/state pages-repo/extensions pages-repo/tmp
else else
echo "branch=nightly" >> $GITHUB_OUTPUT echo "Initializing new OSTree repository..."
mkdir -p pages-repo
ostree init --mode=archive-z2 --repo=pages-repo
fi fi
- name: Modify Manifest shopt -s nullglob
run: | for dir in bundles/ru.arabianq.pwsp-*; do
echo "branch: ${{ steps.set_branch.outputs.branch }}" >> packages/flatpak/ru.arabianq.pwsp.yaml arch=$(basename "$dir" | sed 's/ru.arabianq.pwsp-//')
echo "default-branch: ${{ steps.set_branch.outputs.branch }}" >> packages/flatpak/ru.arabianq.pwsp.yaml
- name: Install SDK Extensions # Find the flatpak file inside the dir
run: | bundle=$(find "$dir" -name "*.flatpak" | head -n 1)
flatpak install -y flathub org.freedesktop.Sdk.Extension.rust-stable//25.08
flatpak install -y flathub org.freedesktop.Sdk.Extension.llvm20//25.08
- name: Build Flatpak if [ -n "$bundle" ]; then
uses: andyholmes/flatter@main echo "Importing $bundle for architecture $arch..."
flatpak build-import-bundle pages-repo "$bundle"
# Copy with a unique name for GitHub Release
cp "$bundle" "release-bundles/ru.arabianq.pwsp-${arch}.flatpak"
fi
done
# Update summary and sign the repository
flatpak build-update-repo --generate-static-deltas --gpg-sign=${{ steps.gpg.outputs.fingerprint }} pages-repo
# Export GPG public key for manual import
gpg --export ${{ steps.gpg.outputs.fingerprint }} > pages-repo/pwsp.gpg
# Generate a .flatpakrepo file for easy installation
GPG_BASE64=$(gpg --export ${{ steps.gpg.outputs.fingerprint }} | base64 -w 0)
cat <<EOF | sed 's/^ *//' > pages-repo/pwsp.flatpakrepo
[Flatpak Repo]
Title=PipeWire Soundpad
Url=https://arabianq.github.io/pipewire-soundpad/
Homepage=https://github.com/arabianq/pipewire-soundpad
Comment=PipeWire Soundpad Flatpak Repository
Description=Official repository for PWSP
GPGKey=${GPG_BASE64}
EOF
- name: Deploy Flatpak Repo to GitHub Pages
uses: peaceiris/actions-gh-pages@v4
with: with:
files: packages/flatpak/ru.arabianq.pwsp.yaml github_token: ${{ secrets.GITHUB_TOKEN }}
gpg-sign: ${{ steps.gpg.outputs.fingerprint }} publish_dir: ./pages-repo
upload-bundles: false destination_dir: .
upload-pages-artifact: true keep_files: true
arch: aarch64
cache: true
deploy: - name: Upload bundles to GitHub Release
name: Deploy to GitHub Pages if: github.event_name == 'release'
runs-on: ubuntu-latest uses: softprops/action-gh-release@v3
needs: flatter-arm64 with:
permissions: files: release-bundles/*.flatpak
pages: write tag_name: ${{ github.event.release.tag_name }}
id-token: write
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
steps:
- name: GitHub Pages
id: deployment
uses: actions/deploy-pages@v4
+117 -112
View File
@@ -23,46 +23,32 @@ jobs:
id: tag id: tag
run: | run: |
set -euo pipefail set -euo pipefail
INPUT_TAG="${{ github.event.inputs.tag || '' }}" INPUT_TAG="${{ github.event.inputs.tag || '' }}"
if [ -n "$INPUT_TAG" ]; then
echo "Using input tag: $INPUT_TAG"
echo "tag=$INPUT_TAG" >> $GITHUB_OUTPUT
exit 0
fi
EVENT_TAG="${{ github.event.release.tag_name || '' }}" EVENT_TAG="${{ github.event.release.tag_name || '' }}"
if [ -n "$EVENT_TAG" ]; then
echo "Using event tag: $EVENT_TAG" if [ -n "$INPUT_TAG" ]; then
echo "tag=$INPUT_TAG" >> $GITHUB_OUTPUT
elif [ -n "$EVENT_TAG" ]; then
echo "tag=$EVENT_TAG" >> $GITHUB_OUTPUT echo "tag=$EVENT_TAG" >> $GITHUB_OUTPUT
exit 0 elif [[ "${GITHUB_REF:-}" =~ ^refs/tags/(.+)$ ]]; then
fi
if [[ "${GITHUB_REF:-}" =~ ^refs/tags/(.+)$ ]]; then
echo "Using GITHUB_REF tag: ${BASH_REMATCH[1]}"
echo "tag=${BASH_REMATCH[1]}" >> $GITHUB_OUTPUT echo "tag=${BASH_REMATCH[1]}" >> $GITHUB_OUTPUT
exit 0 else
fi
echo "No tag in input/event/GITHUB_REF — querying latest release via API..."
LATEST_JSON=$(curl -sSf -H "Authorization: Bearer ${{ secrets.GITHUB_TOKEN }}" -H "Accept: application/vnd.github+json" "https://api.github.com/repos/${{ github.repository }}/releases/latest" || true) LATEST_JSON=$(curl -sSf -H "Authorization: Bearer ${{ secrets.GITHUB_TOKEN }}" -H "Accept: application/vnd.github+json" "https://api.github.com/repos/${{ github.repository }}/releases/latest" || true)
TAG_NAME=$(echo "$LATEST_JSON" | jq -r '.tag_name // empty') TAG_NAME=$(echo "$LATEST_JSON" | jq -r '.tag_name // empty')
if [ -n "$TAG_NAME" ]; then if [ -n "$TAG_NAME" ]; then
echo "Found latest release tag: $TAG_NAME"
echo "tag=$TAG_NAME" >> $GITHUB_OUTPUT echo "tag=$TAG_NAME" >> $GITHUB_OUTPUT
exit 0 else
fi
echo "No tag found"
echo "tag=" >> $GITHUB_OUTPUT echo "tag=" >> $GITHUB_OUTPUT
fi
fi
- name: Fail if no tag determined - name: Fail if no tag determined
if: ${{ steps.tag.outputs.tag == '' }} if: ${{ steps.tag.outputs.tag == '' }}
run: | run: |
echo "ERROR: No tag determined. Provide a tag when running manually or ensure a release exists." echo "ERROR: No tag determined."
exit 1 exit 1
linux-release: build-release:
needs: prepare needs: prepare
strategy: strategy:
matrix: matrix:
@@ -75,7 +61,7 @@ jobs:
runs-on: ${{ matrix.runner }} runs-on: ${{ matrix.runner }}
steps: steps:
- name: Install apt deps (jq/zip + dev-libs) - name: Install apt deps
run: | run: |
sudo apt-get update sudo apt-get update
sudo apt-get install -y \ sudo apt-get install -y \
@@ -84,10 +70,11 @@ jobs:
libclang-dev \ libclang-dev \
libasound2-dev \ libasound2-dev \
libdbus-1-dev \ libdbus-1-dev \
libssl-dev \
pkg-config pkg-config
- name: Checkout code at tag - name: Checkout code at tag
uses: actions/checkout@v4 uses: actions/checkout@v7
with: with:
ref: ${{ needs.prepare.outputs.tag }} ref: ${{ needs.prepare.outputs.tag }}
fetch-depth: 0 fetch-depth: 0
@@ -95,10 +82,13 @@ jobs:
- name: Setup Rust toolchain - name: Setup Rust toolchain
uses: actions-rust-lang/setup-rust-toolchain@v1 uses: actions-rust-lang/setup-rust-toolchain@v1
with: with:
toolchain: 1.96.0 toolchain: stable
cache: false
- name: Rust Cache - name: Rust Cache
uses: swatinem/rust-cache@v2 uses: swatinem/rust-cache@v2
with:
prefix-key: "v3-${{ matrix.runner }}"
- name: Extract all binary names - name: Extract all binary names
id: cargo-meta id: cargo-meta
@@ -113,13 +103,12 @@ jobs:
- name: Build all release binaries - name: Build all release binaries
run: cargo build --release --locked run: cargo build --release --locked
- name: Package all binaries into one archive - name: Package ZIP archive
shell: bash shell: bash
run: | run: |
set -euo pipefail set -euo pipefail
TAG="${{ needs.prepare.outputs.tag }}" TAG="${{ needs.prepare.outputs.tag }}"
ARCHIVE_NAME="pwsp-${TAG}-linux-${{ matrix.arch }}.zip" ARCHIVE_NAME="pwsp-${TAG}-linux-${{ matrix.arch }}.zip"
echo "Creating archive: $ARCHIVE_NAME"
FILES=() FILES=()
while IFS= read -r BIN; do while IFS= read -r BIN; do
@@ -127,71 +116,18 @@ jobs:
FILES+=("target/release/$BIN") FILES+=("target/release/$BIN")
done <<< "${{ steps.cargo-meta.outputs.bin_names }}" done <<< "${{ steps.cargo-meta.outputs.bin_names }}"
if [ "${#FILES[@]}" -eq 0 ]; then
echo "Error: no binaries were discovered via cargo metadata." >&2
exit 1
fi
for f in "${FILES[@]}"; do
if [ ! -f "$f" ]; then
echo "Error: expected binary not found: $f" >&2
exit 1
fi
echo "Will add: $f"
done
zip -j "$ARCHIVE_NAME" "${FILES[@]}" zip -j "$ARCHIVE_NAME" "${FILES[@]}"
- name: Upload zip archive - name: Upload ZIP artifact
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v7
with: with:
name: zip-archive-${{ matrix.arch }} name: zip-archive-${{ matrix.arch }}
path: pwsp-*.zip path: pwsp-*.zip
retention-days: 1 retention-days: 1
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: Rust Cache
uses: swatinem/rust-cache@v2
- name: Build all release binaries
run: cargo build --release --locked
- name: Cache cargo-deb - name: Cache cargo-deb
id: cache-cargo-deb id: cache-cargo-deb
uses: actions/cache@v4 uses: actions/cache@v6
with: with:
path: ~/.cargo/bin/cargo-deb path: ~/.cargo/bin/cargo-deb
key: ${{ runner.os }}-${{ runner.arch }}-cargo-deb-v1 key: ${{ runner.os }}-${{ runner.arch }}-cargo-deb-v1
@@ -200,50 +136,32 @@ jobs:
if: steps.cache-cargo-deb.outputs.cache-hit != 'true' if: steps.cache-cargo-deb.outputs.cache-hit != 'true'
run: cargo install --locked cargo-deb run: cargo install --locked cargo-deb
- name: Create .deb package (release binaries) - name: Create .deb package
shell: bash shell: bash
run: | run: |
set -euo pipefail set -euo pipefail
export PATH="$HOME/.cargo/bin:$PATH" export PATH="$HOME/.cargo/bin:$PATH"
cargo-deb -p pwsp-gui --no-build cargo-deb -p pwsp-gui --no-build
- name: Upload deb package - name: Upload DEB artifact
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v7
with: with:
name: deb-package-${{ matrix.arch }} name: deb-package-${{ matrix.arch }}
path: target/debian/*.deb path: target/debian/*.deb
retention-days: 1 retention-days: 1
publish-release: publish-release:
needs: [prepare, linux-release, deb-release] needs: [prepare, build-release]
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Download zip archive (x64) - name: Download all artifacts
uses: actions/download-artifact@v4 uses: actions/download-artifact@v8
with: 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 path: ./dist
merge-multiple: true
- name: Upload artifacts to Release - name: Upload artifacts to Release
uses: softprops/action-gh-release@v2 uses: softprops/action-gh-release@v3
with: with:
token: ${{ secrets.GITHUB_TOKEN }} token: ${{ secrets.GITHUB_TOKEN }}
tag_name: ${{ needs.prepare.outputs.tag }} tag_name: ${{ needs.prepare.outputs.tag }}
@@ -251,6 +169,10 @@ jobs:
./dist/pwsp-*.zip ./dist/pwsp-*.zip
./dist/*.deb ./dist/*.deb
trigger-copr:
needs: prepare
runs-on: ubuntu-latest
steps:
- name: Install copr-cli - name: Install copr-cli
run: pip install copr-cli run: pip install copr-cli
@@ -265,3 +187,86 @@ jobs:
--commit ${{ needs.prepare.outputs.tag }} \ --commit ${{ needs.prepare.outputs.tag }} \
--spec packages/rpm/pwsp.spec \ --spec packages/rpm/pwsp.spec \
arabianq/pwsp arabianq/pwsp
build-apt-repo:
needs: [prepare, build-release]
runs-on: ubuntu-latest
concurrency: gh-pages-deploy
permissions:
contents: write
steps:
- name: Checkout code
uses: actions/checkout@v7
- name: Setup GPG
id: gpg
uses: crazy-max/ghaction-import-gpg@v7
with:
gpg_private_key: ${{ secrets.GPG_PRIVATE_KEY }}
passphrase: ${{ secrets.GPG_PASSPHRASE }}
- name: Download DEB artifacts
uses: actions/download-artifact@v8
with:
pattern: deb-package-*
path: debs
merge-multiple: true
- name: Checkout existing gh-pages
uses: actions/checkout@v7
with:
ref: gh-pages
path: gh-pages-branch
continue-on-error: true
- name: Generate APT Repository
run: |
sudo apt-get update
sudo apt-get install -y reprepro
mkdir -p apt-repo
# Restore existing repository first so we don't lose old packages
if [ -d "gh-pages-branch/apt" ]; then
cp -r gh-pages-branch/apt/* apt-repo/ || true
fi
# Always rewrite the config and public key to ensure they are up-to-date
mkdir -p apt-repo/conf
cat <<EOF > apt-repo/conf/distributions
Origin: arabianq
Label: PipeWire Soundpad
Codename: stable
Architectures: amd64 arm64
Components: main
Description: APT Repository for PWSP
SignWith: ${{ steps.gpg.outputs.fingerprint }}
Origin: arabianq
Label: PipeWire Soundpad
Codename: nightly
Architectures: amd64 arm64
Components: main
Description: Nightly APT Repository for PWSP
SignWith: ${{ steps.gpg.outputs.fingerprint }}
EOF
gpg --armor --export ${{ steps.gpg.outputs.fingerprint }} > apt-repo/pubkey.gpg
cd apt-repo
for deb in ../debs/*.deb; do
if [ -f "$deb" ]; then
echo "Adding $deb to APT repository..."
reprepro includedeb stable "$deb"
fi
done
- name: Deploy APT Repo to GitHub Pages
uses: peaceiris/actions-gh-pages@v4
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
publish_dir: ./apt-repo
destination_dir: apt
keep_files: true
Generated
+515 -670
View File
File diff suppressed because it is too large Load Diff
+9 -4
View File
@@ -8,7 +8,7 @@ members = [
resolver = "2" resolver = "2"
[workspace.package] [workspace.package]
version = "1.12.2" version = "1.14.0"
edition = "2024" edition = "2024"
authors = ["arabian"] authors = ["arabian"]
homepage = "https://pwsp.arabianq.ru" homepage = "https://pwsp.arabianq.ru"
@@ -52,7 +52,7 @@ rustix = { version = "1.1.4", features = ["process"] }
rust-i18n = "4.0.0" rust-i18n = "4.0.0"
sys-locale = "0.3.2" sys-locale = "0.3.2"
rodio = { git = "https://github.com/arabianq/rodio.git", rev = "4746803c2e5f67004264ac946708d47a9faf510c", default-features = false, features = [ rodio = { git = "https://github.com/arabianq/rodio.git", rev = "610955cf524467690cc9abe1a6277e8bffcadb71", default-features = false, features = [
"symphonia-all", "symphonia-all",
"symphonia-libopus", "symphonia-libopus",
"playback", "playback",
@@ -73,13 +73,18 @@ egui_extras = "0.35.0"
egui_material_icons = "0.7.0" egui_material_icons = "0.7.0"
egui_dnd = "0.16.0" egui_dnd = "0.16.0"
reqwest = "0.13.4" reqwest = { version = "0.13.4", default-features = false, features = ["native-tls"] }
percent-encoding = "2.3.2" percent-encoding = "2.3.2"
[profile.dev]
[profile.dev.package."*"]
opt-level = 3
debug = false
[profile.release] [profile.release]
strip = true strip = true
lto = true lto = true
codegen-units = 1 codegen-units = 1
opt-level = "z" opt-level = "z"
panic = "abort" panic = "abort"
+103 -72
View File
@@ -1,118 +1,149 @@
<div align="center"> # PipeWire Soundpad (PWSP) 🎵
<h1>🎵 PipeWire Soundpad (PWSP)</h1>
<p><b>A simple, modern, and powerful soundboard for Linux, written in Rust.</b></p>
<img src="pwsp-gui/assets/screenshot.png" alt="PWSP Screenshot" width="700"/>
</div>
## 🌟 Overview ![PWSP Screenshot](pwsp-gui/assets/screenshot.png)
**PipeWire Soundpad (PWSP)** is a graphical soundboard application that routes audio directly to your virtual microphone using **PipeWire**. It provides an intuitive interface for managing your audio collection, making it an ideal tool for gamers, streamers, and anyone looking to inject sound effects into voice chats on platforms like Discord, Zoom, or TeamSpeak.
## ✨ Key Features [🇷🇺 Читать на русском](README.ru.md) | [🇺🇸 Read in English](README.md)
* **🎙️ Virtual Microphone Output:** Seamlessly mixes your microphone input with sound effects by automatically managing PipeWire virtual devices.
* **🎵 Multi-Format Support:** Plays popular audio formats including `mp3`, `wav`, `ogg`, `flac`, `mp4`, and `aac`.
* **⚡ Global Hotkeys:** Trigger sounds instantly from anywhere, even when the app is running in the background.
* **📂 Smart Collection Management:** Drag-and-drop folders, quick search, and collapsible tracks to keep your library organized.
* **🎛️ Advanced Playback Controls:** Individual volume sliders, play/pause, position scrubbing, and concurrent multi-track playback.
* **🔌 Plug & Play:** Automatically detects when an input device is connected or disconnected and handles linking/unlinking on the fly.
* **🖥️ Modern GUI:** Clean, responsive, and lightweight interface powered by [egui](https://egui.rs/).
## ⚙️ Architecture [![GitHub Actions Build Status](https://img.shields.io/github/actions/workflow/status/arabianq/pipewire-soundpad/build.yml?branch=main&style=flat-square)](https://github.com/arabianq/pipewire-soundpad/actions)
PWSP is built with a client-server model to ensure stability and separation of concerns: [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg?style=flat-square)](https://opensource.org/licenses/MIT)
* **`pwsp-daemon`**: The background engine. It runs silently, managing PipeWire virtual devices, audio routing, and playback. [![GitHub Release](https://img.shields.io/github/v/release/arabianq/pipewire-soundpad?style=flat-square)](https://github.com/arabianq/pipewire-soundpad/releases/latest)
* **`pwsp-gui`**: The graphical interface. Communicates with the daemon via a Unix socket to control playback and settings. [![Platform](https://img.shields.io/badge/Platform-Linux%20%7C%20PipeWire-blue?style=flat-square)](https://pipewire.org/)
* **`pwsp-cli`**: The command-line tool. Perfect for scripting, hotkey binding, or quick terminal-based control.
**PipeWire Soundpad (PWSP)** is a modern, low-latency application that lets you play audio files directly through your microphone. Designed specifically for Linux, it leverages the power of PipeWire to achieve native integration without the need for Pulseaudio bridges or complex virtual sinks.
---
## ✨ Features
- **Native PipeWire Integration:** Direct communication with the PipeWire API for the lowest possible latency.
- **Modular Architecture:** Consists of a background `daemon`, a command-line interface (`cli`), and a graphical user interface (`gui`).
- **Modern GUI:** Built with `egui`, supporting both Wayland and X11 seamlessly.
- **Global Hotkeys:** Powered by `evdev`, allowing you to play sounds from anywhere.
- **Broad Audio Support:** Powered by `rodio` and `symphonia` to support a wide range of audio formats.
--- ---
## 🚀 Installation ## 🚀 Installation
We provide multiple ways to install PWSP, including stable releases and rolling "nightly" builds directly from the `main` branch.
### 📦 Flatpak (Recommended) ### 📦 Flatpak (Recommended)
Install PWSP via Flatpak from our custom repository:
Add our official OSTree repository and install the application:
```bash ```bash
flatpak remote-add --user --if-not-exists pwsp-repo https://arabianq.github.io/pipewire-soundpad/index.flatpakrepo # Add the repository
flatpak remote-add --if-not-exists pwsp https://arabianq.github.io/pipewire-soundpad/pwsp.flatpakrepo
# Install stable version # Install the Stable version
flatpak install --user arabianq-repo ru.arabianq.pwsp//stable flatpak install pwsp ru.arabianq.pwsp//stable
# Or install the nightly version (latest commit) # OR Install the Nightly version (rolling updates)
flatpak install --user arabianq-repo ru.arabianq.pwsp//nightly flatpak install pwsp ru.arabianq.pwsp//nightly
``` ```
### 🐧 Linux Packages ### 🟠 Debian / Ubuntu (APT Repository)
**Fedora (and derivatives):**
We maintain an official APT repository for seamless updates via `apt`:
```bash
# 1. Download the public GPG key
wget -O- https://arabianq.github.io/pipewire-soundpad/apt/pubkey.gpg | sudo gpg --dearmor -o /etc/apt/keyrings/pwsp.gpg
# 2. Add the repository (Choose STABLE or NIGHTLY)
# For Stable:
echo "deb [signed-by=/etc/apt/keyrings/pwsp.gpg] https://arabianq.github.io/pipewire-soundpad/apt/ stable main" | sudo tee /etc/apt/sources.list.d/pwsp.list
# For Nightly:
# echo "deb [signed-by=/etc/apt/keyrings/pwsp.gpg] https://arabianq.github.io/pipewire-soundpad/apt/ nightly main" | sudo tee /etc/apt/sources.list.d/pwsp.list
# 3. Update and install
sudo apt update
sudo apt install pwsp
```
### 🐧 Fedora / RHEL (COPR)
Available via the Fedora COPR repository:
```bash ```bash
sudo dnf copr enable arabianq/pwsp sudo dnf copr enable arabianq/pwsp
sudo dnf install pwsp sudo dnf install pwsp
``` ```
**Arch Linux (AUR):** ### ⚙️ Manual / Standalone
You can manually download `.deb` packages or standalone `.zip` binaries from the [Releases page](https://github.com/arabianq/pipewire-soundpad/releases).
### 🦀 Build from Source
Make sure you have Rust, Cargo, and the required dependencies (`libpipewire-0.3-dev`, `libclang-dev`, `libasound2-dev`, `libdbus-1-dev`) installed.
```bash ```bash
paru -S pwsp-bin # or 'pwsp' to build from source
```
**Debian / Ubuntu:**
Download pre-built `.deb` packages or standalone binaries from the [Releases page](https://github.com/arabianq/pipewire-soundpad/releases).
### 🦀 Cargo / Source Build
```bash
cargo install pwsp
# OR clone and build manually:
git clone https://github.com/arabianq/pipewire-soundpad.git git clone https://github.com/arabianq/pipewire-soundpad.git
cd pipewire-soundpad cd pipewire-soundpad
cargo build --release cargo build --release --locked
``` ```
*(Note: Requires Rust toolchain and PipeWire running on your system).*
The binaries will be located in `target/release/`.
--- ---
## 🎮 Usage ## 🎮 Usage
### 1. Start the Daemon ### 1. Start the Daemon
Before using the GUI or CLI, the daemon must be running in the background.
PWSP operates via a background daemon that handles the audio routing.
```bash ```bash
# Recommended: Start and enable via systemd (starts on login) # Run the daemon
systemctl --user enable --now pwsp-daemon pwsp-daemon
# Manual start (if not using systemd):
pwsp-daemon &
``` ```
### 2. Using the GUI _(Tip: If installed via package managers, a systemd user service is provided. You can enable it with `systemctl --user enable --now pwsp-daemon.service`)_
1. **Add Sounds:** Click the **"+"** button to add a directory containing your audio files.
2. **Select Mic:** Choose your physical microphone from the dropdown. PWSP will instantly create a virtual microphone combining your voice and the soundboard. ### 2. Launch the GUI
3. **Play:** Click any sound to play it, adjust its volume, or assign a hotkey for quick access.
Simply run the graphical interface to manage and play your sounds:
### 3. Using the CLI
Control the daemon directly from your terminal:
```bash ```bash
pwsp-cli action play /path/to/sound.mp3 pwsp-gui
pwsp-cli get volume ```
pwsp-cli set position 20
pwsp-cli --help # View all commands ### 3. Use the CLI
You can also interact with the daemon directly via the command line:
```bash
pwsp-cli play /path/to/sound.mp3
pwsp-cli stop
pwsp-cli status
``` ```
--- ---
## ⌨️ Shortcuts & Controls ## 📚 Documentation & DeepWiki
| Action | Keyboard | Mouse | For advanced configuration, troubleshooting, architecture details, and custom setups, please visit our official Wiki:
| :----------------------------------- | :--------------------- | :------------------- |
| **Play Track** (Stops others) | | `Left Click` | [![Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/arabianq/pipewire-soundpad)
| **Add Track** (Plays simultaneously) | | `Ctrl + Left Click` |
| **Replace Last Track** | | `Shift + Left Click` |
| **Pause / Resume** | `Space` | |
| **Stop All Tracks** | `Backspace` | |
| **Open / Close Settings** | `I` | |
| **Search** | `/` | |
--- ---
## 🤝 Contributing ## 🤝 Contributing
Contributions, issues, and feature requests are welcome! Feel free to check out the [issues page](https://github.com/arabianq/pipewire-soundpad/issues).
[![Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/arabianq/pipewire-soundpad) Contributions, issues, and feature requests are welcome!
## 📜 License 1. Fork the project.
This project is licensed under the [MIT License](LICENSE). 2. Create your feature branch (`git checkout -b feat/amazing-feature`).
3. Commit your changes (`git commit -m 'Add some amazing feature'`).
4. Push to the branch (`git push origin feat/amazing-feature`).
5. Open a Pull Request.
---
## 📝 License
Distributed under the MIT License. See `LICENSE` for more information.
_Built with ❤️ for the Linux community._
+149
View File
@@ -0,0 +1,149 @@
# PipeWire Soundpad (PWSP) 🎵
![PWSP Screenshot](pwsp-gui/assets/screenshot.png)
[🇷🇺 Читать на русском](README.ru.md) | [🇺🇸 Read in English](README.md)
[![GitHub Actions Build Status](https://img.shields.io/github/actions/workflow/status/arabianq/pipewire-soundpad/build.yml?branch=main&style=flat-square)](https://github.com/arabianq/pipewire-soundpad/actions)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg?style=flat-square)](https://opensource.org/licenses/MIT)
[![GitHub Release](https://img.shields.io/github/v/release/arabianq/pipewire-soundpad?style=flat-square)](https://github.com/arabianq/pipewire-soundpad/releases/latest)
[![Platform](https://img.shields.io/badge/Platform-Linux%20%7C%20PipeWire-blue?style=flat-square)](https://pipewire.org/)
**PipeWire Soundpad (PWSP)** — это современное приложение с низкой задержкой, которое позволяет воспроизводить аудиофайлы прямо в ваш микрофон. Разработано специально для Linux с использованием мощи PipeWire для достижения нативной интеграции без использования мостов Pulseaudio или сложных виртуальных устройств.
---
## ✨ Возможности
- **Нативная интеграция с PipeWire:** Прямое взаимодействие с PipeWire API для минимальной задержки.
- **Модульная архитектура:** Состоит из фонового демона (`daemon`), интерфейса командной строки (`cli`) и графического интерфейса (`gui`).
- **Современный GUI:** Построен на `egui`, плавно работает как на Wayland, так и на X11.
- **Глобальные горячие клавиши:** Работают через `evdev`, позволяя воспроизводить звуки из любого окна.
- **Широкая поддержка форматов:** Использует `rodio` и `symphonia` для поддержки большинства аудиоформатов.
---
## 🚀 Установка
Мы предоставляем несколько способов установки PWSP, включая стабильные релизы и постоянно обновляемые "nightly" сборки напрямую из ветки `main`.
### 📦 Flatpak (Рекомендуется)
Добавьте наш официальный OSTree репозиторий и установите приложение:
```bash
# Добавьте репозиторий
flatpak remote-add --if-not-exists pwsp https://arabianq.github.io/pipewire-soundpad/pwsp.flatpakrepo
# Установите стабильную (Stable) версию
flatpak install pwsp ru.arabianq.pwsp//stable
# ИЛИ установите Nightly версию (самые свежие обновления)
flatpak install pwsp ru.arabianq.pwsp//nightly
```
### 🟠 Debian / Ubuntu (APT Репозиторий)
Мы поддерживаем официальный APT-репозиторий для бесшовных обновлений через `apt`:
```bash
# 1. Скачайте публичный GPG ключ
wget -O- https://arabianq.github.io/pipewire-soundpad/apt/pubkey.gpg | sudo gpg --dearmor -o /etc/apt/keyrings/pwsp.gpg
# 2. Добавьте репозиторий (Выберите STABLE или NIGHTLY)
# Для Stable:
echo "deb [signed-by=/etc/apt/keyrings/pwsp.gpg] https://arabianq.github.io/pipewire-soundpad/apt/ stable main" | sudo tee /etc/apt/sources.list.d/pwsp.list
# Для Nightly:
# echo "deb [signed-by=/etc/apt/keyrings/pwsp.gpg] https://arabianq.github.io/pipewire-soundpad/apt/ nightly main" | sudo tee /etc/apt/sources.list.d/pwsp.list
# 3. Обновите индексы и установите
sudo apt update
sudo apt install pwsp
```
### 🐧 Fedora / RHEL (COPR)
Доступно через репозиторий Fedora COPR:
```bash
sudo dnf copr enable arabianq/pwsp
sudo dnf install pwsp
```
### ⚙️ Ручная установка
Вы можете вручную скачать пакеты `.deb` или готовые бинарники `.zip` на [странице релизов](https://github.com/arabianq/pipewire-soundpad/releases).
### 🦀 Сборка из исходников
Убедитесь, что у вас установлены Rust, Cargo и необходимые зависимости (`libpipewire-0.3-dev`, `libclang-dev`, `libasound2-dev`, `libdbus-1-dev`).
```bash
git clone https://github.com/arabianq/pipewire-soundpad.git
cd pipewire-soundpad
cargo build --release --locked
```
Собранные бинарники будут находиться в папке `target/release/`.
---
## 🎮 Использование
### 1. Запуск демона
PWSP работает через фоновый демон, который маршрутизирует аудио.
```bash
# Запуск демона
pwsp-daemon
```
_(Подсказка: Если вы установили программу через пакетный менеджер, она включает пользовательский systemd-сервис. Вы можете включить его командой: `systemctl --user enable --now pwsp-daemon.service`)_
### 2. Запуск GUI
Просто запустите графический интерфейс для управления и воспроизведения звуков:
```bash
pwsp-gui
```
### 3. Использование CLI
Вы также можете взаимодействовать с демоном напрямую через командную строку:
```bash
pwsp-cli play /path/to/sound.mp3
pwsp-cli stop
pwsp-cli status
```
---
## 📚 Документация и DeepWiki
Для детальной настройки, решения проблем, описания архитектуры и кастомных конфигураций, пожалуйста, посетите нашу официальную Wiki:
[![Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/arabianq/pipewire-soundpad)
---
## 🤝 Вклад в проект (Contributing)
Будем рады вашей помощи, баг-репортам и идеям!
1. Сделайте Fork проекта.
2. Создайте свою ветку (`git checkout -b feat/amazing-feature`).
3. Закоммитьте изменения (`git commit -m 'Add some amazing feature'`).
4. Запушьте ветку (`git push origin feat/amazing-feature`).
5. Откройте Pull Request.
---
## 📝 Лицензия
Распространяется под лицензией MIT. Подробнее см. в файле `LICENSE`.
_Сделано с ❤️ для сообщества Linux._
+4 -4
View File
@@ -1,6 +1,6 @@
pkgbase = pwsp-bin pkgbase = pwsp-bin
pkgdesc = Lets you play audio files through your microphone (Pre-built binaries) pkgdesc = Lets you play audio files through your microphone (Pre-built binaries)
pkgver = 1.12.2 pkgver = 1.14.0
pkgrel = 1 pkgrel = 1
url = https://github.com/arabianq/pipewire-soundpad url = https://github.com/arabianq/pipewire-soundpad
arch = x86_64 arch = x86_64
@@ -10,11 +10,11 @@ pkgbase = pwsp-bin
depends = alsa-lib depends = alsa-lib
provides = pwsp provides = pwsp
conflicts = pwsp conflicts = pwsp
source = pipewire-soundpad-1.12.2.tar.gz :: https://github.com/arabianq/pipewire-soundpad/archive/refs/tags/v1.12.2.tar.gz source = pipewire-soundpad-1.14.0.tar.gz :: https://github.com/arabianq/pipewire-soundpad/archive/refs/tags/v1.14.0.tar.gz
sha256sums = SKIP sha256sums = SKIP
source_x86_64 = pwsp-1.12.2-x86_64.zip :: https://github.com/arabianq/pipewire-soundpad/releases/download/v1.12.2/pwsp-v1.12.2-linux-x64.zip source_x86_64 = pwsp-1.14.0-x86_64.zip :: https://github.com/arabianq/pipewire-soundpad/releases/download/v1.14.0/pwsp-v1.14.0-linux-x64.zip
sha256sums_x86_64 = SKIP sha256sums_x86_64 = SKIP
source_aarch64 = pwsp-1.12.2-aarch64.zip :: https://github.com/arabianq/pipewire-soundpad/releases/download/v1.12.2/pwsp-v1.12.2-linux-arm64.zip source_aarch64 = pwsp-1.14.0-aarch64.zip :: https://github.com/arabianq/pipewire-soundpad/releases/download/v1.14.0/pwsp-v1.14.0-linux-arm64.zip
sha256sums_aarch64 = SKIP sha256sums_aarch64 = SKIP
pkgname = pwsp-bin pkgname = pwsp-bin
+3 -3
View File
@@ -1,13 +1,13 @@
# Maintainer: Alexander Tarasov <a.tevg@ya.ru> # Maintainer: Alexander Tarasov <a.tevg@ya.ru>
pkgname=pwsp-bin pkgname=pwsp-bin
_pkgname=pipewire-soundpad _pkgname=pipewire-soundpad
pkgver=1.12.2 pkgver=1.14.0
pkgrel=1 pkgrel=1
pkgdesc="Lets you play audio files through your microphone (Pre-built binaries)" pkgdesc="Lets you play audio files through your microphone (Pre-built binaries)"
arch=('x86_64' 'aarch64') arch=('x86_64' 'aarch64')
url="https://github.com/arabianq/pipewire-soundpad" url="https://github.com/arabianq/pipewire-soundpad"
license=('MIT') license=('MIT')
depends=('pipewire' 'alsa-lib') depends=('pipewire' 'alsa-lib' 'openssl')
provides=('pwsp') provides=('pwsp')
conflicts=('pwsp') conflicts=('pwsp')
@@ -28,7 +28,7 @@ package() {
install -Dm644 "$_srcsrc/pwsp-gui/assets/pwsp-gui.desktop" "${pkgdir}/usr/share/applications/pwsp-gui.desktop" install -Dm644 "$_srcsrc/pwsp-gui/assets/pwsp-gui.desktop" "${pkgdir}/usr/share/applications/pwsp-gui.desktop"
install -Dm644 "$_srcsrc/pwsp-gui/assets/icon.png" "${pkgdir}/usr/share/icons/hicolor/256x256/apps/pwsp.png" install -Dm644 "$_srcsrc/pwsp-gui/assets/icon.png" "${pkgdir}/usr/share/icons/hicolor/256x256/apps/pwsp.png"
install -Dm644 "$_srcsrc/pwsp-gui/assets/pwsp-daemon.service" "${pkgdir}/usr/lib/systemd/user/pwsp-daemon.service" install -Dm644 "$_srcsrc/pwsp-daemon/assets/pwsp-daemon.service" "${pkgdir}/usr/lib/systemd/user/pwsp-daemon.service"
install -Dm644 "$_srcsrc/LICENSE" "${pkgdir}/usr/share/licenses/${pkgname}/LICENSE" install -Dm644 "$_srcsrc/LICENSE" "${pkgdir}/usr/share/licenses/${pkgname}/LICENSE"
} }
+2 -2
View File
@@ -1,6 +1,6 @@
pkgbase = pwsp pkgbase = pwsp
pkgdesc = Lets you play audio files through your microphone pkgdesc = Lets you play audio files through your microphone
pkgver = 1.12.2 pkgver = 1.14.0
pkgrel = 1 pkgrel = 1
url = https://github.com/arabianq/pipewire-soundpad url = https://github.com/arabianq/pipewire-soundpad
arch = x86_64 arch = x86_64
@@ -12,7 +12,7 @@ pkgbase = pwsp
makedepends = cmake makedepends = cmake
makedepends = pipewire makedepends = pipewire
makedepends = alsa-lib makedepends = alsa-lib
source = https://github.com/arabianq/pipewire-soundpad/archive/refs/tags/v1.12.2.tar.gz source = https://github.com/arabianq/pipewire-soundpad/archive/refs/tags/v1.14.0.tar.gz
sha256sums = SKIP sha256sums = SKIP
pkgname = pwsp pkgname = pwsp
+3 -2
View File
@@ -1,12 +1,13 @@
# Maintainer: Alexander Tarasov <a.tevg@ya.ru> # Maintainer: Alexander Tarasov <a.tevg@ya.ru>
pkgsubn=pwsp pkgsubn=pwsp
pkgname=pwsp pkgname=pwsp
pkgver=1.12.2 pkgver=1.14.0
pkgrel=1 pkgrel=1
pkgdesc="Lets you play audio files through your microphone" pkgdesc="Lets you play audio files through your microphone"
arch=('x86_64' 'aarch64') arch=('x86_64' 'aarch64')
url="https://github.com/arabianq/pipewire-soundpad" url="https://github.com/arabianq/pipewire-soundpad"
license=('MIT') license=('MIT')
depends=('pipewire' 'alsa-lib' 'openssl')
makedepends=(clang rust cargo cmake pipewire alsa-lib) makedepends=(clang rust cargo cmake pipewire alsa-lib)
source=("$url/archive/refs/tags/v$pkgver.tar.gz") source=("$url/archive/refs/tags/v$pkgver.tar.gz")
sha256sums=('SKIP') sha256sums=('SKIP')
@@ -43,5 +44,5 @@ package() {
install -Dm644 "pwsp-gui/assets/pwsp-gui.desktop" "${pkgdir}/usr/share/applications/pwsp-gui.desktop" 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 "pwsp-gui/assets/icon.png" "${pkgdir}/usr/share/icons/hicolor/256x256/apps/pwsp.png"
install -Dm644 "pwsp-gui/assets/pwsp-daemon.service" "${pkgdir}/usr/lib/systemd/user/pwsp-daemon.service" install -Dm644 "pwsp-daemon/assets/pwsp-daemon.service" "${pkgdir}/usr/lib/systemd/user/pwsp-daemon.service"
} }
File diff suppressed because one or more lines are too long
@@ -25,7 +25,7 @@
<name>arabian</name> <name>arabian</name>
</developer> </developer>
<releases> <releases>
<release version="1.12.2" date="2026-07-02" /> <release version="1.14.0" date="2026-07-24" />
</releases> </releases>
<content_rating type="oars-1.1" /> <content_rating type="oars-1.1" />
</component> </component>
+1
View File
@@ -30,6 +30,7 @@ modules:
- name: pwsp - name: pwsp
buildsystem: simple buildsystem: simple
build-commands: build-commands:
- if [ -w /run/ccache ]; then mkdir -p /run/ccache/cargo-target && rm -rf target && ln -s /run/ccache/cargo-target target; fi
- export CARGO_HOME=$PWD/cargo && cargo build --release --offline - export CARGO_HOME=$PWD/cargo && cargo build --release --offline
- install -Dm755 target/release/pwsp-daemon /app/bin/pwsp-daemon - install -Dm755 target/release/pwsp-daemon /app/bin/pwsp-daemon
- install -Dm755 target/release/pwsp-cli /app/bin/pwsp-cli - install -Dm755 target/release/pwsp-cli /app/bin/pwsp-cli
+12 -1
View File
@@ -23,9 +23,11 @@ BuildRequires: pipewire-devel
%if 0%{?suse_version} %if 0%{?suse_version}
BuildRequires: alsa-devel BuildRequires: alsa-devel
BuildRequires: dbus-1-devel BuildRequires: dbus-1-devel
BuildRequires: libopenssl-devel
%else %else
BuildRequires: alsa-lib-devel BuildRequires: alsa-lib-devel
BuildRequires: dbus-devel BuildRequires: dbus-devel
BuildRequires: openssl-devel
%endif %endif
BuildRequires: clang-devel BuildRequires: clang-devel
BuildRequires: cmake BuildRequires: cmake
@@ -66,7 +68,16 @@ install -Dm755 target/release/pwsp-gui %{buildroot}%{_bindir}/pwsp-gui
install -Dm644 pwsp-gui/assets/pwsp-gui.desktop %{buildroot}%{_datadir}/applications/pwsp.desktop 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 pwsp-gui/assets/icon.png %{buildroot}%{_datadir}/icons/hicolor/256x256/apps/pwsp.png
install -Dm644 pwsp-gui/assets/pwsp-daemon.service %{buildroot}/usr/lib/systemd/user/pwsp-daemon.service install -Dm644 pwsp-daemon/assets/pwsp-daemon.service %{buildroot}/usr/lib/systemd/user/pwsp-daemon.service
%post
%systemd_user_post pwsp-daemon.service
%preun
%systemd_user_preun pwsp-daemon.service
%postun
%systemd_user_postun_with_restart pwsp-daemon.service
%files %files
%license LICENSE %license LICENSE
+13 -2
View File
@@ -13,7 +13,7 @@
Name: pwsp Name: pwsp
Version: 1.12.2 Version: 1.14.0
Release: %autorelease Release: %autorelease
Summary: Lets you play audio files through your microphone Summary: Lets you play audio files through your microphone
@@ -28,9 +28,11 @@ BuildRequires: pipewire-devel
%if 0%{?suse_version} %if 0%{?suse_version}
BuildRequires: alsa-devel BuildRequires: alsa-devel
BuildRequires: dbus-1-devel BuildRequires: dbus-1-devel
BuildRequires: libopenssl-devel
%else %else
BuildRequires: alsa-lib-devel BuildRequires: alsa-lib-devel
BuildRequires: dbus-devel BuildRequires: dbus-devel
BuildRequires: openssl-devel
%endif %endif
BuildRequires: clang-devel BuildRequires: clang-devel
BuildRequires: cmake BuildRequires: cmake
@@ -66,7 +68,16 @@ install -Dm755 target/release/pwsp-gui %{buildroot}%{_bindir}/pwsp-gui
install -Dm644 pwsp-gui/assets/pwsp-gui.desktop %{buildroot}%{_datadir}/applications/pwsp.desktop 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 pwsp-gui/assets/icon.png %{buildroot}%{_datadir}/icons/hicolor/256x256/apps/pwsp.png
install -Dm644 pwsp-gui/assets/pwsp-daemon.service %{buildroot}/usr/lib/systemd/user/pwsp-daemon.service install -Dm644 pwsp-daemon/assets/pwsp-daemon.service %{buildroot}/usr/lib/systemd/user/pwsp-daemon.service
%post
%systemd_user_post pwsp-daemon.service
%preun
%systemd_user_preun pwsp-daemon.service
%postun
%systemd_user_postun_with_restart pwsp-daemon.service
%files %files
%license LICENSE %license LICENSE
+12
View File
@@ -69,6 +69,8 @@ enum Actions {
#[clap(short, long)] #[clap(short, long)]
id: Option<u32>, id: Option<u32>,
}, },
/// Daemon configuration
SaveDaemonConfig,
/// Play a sound by hotkey slot name /// Play a sound by hotkey slot name
PlayHotkey { slot: String }, PlayHotkey { slot: String },
/// Remove the hotkey slot /// Remove the hotkey slot
@@ -86,6 +88,8 @@ enum GetCommands {
#[clap(short, long)] #[clap(short, long)]
id: Option<u32>, id: Option<u32>,
}, },
/// Volume multiplier for all tracks
VolumeMultiplier,
/// Playback position (in seconds) /// Playback position (in seconds)
Position { Position {
#[clap(short, long)] #[clap(short, long)]
@@ -106,6 +110,8 @@ enum GetCommands {
Inputs, Inputs,
/// Version of the daemon /// Version of the daemon
DaemonVersion, DaemonVersion,
/// Daemon configuration
DaemonConfig,
/// Full player state /// Full player state
FullState, FullState,
/// All hotkey slots /// All hotkey slots
@@ -120,6 +126,8 @@ enum SetCommands {
#[clap(short, long)] #[clap(short, long)]
id: Option<u32>, id: Option<u32>,
}, },
/// Volume multiplier for all tracks
VolumeMultiplier { volume: f32 },
/// Playback position (in seconds) /// Playback position (in seconds)
Position { Position {
position: f32, position: f32,
@@ -165,6 +173,7 @@ async fn main() -> Result<()> {
concurrent, concurrent,
} => Request::play(&file_path.to_string_lossy(), concurrent), } => Request::play(&file_path.to_string_lossy(), concurrent),
Actions::ToggleLoop { id } => Request::toggle_loop(id), Actions::ToggleLoop { id } => Request::toggle_loop(id),
Actions::SaveDaemonConfig => Request::save_daemon_config(),
Actions::PlayHotkey { slot } => Request::play_hotkey(&slot), Actions::PlayHotkey { slot } => Request::play_hotkey(&slot),
Actions::ClearHotkey { slot } => Request::clear_hotkey(&slot), Actions::ClearHotkey { slot } => Request::clear_hotkey(&slot),
Actions::ClearHotkeyKey { slot } => Request::clear_hotkey_key(&slot), Actions::ClearHotkeyKey { slot } => Request::clear_hotkey_key(&slot),
@@ -172,6 +181,7 @@ async fn main() -> Result<()> {
Commands::Get { parameter } => match parameter { Commands::Get { parameter } => match parameter {
GetCommands::IsPaused => Request::get_is_paused(), GetCommands::IsPaused => Request::get_is_paused(),
GetCommands::Volume { id } => Request::get_volume(id), GetCommands::Volume { id } => Request::get_volume(id),
GetCommands::VolumeMultiplier => Request::get_volume_multiplier(),
GetCommands::Position { id } => Request::get_position(id), GetCommands::Position { id } => Request::get_position(id),
GetCommands::Duration { id } => Request::get_duration(id), GetCommands::Duration { id } => Request::get_duration(id),
GetCommands::State => Request::get_state(), GetCommands::State => Request::get_state(),
@@ -179,11 +189,13 @@ async fn main() -> Result<()> {
GetCommands::Input => Request::get_input(), GetCommands::Input => Request::get_input(),
GetCommands::Inputs => Request::get_inputs(), GetCommands::Inputs => Request::get_inputs(),
GetCommands::DaemonVersion => Request::get_daemon_version(), GetCommands::DaemonVersion => Request::get_daemon_version(),
GetCommands::DaemonConfig => Request::get_daemon_config(),
GetCommands::FullState => Request::get_full_state(), GetCommands::FullState => Request::get_full_state(),
GetCommands::Hotkeys => Request::get_hotkeys(), GetCommands::Hotkeys => Request::get_hotkeys(),
}, },
Commands::Set { parameter } => match parameter { Commands::Set { parameter } => match parameter {
SetCommands::Volume { volume, id } => Request::set_volume(volume, id), SetCommands::Volume { volume, id } => Request::set_volume(volume, id),
SetCommands::VolumeMultiplier { volume } => Request::set_volume_multiplier(volume),
SetCommands::Position { position, id } => Request::seek(position, id), SetCommands::Position { position, id } => Request::seek(position, id),
SetCommands::Input { name } => Request::set_input(&name), SetCommands::Input { name } => Request::set_input(&name),
SetCommands::Loop { enabled, id } => Request::set_loop(&enabled, id), SetCommands::Loop { enabled, id } => Request::set_loop(&enabled, id),
+16
View File
@@ -0,0 +1,16 @@
[Unit]
Description=Pipewire Soundpad Daemon
Requires=pipewire.service
After=pipewire.service
PartOf=graphical-session.target
[Service]
Type=simple
ExecStart=/usr/bin/pwsp-daemon
Restart=on-failure
RestartSec=3
RuntimeDirectory=pwsp
SyslogIdentifier=pwsp-daemon
[Install]
WantedBy=default.target
+6 -1
View File
@@ -20,6 +20,7 @@ anyhow.workspace = true
serde.workspace = true serde.workspace = true
serde_json.workspace = true serde_json.workspace = true
dirs.workspace = true
egui.workspace = true egui.workspace = true
eframe.workspace = true eframe.workspace = true
@@ -35,6 +36,10 @@ reqwest.workspace = true
percent-encoding.workspace = true percent-encoding.workspace = true
[package.metadata.deb] [package.metadata.deb]
name = "pwsp"
section = "sound"
priority = "optional"
maintainer-scripts = "maintainer-scripts"
assets = [ assets = [
[ [
"target/release/pwsp-daemon", "target/release/pwsp-daemon",
@@ -62,7 +67,7 @@ assets = [
"644", "644",
], ],
[ [
"assets/pwsp-daemon.service", "../pwsp-daemon/assets/pwsp-daemon.service",
"usr/lib/systemd/user/pwsp-daemon.service", "usr/lib/systemd/user/pwsp-daemon.service",
"644", "644",
], ],
-11
View File
@@ -1,11 +0,0 @@
[Unit]
Description=Pipewire Soundpad Daemon
After=pipewire.service
[Service]
ExecStart=/usr/bin/pwsp-daemon
Restart=no
RuntimeDirectory=pwsp
[Install]
WantedBy=default.target
+33
View File
@@ -217,6 +217,28 @@ kz = "Әрқашан дыбыс деңгейін есте сақтау"
he = "זכור תמיד עוצמת קול" he = "זכור תמיד עוצמת קול"
pt-BR = "Lembrar volume" pt-BR = "Lembrar volume"
[gui.settings.remember_volume_multiplier]
en = "Always remember volume multiplier"
ru = "Всегда запоминать множитель громкости"
es = "Recordar siempre el multiplicador de volumen"
fr = "Toujours se souvenir du multiplicateur de volume"
zh = "始终记住音量乘数"
ar = "تذكر ضرب الصوت دائمًا"
kz = "Әрқашан дыбыс деңгейін есте сақтау"
he = "זכור תמיד עוצמת קול"
pt-BR = "Lembrar volume"
[gui.settings.volume_multiplier]
en = "Volume multiplier"
ru = "Множитель громкости"
es = "Multiplicador de volumen"
fr = "Multiplicateur de volume"
zh = "音量乘数"
ar = "ضرب الصوت"
kz = "Дыбыс деңгейі"
he = "עוצמת קול"
pt-BR = "Volume"
[gui.settings.remember_mic] [gui.settings.remember_mic]
en = "Always remember microphone" en = "Always remember microphone"
ru = "Всегда запоминать микрофон" ru = "Всегда запоминать микрофон"
@@ -331,6 +353,17 @@ kz = "Ыстық пернелерді іздеу..."
he = "חפש מקשי קיצור..." he = "חפש מקשי קיצור..."
pt-BR = "Buscar atalhos..." pt-BR = "Buscar atalhos..."
[gui.search_truncated_warning]
en = "... and %{count} more results"
ru = "... и ещё %{count} результатов"
es = "... y %{count} resultados más"
fr = "... et %{count} résultats de plus"
zh = "... 还有 %{count} 个结果"
ar = "... و %{count} نتائج أخرى"
kz = "... және тағы %{count} нәтиже"
he = "... ועוד %{count} תוצאות"
pt-BR = "... e mais %{count} resultados"
[gui.hotkeys.add_command_select] [gui.hotkeys.add_command_select]
en = "Add Command" en = "Add Command"
ru = "Добавить команду" ru = "Добавить команду"
+21
View File
@@ -0,0 +1,21 @@
#!/bin/sh
set -e
if [ "$1" = "configure" ]; then
if [ -x "/usr/bin/systemctl" ]; then
systemctl --global enable pwsp-daemon.service || true
for uid_dir in /run/user/*; do
if [ -d "$uid_dir/systemd" ]; then
uid=$(basename "$uid_dir")
if [ "$uid" != "0" ]; then
username=$(id -un "$uid" 2>/dev/null) || continue
su -s /bin/sh -c "XDG_RUNTIME_DIR=$uid_dir systemctl --user daemon-reload" "$username" || true
su -s /bin/sh -c "XDG_RUNTIME_DIR=$uid_dir systemctl --user try-restart pwsp-daemon.service" "$username" || true
fi
fi
done
fi
fi
exit 0
+18
View File
@@ -0,0 +1,18 @@
#!/bin/sh
set -e
if [ "$1" = "remove" ]; then
if [ -x "/usr/bin/systemctl" ]; then
for uid_dir in /run/user/*; do
if [ -d "$uid_dir/systemd" ]; then
uid=$(basename "$uid_dir")
if [ "$uid" != "0" ]; then
username=$(id -un "$uid" 2>/dev/null) || continue
su -s /bin/sh -c "XDG_RUNTIME_DIR=$uid_dir systemctl --user daemon-reload" "$username" || true
fi
fi
done
fi
fi
exit 0
+20
View File
@@ -0,0 +1,20 @@
#!/bin/sh
set -e
if [ "$1" = "remove" ] || [ "$1" = "deconfigure" ]; then
if [ -x "/usr/bin/systemctl" ]; then
systemctl --global disable pwsp-daemon.service || true
for uid_dir in /run/user/*; do
if [ -d "$uid_dir/systemd" ]; then
uid=$(basename "$uid_dir")
if [ "$uid" != "0" ]; then
username=$(id -un "$uid" 2>/dev/null) || continue
su -s /bin/sh -c "XDG_RUNTIME_DIR=$uid_dir systemctl --user stop pwsp-daemon.service" "$username" || true
fi
fi
done
fi
fi
exit 0
+7 -6
View File
@@ -92,7 +92,7 @@ impl SoundpadGui {
} }
pub fn handle_input(&mut self, ctx: &Context) { pub fn handle_input(&mut self, ctx: &Context) {
let _modifiers = self.modifiers(ctx); let modifiers = self.modifiers(ctx);
let search_focused = { let search_focused = {
if let Some(focused_id) = self.get_focused(ctx) if let Some(focused_id) = self.get_focused(ctx)
&& let Some(search_id) = self.app_state.search_field_id && let Some(search_id) = self.app_state.search_field_id
@@ -185,14 +185,15 @@ impl SoundpadGui {
} }
// Focus search field // Focus search field
if self.key_pressed(ctx, Key::Slash) { if modifiers.ctrl && self.key_pressed(ctx, Key::F) {
if search_focused { self.app_state.force_focus_search = true;
}
// Unfocus search field
if search_focused && self.key_pressed(ctx, Key::Escape) {
ctx.memory_mut(|m| { ctx.memory_mut(|m| {
m.request_focus(Id::NULL); m.request_focus(Id::NULL);
}); });
} else {
self.app_state.force_focus_search = true;
}
} }
// Check for hotkey chord triggers // Check for hotkey chord triggers
+162 -30
View File
@@ -9,22 +9,24 @@ use itertools::Itertools;
use pwsp_lib::{ use pwsp_lib::{
types::{ types::{
audio_player::PlayerState, audio_player::PlayerState,
config::GuiConfig, config::{GuiConfig, HotkeyConfig},
config::HotkeyConfig,
gui::{AppState, AudioPlayerState}, gui::{AppState, AudioPlayerState},
socket::Request, socket::Request,
}, },
utils::{ utils::gui::{
daemon::get_daemon_config, get_daemon_config, get_gui_config, make_request_async, make_request_sync,
gui::{get_gui_config, make_request_async, make_request_sync, start_app_state_thread}, start_app_state_thread, update_daemon_config,
}, },
}; };
use rfd::FileDialog; use rfd::FileDialog;
use std::{ use std::{
cmp::Ordering, cmp::Ordering,
collections::{HashMap, HashSet},
fs, fs,
hash::{DefaultHasher, Hash, Hasher},
path::{Path, PathBuf}, path::{Path, PathBuf},
sync::{Arc, Mutex}, sync::{Arc, Mutex},
thread,
}; };
use system_fonts::{FontStyle, FoundFontSource, find_for_locale}; use system_fonts::{FontStyle, FoundFontSource, find_for_locale};
@@ -32,6 +34,16 @@ const SUPPORTED_EXTENSIONS: [&str; 13] = [
"mp3", "wav", "ogg", "flac", "mp4", "m4a", "aac", "mov", "mkv", "mka", "webm", "avi", "opus", "mp3", "wav", "ogg", "flac", "mp4", "m4a", "aac", "mov", "mkv", "mka", "webm", "avi", "opus",
]; ];
fn get_cache_file_for(path: &Path) -> PathBuf {
let mut hasher = DefaultHasher::new();
path.hash(&mut hasher);
let hash = hasher.finish();
dirs::cache_dir()
.unwrap_or_else(|| PathBuf::from("/tmp"))
.join("pwsp")
.join(format!("{:x}.json", hash))
}
struct SoundpadGui { struct SoundpadGui {
pub app_state: AppState, pub app_state: AppState,
pub config: GuiConfig, pub config: GuiConfig,
@@ -107,20 +119,120 @@ impl SoundpadGui {
} }
} }
pub fn open_dir(&mut self, path: &PathBuf) { pub fn open_dir(&mut self, path: &PathBuf, ctx: Option<Context>, force_rescan: bool) {
self.app_state.current_dir = Some(path.clone()); self.app_state.current_dir = Some(path.clone());
match path.read_dir() {
if !self.app_state.dir_cache.contains_key(path) || force_rescan {
match fs::read_dir(path) {
Ok(read_dir) => { Ok(read_dir) => {
self.app_state.listed_files = read_dir let files = read_dir
.filter_map(|res| res.ok()) .filter_map(|res| res.ok())
.map(|entry| entry.path()) .map(|entry| entry.path())
.collect(); .collect::<Vec<_>>();
self.app_state.listed_files = files.iter().cloned().collect();
self.app_state.dir_cache.insert(path.clone(), files);
} }
Err(e) => { Err(e) => {
eprintln!("Failed to read directory {:?}: {}", path, e); eprintln!("Failed to read directory {:?}: {}", path, e);
self.app_state.listed_files.clear(); self.app_state.listed_files.clear();
} }
} }
} else {
self.app_state.listed_files = self
.app_state
.dir_cache
.get(path)
.unwrap()
.clone()
.into_iter()
.collect();
}
let is_scanning = self.app_state.scanning_dirs.lock().unwrap().contains(path);
let is_cached = self.app_state.recursive_files_cache.contains_key(path);
let has_scanned_this_session = self.app_state.scanned_this_session.contains(path);
let needs_scan = force_rescan || !has_scanned_this_session || !is_cached;
if !is_scanning && needs_scan {
self.app_state
.scanning_dirs
.lock()
.unwrap()
.insert(path.clone());
self.app_state.scanned_this_session.insert(path.clone());
let finished_scans = self.app_state.finished_scans.clone();
let scanning_dirs = self.app_state.scanning_dirs.clone();
let path_clone = path.clone();
thread::spawn(move || {
let cache_file = get_cache_file_for(&path_clone);
// 1. Try to load from disk cache if we don't have it in memory yet
if !is_cached
&& let Ok(data) = fs::read_to_string(&cache_file)
&& let Ok((all_files, dir_updates)) = serde_json::from_str::<(
Vec<PathBuf>,
HashMap<PathBuf, Vec<PathBuf>>,
)>(&data)
{
finished_scans.lock().unwrap().push((
path_clone.clone(),
all_files,
dir_updates,
));
if let Some(ctx) = ctx.as_ref() {
ctx.request_repaint();
}
}
// 2. Scan recursively
let mut all_files = Vec::new();
let mut dir_updates = HashMap::new();
let mut dirs_to_visit = vec![path_clone.clone()];
while let Some(dir) = dirs_to_visit.pop() {
if let Ok(entries) = fs::read_dir(&dir) {
let mut children = Vec::new();
for entry in entries.filter_map(|e| e.ok()) {
let p = entry.path();
if p.is_dir() {
dirs_to_visit.push(p.clone());
children.push(p);
} else if crate::gui::SUPPORTED_EXTENSIONS.contains(
&p.extension()
.unwrap_or_default()
.to_str()
.unwrap_or_default(),
) {
all_files.push(p.clone());
children.push(p);
}
}
dir_updates.insert(dir, children);
}
}
// 3. Save to disk cache
if let Some(parent) = cache_file.parent() {
fs::create_dir_all(parent).ok();
}
if let Ok(json) = serde_json::to_string(&(all_files.clone(), dir_updates.clone())) {
fs::write(&cache_file, json).ok();
}
// 4. Send to UI
finished_scans
.lock()
.unwrap()
.push((path_clone.clone(), all_files, dir_updates));
scanning_dirs.lock().unwrap().remove(&path_clone);
if let Some(ctx) = ctx {
ctx.request_repaint();
}
});
}
} }
pub fn play_file(&mut self, path: &Path, concurrent: bool) { pub fn play_file(&mut self, path: &Path, concurrent: bool) {
@@ -130,10 +242,11 @@ impl SoundpadGui {
pub fn set_input(&mut self, name: String) { pub fn set_input(&mut self, name: String) {
make_request_async(Request::set_input(&name)); make_request_async(Request::set_input(&name));
if self.config.save_input { if self.config.save_input
let mut daemon_config = get_daemon_config(); && let Ok(mut daemon_config) = get_daemon_config()
{
daemon_config.default_input_name = Some(name); daemon_config.default_input_name = Some(name);
daemon_config.save_to_file().ok(); update_daemon_config(&daemon_config).ok();
} }
} }
@@ -157,7 +270,14 @@ impl SoundpadGui {
make_request_async(Request::play_hotkey(slot)); make_request_async(Request::play_hotkey(slot));
} }
pub fn get_filtered_files(&self) -> Vec<PathBuf> { pub fn get_filtered_files(
&self,
matching_dirs: &HashSet<PathBuf>,
matching_files: &HashSet<PathBuf>,
) -> Vec<PathBuf> {
let search_query = self.app_state.search_query.to_lowercase();
let search_query = search_query.trim();
let mut files: Vec<PathBuf> = self.app_state.listed_files.iter().cloned().collect(); let mut files: Vec<PathBuf> = self.app_state.listed_files.iter().cloned().collect();
let sort_order = self let sort_order = self
.app_state .app_state
@@ -178,33 +298,42 @@ impl SoundpadGui {
} }
}); });
let search_query = self.app_state.search_query.to_lowercase(); let is_cached = self
let search_query = search_query.trim(); .app_state
.current_dir
.as_ref()
.is_some_and(|d| self.app_state.recursive_files_cache.contains_key(d));
if !search_query.is_empty() && is_cached {
files
.into_iter()
.filter(|p| {
if p.is_dir() {
matching_dirs.contains(p)
} else {
matching_files.contains(p)
}
})
.collect()
} else {
files files
.into_iter() .into_iter()
.filter(|entry_path| { .filter(|entry_path| {
if entry_path.is_dir() { if !entry_path.is_dir()
return true; && !SUPPORTED_EXTENSIONS.contains(
}
if !SUPPORTED_EXTENSIONS.contains(
&entry_path &entry_path
.extension() .extension()
.unwrap_or_default() .unwrap_or_default()
.to_str() .to_str()
.unwrap_or_default(), .unwrap_or_default(),
) { )
{
return false; return false;
} }
if !search_query.is_empty() { if !search_query.is_empty() {
let file_name = entry_path let file_name =
.file_name() entry_path.file_name().unwrap_or_default().to_string_lossy();
.unwrap_or_default()
.to_string_lossy()
.to_string();
if !file_name.to_lowercase().contains(search_query) { if !file_name.to_lowercase().contains(search_query) {
return false; return false;
} }
@@ -215,6 +344,7 @@ impl SoundpadGui {
.collect() .collect()
} }
} }
}
fn add_font(font_name: &str, font_bytes: &[u8], fonts: &mut FontDefinitions) -> Result<()> { fn add_font(font_name: &str, font_bytes: &[u8], fonts: &mut FontDefinitions) -> Result<()> {
let font_data = FontData::from_owned(font_bytes.to_vec()).tweak(FontTweak { let font_data = FontData::from_owned(font_bytes.to_vec()).tweak(FontTweak {
@@ -329,14 +459,16 @@ mod tests {
// On the real OS filesystem, these paths don't exist, so they are treated as files. // On the real OS filesystem, these paths don't exist, so they are treated as files.
// Unsupported extensions (like .txt) will be filtered out. // Unsupported extensions (like .txt) will be filtered out.
// So we expect only file_b and file_c, sorted alphabetically. // So we expect only file_b and file_c, sorted alphabetically.
let filtered = gui.get_filtered_files(); let empty_dirs = HashSet::new();
let empty_files = HashSet::new();
let filtered = gui.get_filtered_files(&empty_dirs, &empty_files);
assert_eq!(filtered.len(), 2); assert_eq!(filtered.len(), 2);
assert_eq!(filtered[0], file_b); assert_eq!(filtered[0], file_b);
assert_eq!(filtered[1], file_c); assert_eq!(filtered[1], file_c);
// Test search query // Test search query
gui.app_state.search_query = "c_fi".to_string(); gui.app_state.search_query = "c_fi".to_string();
let filtered_search = gui.get_filtered_files(); let filtered_search = gui.get_filtered_files(&empty_dirs, &empty_files);
assert_eq!(filtered_search.len(), 1); assert_eq!(filtered_search.len(), 1);
assert_eq!(filtered_search[0], file_c); assert_eq!(filtered_search[0], file_c);
@@ -349,7 +481,7 @@ mod tests {
}, },
); );
gui.app_state.search_query = String::new(); gui.app_state.search_query = String::new();
let filtered_desc = gui.get_filtered_files(); let filtered_desc = gui.get_filtered_files(&empty_dirs, &empty_files);
assert_eq!(filtered_desc.len(), 2); assert_eq!(filtered_desc.len(), 2);
assert_eq!(filtered_desc[0], file_c); assert_eq!(filtered_desc[0], file_c);
assert_eq!(filtered_desc[1], file_b); assert_eq!(filtered_desc[1], file_b);
+23 -4
View File
@@ -3,7 +3,7 @@ use eframe::{App, Frame as EFrame};
use egui::{CentralPanel, Context, ThemePreference}; use egui::{CentralPanel, Context, ThemePreference};
use pwsp_lib::{ use pwsp_lib::{
types::{config::PreferredTheme, socket::Request}, types::{config::PreferredTheme, socket::Request},
utils::{daemon::get_daemon_config, gui::make_request_async}, utils::gui::{get_daemon_config, make_request_async, update_daemon_config},
}; };
use std::time::{Duration, Instant}; use std::time::{Duration, Instant};
@@ -85,10 +85,29 @@ impl App for SoundpadGui {
self.app_state.ignore_volume_update_until = self.app_state.ignore_volume_update_until =
Some(Instant::now() + Duration::from_millis(300)); Some(Instant::now() + Duration::from_millis(300));
if self.config.save_volume { if self.config.save_volume
let mut daemon_config = get_daemon_config(); && let Ok(mut daemon_config) = get_daemon_config()
{
daemon_config.default_volume = Some(self.app_state.volume_slider_value); daemon_config.default_volume = Some(self.app_state.volume_slider_value);
daemon_config.save_to_file().ok(); update_daemon_config(&daemon_config).ok();
}
}
if self.app_state.volume_multiplier_dragged {
make_request_async(Request::set_volume_multiplier(
self.app_state.volume_multiplier_slider_value,
));
self.app_state.volume_multiplier_dragged = false;
self.app_state.ignore_volume_multiplier_update_until =
Some(Instant::now() + Duration::from_millis(300));
if self.config.save_volume_multiplier
&& let Ok(mut daemon_config) = get_daemon_config()
{
daemon_config.default_volume_multiplier =
Some(self.app_state.volume_multiplier_slider_value);
update_daemon_config(&daemon_config).ok();
} }
} }
+183 -17
View File
@@ -10,7 +10,7 @@ use pwsp_lib::types::{
gui::{AppState, AudioPlayerState}, gui::{AppState, AudioPlayerState},
}; };
use rust_i18n::t; use rust_i18n::t;
use std::{cmp::Ordering, path::Path, path::PathBuf}; use std::{cmp::Ordering, collections::HashSet, path::Path, path::PathBuf};
pub(crate) enum FileAction { pub(crate) enum FileAction {
Play(PathBuf, bool), Play(PathBuf, bool),
@@ -195,7 +195,7 @@ impl SoundpadGui {
.sort_order = order; .sort_order = order;
self.config.save_to_file().ok(); self.config.save_to_file().ok();
self.app_state.dir_cache.remove(path); self.app_state.dir_cache.remove(path);
self.open_dir(path); self.open_dir(path, Some(ui.ctx().clone()), false);
} }
}); });
}); });
@@ -203,7 +203,7 @@ impl SoundpadGui {
self.app_state.dirs = dirs; self.app_state.dirs = dirs;
if let Some(path) = dir_to_open { if let Some(path) = dir_to_open {
self.open_dir(&path); self.open_dir(&path, Some(ui.ctx().clone()), false);
} }
ui.horizontal(|ui| { ui.horizontal(|ui| {
@@ -227,12 +227,41 @@ impl SoundpadGui {
fn draw_files_search_field(&mut self, ui: &mut Ui) { fn draw_files_search_field(&mut self, ui: &mut Ui) {
ui.horizontal(|ui| { ui.horizontal(|ui| {
let is_scanning = self
.app_state
.current_dir
.as_ref()
.is_some_and(|d| self.app_state.scanning_dirs.lock().unwrap().contains(d));
let spinner_width = if is_scanning { 20.0 } else { 0.0 };
let refresh_button = Button::new(ICON_REFRESH).frame(false);
let refresh_width = 24.0;
let search_field_response = ui.add_sized( let search_field_response = ui.add_sized(
[ui.available_width(), 22.0], [
f32::max(
0.0,
ui.available_width() - spinner_width - refresh_width - 8.0,
),
22.0,
],
TextEdit::singleline(&mut self.app_state.search_query) TextEdit::singleline(&mut self.app_state.search_query)
.hint_text(t!("gui.search_placeholder")), .hint_text(t!("gui.search_placeholder")),
); );
if is_scanning {
ui.spinner();
}
let refresh_response = ui
.add_sized([refresh_width, 22.0], refresh_button)
.on_hover_text("Refresh");
if refresh_response.clicked()
&& let Some(dir) = self.app_state.current_dir.clone()
{
self.open_dir(&dir, Some(ui.ctx().clone()), true);
}
if self.app_state.force_focus_search { if self.app_state.force_focus_search {
search_field_response.request_focus(); search_field_response.request_focus();
self.app_state.force_focus_search = false; self.app_state.force_focus_search = false;
@@ -247,9 +276,54 @@ impl SoundpadGui {
ui.set_min_width(area_size.x); ui.set_min_width(area_size.x);
ui.set_min_height(area_size.y); ui.set_min_height(area_size.y);
let finished = {
let mut scans = self.app_state.finished_scans.lock().unwrap();
std::mem::take(&mut *scans)
};
for (top_dir, all_files, dir_updates) in finished {
self.app_state
.recursive_files_cache
.insert(top_dir, all_files);
self.app_state.dir_cache.extend(dir_updates);
}
ui.vertical(|ui| { ui.vertical(|ui| {
let mut actions = Vec::new(); let mut actions = Vec::new();
let files = self.get_filtered_files();
let search_query = self.app_state.search_query.to_lowercase();
let search_query = search_query.trim();
let mut matching_dirs = std::collections::HashSet::new();
let mut matching_files = std::collections::HashSet::new();
if !search_query.is_empty()
&& let Some(current_dir) = &self.app_state.current_dir
&& let Some(all_files) = self.app_state.recursive_files_cache.get(current_dir)
{
for file in all_files {
if let Ok(rel_path) = file.strip_prefix(current_dir) {
let rel_path_str =
rel_path.to_string_lossy().to_lowercase().replace("\\", "/");
if rel_path_str.contains(search_query) {
matching_files.insert(file.clone());
let mut p = file.parent();
while let Some(parent) = p {
if parent == current_dir {
break;
}
matching_dirs.insert(parent.to_path_buf());
p = parent.parent();
}
}
}
}
}
let max_results = 300;
let is_truncated = !search_query.is_empty() && matching_files.len() > max_results;
let files = self.get_filtered_files(&matching_dirs, &matching_files);
let mut rendered_count = 0;
for entry_path in files { for entry_path in files {
Self::draw_tree_node( Self::draw_tree_node(
ui, ui,
@@ -258,6 +332,22 @@ impl SoundpadGui {
&mut self.app_state, &mut self.app_state,
&self.audio_player_state, &self.audio_player_state,
&mut actions, &mut actions,
&matching_dirs,
&matching_files,
search_query,
&mut rendered_count,
max_results,
);
}
if is_truncated {
ui.add_space(8.0);
let text = t!(
"gui.search_truncated_warning",
count = matching_files.len() - max_results
);
ui.label(
egui::RichText::new(text).color(egui::Color32::from_rgb(200, 150, 50)),
); );
} }
@@ -286,6 +376,7 @@ impl SoundpadGui {
}); });
} }
#[allow(clippy::too_many_arguments)]
fn draw_tree_node_dir( fn draw_tree_node_dir(
ui: &mut Ui, ui: &mut Ui,
path: std::path::PathBuf, path: std::path::PathBuf,
@@ -293,15 +384,39 @@ impl SoundpadGui {
app_state: &mut AppState, app_state: &mut AppState,
audio_player_state: &AudioPlayerState, audio_player_state: &AudioPlayerState,
actions: &mut Vec<FileAction>, actions: &mut Vec<FileAction>,
matching_dirs: &std::collections::HashSet<PathBuf>,
matching_files: &std::collections::HashSet<PathBuf>,
search_query: &str,
rendered_count: &mut usize,
max_results: usize,
) { ) {
if *rendered_count >= max_results && !search_query.is_empty() {
return;
}
let dir_name = path let dir_name = path
.file_name() .file_name()
.unwrap_or_default() .unwrap_or_default()
.to_string_lossy() .to_string_lossy()
.to_string(); .to_string();
CollapsingHeader::new(dir_name)
.id_salt(&path) let id_salt = if search_query.is_empty() {
.show(ui, |ui| { format!("normal_{:?}", path)
} else {
format!("search_{:?}", path)
};
let mut header = CollapsingHeader::new(dir_name).id_salt(id_salt);
let is_cached = app_state
.current_dir
.as_ref()
.is_some_and(|d| app_state.recursive_files_cache.contains_key(d));
if !search_query.is_empty() && is_cached {
header = header.default_open(true);
}
header.show(ui, |ui| {
let children = if let Some(cached) = app_state.dir_cache.get(&path) { let children = if let Some(cached) = app_state.dir_cache.get(&path) {
cached.clone() cached.clone()
} else { } else {
@@ -339,21 +454,40 @@ impl SoundpadGui {
read read
}; };
let search_query = app_state.search_query.to_lowercase();
let search_query = search_query.trim();
for child in children { for child in children {
if !child.is_dir() && !search_query.is_empty() { if *rendered_count >= max_results && !search_query.is_empty() {
break;
}
if !search_query.is_empty() && is_cached {
if child.is_dir() && !matching_dirs.contains(&child) {
continue;
}
if !child.is_dir() && !matching_files.contains(&child) {
continue;
}
} else if !search_query.is_empty() && !child.is_dir() {
let file_name = child let file_name = child
.file_name() .file_name()
.unwrap_or_default() .unwrap_or_default()
.to_string_lossy() .to_string_lossy()
.to_string(); .to_lowercase();
if !file_name.to_lowercase().contains(search_query) { if !file_name.contains(search_query) {
continue; continue;
} }
} }
Self::draw_tree_node(ui, child, config, app_state, audio_player_state, actions); Self::draw_tree_node(
ui,
child,
config,
app_state,
audio_player_state,
actions,
matching_dirs,
matching_files,
search_query,
rendered_count,
max_results,
);
} }
}); });
} }
@@ -364,7 +498,10 @@ impl SoundpadGui {
app_state: &mut AppState, app_state: &mut AppState,
audio_player_state: &AudioPlayerState, audio_player_state: &AudioPlayerState,
actions: &mut Vec<FileAction>, actions: &mut Vec<FileAction>,
rendered_count: &mut usize,
) { ) {
*rendered_count += 1;
let file_name = path let file_name = path
.file_name() .file_name()
.unwrap_or_default() .unwrap_or_default()
@@ -500,6 +637,7 @@ impl SoundpadGui {
}); });
} }
#[allow(clippy::too_many_arguments)]
fn draw_tree_node( fn draw_tree_node(
ui: &mut Ui, ui: &mut Ui,
path: std::path::PathBuf, path: std::path::PathBuf,
@@ -507,11 +645,39 @@ impl SoundpadGui {
app_state: &mut AppState, app_state: &mut AppState,
audio_player_state: &AudioPlayerState, audio_player_state: &AudioPlayerState,
actions: &mut Vec<FileAction>, actions: &mut Vec<FileAction>,
matching_dirs: &HashSet<PathBuf>,
matching_files: &HashSet<PathBuf>,
search_query: &str,
rendered_count: &mut usize,
max_results: usize,
) { ) {
if *rendered_count >= max_results && !search_query.is_empty() {
return;
}
if path.is_dir() { if path.is_dir() {
Self::draw_tree_node_dir(ui, path, config, app_state, audio_player_state, actions); Self::draw_tree_node_dir(
ui,
path,
config,
app_state,
audio_player_state,
actions,
matching_dirs,
matching_files,
search_query,
rendered_count,
max_results,
);
} else { } else {
Self::draw_tree_node_file(ui, path, app_state, audio_player_state, actions); Self::draw_tree_node_file(
ui,
path,
app_state,
audio_player_state,
actions,
rendered_count,
);
} }
} }
} }
+39 -1
View File
@@ -1,5 +1,5 @@
use crate::gui::SoundpadGui; use crate::gui::SoundpadGui;
use egui::{Align, Button, Color32, ComboBox, Layout, RichText, Ui}; use egui::{Align, Button, Color32, ComboBox, Layout, RichText, Slider, Ui};
use egui_material_icons::icons::ICON_ARROW_BACK; use egui_material_icons::icons::ICON_ARROW_BACK;
use pwsp_lib::types::config::PreferredTheme; use pwsp_lib::types::config::PreferredTheme;
use rust_i18n::t; use rust_i18n::t;
@@ -34,6 +34,10 @@ impl SoundpadGui {
&mut self.config.save_volume, &mut self.config.save_volume,
t!("gui.settings.remember_volume"), t!("gui.settings.remember_volume"),
); );
let save_volume_multiplier_response = ui.checkbox(
&mut self.config.save_volume_multiplier,
t!("gui.settings.remember_volume_multiplier"),
);
let save_input_response = let save_input_response =
ui.checkbox(&mut self.config.save_input, t!("gui.settings.remember_mic")); ui.checkbox(&mut self.config.save_input, t!("gui.settings.remember_mic"));
let save_scale_response = ui.checkbox( let save_scale_response = ui.checkbox(
@@ -46,6 +50,7 @@ impl SoundpadGui {
); );
if save_volume_response.changed() if save_volume_response.changed()
|| save_volume_multiplier_response.changed()
|| save_input_response.changed() || save_input_response.changed()
|| save_scale_response.changed() || save_scale_response.changed()
|| pause_on_exit_response.changed() || pause_on_exit_response.changed()
@@ -88,6 +93,39 @@ impl SoundpadGui {
} }
// -------------------------------- // --------------------------------
ui.separator();
// ----------- Sliders ------------
// Volume multiplier
let should_update_multiplier = !self.app_state.volume_multiplier_dragged
&& self
.app_state
.ignore_volume_multiplier_update_until
.map(|t| std::time::Instant::now() > t)
.unwrap_or(true);
if should_update_multiplier {
self.app_state.volume_multiplier_slider_value =
self.audio_player_state.volume_multiplier;
}
ui.horizontal(|ui| {
let slider = Slider::new(
&mut self.app_state.volume_multiplier_slider_value,
0.01..=3.0,
);
let response = ui.add(slider);
ui.label(t!("gui.settings.volume_multiplier"));
if response.changed() {
// This condition is required to avoid spamming requests while dragging, but to allow changing the value via TextEdit
if !response.dragged() || (response.dragged() && response.drag_stopped()) {
self.app_state.volume_multiplier_dragged = true;
}
}
});
// --------------------------------
ui.with_layout(Layout::bottom_up(Align::Min), |ui| { ui.with_layout(Layout::bottom_up(Align::Min), |ui| {
ui.label(t!( ui.label(t!(
"gui.settings.version", "gui.settings.version",
+25 -9
View File
@@ -1,9 +1,13 @@
mod gui; mod gui;
use anyhow::{Context, Result}; use anyhow::{Context, Result, anyhow};
use pwsp_lib::utils::gui::ensure_pwsp_audio_dir; use pwsp_lib::utils::gui::ensure_pwsp_audio_dir;
use rust_i18n::i18n; use rust_i18n::i18n;
use std::{env, path::PathBuf}; use std::{
env,
path::{Path, PathBuf},
time::{SystemTime, UNIX_EPOCH},
};
i18n!("locales", fallback = "en"); i18n!("locales", fallback = "en");
@@ -31,19 +35,31 @@ async fn download_audio_from_url(uri: &str) -> Result<PathBuf> {
let target_url = uri let target_url = uri
.strip_prefix(prefix) .strip_prefix(prefix)
.ok_or_else(|| anyhow::anyhow!("URI does not containt an expected prefix: {}", prefix))?; .ok_or_else(|| anyhow!("URI does not containt an expected prefix: {}", prefix))?;
let file_name_encoded = target_url let file_name_encoded = match target_url.split('/').next_back() {
.split('/') Some(path) => path.to_string(),
.next_back() None => {
.unwrap_or("downloaded_audio.mp3"); let id = SystemTime::now()
.duration_since(UNIX_EPOCH)
.expect("Time went back")
.as_nanos();
format!("downloaded_audio_{}.mp3", id)
}
};
let file_name = percent_encoding::percent_decode_str(file_name_encoded) let file_name = percent_encoding::percent_decode_str(&file_name_encoded.clone())
.decode_utf8() .decode_utf8()
.unwrap_or_else(|_| file_name_encoded.into()) .unwrap_or_else(|_| file_name_encoded.into())
.into_owned(); .into_owned();
let save_path = ensure_pwsp_audio_dir().join(file_name); let normalized_file_name = file_name.replace('\\', "/");
let sanitized_file_name = Path::new(&normalized_file_name)
.file_name()
.and_then(|n| n.to_str())
.unwrap_or("downloaded_audio.mp3");
let save_path = ensure_pwsp_audio_dir()?.join(sanitized_file_name);
let response = reqwest::get(target_url) let response = reqwest::get(target_url)
.await? .await?
+20 -7
View File
@@ -1,7 +1,7 @@
use crate::{ use crate::{
types::pipewire::DeviceType, types::pipewire::DeviceType,
utils::{ utils::{
daemon::get_daemon_config, daemon::with_daemon_config,
pipewire::{PwTerminator, create_link, get_device, link_player_to_virtual_mic}, pipewire::{PwTerminator, create_link, get_device, link_player_to_virtual_mic},
}, },
}; };
@@ -40,6 +40,7 @@ pub struct FullState {
pub state: PlayerState, pub state: PlayerState,
pub tracks: Vec<TrackInfo>, pub tracks: Vec<TrackInfo>,
pub volume: f32, pub volume: f32,
pub volume_multiplier: f32,
pub current_input: String, pub current_input: String,
pub all_inputs: HashMap<String, String>, pub all_inputs: HashMap<String, String>,
} }
@@ -63,12 +64,19 @@ pub struct AudioPlayer {
pub input_device_name: Option<String>, pub input_device_name: Option<String>,
pub volume: f32, // Master volume pub volume: f32, // Master volume
pub volume_multiplier: f32,
} }
impl AudioPlayer { impl AudioPlayer {
pub async fn new() -> Result<Self> { pub async fn new() -> Result<Self> {
let daemon_config = get_daemon_config(); let (default_input_name, default_volume, default_volume_multiplier) =
let default_volume = daemon_config.default_volume.unwrap_or(1.0); with_daemon_config(|c| {
(
c.default_input_name.clone(),
c.default_volume.unwrap_or(1.0),
c.default_volume_multiplier.unwrap_or(1.0),
)
});
let mut audio_player = AudioPlayer { let mut audio_player = AudioPlayer {
stream_handle: None, stream_handle: None,
@@ -77,9 +85,10 @@ impl AudioPlayer {
input_link_sender: None, input_link_sender: None,
player_link_sender: None, player_link_sender: None,
input_device_name: daemon_config.default_input_name.clone(), input_device_name: default_input_name,
volume: default_volume, volume: default_volume,
volume_multiplier: default_volume_multiplier,
}; };
if audio_player.input_device_name.is_some() { if audio_player.input_device_name.is_some() {
@@ -262,12 +271,16 @@ impl AudioPlayer {
if let Some(id) = id { if let Some(id) = id {
if let Some(sound) = self.tracks.get_mut(&id) { if let Some(sound) = self.tracks.get_mut(&id) {
sound.volume = volume; sound.volume = volume;
sound.sink.set_volume(self.volume * volume); sound
.sink
.set_volume(self.volume * sound.volume * self.volume_multiplier);
} }
} else { } else {
self.volume = volume; self.volume = volume;
for sound in self.tracks.values_mut() { for sound in self.tracks.values_mut() {
sound.sink.set_volume(self.volume * sound.volume); sound
.sink
.set_volume(self.volume * sound.volume * self.volume_multiplier);
} }
} }
} }
@@ -347,7 +360,7 @@ impl AudioPlayer {
.ok_or_else(|| anyhow::anyhow!("stream_handle is unexpectedly missing"))? .ok_or_else(|| anyhow::anyhow!("stream_handle is unexpectedly missing"))?
.mixer(); .mixer();
let sink = Player::connect_new(mixer); let sink = Player::connect_new(mixer);
sink.set_volume(self.volume); // Default volume is 1.0 * master sink.set_volume(self.volume * self.volume_multiplier); // Default volume is 1.0 * master
sink.append(source); sink.append(source);
sink.play(); sink.play();
+82 -2
View File
@@ -1,12 +1,12 @@
use crate::{ use crate::{
types::{ types::{
audio_player::{FullState, PlayerState}, audio_player::{FullState, PlayerState},
config::HotkeyConfig, config::{DaemonConfig, HotkeyConfig},
socket::{Request, Response}, socket::{Request, Response},
}, },
utils::{ utils::{
commands::parse_command, commands::parse_command,
daemon::get_audio_player, daemon::{get_audio_player, with_daemon_config},
pipewire::{get_all_devices, get_device}, pipewire::{get_all_devices, get_device},
}, },
}; };
@@ -46,11 +46,17 @@ pub struct GetVolumeCommand {
pub id: Option<u32>, pub id: Option<u32>,
} }
pub struct GetVolumeMultiplierCommand {}
pub struct SetVolumeCommand { pub struct SetVolumeCommand {
pub volume: Option<f32>, pub volume: Option<f32>,
pub id: Option<u32>, pub id: Option<u32>,
} }
pub struct SetVolumeMultiplierCommand {
pub volume_multiplier: Option<f32>,
}
pub struct GetPositionCommand { pub struct GetPositionCommand {
pub id: Option<u32>, pub id: Option<u32>,
} }
@@ -127,6 +133,14 @@ pub struct ClearHotkeyKeyCommand {
pub slot: Option<String>, pub slot: Option<String>,
} }
pub struct GetDaemonConfigCommand {}
pub struct SaveDaemonConfigCommand {}
pub struct UpdateDaemonConfigCommand {
pub new_config: DaemonConfig,
}
#[async_trait] #[async_trait]
impl Executable for PingCommand { impl Executable for PingCommand {
async fn execute(&self) -> Response { async fn execute(&self) -> Response {
@@ -262,6 +276,18 @@ impl Executable for GetVolumeCommand {
} }
} }
#[async_trait]
impl Executable for GetVolumeMultiplierCommand {
async fn execute(&self) -> Response {
let audio_player = match get_audio_player().await {
Ok(player) => player.lock().await,
Err(err) => return Response::new(false, format!("Audio player error: {}", err)),
};
Response::new(true, audio_player.volume_multiplier.to_string())
}
}
#[async_trait] #[async_trait]
impl Executable for SetVolumeCommand { impl Executable for SetVolumeCommand {
async fn execute(&self) -> Response { async fn execute(&self) -> Response {
@@ -278,6 +304,26 @@ impl Executable for SetVolumeCommand {
} }
} }
#[async_trait]
impl Executable for SetVolumeMultiplierCommand {
async fn execute(&self) -> Response {
if let Some(volume_multiplier) = self.volume_multiplier {
let mut audio_player = match get_audio_player().await {
Ok(player) => player.lock().await,
Err(err) => return Response::new(false, format!("Audio player error: {}", err)),
};
audio_player.volume_multiplier = volume_multiplier;
audio_player.set_volume(volume_multiplier, None); // Reset current volume for all tracks to apply multiplier
Response::new(
true,
format!("Audio volume multiplier was set to {}", volume_multiplier),
)
} else {
Response::new(false, "Invalid volume multiplier value")
}
}
}
#[async_trait] #[async_trait]
impl Executable for GetPositionCommand { impl Executable for GetPositionCommand {
async fn execute(&self) -> Response { async fn execute(&self) -> Response {
@@ -508,6 +554,7 @@ impl Executable for GetFullStateCommand {
state: audio_player.get_state(), state: audio_player.get_state(),
tracks: audio_player.get_tracks(), tracks: audio_player.get_tracks(),
volume: audio_player.volume, volume: audio_player.volume,
volume_multiplier: audio_player.volume_multiplier,
current_input: current_input_nick, current_input: current_input_nick,
all_inputs, all_inputs,
}; };
@@ -723,3 +770,36 @@ impl Executable for ClearHotkeyKeyCommand {
} }
} }
} }
#[async_trait]
impl Executable for GetDaemonConfigCommand {
async fn execute(&self) -> Response {
let serialized = with_daemon_config(|c| serde_json::to_string(&c));
match serialized {
Ok(s) => Response::new(true, s),
Err(err) => Response::new(false, format!("Failed to serialize daemon config: {}", err)),
}
}
}
#[async_trait]
impl Executable for SaveDaemonConfigCommand {
async fn execute(&self) -> Response {
match with_daemon_config(|c| c.save_to_file()) {
Ok(_) => Response::new(true, "Daemon config saved successfully"),
Err(err) => Response::new(false, format!("Failed to save daemon config: {}", err)),
}
}
}
#[async_trait]
impl Executable for UpdateDaemonConfigCommand {
async fn execute(&self) -> Response {
with_daemon_config(|c| {
c.clone_from(&self.new_config);
});
Response::new(true, "Daemon config updated successfully")
}
}
+4 -1
View File
@@ -17,6 +17,7 @@ use std::{
pub struct DaemonConfig { pub struct DaemonConfig {
pub default_input_name: Option<String>, pub default_input_name: Option<String>,
pub default_volume: Option<f32>, pub default_volume: Option<f32>,
pub default_volume_multiplier: Option<f32>,
} }
impl DaemonConfig { impl DaemonConfig {
@@ -73,6 +74,7 @@ pub struct GuiConfig {
pub left_panel_width: f32, pub left_panel_width: f32,
pub save_volume: bool, pub save_volume: bool,
pub save_volume_multiplier: bool,
pub save_input: bool, pub save_input: bool,
pub save_scale_factor: bool, pub save_scale_factor: bool,
pub pause_on_exit: bool, pub pause_on_exit: bool,
@@ -117,11 +119,12 @@ impl Default for GuiConfig {
left_panel_width: 280.0, left_panel_width: 280.0,
save_volume: false, save_volume: false,
save_volume_multiplier: false,
save_input: false, save_input: false,
save_scale_factor: false, save_scale_factor: false,
pause_on_exit: false, pause_on_exit: false,
dirs: vec![ensure_pwsp_audio_dir()], dirs: vec![ensure_pwsp_audio_dir().unwrap()],
preferred_theme: PreferredTheme::System, preferred_theme: PreferredTheme::System,
dirs_settings: HashMap::new(), dirs_settings: HashMap::new(),
+12
View File
@@ -8,9 +8,12 @@ use egui::Id;
use std::{ use std::{
collections::{HashMap, HashSet}, collections::{HashMap, HashSet},
path::PathBuf, path::PathBuf,
sync::{Arc, Mutex},
time::Instant, time::Instant,
}; };
pub type ScanResult = (PathBuf, Vec<PathBuf>, HashMap<PathBuf, Vec<PathBuf>>);
#[derive(Default, Debug)] #[derive(Default, Debug)]
pub struct TrackUiState { pub struct TrackUiState {
pub position_slider_value: f32, pub position_slider_value: f32,
@@ -31,13 +34,16 @@ pub struct AppState {
pub show_settings: bool, pub show_settings: bool,
pub volume_dragged: bool, pub volume_dragged: bool,
pub volume_multiplier_dragged: bool,
pub force_focus_search: bool, pub force_focus_search: bool,
pub volume_slider_value: f32, pub volume_slider_value: f32,
pub volume_multiplier_slider_value: f32,
pub search_field_id: Option<Id>, pub search_field_id: Option<Id>,
pub ignore_volume_update_until: Option<Instant>, pub ignore_volume_update_until: Option<Instant>,
pub ignore_volume_multiplier_update_until: Option<Instant>,
pub current_dir: Option<PathBuf>, pub current_dir: Option<PathBuf>,
pub dirs: Vec<PathBuf>, pub dirs: Vec<PathBuf>,
@@ -47,6 +53,11 @@ pub struct AppState {
pub listed_dirs: HashSet<PathBuf>, pub listed_dirs: HashSet<PathBuf>,
pub dir_cache: HashMap<PathBuf, Vec<PathBuf>>, pub dir_cache: HashMap<PathBuf, Vec<PathBuf>>,
pub scanning_dirs: Arc<Mutex<HashSet<PathBuf>>>,
pub scanned_this_session: HashSet<PathBuf>,
pub finished_scans: Arc<Mutex<Vec<ScanResult>>>,
pub recursive_files_cache: HashMap<PathBuf, Vec<PathBuf>>,
pub show_hotkeys: bool, pub show_hotkeys: bool,
pub hotkey_capture_active: bool, pub hotkey_capture_active: bool,
@@ -65,6 +76,7 @@ pub struct AudioPlayerState {
pub tracks: Vec<TrackInfo>, pub tracks: Vec<TrackInfo>,
pub volume: f32, // Master volume pub volume: f32, // Master volume
pub volume_multiplier: f32,
pub current_input: String, pub current_input: String,
pub all_inputs: HashMap<String, String>, pub all_inputs: HashMap<String, String>,
+30
View File
@@ -1,3 +1,4 @@
use crate::types::config::DaemonConfig;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::collections::HashMap; use std::collections::HashMap;
@@ -94,6 +95,10 @@ impl Request {
Request::new("get_volume", args) Request::new("get_volume", args)
} }
pub fn get_volume_multiplier() -> Self {
Request::new("get_volume_multiplier", vec![])
}
pub fn get_position(id: Option<u32>) -> Self { pub fn get_position(id: Option<u32>) -> Self {
let mut args = vec![]; let mut args = vec![];
let id_str; let id_str;
@@ -138,6 +143,13 @@ impl Request {
Request::new("set_volume".to_string(), args) Request::new("set_volume".to_string(), args)
} }
pub fn set_volume_multiplier(volume: f32) -> Self {
Request::new(
"set_volume_multiplier",
vec![("volume_multiplier", &volume.to_string())],
)
}
pub fn seek(position: f32, id: Option<u32>) -> Self { pub fn seek(position: f32, id: Option<u32>) -> Self {
let mut args = vec![("position".to_string(), position.to_string())]; let mut args = vec![("position".to_string(), position.to_string())];
if let Some(id) = id { if let Some(id) = id {
@@ -172,6 +184,24 @@ impl Request {
Request::new("get_daemon_version", vec![]) Request::new("get_daemon_version", vec![])
} }
pub fn get_daemon_config() -> Self {
Request::new("get_daemon_config", vec![])
}
pub fn save_daemon_config() -> Self {
Request::new("save_daemon_config", vec![])
}
pub fn update_daemon_config(new_config: &DaemonConfig) -> Self {
Request::new(
"update_daemon_config",
vec![(
"new_config",
&serde_json::to_string(&new_config).unwrap_or_default(),
)],
)
}
pub fn get_full_state() -> Self { pub fn get_full_state() -> Self {
Request::new("get_full_state", vec![]) Request::new("get_full_state", vec![])
} }
+22 -1
View File
@@ -1,4 +1,4 @@
use crate::types::{commands::*, socket::Request}; use crate::types::{commands::*, config::DaemonConfig, socket::Request};
use std::path::PathBuf; use std::path::PathBuf;
@@ -15,6 +15,7 @@ pub fn parse_command(request: &Request) -> Option<Box<dyn Executable + Send>> {
"is_paused" => Some(Box::new(IsPausedCommand {})), "is_paused" => Some(Box::new(IsPausedCommand {})),
"get_state" => Some(Box::new(GetStateCommand {})), "get_state" => Some(Box::new(GetStateCommand {})),
"get_volume" => Some(Box::new(GetVolumeCommand { id })), "get_volume" => Some(Box::new(GetVolumeCommand { id })),
"get_volume_multiplier" => Some(Box::new(GetVolumeMultiplierCommand {})),
"set_volume" => { "set_volume" => {
let volume = request let volume = request
.args .args
@@ -24,6 +25,15 @@ pub fn parse_command(request: &Request) -> Option<Box<dyn Executable + Send>> {
.ok(); .ok();
Some(Box::new(SetVolumeCommand { volume, id })) Some(Box::new(SetVolumeCommand { volume, id }))
} }
"set_volume_multiplier" => {
let volume_multiplier = request
.args
.get("volume_multiplier")
.unwrap_or(&String::new())
.parse::<f32>()
.ok();
Some(Box::new(SetVolumeMultiplierCommand { volume_multiplier }))
}
"get_position" => Some(Box::new(GetPositionCommand { id })), "get_position" => Some(Box::new(GetPositionCommand { id })),
"seek" => { "seek" => {
let position = request let position = request
@@ -119,6 +129,17 @@ pub fn parse_command(request: &Request) -> Option<Box<dyn Executable + Send>> {
key_chord, key_chord,
})) }))
} }
"get_daemon_config" => Some(Box::new(GetDaemonConfigCommand {})),
"save_daemon_config" => Some(Box::new(SaveDaemonConfigCommand {})),
"update_daemon_config" => {
let new_config = request
.args
.get("new_config")
.and_then(|nc| serde_json::from_str::<DaemonConfig>(nc).ok())
.unwrap_or_default();
Some(Box::new(UpdateDaemonConfigCommand { new_config }))
}
_ => None, _ => None,
} }
} }
+2 -2
View File
@@ -1,7 +1,7 @@
use anyhow::Result; use anyhow::{Context, Result};
use std::path::PathBuf; use std::path::PathBuf;
pub fn get_config_path() -> Result<PathBuf> { pub fn get_config_path() -> Result<PathBuf> {
let config_path = dirs::config_dir().expect("Failed to obtain config dir"); let config_path = dirs::config_dir().context("Failed to obtain config dir")?;
Ok(config_path.join("pwsp")) Ok(config_path.join("pwsp"))
} }
+23 -7
View File
@@ -7,36 +7,52 @@ use crate::types::{
use anyhow::Result; use anyhow::Result;
use std::os::unix::fs::{DirBuilderExt, MetadataExt, PermissionsExt}; use std::os::unix::fs::{DirBuilderExt, MetadataExt, PermissionsExt};
use std::path::PathBuf; use std::path::PathBuf;
use std::{env, error::Error, fs}; use std::{
env,
error::Error,
fs,
sync::{Arc, Mutex, OnceLock},
};
use tokio::{ use tokio::{
io::{AsyncReadExt, AsyncWriteExt}, io::{AsyncReadExt, AsyncWriteExt},
net::UnixStream, net::UnixStream,
sync::{Mutex, OnceCell}, sync::{Mutex as AsyncMutex, OnceCell},
time::{Duration, sleep}, time::{Duration, sleep},
}; };
static AUDIO_PLAYER: OnceCell<Mutex<AudioPlayer>> = OnceCell::const_new(); static AUDIO_PLAYER: OnceCell<AsyncMutex<AudioPlayer>> = OnceCell::const_new();
static DAEMON_CONFIG: OnceLock<Arc<Mutex<DaemonConfig>>> = OnceLock::new();
pub async fn get_audio_player() -> Result<&'static Mutex<AudioPlayer>, String> { pub async fn get_audio_player() -> Result<&'static AsyncMutex<AudioPlayer>, String> {
AUDIO_PLAYER AUDIO_PLAYER
.get_or_try_init(|| async { .get_or_try_init(|| async {
println!("Initializing audio player"); println!("Initializing audio player");
match AudioPlayer::new().await { match AudioPlayer::new().await {
Ok(player) => Ok(Mutex::new(player)), Ok(player) => Ok(AsyncMutex::new(player)),
Err(err) => Err(err.to_string()), Err(err) => Err(err.to_string()),
} }
}) })
.await .await
} }
pub fn get_daemon_config() -> DaemonConfig { pub fn get_daemon_config() -> &'static Arc<Mutex<DaemonConfig>> {
DaemonConfig::load_from_file().unwrap_or_else(|_| { DAEMON_CONFIG.get_or_init(|| {
Arc::new(Mutex::new(DaemonConfig::load_from_file().unwrap_or_else(
|_| {
let config = DaemonConfig::default(); let config = DaemonConfig::default();
config.save_to_file().ok(); config.save_to_file().ok();
config config
},
)))
}) })
} }
pub fn with_daemon_config<R>(f: impl FnOnce(&mut DaemonConfig) -> R) -> R {
let config = get_daemon_config().clone();
let mut guard = config.lock().unwrap();
f(&mut guard)
}
fn get_current_uid() -> u32 { fn get_current_uid() -> u32 {
rustix::process::geteuid().as_raw() rustix::process::geteuid().as_raw()
} }
+24 -5
View File
@@ -1,7 +1,7 @@
use crate::{ use crate::{
types::{ types::{
audio_player::FullState, audio_player::FullState,
config::{GuiConfig, HotkeyConfig}, config::{DaemonConfig, GuiConfig, HotkeyConfig},
gui::AudioPlayerState, gui::AudioPlayerState,
socket::{Request, Response}, socket::{Request, Response},
}, },
@@ -9,6 +9,7 @@ use crate::{
}; };
use anyhow::{Result, anyhow}; use anyhow::{Result, anyhow};
use std::{ use std::{
fs,
path::PathBuf, path::PathBuf,
sync::{Arc, Mutex}, sync::{Arc, Mutex},
time::Instant, time::Instant,
@@ -23,6 +24,19 @@ pub fn get_gui_config() -> GuiConfig {
}) })
} }
pub fn update_daemon_config(new_config: &DaemonConfig) -> Result<()> {
make_request_sync(Request::update_daemon_config(new_config))?;
make_request_async(Request::save_daemon_config());
Ok(())
}
pub fn get_daemon_config() -> Result<DaemonConfig> {
let response = make_request_sync(Request::get_daemon_config())?;
let config: DaemonConfig = serde_json::from_str(&response.message)?;
Ok(config)
}
pub fn make_request_sync(request: Request) -> Result<Response> { pub fn make_request_sync(request: Request) -> Result<Response> {
tokio::task::block_in_place(|| { tokio::task::block_in_place(|| {
tokio::runtime::Handle::current() tokio::runtime::Handle::current()
@@ -37,15 +51,19 @@ pub fn make_request_async(request: Request) {
}); });
} }
pub fn ensure_pwsp_audio_dir() -> PathBuf { pub fn ensure_pwsp_audio_dir() -> Result<PathBuf> {
let audio_dir = dirs::audio_dir().unwrap_or("~/Music".into()); let audio_dir = dirs::audio_dir().unwrap_or(
dirs::home_dir()
.map(|p| p.join("Music"))
.ok_or_else(|| anyhow!("Failed to get home directory. Is your system ok?"))?,
);
let pwsp_audio_dir = audio_dir.join("PWSP"); let pwsp_audio_dir = audio_dir.join("PWSP");
if !pwsp_audio_dir.exists() { if !pwsp_audio_dir.exists() {
std::fs::create_dir_all(&pwsp_audio_dir).ok(); fs::create_dir_all(&pwsp_audio_dir)?;
} }
pwsp_audio_dir Ok(pwsp_audio_dir)
} }
pub fn format_time_pair(position: f32, duration: f32) -> String { pub fn format_time_pair(position: f32, duration: f32) -> String {
@@ -98,6 +116,7 @@ pub fn start_app_state_thread(audio_player_state_shared: Arc<Mutex<AudioPlayerSt
}; };
guard.tracks = full_state.tracks; guard.tracks = full_state.tracks;
guard.volume = full_state.volume; guard.volume = full_state.volume;
guard.volume_multiplier = full_state.volume_multiplier;
guard.current_input = full_state guard.current_input = full_state
.current_input .current_input
.split(" - ") .split(" - ")
+6 -12
View File
@@ -27,17 +27,17 @@ with open(cargo_toml_path, "r", encoding="utf-8") as f:
# We want to match version in [workspace.package] # We want to match version in [workspace.package]
# First, let's find the [workspace.package] section # First, let's find the [workspace.package] section
workspace_package_match = re.search( workspace_package_match = re.search(r"\[workspace\.package\](.*?)(?=\n\[|$)", cargo_toml_content, re.DOTALL)
r"\[workspace\.package\](.*?)(?=\n\[|$)", cargo_toml_content, re.DOTALL
)
if not workspace_package_match: if not workspace_package_match:
fatal("Could not find [workspace.package] section in Cargo.toml.") fatal("Could not find [workspace.package] section in Cargo.toml.")
if isinstance(workspace_package_match, re.Match):
workspace_package_sec = workspace_package_match.group(1) workspace_package_sec = workspace_package_match.group(1)
version_match = re.search(r'version\s*=\s*"([^"]+)"', workspace_package_sec) version_match = re.search(r'version\s*=\s*"([^"]+)"', workspace_package_sec)
if not version_match: if not version_match:
fatal("Could not find version in [workspace.package] in Cargo.toml.") fatal("Could not find version in [workspace.package] in Cargo.toml.")
if isinstance(version_match, re.Match):
current_version = version_match.group(1) current_version = version_match.group(1)
print(f"Current version detected: {current_version}") print(f"Current version detected: {current_version}")
@@ -62,9 +62,7 @@ print("Updating Cargo.toml...")
def replace_version_in_workspace(match): def replace_version_in_workspace(match):
section_content = match.group(1) section_content = match.group(1)
updated_section_content = re.sub( updated_section_content = re.sub(r'(version\s*=\s*")[^"]+(")', rf"\g<1>{new_version}\g<2>", section_content)
r'(version\s*=\s*")[^"]+(")', rf"\g<1>{new_version}\g<2>", section_content
)
return f"[workspace.package]{updated_section_content}" return f"[workspace.package]{updated_section_content}"
@@ -127,9 +125,7 @@ def update_srcinfo(directory, pkgbuild_path, srcinfo_path):
f.write(result.stdout) f.write(result.stdout)
return return
except Exception as e: except Exception as e:
print( print(f"Warning: makepkg failed in {directory}: {e}. Falling back to text replacement.")
f"Warning: makepkg failed in {directory}: {e}. Falling back to text replacement."
)
# Text replacement fallback # Text replacement fallback
with open(srcinfo_path, "r", encoding="utf-8") as f: with open(srcinfo_path, "r", encoding="utf-8") as f:
@@ -142,9 +138,7 @@ def update_srcinfo(directory, pkgbuild_path, srcinfo_path):
update_srcinfo("packages/aur/bin", pkgbuild_bin_path, "packages/aur/bin/.SRCINFO") update_srcinfo("packages/aur/bin", pkgbuild_bin_path, "packages/aur/bin/.SRCINFO")
update_srcinfo( update_srcinfo("packages/aur/standart", pkgbuild_std_path, "packages/aur/standart/.SRCINFO")
"packages/aur/standart", pkgbuild_std_path, "packages/aur/standart/.SRCINFO"
)
# 4. Update packages/flatpak/ru.arabianq.pwsp.metainfo.xml # 4. Update packages/flatpak/ru.arabianq.pwsp.metainfo.xml
flatpak_xml_path = "packages/flatpak/ru.arabianq.pwsp.metainfo.xml" flatpak_xml_path = "packages/flatpak/ru.arabianq.pwsp.metainfo.xml"