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
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<dyn Error>> {
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<dyn Error>> {
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::<Request>(&action)?,
&key_chord,
),
},
};
+8 -3
View File
@@ -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;
}
+66 -20
View File
@@ -99,22 +99,28 @@ pub struct SetHotkeyCommand {
pub file_path: Option<PathBuf>,
}
pub struct SetHotkeyActionCommand {
pub slot: Option<String>,
pub action: Option<Request>,
}
pub struct SetHotkeyKeyCommand {
pub slot: Option<String>,
pub key_chord: Option<String>,
}
pub struct ClearHotkeyCommand {
pub struct SetHotkeyActionAndKeyCommand {
pub slot: Option<String>,
pub action: Option<Request>,
pub key_chord: Option<String>,
}
pub struct PlayHotkeyCommand {
pub slot: Option<String>,
}
pub struct SetHotkeyActionCommand {
pub struct ClearHotkeyCommand {
pub slot: Option<String>,
pub action: Option<Request>,
}
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))
}
}
}
+1 -2
View File
@@ -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<String>,
}
@@ -121,7 +120,7 @@ impl HotkeyConfig {
let bytes = fs::read(&path)?;
match serde_json::from_slice::<HotkeyConfig>(&bytes) {
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 {
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)]
+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();
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,
}
}