fix: hotkeys setting from pwsp-gui (#56)

* refactor: do not overwrite incorrect hotkeys config

* fix: hotkeys not saved via pwsp-gui
This commit is contained in:
Tarasov Aleksandr
2026-04-12 17:05:10 +03:00
committed by GitHub
parent cb56cb3a04
commit 42c0170044
6 changed files with 119 additions and 26 deletions
+19 -1
View File
@@ -70,8 +70,10 @@ enum Actions {
}, },
/// Play a sound by hotkey slot name /// Play a sound by hotkey slot name
PlayHotkey { slot: String }, PlayHotkey { slot: String },
/// Remove a hotkey slot /// Remove the hotkey slot
ClearHotkey { slot: String }, ClearHotkey { slot: String },
/// Clear the key chord for a hotkey slot
ClearHotkeyKey { slot: String },
} }
#[derive(Subcommand, Debug)] #[derive(Subcommand, Debug)]
@@ -135,6 +137,12 @@ enum SetCommands {
Hotkey { slot: String, file_path: PathBuf }, Hotkey { slot: String, file_path: PathBuf },
/// Set the key chord for a hotkey slot (e.g. "Ctrl+Alt+1") /// Set the key chord for a hotkey slot (e.g. "Ctrl+Alt+1")
HotkeyKey { slot: String, key_chord: String }, 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] #[tokio::main]
@@ -158,6 +166,7 @@ async fn main() -> Result<(), Box<dyn Error>> {
Actions::ToggleLoop { id } => Request::toggle_loop(id), Actions::ToggleLoop { id } => Request::toggle_loop(id),
Actions::PlayHotkey { slot } => Request::play_hotkey(&slot), Actions::PlayHotkey { slot } => Request::play_hotkey(&slot),
Actions::ClearHotkey { slot } => Request::clear_hotkey(&slot), Actions::ClearHotkey { slot } => Request::clear_hotkey(&slot),
Actions::ClearHotkeyKey { slot } => Request::clear_hotkey_key(&slot),
}, },
Commands::Get { parameter } => match parameter { Commands::Get { parameter } => match parameter {
GetCommands::IsPaused => Request::get_is_paused(), GetCommands::IsPaused => Request::get_is_paused(),
@@ -183,6 +192,15 @@ async fn main() -> Result<(), Box<dyn Error>> {
SetCommands::HotkeyKey { slot, key_chord } => { SetCommands::HotkeyKey { slot, key_chord } => {
Request::set_hotkey_key(&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::<Request>(&action)?,
&key_chord,
),
}, },
}; };
+8 -3
View File
@@ -221,16 +221,21 @@ impl SoundpadGui {
.to_string_lossy() .to_string_lossy()
.to_string(); .to_string();
let action = Request::play(&file_path.to_string_lossy(), false); 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 self.app_state
.hotkey_config .hotkey_config
.set_slot(slot_name.clone(), action); .set_slot(slot_name.clone(), action);
self.app_state self.app_state
.hotkey_config .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.hotkey_capture_active = false;
self.app_state.assigning_hotkey_slot = None;
self.app_state.assigning_hotkey_for_file = None;
} }
return; return;
} }
+63 -17
View File
@@ -99,22 +99,28 @@ pub struct SetHotkeyCommand {
pub file_path: Option<PathBuf>, pub file_path: Option<PathBuf>,
} }
pub struct SetHotkeyActionCommand {
pub slot: Option<String>,
pub action: Option<Request>,
}
pub struct SetHotkeyKeyCommand { pub struct SetHotkeyKeyCommand {
pub slot: Option<String>, pub slot: Option<String>,
pub key_chord: Option<String>, pub key_chord: Option<String>,
} }
pub struct ClearHotkeyCommand { pub struct SetHotkeyActionAndKeyCommand {
pub slot: Option<String>, pub slot: Option<String>,
pub action: Option<Request>,
pub key_chord: Option<String>,
} }
pub struct PlayHotkeyCommand { pub struct PlayHotkeyCommand {
pub slot: Option<String>, pub slot: Option<String>,
} }
pub struct SetHotkeyActionCommand { pub struct ClearHotkeyCommand {
pub slot: Option<String>, pub slot: Option<String>,
pub action: Option<Request>,
} }
pub struct ClearHotkeyKeyCommand { 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] #[async_trait]
impl Executable for SetHotkeyKeyCommand { impl Executable for SetHotkeyKeyCommand {
async fn execute(&self) -> Response { async fn execute(&self) -> Response {
@@ -583,24 +613,41 @@ impl Executable for SetHotkeyKeyCommand {
} }
#[async_trait] #[async_trait]
impl Executable for ClearHotkeyCommand { impl Executable for SetHotkeyActionAndKeyCommand {
async fn execute(&self) -> Response { async fn execute(&self) -> Response {
let Some(slot) = &self.slot else { let Some(slot) = &self.slot else {
return Response::new(false, "Missing slot name"); 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() { let mut config = match HotkeyConfig::load() {
Ok(c) => c, Ok(c) => c,
Err(err) => return Response::new(false, format!("Failed to load hotkeys: {}", err)), Err(err) => return Response::new(false, format!("Failed to load hotkeys: {}", err)),
}; };
if config.remove_slot(slot) { // Set the action and then the key chord
match config.save() { config.set_slot(slot.clone(), action.clone());
Ok(_) => Response::new(true, format!("Hotkey slot '{}' cleared", slot)), if !config.set_key_chord(slot, Some(key_chord.clone())) {
Err(err) => Response::new(false, format!("Failed to save hotkeys: {}", err)), return Response::new(
false,
format!("Slot '{}' not found after setting action", slot),
);
} }
} else {
Response::new(false, format!("Slot '{}' not found", 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,26 +679,25 @@ impl Executable for PlayHotkeyCommand {
} }
#[async_trait] #[async_trait]
impl Executable for SetHotkeyActionCommand { impl Executable for ClearHotkeyCommand {
async fn execute(&self) -> Response { async fn execute(&self) -> Response {
let Some(slot) = &self.slot else { let Some(slot) = &self.slot else {
return Response::new(false, "Missing slot name"); 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() { let mut config = match HotkeyConfig::load() {
Ok(c) => c, Ok(c) => c,
Err(err) => return Response::new(false, format!("Failed to load hotkeys: {}", err)), Err(err) => return Response::new(false, format!("Failed to load hotkeys: {}", err)),
}; };
config.set_slot(slot.clone(), action.clone()); if config.remove_slot(slot) {
match config.save() { match config.save() {
Ok(_) => Response::new(true, format!("Hotkey slot '{}' set", slot)), Ok(_) => Response::new(true, format!("Hotkey slot '{}' cleared", slot)),
Err(err) => Response::new(false, format!("Failed to save hotkeys: {}", err)), Err(err) => Response::new(false, format!("Failed to save hotkeys: {}", err)),
} }
} else {
Response::new(false, format!("Slot '{}' not found", slot))
}
} }
} }
+1 -2
View File
@@ -98,7 +98,6 @@ impl GuiConfig {
pub struct HotkeySlot { pub struct HotkeySlot {
pub slot: String, pub slot: String,
pub action: Request, pub action: Request,
#[serde(skip_serializing_if = "Option::is_none")]
pub key_chord: Option<String>, pub key_chord: Option<String>,
} }
@@ -121,7 +120,7 @@ impl HotkeyConfig {
let bytes = fs::read(&path)?; let bytes = fs::read(&path)?;
match serde_json::from_slice::<HotkeyConfig>(&bytes) { match serde_json::from_slice::<HotkeyConfig>(&bytes) {
Ok(config) => Ok(config), Ok(config) => Ok(config),
Err(_) => Ok(HotkeyConfig::default()), Err(e) => Err(e.into()),
} }
} }
+12
View File
@@ -208,6 +208,18 @@ impl Request {
pub fn clear_hotkey_key(slot: &str) -> Self { pub fn clear_hotkey_key(slot: &str) -> Self {
Request::new("clear_hotkey_key", vec![("slot", slot)]) 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)] #[derive(Default, Debug, Clone, Serialize, Deserialize)]
+13
View File
@@ -106,6 +106,19 @@ pub fn parse_command(request: &Request) -> Option<Box<dyn Executable + Send>> {
let slot = request.args.get("slot").cloned(); let slot = request.args.get("slot").cloned();
Some(Box::new(ClearHotkeyKeyCommand { slot })) 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::<Request>(s).ok());
let key_chord = request.args.get("key_chord").cloned();
Some(Box::new(SetHotkeyActionAndKeyCommand {
slot,
action,
key_chord,
}))
}
_ => None, _ => None,
} }
} }