Each of the three groups listed its subcommands by hand, and all three lists had
drifted: `get` advertised a `current-file-path` that does not exist while
omitting tracks, outputs, hotkeys and both new volumes; `set` omitted loop,
output and hotkeys; `action` omitted kill, toggle-loop and the hotkey commands.
Replaced the hand-kept lists with a short description of what each group is for.
Clap already prints the real subcommands underneath, so duplicating them in the
parent was guaranteed to rot.
Also `save-daemon-config`, an action, was described as "Daemon configuration",
which says nothing about what it does.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Not one of the three CLI examples worked: commands live under `action`, `get` and
`set`, and `status` never existed. Replaced with commands that were actually run
against a live daemon, plus the separate monitoring/mic volumes.
Other corrections:
- Build deps were missing libssl-dev and pkg-config, which CI installs and
openssl-sys needs — building from source failed by following the README.
- The Flatpak, which the README recommends first, has a single entry point, so
none of the bare pwsp-* commands apply there. Documented `flatpak run`.
- Global hotkeys are a headline feature but need read access to /dev/input,
i.e. membership in the `input` group. Nothing said so, and the failure is
silent.
- AUR packages exist in packages/aur but were not mentioned.
- "without ... complex virtual sinks" was false: PWSP creates a
support.null-audio-sink node itself. Reworded to say what it actually does.
- "Direct communication with the PipeWire API for the lowest possible latency"
overstated it. The PipeWire API is used for the graph — the virtual mic,
device discovery, links — while audio goes through rodio, which is why the
node shows up as alsa_playback.pwsp-daemon.
- /etc/apt/keyrings does not exist on older Debian/Ubuntu; added mkdir -p.
- DeepWiki was called "our official Wiki"; it is generated from the source
automatically, so it is now described as such.
- Each language link pointed at the file you were already reading.
- Features listed neither the separate volumes nor the localization.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The interface followed the system locale with no way to override it. Settings now
carry a language picker, stored as forced_lang in gui.json, with "System" listed
first so the choice stays undoable.
rust-i18n keeps the current locale in a process-wide atomic and t! reads it on
every lookup, so switching takes effect on the next frame without a restart.
Languages are listed by their own name rather than a translated one, since that
is what a reader looking for their language recognises. A test asserts every
shipped locale has such a name, so adding a translation without naming it fails
rather than silently showing a bare language code.
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
* feat: split playback into separate monitoring and mic streams
AudioPlayer held a single rodio stream. WirePlumber auto-connected its node to
the default sink (that is the monitoring path) while PWSP explicitly linked the
very same node into pwsp-virtual-mic. Both paths were fed by one Player, so
Player::set_volume scaled them together and they could not be separated — hence
the report of "volume 2 is fine on the other side but deafening locally".
There are now two independent streams with independent gains:
mic stream --ensure_route()--> pwsp-virtual-mic
monitoring stream --ensure_route()--> selected Audio/Sink (or WirePlumber's
choice when no device is pinned)
PipeWire side:
- DeviceType::Sink, so sinks are discoverable like inputs already were, plus the
playback_* port mapping that Audio/Sink nodes need (monitor_* stays unmapped:
linking a stream there would be a feedback loop).
- Link globals are tracked in the registry listener; GetLinks/DestroyGlobal make
ensure_route() possible. It points a node at one target and prunes every other
link leaving it, creating before pruning so the node is never left unlinked —
otherwise WirePlumber's autoconnect would re-attach it behind our back.
Replaces link_player_to_virtual_mic().
Audio side:
- PlayerPair wraps the two rodio Players of a track so transport controls reach
both from one place instead of being fanned out at every call site.
- Streams are identified by diffing our own stream nodes before and after opening
one, not by indexing a sorted list: PipeWire reuses freed node ids, so the
stream opened second can end up with the lower id. Only nodes that look like
ours are ever considered — pruning a stranger's node would silence another
application. Opens are therefore sequential, and node discovery checks the
graph before its first sleep to stay off the play latency path.
- Streams are dropped once nothing is playing, as before: an open stream keeps
the audio device busy and stops laptops from suspending. The routing is
rebuilt on the next play.
- Each track decodes twice, once per path. rodio's Buffered is the only shareable
source and it cannot seek, which the position slider depends on.
- effective_gain() is the single home of the master * track * multiplier math and
collapses non-finite or negative gains to silence.
Protocol, config and GUI:
- get/set_monitoring_volume, get/set_mic_volume, get/set_output, get_outputs.
- DaemonConfig gains default_output_name, default_monitoring_volume and
default_mic_volume.
- Volumes arriving over IPC are validated: hotkeys store raw Request JSON, so a
bad value can reach the daemon without passing through the CLI.
- The footer carries a monitoring slider and a mic slider, both running to 200%,
plus an output combo box; with four widgets it no longer fits on one line and
is laid out as two rows.
- SliderLatch owns the "local value wins while the user is interacting" logic
every slider needs — the daemon is polled at 60 Hz, so without it a slider
fights the user mid-drag and snaps back on release. That was previously
copy-pasted per slider as a value/dragged/ignore-until triple.
Fixes along the way: setting the volume multiplier no longer overwrites the
master volume, and get_volume(Some(id)) reports the track's own volume instead of
the product of all three factors.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* feat(cli): add monitoring-volume, mic-volume and output commands
get/set volume keeps its old meaning — without --id it moves both masters at
once, which is the "make everything quieter" shortcut. The new commands address
one path each, and values above 1.0 pass through on purpose: amplifying what
goes into the microphone without deafening yourself is the point.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix(gui): move device selection into settings
The footer tried to hold two combo boxes, two sliders and two buttons on one
line. Their labels alone run ~300px, so at the 800px minimum window size the
sliders and buttons were pushed off the edge and simply vanished.
Device pickers are set-once controls and belong next to the theme selector, so
they move to the settings screen. The footer keeps the two volume sliders, which
are the ones worth reaching for mid-call, and now fits at the minimum width.
The right-edge spacer is also clamped at zero: computed negative, it used to
shove the buttons out of view rather than merely crowding them.
Verified with screenshots at 800px and 1200px.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix(gui): align the footer volume icons and sliders
The speaker icon and its slider sat a couple of pixels above the microphone
pair. A horizontal layout centres each widget against the row height known when
that widget is placed, so a row that grows while being filled leaves whatever
was added first sitting too high.
Every footer element is now allocated the same box height, which makes the
centring independent of placement order, and the row is given that height up
front. Measured from screenshots at 800px: icon ink centres were 776.5 and
780.5, now both 776.5; slider rails were 2.5px apart, now 0.5px, which is
sub-pixel rounding.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix(gui): let the output device be unpinned again
The output picker listed devices but no way back, so choosing one was a one-way
door. It now offers "system default" as the first entry, which sends set_output
with an empty name; the daemon takes that as "stop pinning".
Unpinning deliberately leaves the existing link alone rather than tearing it
down. WirePlumber's autoconnect only runs when a node first appears, so removing
the link would strand the monitoring stream with no output at all. Streams close
as soon as playback stops, so the next sound opens a fresh node that gets routed
like any other application's.
Picking the default target ourselves was tried and reverted: the global
default.audio.sink is only a fallback, and a per-stream target.node overrides it.
On this machine that meant monitoring would have gone straight to the hardware
sink, bypassing the user's EasyEffects chain that WirePlumber routes it through.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* refactor: trim comments and drop an abstraction that earned nothing
Removes the RouteTarget trait and its two unit-struct implementations. They
existed to let route() resolve a target lazily, but both call sites resolve one
line earlier just as well, so a trait, two types and an impl block collapse into
one parameter.
Narrows should_sync, commit and prune_links_from to private: all three were
public but only ever called from within their own module.
Comment pass: drops the decorative section dividers that this file never had,
and rewrites the ones that leaned on a particular machine's setup or narrated a
past bug. What is left explains things the code cannot: why streams close when
idle, why they are opened one at a time, why a link is created before stale ones
are pruned, why playback_* maps to inputs while monitor_* stays unmapped, and
why the footer row height is pinned up front.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
* 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
* 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>
* 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
* 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>
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>