mirror of
https://github.com/arabianq/pipewire-soundpad.git
synced 2026-04-28 06:21:23 +00:00
a156df346b
* 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>
196 lines
5.6 KiB
Rust
196 lines
5.6 KiB
Rust
use clap::{Parser, Subcommand};
|
|
use pwsp::{
|
|
types::socket::Request,
|
|
utils::daemon::{make_request, wait_for_daemon},
|
|
};
|
|
use std::{error::Error, path::PathBuf};
|
|
|
|
#[derive(Parser, Debug)]
|
|
#[command(version, about, long_about = None)]
|
|
struct Cli {
|
|
#[clap(subcommand)]
|
|
command: Commands,
|
|
}
|
|
|
|
#[derive(Subcommand, Debug)]
|
|
enum Commands {
|
|
/// Perform an action (ping, pause, resume, toggle-pause, stop, play)
|
|
Action {
|
|
#[clap(subcommand)]
|
|
action: Actions,
|
|
},
|
|
/// Get information from the player (is paused, volume, position, duration, state, current-file-path, input, inputs)
|
|
Get {
|
|
#[clap(subcommand)]
|
|
parameter: GetCommands,
|
|
},
|
|
/// Set information in the player (volume, position, input)
|
|
Set {
|
|
#[clap(subcommand)]
|
|
parameter: SetCommands,
|
|
},
|
|
}
|
|
|
|
#[derive(Subcommand, Debug)]
|
|
enum Actions {
|
|
/// Ping the daemon
|
|
Ping,
|
|
/// Kill the daemon
|
|
Kill,
|
|
/// Pause audio playback
|
|
Pause {
|
|
#[clap(short, long)]
|
|
id: Option<u32>,
|
|
},
|
|
/// Resume audio playback
|
|
Resume {
|
|
#[clap(short, long)]
|
|
id: Option<u32>,
|
|
},
|
|
/// Toggle pause
|
|
TogglePause {
|
|
#[clap(short, long)]
|
|
id: Option<u32>,
|
|
},
|
|
/// Stop audio playback and clear the queue
|
|
Stop {
|
|
#[clap(short, long)]
|
|
id: Option<u32>,
|
|
},
|
|
/// Play a file
|
|
Play {
|
|
file_path: PathBuf,
|
|
#[clap(short, long)]
|
|
concurrent: bool,
|
|
},
|
|
/// Toggle loop
|
|
ToggleLoop {
|
|
#[clap(short, long)]
|
|
id: Option<u32>,
|
|
},
|
|
/// Play a sound by hotkey slot name
|
|
PlayHotkey { slot: String },
|
|
/// Remove a hotkey slot
|
|
ClearHotkey { slot: String },
|
|
}
|
|
|
|
#[derive(Subcommand, Debug)]
|
|
enum GetCommands {
|
|
/// Check if the player is paused
|
|
IsPaused,
|
|
/// Playback volume
|
|
Volume {
|
|
#[clap(short, long)]
|
|
id: Option<u32>,
|
|
},
|
|
/// Playback position (in seconds)
|
|
Position {
|
|
#[clap(short, long)]
|
|
id: Option<u32>,
|
|
},
|
|
/// Duration of the current file
|
|
Duration {
|
|
#[clap(short, long)]
|
|
id: Option<u32>,
|
|
},
|
|
/// Player state (Playing, Paused or Stopped)
|
|
State,
|
|
/// Get all playing tracks
|
|
Tracks,
|
|
/// Current audio input
|
|
Input,
|
|
/// All audio inputs
|
|
Inputs,
|
|
/// Version of the daemon
|
|
DaemonVersion,
|
|
/// Full player state
|
|
FullState,
|
|
/// All hotkey slots
|
|
Hotkeys,
|
|
}
|
|
|
|
#[derive(Subcommand, Debug)]
|
|
enum SetCommands {
|
|
/// Playback volume
|
|
Volume {
|
|
volume: f32,
|
|
#[clap(short, long)]
|
|
id: Option<u32>,
|
|
},
|
|
/// Playback position (in seconds)
|
|
Position {
|
|
position: f32,
|
|
#[clap(short, long)]
|
|
id: Option<u32>,
|
|
},
|
|
/// Audio input id (see pwsp-cli get inputs)
|
|
Input { name: String },
|
|
/// Enable or disable loop (true or false)
|
|
Loop {
|
|
enabled: String,
|
|
#[clap(short, long)]
|
|
id: Option<u32>,
|
|
},
|
|
/// Assign a sound file to a hotkey slot
|
|
Hotkey { slot: String, file_path: PathBuf },
|
|
/// Set the key chord for a hotkey slot (e.g. "Ctrl+Alt+1")
|
|
HotkeyKey { slot: String, key_chord: String },
|
|
}
|
|
|
|
#[tokio::main]
|
|
async fn main() -> Result<(), Box<dyn Error>> {
|
|
let cli = Cli::parse();
|
|
|
|
wait_for_daemon().await?;
|
|
|
|
let request = match cli.command {
|
|
Commands::Action { action } => match action {
|
|
Actions::Ping => Request::ping(),
|
|
Actions::Kill => Request::kill(),
|
|
Actions::Pause { id } => Request::pause(id),
|
|
Actions::Resume { id } => Request::resume(id),
|
|
Actions::TogglePause { id } => Request::toggle_pause(id),
|
|
Actions::Stop { id } => Request::stop(id),
|
|
Actions::Play {
|
|
file_path,
|
|
concurrent,
|
|
} => Request::play(&file_path.to_string_lossy(), concurrent),
|
|
Actions::ToggleLoop { id } => Request::toggle_loop(id),
|
|
Actions::PlayHotkey { slot } => Request::play_hotkey(&slot),
|
|
Actions::ClearHotkey { slot } => Request::clear_hotkey(&slot),
|
|
},
|
|
Commands::Get { parameter } => match parameter {
|
|
GetCommands::IsPaused => Request::get_is_paused(),
|
|
GetCommands::Volume { id } => Request::get_volume(id),
|
|
GetCommands::Position { id } => Request::get_position(id),
|
|
GetCommands::Duration { id } => Request::get_duration(id),
|
|
GetCommands::State => Request::get_state(),
|
|
GetCommands::Tracks => Request::get_tracks(),
|
|
GetCommands::Input => Request::get_input(),
|
|
GetCommands::Inputs => Request::get_inputs(),
|
|
GetCommands::DaemonVersion => Request::get_daemon_version(),
|
|
GetCommands::FullState => Request::get_full_state(),
|
|
GetCommands::Hotkeys => Request::get_hotkeys(),
|
|
},
|
|
Commands::Set { parameter } => match parameter {
|
|
SetCommands::Volume { volume, id } => Request::set_volume(volume, id),
|
|
SetCommands::Position { position, id } => Request::seek(position, id),
|
|
SetCommands::Input { name } => Request::set_input(&name),
|
|
SetCommands::Loop { enabled, id } => Request::set_loop(&enabled, id),
|
|
SetCommands::Hotkey { slot, file_path } => {
|
|
Request::set_hotkey(&slot, &file_path.to_string_lossy())
|
|
}
|
|
SetCommands::HotkeyKey { slot, key_chord } => {
|
|
Request::set_hotkey_key(&slot, &key_chord)
|
|
}
|
|
},
|
|
};
|
|
|
|
let response = make_request(request)
|
|
.await
|
|
.map_err(|e| e as Box<dyn Error>)?;
|
|
println!("{} : {}", response.status, response.message);
|
|
|
|
Ok(())
|
|
}
|