Replaced the monolithic `src/gui/draw.rs` with a new `src/gui/views` directory module.
The GUI drawing logic is now cleanly separated into distinct files:
- `body.rs`
- `footer.rs`
- `header.rs`
- `hotkey_capture.rs`
- `hotkeys.rs`
- `settings.rs`
- `waiting_for_daemon.rs`
This organization vastly improves readability and maintainability without altering functionality. All shared helpers are centralized in `src/gui/views/mod.rs` and imports are strictly managed.
Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com>
* 🔒 Fix insecure fallback directory and secure file creation
The daemon's fallback runtime directory `get_runtime_dir()` was hardcoded to `/run/pwsp`, creating a risk of shared, insecure access in multi-user systems.
This commit secures the fallback logic by:
1. Creating a user-specific temporary directory (`/tmp/pwsp-$UID`).
2. Ensuring directory creation happens atomically with `0o700` permissions using `std::fs::DirBuilder`.
3. Validating the fallback directory strictly (checking UID, 0o700 permissions, and symlink status) if it already exists to mitigate symlink attacks.
4. Using `libc::geteuid()` for robust cross-platform UID extraction.
5. Fixing `is_daemon_running` and locking logic to use `fs::OpenOptions` instead of `fs::File::create` to prevent accidental file truncation on active lock files.
Co-authored-by: arabianq <55220741+arabianq@users.noreply.github.com>
* 🔒 Fix insecure fallback directory and secure file creation
The daemon's fallback runtime directory `get_runtime_dir()` was hardcoded to `/run/pwsp`, creating a risk of shared, insecure access in multi-user systems.
This commit secures the fallback logic by:
1. Creating a user-specific temporary directory (`/tmp/pwsp-$UID`).
2. Ensuring directory creation happens atomically with `0o700` permissions using `std::fs::DirBuilder`.
3. Validating the fallback directory strictly (checking UID, 0o700 permissions, and symlink status) if it already exists to mitigate symlink attacks.
4. Using safe `rustix::process::geteuid()` for robust cross-platform UID extraction, avoiding `unsafe` blocks.
5. Fixing `is_daemon_running` and locking logic to use `fs::OpenOptions` instead of `fs::File::create` to prevent accidental file truncation on active lock files.
Co-authored-by: arabianq <55220741+arabianq@users.noreply.github.com>
* small refactor
---------
Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com>
- Extracted search field rendering to `draw_files_search_field`
- Extracted list rendering to `draw_files_list`
- Split `draw_tree_node` file and directory branch logic to `draw_tree_node_file` and `draw_tree_node_dir`
Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com>
This commit optimizes the GUI render loop in `src/gui/draw.rs` during the rendering of the drag and drop directory list. Previously, `self.app_state.dirs.clone()` was cloning the entire vector of `PathBuf`s on every frame, which caused unnecessary allocations.
Now, `std::mem::take` temporarily removes the list of directories from `app_state.dirs` inside `show_vec`, and items are passed by reference rather than being cloned (`let path = item;` instead of `item.clone()`). Finally, the original list is restored into `app_state.dirs`. To ensure the state doesn't mutate or invalidate when `self.open_dir(&path)` is clicked, this logic has been deferred to run after the `app_state` vector is restored.
Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com>
This optimization removes an unnecessary `.cloned()` call inside `draw_hotkeys_table`
which previously forced a clone of every filtered `HotkeySlot` on every frame render.
Instead, we now hold a `Vec<&HotkeySlot>` and only clone `slot.slot` exactly when
a user interaction requires ownership to dispatch a `HotkeyAction`.
This eliminates constant heap allocations of `String` and `Request` components
while scrolling or idling in the hotkeys view.
Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com>
- Break down the monolithic `draw_hotkeys` method into smaller,
focused component functions: `draw_hotkeys_header`,
`draw_hotkeys_search`, `draw_hotkeys_table`, and
`handle_hotkey_action`.
- Improve readability and maintainability of the `src/gui/draw.rs` file
while preserving identical behavior.
Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com>
Extracted distinct UI sections (playback controls, position slider, volume controls, stop button) into their own well-scoped helper functions within `SoundpadGui`. This significantly improves the maintainability and readability of `draw_track_control` while preserving the existing layout structure and state mutation behavior.
Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com>
* perf: optimize UI rendering loop by removing unnecessary Vec clone\n\n- Removed `clone()` on `self.audio_player_state.tracks` in `draw_header`\n- Iterated by reference instead of using an owned collection\n- Benchmarked and showed a significant performance improvement (7us -> 87ns)
Co-authored-by: arabianq <55220741+arabianq@users.noreply.github.com>
* build(flatpak): update cargo-sources.json to include criterion\n\nThe CI failed during the offline flatpak build because the newly added `criterion` dev-dependency was missing from `cargo-sources.json`. Regenerated `packages/flatpak/cargo-sources.json` to fix it.
Co-authored-by: arabianq <55220741+arabianq@users.noreply.github.com>
* Delete benches/ui_benchmark.rs
* refactor: remove garbage
---------
Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com>
This commit addresses a security and stability vulnerability where failures in PipeWire context setup, connection, or registry acquisition would crash the entire thread (or daemon) via `.expect()` panics.
We now gracefully capture and propagate initialization errors up the call stack. A `sync_channel(0)` is used to signal the success or failure of the initial pipewire setup back to the calling functions (`get_all_devices`, `create_virtual_mic`, `create_link`). This prevents unexpected crashes and improves error resilience.
Also removed unneeded `pw_sender` panics on channel termination by simply dropping/ignoring the result.
Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com>
* fix: truncate file button text in draw function so footer is no clipped
* fix(gui): fix hotkeys table clipping with egui_extras::TableBuilder
fully reworked hotkeys page
* deps: update flatpak cargo-sources.json
Add unit tests for parse_command in src/utils/commands.rs to ensure
robust handling of set_volume edge cases including missing or
invalid volume and id arguments.
Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com>
Replaced `.chars().next().unwrap()` with `.chars().next().is_some_and(...)` in `chord_from_event` and `parse_chord` functions in `src/gui/input.rs`. This ensures that even if the string is empty, the application will not panic, adhering to the project's safety guidelines and resolving a potential security vulnerability.
Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com>
Extract duplicated `pipewire::init()`, `MainLoopRc::new()`, and `ContextRc::new()` setup code from `pw_get_global_objects_thread`, `create_virtual_mic`, and `create_link` into a shared `setup_pipewire_context` helper in `src/utils/pipewire.rs`. Also ran codebase-wide linters to improve code quality.
Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com>
Extract sections from the long `handle_input` function into smaller,
context-specific helper methods such as `handle_hotkey_assignment`,
`handle_toggles`, `handle_playback_and_focus`, `handle_file_playback`,
`handle_navigation`, and `handle_hotkey_triggers`. This significantly
improves the maintainability and readability of `src/gui/input.rs`
while preserving original functionality.
In addition, ran `cargo clippy --fix` on the project to resolve a few
other minor health issues, like collapsing nested `if` statements and
reducing unnecessary allocations.
Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com>
Changed `HotkeyConfig::find_conflicts` to return a `Vec<(&str, &str)>` rather than allocating owned `Strings`. In `src/gui/draw.rs`, the code now builds a `HashSet<&str>` directly from the borrowed strings using array-based flat-mapping, avoiding intermediate `Vec` allocations and redundant clones.
Benchmarked to be approximately 3.5x faster in scenarios involving many configured slots.
Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com>
* Fix virtual mic audio linking by managing it in AudioPlayer lifecycle
- Moved `link_player_to_virtual_mic` to `src/utils/pipewire.rs` and updated it to return a termination sender.
- Added `player_link_sender` to `AudioPlayer` to manage the PipeWire link between the daemon and the virtual mic.
- Integrated linking logic into `AudioPlayer::play` and `AudioPlayer::update` to ensure the link is established when audio starts playing.
- Ensured the link is terminated in `AudioPlayer::drop_stream` when the audio sink is closed.
- Removed redundant and potentially failing startup linking loop from the daemon.
- Fixed log spam by ensuring `link_player` is only attempted when necessary and errors are handled gracefully.
- Maintained compatibility with stable Rust by avoiding unstable features.
Co-authored-by: arabianq <55220741+arabianq@users.noreply.github.com>
* small refactor
* refactor
---------
Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com>
Replaced the large match blocks in `chord_from_event` and `parse_chord`
with `egui::Key::name()` and `egui::Key::from_name()`. This drastically
reduces boilerplate code while maintaining the existing behavior that
strictly allows only single-character alphanumeric keys and 'F' keys for
chords.
Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com>
Addresses a security vulnerability where the daemon or client could be
forced to allocate up to 10MB of memory per malformed socket message,
potentially leading to Out-Of-Memory (OOM) crashes.
Changes:
- Introduced a central `MAX_MESSAGE_SIZE` constant of 128KB in `src/types/socket.rs`.
- Enforced the 128KB limit on incoming requests in `src/bin/daemon.rs`.
- Enforced the 128KB limit on incoming responses in `src/utils/daemon.rs`.
- Preserved detailed `eprintln!` logging when messages are rejected.
Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com>
* refactor: removed garbage
* change version to 1.7.1
* cargo fmt
* cargo update
* docs: add information about hotkeys to README
* docs: small refactor
* fix: drop audio stream when idle to allow system suspend
The daemon kept its ALSA playback stream open permanently, which
PipeWire reported as a running Stream/Output/Audio node even with
no tracks playing. This prevented desktop environments from detecting
idle state and entering suspend.
- Make the audio sink on-demand: created when playback starts,
dropped when all tracks finish
- Reduce player loop polling from 100ms to 2s when idle
- Throttle PipeWire device enumeration to every ~5s while playing
- Log only first and last link retry attempt instead of all 60
Replaced an unsafe `.unwrap()` with `.unwrap_or_default()` in `src/gui/draw.rs`
when parsing file names. This prevents potential panics on invalid paths.
Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com>
* feat: add hotkey system for playing individual sounds
Slot-based hotkey mappings stored in ~/.config/pwsp/hotkeys.json.
Daemon serves hotkey IPC commands for CLI/compositor bindings.
GUI supports focused hotkey triggers, a dedicated Hotkeys panel
with search and conflict detection, file badges, and a key chord
capture dialog. CLI gains play-hotkey, get hotkeys, set hotkey,
set hotkey-key, and clear-hotkey subcommands.
* feat: add global hotkey support via evdev
Listen for keyboard events directly from /dev/input using evdev,
enabling hotkeys to work system-wide regardless of window focus
or display server (X11, GNOME, KDE Plasma, Hyprland).
The daemon spawns async listeners for each keyboard device at
startup, tracks modifier state, and triggers playback when a
configured chord matches. Requires the user to be in the 'input'
group; logs a warning and continues without global hotkeys if
devices are inaccessible.
* various changes
* refactor: route hotkey mutations through daemon IPC
GUI no longer writes hotkey config directly to disk. Instead, all
mutations (set slot, set key chord, clear chord, remove slot) are
sent to the daemon via IPC, which persists the changes. The state
thread periodically syncs the hotkey config back from the daemon,
so CLI-made changes are reflected in the GUI.
New IPC commands: set_hotkey_action (arbitrary action per slot),
clear_hotkey_key (remove key chord without removing the slot).
Also removes unreachable capture overlay from draw_hotkeys().
* small refactor
---------
Co-authored-by: arabian <a.tevg@ya.ru>
Since the mka and mkv extensions are both Matroska format and share
magic bytes, mka should work perfectly fine, even though it isn't
explicitly mentioned by Symphonia. Tested it and it works, (as long as
the audio codec is supported).
* cargo fmt
* deps: bump clap to 4.6.0
* deps: cargo update
* Fix daemon autostart issue caused by sync pipewire retry loop (#43) (#44)
At boot time, PipeWire takes some time to register the `pwsp-daemon` and `pwsp-virtual-mic` devices. Previously, the daemon's retry loop for `link_player_to_virtual_mic()` was synchronous and limited to 5 attempts (1.5 seconds total). This caused systemd autostarts to fail with a code 1 if the devices were not yet available.
This change replaces the synchronous wait with an asynchronous `tokio::spawn` task. It will retry the link attempt up to 60 times with a 1-second delay without blocking the startup of the rest of the daemon. This prevents it from exiting abruptly during autostart.
Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com>
* deps: bump egui & eframe version to 0.34.1
* feat: replaced App::update with new App::logic and App::ui
* deps: bump egui_material_icons to 0.6.0
* deps: bump egui_dnd to 0.15.0
* fix: use .codepoint for icons
* refactor
* refactor: replaced deprecated CentralPanel::show with CentralPanel::show_inside
* refactor
* change version to 1.6.3
* update rust toolchain in github actions
* update freedesktop platform version to 25.08 for flaptak
* update github actions for flatpak builds
* add flatpak-builder installation inside actions
* add flathub configuration to actions
* add --user flag to flathub configuration
* remove sudo from flatpak actions
* Fix/dev flatpak actions 6082245116761610541 (#47)
* remove sudo
* remove steps
---------
Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com>
At boot time, PipeWire takes some time to register the `pwsp-daemon` and `pwsp-virtual-mic` devices. Previously, the daemon's retry loop for `link_player_to_virtual_mic()` was synchronous and limited to 5 attempts (1.5 seconds total). This caused systemd autostarts to fail with a code 1 if the devices were not yet available.
This change replaces the synchronous wait with an asynchronous `tokio::spawn` task. It will retry the link attempt up to 60 times with a 1-second delay without blocking the startup of the rest of the daemon. This prevents it from exiting abruptly during autostart.
Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com>
This commit replaces two instances of `contains_key` followed by
`get_mut().unwrap()` with the more idiomatic `if let Some(...)` pattern
in `src/utils/pipewire.rs`. This reduces redundant hash map lookups and
eliminates potential panics from `unwrap()`.
Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com>
Refactored the navigation logic in `src/gui/input.rs` to use idiomatic Rust patterns.
Replaced clunky manual index calculations and type casting with `match` expressions and modular arithmetic on `usize`.
This improvement enhances readability and maintainability by eliminating nested `if/else` blocks and potential overflow issues from integer casts.
🎯 **What:** The code health issue addressed
- Refactored directory and file navigation logic to use `usize` and modular arithmetic.
- Replaced manual wrap-around logic with idiomatic `match` expressions.
💡 **Why:** How this improves maintainability
- Eliminates unnecessary and potentially risky type casting (e.g., `i8`, `i64`).
- Reduces code nesting and complexity, making it easier to read and extend.
- Standardizes the circular navigation pattern across the GUI.
✅ **Verification:** How you confirmed the change is safe
- Manually reviewed and verified the logic for all key combinations (ArrowUp, ArrowDown, both, or none).
- Confirmed correct behavior for both initial selection (None) and existing selection (Some) states.
✨ **Result:** The improvement achieved
- Cleaner, more idiomatic Rust code for list navigation.
- Reduced potential for index-related bugs.
Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com>
Replaces manual vector extension and linear search with an iterator
chain. This avoids an unnecessary allocation and potential reallocation
of the `input_devices` vector and allows for short-circuiting if the
device is found early.
Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com>
Directly attempt to remove the daemon socket file and handle NotFound errors
instead of checking for its existence first. This prevents a potential
race condition where the file could be replaced between the check and
the removal.
Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com>
* 🧹 Refactor: Replace unsafe unwrap in get_audio_player
Co-authored-by: arabianq <55220741+arabianq@users.noreply.github.com>
* 🧹 Refactor: Replace unsafe unwrap in get_audio_player
Resolved GitHub CI failure where a syntax error was introduced due to a bad automated merge with main. Rebased cleanly to ensure only the get_audio_player code health changes are included.
Co-authored-by: arabianq <55220741+arabianq@users.noreply.github.com>
* Delete tests/perf_play.rs
---------
Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com>
* perf(audio_player): offload synchronous I/O and decoder init to spawn_blocking
Moved synchronous file system operations (`fs::File::open` and `file_path.exists()`) and CPU-bound decoder initialization (`Decoder::try_from`) inside the async `AudioPlayer::play` method to `tokio::task::spawn_blocking`.
This prevents starving the Tokio worker threads during disk operations and significantly reduces event loop latency.
Co-authored-by: arabianq <55220741+arabianq@users.noreply.github.com>
* perf(audio_player): offload synchronous I/O and decoder init to spawn_blocking
Moved synchronous file system operations (`fs::File::open` and `file_path.exists()`) and CPU-bound decoder initialization (`Decoder::try_from`) inside the async `AudioPlayer::play` method to `tokio::task::spawn_blocking`.
This prevents starving the Tokio worker threads during disk operations and significantly reduces event loop latency.
Co-authored-by: arabianq <55220741+arabianq@users.noreply.github.com>
---------
Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com>
Replaced sequential unwraps on pipewire properties ("node.id", "port.id", "port.name")
with an `if let` and `and_then()` pattern in `src/utils/pipewire.rs`. This provides
safety against daemon crashes when properties are missing or malformed by silently
returning instead of panicking.
Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com>
Offload synchronous `fs::File::open` and `Decoder::try_from` operations to `tokio::task::spawn_blocking` in `AudioPlayer::update`. This allows the Tokio runtime to process other asynchronous tasks concurrently without being blocked by file I/O operations and audio header decoding during looped playback.
Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com>
The daemon was allocating memory based on an unverified length prefix
sent over the unauthenticated Unix socket, potentially allowing a malicious
client to cause an Out-Of-Memory panic (DoS). A 10 MB size limit has been
introduced.
Note: The previously reported `unwrap()` panic on invalid JSON payloads
was already fixed and replaced with a safe `match` block in a prior commit.
Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com>
Moves the check `if let Some(current_input_name) = &audio_player.input_device_name`
outside the loop over `input_devices` in `GetFullStateCommand::execute`.
By creating two separate loops (one for when an input device name is selected,
and one for when it is not), we eliminate the overhead of evaluating the `Option`
on every single iteration of the `input_devices` array. This effectively unswitches
the loop and avoids repeatedly accessing the `audio_player` struct field.
Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com>