Compare commits

...

16 Commits

Author SHA1 Message Date
arabianq 9fbe42c201 update version to 1.5.1 2026-01-28 23:34:12 +03:00
arabianq fac04c4533 docs: update AUR installation command to include binary option 2026-01-28 23:32:09 +03:00
arabianq f93852bf8e add submodules for aur/standart and aur/bin 2026-01-28 23:31:15 +03:00
arabianq 1bb0aa959a rename workflow from Flatpak CI to Git Flatpak 2026-01-28 22:56:47 +03:00
arabianq 7a1723fbcb fix: enable build-bundle option in Flatpak workflows and adjust source path 2026-01-28 22:50:15 +03:00
arabianq 712a0968a7 fix: remove unnecessary file sources from Flatpak manifest 2026-01-28 22:45:33 +03:00
arabianq e98e6bc2f3 fix: remove submodule checkout option from Flatpak workflows 2026-01-28 22:41:40 +03:00
arabianq 5007b483aa refactor 2026-01-28 22:38:10 +03:00
arabianq b936b58e75 add Flatpak CI and release workflows, update paths in manifest 2026-01-28 22:37:36 +03:00
arabianq 502ef2ed89 create wrapper for flatpak, flatpak yaml, desktop entry and metainfo 2026-01-28 22:33:34 +03:00
arabianq ce5910b9a6 fix: improve device lookup in get_device function and update daemon device name 2026-01-28 22:30:33 +03:00
arabianq b0c670235e fix: impossible to remove directories 2026-01-28 22:07:05 +03:00
arabianq f1d4ffd7fa fix: handle errors when opening a directory in SoundpadGui 2026-01-28 21:44:00 +03:00
arabianq 6f35ab7b8b update github workflows 2026-01-28 21:20:56 +03:00
arabianq 9a67f5479a add submodule for AUR package 2026-01-28 21:16:58 +03:00
arabianq b727eba988 move pwsp.spec for rpm into packages/rpm directory 2026-01-28 21:15:37 +03:00
23 changed files with 369 additions and 35 deletions
+84
View File
@@ -0,0 +1,84 @@
name: Git archive
permissions:
contents: write
packages: write
on:
push:
branches: [main, master]
workflow_dispatch:
jobs:
build-and-upload:
runs-on: ubuntu-latest
steps:
- name: Install apt deps (jq/zip + dev-libs)
run: |
sudo apt-get update
sudo apt-get install -y \
zip jq \
libpipewire-0.3-dev \
libclang-dev \
libasound2-dev
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Setup Rust toolchain
uses: actions-rs/toolchain@v1
with:
toolchain: stable
- name: Extract all binary names
id: cargo-meta
run: |
set -euo pipefail
BIN_NAMES=$(cargo metadata --no-deps --format-version 1 \
| jq -r '.packages[0].targets[] | select(.kind[] | contains("bin")) | .name')
# сохраним построчно в выход
echo "bin_names<<EOF" >> $GITHUB_OUTPUT
echo "$BIN_NAMES" >> $GITHUB_OUTPUT
echo "EOF" >> $GITHUB_OUTPUT
- name: Build all release binaries
run: cargo build --release --locked
- name: Package all binaries into one archive
shell: bash
run: |
set -euo pipefail
COMMIT_SHA="${{ github.sha }}"
ARCHIVE_NAME="pwsp-${COMMIT_SHA}-linux-x64.zip"
echo "Creating archive: $ARCHIVE_NAME"
FILES=()
while IFS= read -r BIN; do
[ -z "$BIN" ] && continue
FILES+=("target/release/$BIN")
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[@]}"
- name: Upload archive as artifact
uses: actions/upload-artifact@v4
with:
name: archive
path: pwsp-*.zip
retention-days: 7
+53
View File
@@ -0,0 +1,53 @@
name: Git deb
permissions:
contents: write
packages: write
on:
push:
branches: [main, master]
workflow_dispatch:
jobs:
build-and-upload:
runs-on: ubuntu-latest
steps:
- name: Install apt deps (jq/zip + dev-libs)
run: |
sudo apt-get update
sudo apt-get install -y \
zip jq \
libpipewire-0.3-dev \
libclang-dev \
libasound2-dev
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Setup Rust toolchain
uses: actions-rs/toolchain@v1
with:
toolchain: stable
- name: Build all release binaries
run: cargo build --release --locked
- name: Install cargo-deb and create .deb
shell: bash
run: |
set -euo pipefail
cargo install --locked cargo-deb
export PATH="$HOME/.cargo/bin:$PATH"
cargo-deb
- name: Upload .deb(s) as artifacts
uses: actions/upload-artifact@v4
with:
name: deb-packages
path: target/debian/*.deb
retention-days: 7
+27
View File
@@ -0,0 +1,27 @@
name: Git Flatpak
on:
push:
branches: [main, master]
pull_request:
branches: [main, master]
workflow_dispatch:
jobs:
flatpak-build:
runs-on: ubuntu-latest
container:
image: bilelmoussaoui/flatpak-github-actions:freedesktop-24.08
options: --privileged
steps:
- uses: actions/checkout@v4
- name: Build Flatpak
uses: flatpak/flatpak-github-actions/flatpak-builder@v6
with:
bundle: ru.arabianq.pwsp.flatpak
manifest-path: packages/flatpak/ru.arabianq.pwsp.yaml
cache: true
branch: master
build-bundle: true
+1 -9
View File
@@ -10,7 +10,7 @@ on:
workflow_dispatch: workflow_dispatch:
inputs: inputs:
tag: tag:
description: 'Tag to attach assets to (e.g. v1.0.0)' description: "Tag to attach assets to (e.g. v1.0.0)"
required: false required: false
jobs: jobs:
@@ -32,7 +32,6 @@ jobs:
run: | run: |
set -euo pipefail set -euo pipefail
# приоритет 1: входной параметр workflow_dispatch
INPUT_TAG="${{ github.event.inputs.tag || '' }}" INPUT_TAG="${{ github.event.inputs.tag || '' }}"
if [ -n "$INPUT_TAG" ]; then if [ -n "$INPUT_TAG" ]; then
echo "Using input tag: $INPUT_TAG" echo "Using input tag: $INPUT_TAG"
@@ -40,7 +39,6 @@ jobs:
exit 0 exit 0
fi fi
# приоритет 2: если запущено событием release
EVENT_TAG="${{ github.event.release.tag_name || '' }}" EVENT_TAG="${{ github.event.release.tag_name || '' }}"
if [ -n "$EVENT_TAG" ]; then if [ -n "$EVENT_TAG" ]; then
echo "Using event tag: $EVENT_TAG" echo "Using event tag: $EVENT_TAG"
@@ -48,14 +46,12 @@ jobs:
exit 0 exit 0
fi fi
# приоритет 3: если GITHUB_REF — refs/tags/...
if [[ "${GITHUB_REF:-}" =~ ^refs/tags/(.+)$ ]]; then if [[ "${GITHUB_REF:-}" =~ ^refs/tags/(.+)$ ]]; then
echo "Using GITHUB_REF tag: ${BASH_REMATCH[1]}" echo "Using GITHUB_REF tag: ${BASH_REMATCH[1]}"
echo "tag=${BASH_REMATCH[1]}" >> $GITHUB_OUTPUT echo "tag=${BASH_REMATCH[1]}" >> $GITHUB_OUTPUT
exit 0 exit 0
fi fi
# приоритет 4: пробуем получить последний релиз через API
echo "No tag in input/event/GITHUB_REF — querying latest release via API..." echo "No tag in input/event/GITHUB_REF — querying latest release via API..."
LATEST_JSON=$(curl -sSf -H "Authorization: Bearer $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 $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')
@@ -91,7 +87,6 @@ jobs:
set -euo pipefail set -euo pipefail
BIN_NAMES=$(cargo metadata --no-deps --format-version 1 \ BIN_NAMES=$(cargo metadata --no-deps --format-version 1 \
| jq -r '.packages[0].targets[] | select(.kind[] | contains("bin")) | .name') | jq -r '.packages[0].targets[] | select(.kind[] | contains("bin")) | .name')
# сохраним построчно в выход
echo "bin_names<<EOF" >> $GITHUB_OUTPUT echo "bin_names<<EOF" >> $GITHUB_OUTPUT
echo "$BIN_NAMES" >> $GITHUB_OUTPUT echo "$BIN_NAMES" >> $GITHUB_OUTPUT
echo "EOF" >> $GITHUB_OUTPUT echo "EOF" >> $GITHUB_OUTPUT
@@ -107,7 +102,6 @@ jobs:
ARCHIVE_NAME="pwsp-${TAG}-linux-x64.zip" ARCHIVE_NAME="pwsp-${TAG}-linux-x64.zip"
echo "Creating archive: $ARCHIVE_NAME" echo "Creating archive: $ARCHIVE_NAME"
# читаем построчно список бинарников и формируем массив файлов
FILES=() FILES=()
while IFS= read -r BIN; do while IFS= read -r BIN; do
[ -z "$BIN" ] && continue [ -z "$BIN" ] && continue
@@ -119,7 +113,6 @@ jobs:
exit 1 exit 1
fi fi
# проверим, что все бинарники действительно есть
for f in "${FILES[@]}"; do for f in "${FILES[@]}"; do
if [ ! -f "$f" ]; then if [ ! -f "$f" ]; then
echo "Error: expected binary not found: $f" >&2 echo "Error: expected binary not found: $f" >&2
@@ -128,7 +121,6 @@ jobs:
echo "Will add: $f" echo "Will add: $f"
done done
# создаём архив с бинарниками внутри как просто pwsp-gui, pwsp-daemon, pwsp-cli
zip -j "$ARCHIVE_NAME" "${FILES[@]}" zip -j "$ARCHIVE_NAME" "${FILES[@]}"
- name: Upload release archive - name: Upload release archive
+1 -1
View File
@@ -10,7 +10,7 @@ on:
workflow_dispatch: workflow_dispatch:
inputs: inputs:
tag: tag:
description: 'Tag to attach assets to (e.g. v1.0.0)' description: "Tag to attach assets to (e.g. v1.0.0)"
required: false required: false
jobs: jobs:
+72
View File
@@ -0,0 +1,72 @@
name: Release Flatpak
permissions:
contents: write
on:
release:
types: [created]
workflow_dispatch:
inputs:
tag:
description: "Tag to attach assets to (e.g. v1.0.0)"
required: false
jobs:
flatpak-release:
runs-on: ubuntu-latest
container:
image: bilelmoussaoui/flatpak-github-actions:freedesktop-24.08
options: --privileged
steps:
- name: Determine tag to use
id: tag
run: |
set -euo pipefail
INPUT_TAG="${{ github.event.inputs.tag || '' }}"
if [ -n "$INPUT_TAG" ]; then
echo "tag=$INPUT_TAG" >> $GITHUB_OUTPUT
exit 0
fi
EVENT_TAG="${{ github.event.release.tag_name || '' }}"
if [ -n "$EVENT_TAG" ]; then
echo "tag=$EVENT_TAG" >> $GITHUB_OUTPUT
exit 0
fi
if [[ "${GITHUB_REF:-}" =~ ^refs/tags/(.+)$ ]]; then
echo "tag=${BASH_REMATCH[1]}" >> $GITHUB_OUTPUT
exit 0
fi
echo "No tag found"
echo "tag=" >> $GITHUB_OUTPUT
- name: Fail if no tag determined
if: ${{ steps.tag.outputs.tag == '' }}
run: |
echo "ERROR: No tag determined."
exit 1
- uses: actions/checkout@v4
with:
ref: ${{ steps.tag.outputs.tag }}
- name: Build Flatpak
uses: flatpak/flatpak-github-actions/flatpak-builder@v6
with:
bundle: ru.arabianq.pwsp.flatpak
manifest-path: packages/flatpak/ru.arabianq.pwsp.yaml
cache: true
branch: master
build-bundle: true
- name: Upload Flatpak to release
uses: softprops/action-gh-release@v2
with:
token: ${{ secrets.GITHUB_TOKEN }}
tag_name: ${{ steps.tag.outputs.tag }}
files: ru.arabianq.pwsp.flatpak
+9
View File
@@ -0,0 +1,9 @@
[submodule "packages/aur"]
path = packages/aur
url = ssh://aur@aur.archlinux.org/pwsp.git
[submodule "packages/aur/standart"]
path = packages/aur/standart
url = ssh://aur@aur.archlinux.org/pwsp.git
[submodule "packages/aur/bin"]
path = packages/aur/bin
url = ssh://aur@aur.archlinux.org/pwsp-bin.git
Generated
+1 -1
View File
@@ -2292,7 +2292,7 @@ checksum = "3eb8486b569e12e2c32ad3e204dbaba5e4b5b216e9367044f25f1dba42341773"
[[package]] [[package]]
name = "pwsp" name = "pwsp"
version = "1.5.0" version = "1.5.1"
dependencies = [ dependencies = [
"async-trait", "async-trait",
"clap", "clap",
+1 -1
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "pwsp" name = "pwsp"
version = "1.5.0" version = "1.5.1"
edition = "2024" edition = "2024"
authors = ["arabian"] authors = ["arabian"]
description = "PWSP lets you play audio files through your microphone. Has both CLI and GUI clients." description = "PWSP lets you play audio files through your microphone. Has both CLI and GUI clients."
+1 -1
View File
@@ -76,7 +76,7 @@ sudo dnf install pwsp
There is pwsp package in AUR. There is pwsp package in AUR.
You can install it using yay, paru or any other AUR helper. You can install it using yay, paru or any other AUR helper.
```bash ```bash
paru pwsp paru pwsp-bin # or paru pwsp to build it locally
``` ```
## **Installing using cargo** ## **Installing using cargo**
+1
Submodule packages/aur/bin added at 446179bb91
+4
View File
@@ -0,0 +1,4 @@
#!/bin/sh
pwsp-daemon &
exec pwsp-gui "$@"
@@ -0,0 +1,9 @@
[Desktop Entry]
Name=PWSP (Soundpad)
Comment=Let's you play audio files through you microphone
Exec=pwsp-wrapper.sh %u
Icon=ru.arabianq.pwsp
Terminal=false
Type=Application
Categories=Audio;Utility;
Keywords=soundpad;pipewire;audio;
@@ -0,0 +1,25 @@
<?xml version="1.0" encoding="UTF-8"?>
<component type="desktop-application">
<id>ru.arabianq.pwsp</id>
<metadata_license>CC0-1.0</metadata_license>
<project_license>MIT</project_license>
<name>PWSP</name>
<summary>Play audio files through your microphone using PipeWire</summary>
<description>
<p>
PWSP (PipeWire Soundpad) is a tool that allows you to play audio files through your
microphone.
It features both a graphical user interface and a command-line interface.
</p>
</description>
<launchable type="desktop-id">ru.arabianq.pwsp.desktop</launchable>
<screenshots>
<screenshot type="default">
<image>
https://raw.githubusercontent.com/arabianq/pipewire-soundpad/master/assets/screenshot.png</image>
</screenshot>
</screenshots>
<url type="homepage">https://pwsp.arabianq.ru</url>
<developer_name>arabian</developer_name>
<content_rating type="oars-1.1" />
</component>
+47
View File
@@ -0,0 +1,47 @@
app-id: ru.arabianq.pwsp
runtime: org.freedesktop.Platform
runtime-version: "24.08"
sdk: org.freedesktop.Sdk
sdk-extensions:
- org.freedesktop.Sdk.Extension.rust-stable
- org.freedesktop.Sdk.Extension.llvm20
command: pwsp-wrapper.sh
finish-args:
- --share=ipc
- --socket=fallback-x11
- --socket=wayland
- --socket=pulseaudio
- --filesystem=xdg-run/pipewire-0
- --filesystem=xdg-run/pwsp:create
- --filesystem=xdg-run/app/ru.arabianq.pwsp:create
- --filesystem=host
- --device=all
- --device=dri
- --share=network
- --talk-name=org.freedesktop.portal.Desktop
- --talk-name=org.freedesktop.portal.Documents
build-options:
append-path: /usr/lib/sdk/rust-stable/bin:/usr/lib/sdk/llvm20/bin
env:
CARGO_HOME: /run/build/pwsp/cargo
LIBCLANG_PATH: /usr/lib/sdk/llvm20/lib
modules:
- name: pwsp
buildsystem: simple
build-options:
build-args:
- --share=network
build-commands:
- cargo build --release
- 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-gui /app/bin/pwsp-gui
- install -Dm755 packages/flatpak/pwsp-wrapper.sh /app/bin/pwsp-wrapper.sh
- install -Dm644 assets/icon.png /app/share/icons/hicolor/256x256/apps/ru.arabianq.pwsp.png
- install -Dm644 packages/flatpak/ru.arabianq.pwsp.desktop /app/share/applications/ru.arabianq.pwsp.desktop
- install -Dm644 packages/flatpak/ru.arabianq.pwsp.metainfo.xml /app/share/metainfo/ru.arabianq.pwsp.metainfo.xml
sources:
- type: dir
path: ../../
+1 -1
View File
@@ -4,7 +4,7 @@
%global cargo_install_lib 0 %global cargo_install_lib 0
Name: pwsp Name: pwsp
Version: 1.5.0 Version: 1.5.1
Release: %autorelease Release: %autorelease
Summary: Lets you play audio files through your microphone Summary: Lets you play audio files through your microphone
+1 -1
View File
@@ -337,7 +337,7 @@ impl SoundpadGui {
let delete_dir_button_response = let delete_dir_button_response =
ui.add_sized([18.0, 18.0], delete_dir_button); ui.add_sized([18.0, 18.0], delete_dir_button);
if delete_dir_button_response.clicked() { if delete_dir_button_response.clicked() {
self.remove_dir(&path.clone()); self.app_state.dirs_to_remove.insert(path.clone());
} }
}); });
}); });
+9 -15
View File
@@ -96,27 +96,21 @@ impl SoundpadGui {
} }
} }
pub fn remove_dir(&mut self, path: &PathBuf) {
self.app_state.dirs.retain(|x| x != path);
if let Some(current_dir) = &self.app_state.current_dir
&& current_dir == path
{
self.app_state.current_dir = None;
self.app_state.files.clear();
}
self.config.dirs = self.app_state.dirs.clone();
self.config.save_to_file().ok();
}
pub fn open_dir(&mut self, path: &PathBuf) { pub fn open_dir(&mut self, path: &PathBuf) {
self.app_state.current_dir = Some(path.clone()); self.app_state.current_dir = Some(path.clone());
self.app_state.files = path match path.read_dir() {
.read_dir() Ok(read_dir) => {
.unwrap() self.app_state.files = read_dir
.filter_map(|res| res.ok()) .filter_map(|res| res.ok())
.map(|entry| entry.path()) .map(|entry| entry.path())
.collect(); .collect();
} }
Err(e) => {
eprintln!("Failed to read directory {:?}: {}", path, e);
self.app_state.files.clear();
}
}
}
pub fn play_file(&mut self, path: &PathBuf, concurrent: bool) { pub fn play_file(&mut self, path: &PathBuf, concurrent: bool) {
make_request_async(Request::play(path.to_str().unwrap(), concurrent)); make_request_async(Request::play(path.to_str().unwrap(), concurrent));
+11
View File
@@ -9,6 +9,17 @@ use std::time::{Duration, Instant};
impl App for SoundpadGui { impl App for SoundpadGui {
fn update(&mut self, ctx: &Context, _frame: &mut EFrame) { fn update(&mut self, ctx: &Context, _frame: &mut EFrame) {
// Remove directories
for path in self.app_state.dirs_to_remove.drain() {
self.app_state.dirs.retain(|x| x != &path);
if let Some(current_dir) = &self.app_state.current_dir
&& current_dir == &path
{
self.app_state.current_dir = None;
self.app_state.files.clear();
}
}
// Save directories if changed // Save directories if changed
if !self.config.dirs.eq(&self.app_state.dirs) { if !self.config.dirs.eq(&self.app_state.dirs) {
self.config.dirs = self.app_state.dirs.clone(); self.config.dirs = self.app_state.dirs.clone();
+1
View File
@@ -38,6 +38,7 @@ pub struct AppState {
pub current_dir: Option<PathBuf>, pub current_dir: Option<PathBuf>,
pub dirs: Vec<PathBuf>, pub dirs: Vec<PathBuf>,
pub dirs_to_remove: HashSet<PathBuf>,
pub selected_file: Option<PathBuf>, pub selected_file: Option<PathBuf>,
pub files: HashSet<PathBuf>, pub files: HashSet<PathBuf>,
+1 -1
View File
@@ -36,7 +36,7 @@ pub fn get_daemon_config() -> DaemonConfig {
pub async fn link_player_to_virtual_mic() -> Result<(), Box<dyn Error>> { pub async fn link_player_to_virtual_mic() -> Result<(), Box<dyn Error>> {
let pwsp_daemon_output; let pwsp_daemon_output;
if let Ok(device) = get_device("alsa_playback.pwsp-daemon").await { if let Ok(device) = get_device("pwsp-daemon").await {
pwsp_daemon_output = device; pwsp_daemon_output = device;
} else { } else {
eprintln!("Could not find alsa_playback.pwsp-daemon device, skipping device linking"); eprintln!("Could not find alsa_playback.pwsp-daemon device, skipping device linking");
+5 -1
View File
@@ -210,7 +210,11 @@ pub async fn get_device(device_name: &str) -> Result<AudioDevice, Box<dyn Error>
input_devices.extend(output_devices); input_devices.extend(output_devices);
for device in input_devices { for device in input_devices {
if device.name == device_name { if device.name == device_name
|| device.nick == device_name
|| device.name.contains(device_name)
|| device.nick.contains(device_name)
{
return Ok(device); return Ok(device);
} }
} }