From 42c0170044a35fd95a1089808a3646bfa9830624 Mon Sep 17 00:00:00 2001 From: Tarasov Aleksandr <55220741+arabianq@users.noreply.github.com> Date: Sun, 12 Apr 2026 17:05:10 +0300 Subject: [PATCH] fix: hotkeys setting from pwsp-gui (#56) * refactor: do not overwrite incorrect hotkeys config * fix: hotkeys not saved via pwsp-gui --- src/bin/cli.rs | 20 +++++++++- src/gui/input.rs | 11 ++++-- src/types/commands.rs | 86 +++++++++++++++++++++++++++++++++---------- src/types/config.rs | 3 +- src/types/socket.rs | 12 ++++++ src/utils/commands.rs | 13 +++++++ 6 files changed, 119 insertions(+), 26 deletions(-) diff --git a/src/bin/cli.rs b/src/bin/cli.rs index c509ff5..88223c3 100644 --- a/src/bin/cli.rs +++ b/src/bin/cli.rs @@ -70,8 +70,10 @@ enum Actions { }, /// Play a sound by hotkey slot name PlayHotkey { slot: String }, - /// Remove a hotkey slot + /// Remove the hotkey slot ClearHotkey { slot: String }, + /// Clear the key chord for a hotkey slot + ClearHotkeyKey { slot: String }, } #[derive(Subcommand, Debug)] @@ -135,6 +137,12 @@ enum SetCommands { 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 }, + /// Atomically set the action and key chord for a hotkey slot + HotkeyActionAndKey { + slot: String, + action: String, + key_chord: String, + }, } #[tokio::main] @@ -158,6 +166,7 @@ async fn main() -> Result<(), Box> { Actions::ToggleLoop { id } => Request::toggle_loop(id), Actions::PlayHotkey { slot } => Request::play_hotkey(&slot), Actions::ClearHotkey { slot } => Request::clear_hotkey(&slot), + Actions::ClearHotkeyKey { slot } => Request::clear_hotkey_key(&slot), }, Commands::Get { parameter } => match parameter { GetCommands::IsPaused => Request::get_is_paused(), @@ -183,6 +192,15 @@ async fn main() -> Result<(), Box> { SetCommands::HotkeyKey { slot, key_chord } => { Request::set_hotkey_key(&slot, &key_chord) } + SetCommands::HotkeyActionAndKey { + slot, + action, + key_chord, + } => Request::set_hotkey_action_and_key( + &slot, + &serde_json::from_str::(&action)?, + &key_chord, + ), }, }; diff --git a/src/gui/input.rs b/src/gui/input.rs index 9ca1c7e..9b706d9 100644 --- a/src/gui/input.rs +++ b/src/gui/input.rs @@ -221,16 +221,21 @@ impl SoundpadGui { .to_string_lossy() .to_string(); let action = Request::play(&file_path.to_string_lossy(), false); - make_request_async(Request::set_hotkey_action(&slot_name, &action)); - make_request_async(Request::set_hotkey_key(&slot_name, &chord)); + + make_request_async(Request::set_hotkey_action_and_key( + &slot_name, &action, &chord, + )); + self.app_state .hotkey_config .set_slot(slot_name.clone(), action); self.app_state .hotkey_config - .set_key_chord(&slot_name, Some(chord)); + .set_key_chord(&slot_name, Some(chord.clone())); } self.app_state.hotkey_capture_active = false; + self.app_state.assigning_hotkey_slot = None; + self.app_state.assigning_hotkey_for_file = None; } return; } diff --git a/src/types/commands.rs b/src/types/commands.rs index 2aa2dbe..00acd24 100644 --- a/src/types/commands.rs +++ b/src/types/commands.rs @@ -99,22 +99,28 @@ pub struct SetHotkeyCommand { pub file_path: Option, } +pub struct SetHotkeyActionCommand { + pub slot: Option, + pub action: Option, +} + pub struct SetHotkeyKeyCommand { pub slot: Option, pub key_chord: Option, } -pub struct ClearHotkeyCommand { +pub struct SetHotkeyActionAndKeyCommand { pub slot: Option, + pub action: Option, + pub key_chord: Option, } pub struct PlayHotkeyCommand { pub slot: Option, } -pub struct SetHotkeyActionCommand { +pub struct ClearHotkeyCommand { pub slot: Option, - pub action: Option, } pub struct ClearHotkeyKeyCommand { @@ -553,6 +559,30 @@ impl Executable for SetHotkeyCommand { } } +#[async_trait] +impl Executable for SetHotkeyActionCommand { + async fn execute(&self) -> Response { + let Some(slot) = &self.slot else { + return Response::new(false, "Missing slot name"); + }; + let Some(action) = &self.action else { + return Response::new(false, "Missing or invalid action"); + }; + + let mut config = match HotkeyConfig::load() { + Ok(c) => c, + Err(err) => return Response::new(false, format!("Failed to load hotkeys: {}", err)), + }; + + config.set_slot(slot.clone(), action.clone()); + + match config.save() { + Ok(_) => Response::new(true, format!("Hotkey slot '{}' set", slot)), + Err(err) => Response::new(false, format!("Failed to save hotkeys: {}", err)), + } + } +} + #[async_trait] impl Executable for SetHotkeyKeyCommand { async fn execute(&self) -> Response { @@ -583,24 +613,41 @@ impl Executable for SetHotkeyKeyCommand { } #[async_trait] -impl Executable for ClearHotkeyCommand { +impl Executable for SetHotkeyActionAndKeyCommand { async fn execute(&self) -> Response { let Some(slot) = &self.slot else { return Response::new(false, "Missing slot name"); }; + let Some(action) = &self.action else { + return Response::new(false, "Missing or invalid action"); + }; + let Some(key_chord) = &self.key_chord else { + return Response::new(false, "Missing key chord"); + }; let mut config = match HotkeyConfig::load() { Ok(c) => c, Err(err) => return Response::new(false, format!("Failed to load hotkeys: {}", err)), }; - if config.remove_slot(slot) { - match config.save() { - Ok(_) => Response::new(true, format!("Hotkey slot '{}' cleared", slot)), - Err(err) => Response::new(false, format!("Failed to save hotkeys: {}", err)), - } - } else { - Response::new(false, format!("Slot '{}' not found", slot)) + // Set the action and then the key chord + config.set_slot(slot.clone(), action.clone()); + if !config.set_key_chord(slot, Some(key_chord.clone())) { + return Response::new( + false, + format!("Slot '{}' not found after setting action", slot), + ); + } + + match config.save() { + Ok(_) => Response::new( + true, + format!( + "Hotkey slot '{}' set with action and key chord '{}'", + slot, key_chord + ), + ), + Err(err) => Response::new(false, format!("Failed to save hotkeys: {}", err)), } } } @@ -632,25 +679,24 @@ impl Executable for PlayHotkeyCommand { } #[async_trait] -impl Executable for SetHotkeyActionCommand { +impl Executable for ClearHotkeyCommand { async fn execute(&self) -> Response { let Some(slot) = &self.slot else { return Response::new(false, "Missing slot name"); }; - let Some(action) = &self.action else { - return Response::new(false, "Missing or invalid action"); - }; let mut config = match HotkeyConfig::load() { Ok(c) => c, Err(err) => return Response::new(false, format!("Failed to load hotkeys: {}", err)), }; - config.set_slot(slot.clone(), action.clone()); - - match config.save() { - Ok(_) => Response::new(true, format!("Hotkey slot '{}' set", slot)), - Err(err) => Response::new(false, format!("Failed to save hotkeys: {}", err)), + if config.remove_slot(slot) { + match config.save() { + Ok(_) => Response::new(true, format!("Hotkey slot '{}' cleared", slot)), + Err(err) => Response::new(false, format!("Failed to save hotkeys: {}", err)), + } + } else { + Response::new(false, format!("Slot '{}' not found", slot)) } } } diff --git a/src/types/config.rs b/src/types/config.rs index 0c6ce76..81127bd 100644 --- a/src/types/config.rs +++ b/src/types/config.rs @@ -98,7 +98,6 @@ impl GuiConfig { pub struct HotkeySlot { pub slot: String, pub action: Request, - #[serde(skip_serializing_if = "Option::is_none")] pub key_chord: Option, } @@ -121,7 +120,7 @@ impl HotkeyConfig { let bytes = fs::read(&path)?; match serde_json::from_slice::(&bytes) { Ok(config) => Ok(config), - Err(_) => Ok(HotkeyConfig::default()), + Err(e) => Err(e.into()), } } diff --git a/src/types/socket.rs b/src/types/socket.rs index 7fda4cd..769901c 100644 --- a/src/types/socket.rs +++ b/src/types/socket.rs @@ -208,6 +208,18 @@ impl Request { pub fn clear_hotkey_key(slot: &str) -> Self { Request::new("clear_hotkey_key", vec![("slot", slot)]) } + + pub fn set_hotkey_action_and_key(slot: &str, action: &Request, key_chord: &str) -> Self { + let action_json = serde_json::to_string(action).unwrap_or_default(); + Request::new( + "set_hotkey_action_and_key", + vec![ + ("slot", slot), + ("action", &action_json), + ("key_chord", key_chord), + ], + ) + } } #[derive(Default, Debug, Clone, Serialize, Deserialize)] diff --git a/src/utils/commands.rs b/src/utils/commands.rs index 048af7d..8864e07 100644 --- a/src/utils/commands.rs +++ b/src/utils/commands.rs @@ -106,6 +106,19 @@ pub fn parse_command(request: &Request) -> Option> { let slot = request.args.get("slot").cloned(); Some(Box::new(ClearHotkeyKeyCommand { slot })) } + "set_hotkey_action_and_key" => { + let slot = request.args.get("slot").cloned(); + let action = request + .args + .get("action") + .and_then(|s| serde_json::from_str::(s).ok()); + let key_chord = request.args.get("key_chord").cloned(); + Some(Box::new(SetHotkeyActionAndKeyCommand { + slot, + action, + key_chord, + })) + } _ => None, } }