Files
pipewire-soundpad/src/bin/cli.rs
T
RiDDiX a156df346b feat: add hotkey system (#48)
* 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>
2026-04-06 21:43:41 +03:00

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(())
}