use super::ProfileModalUI;
use crate::input_tab::{capture_key_combo, display_key_combo};
use par_term_config::{KeyBinding, ProfileId};
#[derive(Debug, Clone, PartialEq)]
pub(crate) struct PendingBinding {
pub(crate) profile: ProfileId,
pub(crate) chord: Option<String>,
}
pub(crate) fn open_profile_action(profile: ProfileId) -> String {
format!("open_profile:{profile}")
}
pub(crate) fn apply_binding(bindings: &mut Vec<KeyBinding>, change: &PendingBinding) {
let action = open_profile_action(change.profile);
bindings.retain(|b| b.action != action);
if let Some(chord) = &change.chord {
bindings.push(KeyBinding {
key: chord.clone(),
action,
});
}
}
pub(crate) fn bound_chord(bindings: &[KeyBinding], profile: ProfileId) -> Option<String> {
let action = open_profile_action(profile);
bindings
.iter()
.find(|b| b.action == action)
.map(|b| b.key.clone())
}
impl ProfileModalUI {
pub(crate) fn editing_profile(&self) -> Option<ProfileId> {
self.editing_id
}
pub(crate) fn set_config_shortcut(&mut self, chord: Option<String>) {
self.config_shortcut = chord;
}
pub(crate) fn take_pending_binding(&mut self) -> Option<PendingBinding> {
self.pending_binding.take()
}
pub(crate) fn take_chord_to_check(&mut self) -> Option<(String, String)> {
let chord = self.unchecked_chord.take()?;
let id = self.editing_id?;
Some((chord, open_profile_action(id)))
}
pub(crate) fn set_shortcut_conflict(&mut self, conflict: Option<String>) {
self.shortcut_conflict = conflict;
}
pub(super) fn reset_shortcut_state(&mut self) {
self.staged_shortcut = None;
self.shortcut_recording = false;
self.shortcut_conflict = None;
self.unchecked_chord = None;
}
pub(super) fn commit_shortcut(&mut self, profile: ProfileId) {
if let Some(chord) = self.staged_shortcut.take() {
self.pending_binding = Some(PendingBinding { profile, chord });
}
self.reset_shortcut_state();
}
fn shown_shortcut(&self) -> Option<String> {
match &self.staged_shortcut {
Some(staged) => staged.clone(),
None => self.config_shortcut.clone().or_else(|| {
(!self.temp_keyboard_shortcut.is_empty())
.then(|| self.temp_keyboard_shortcut.clone())
}),
}
}
pub(super) fn render_shortcut_row(&mut self, ui: &mut egui::Ui) {
ui.label("Keyboard Shortcut:");
ui.horizontal(|ui| {
if self.shortcut_recording {
if let Some(chord) = capture_key_combo(ui) {
self.staged_shortcut = Some(Some(chord.clone()));
self.temp_keyboard_shortcut.clear();
self.unchecked_chord = Some(chord);
self.shortcut_recording = false;
} else if ui.input(|i| i.key_pressed(egui::Key::Escape)) {
self.shortcut_recording = false;
}
}
if self.shortcut_recording {
ui.colored_label(egui::Color32::YELLOW, "Press key combo... (Esc to cancel)");
} else {
match self.shown_shortcut() {
Some(chord) => ui.monospace(display_key_combo(&chord)),
None => ui.weak("(not set)"),
};
}
let label = if self.shortcut_recording {
"Cancel"
} else {
"Record"
};
if ui.button(label).clicked() {
self.shortcut_recording = !self.shortcut_recording;
self.shortcut_conflict = None;
}
if !self.shortcut_recording
&& self.shown_shortcut().is_some()
&& ui.button("Clear").clicked()
{
self.staged_shortcut = Some(None);
self.temp_keyboard_shortcut.clear();
self.shortcut_conflict = None;
}
});
ui.end_row();
if let Some(conflict) = &self.shortcut_conflict {
ui.label("");
ui.colored_label(
egui::Color32::RED,
format!("Keybinding conflict: {conflict}"),
);
ui.end_row();
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn kb(key: &str, action: &str) -> KeyBinding {
KeyBinding {
key: key.to_string(),
action: action.to_string(),
}
}
#[test]
fn recording_replaces_the_profiles_open_binding() {
let id = uuid::Uuid::new_v4();
let other = uuid::Uuid::new_v4();
let mut bindings = vec![
kb("Cmd+1", &open_profile_action(id)),
kb("Cmd+2", &open_profile_action(other)),
kb("Cmd+3", "new_tab"),
];
apply_binding(
&mut bindings,
&PendingBinding {
profile: id,
chord: Some("Cmd+9".to_string()),
},
);
assert_eq!(bound_chord(&bindings, id).as_deref(), Some("Cmd+9"));
assert_eq!(
bindings
.iter()
.filter(|b| b.action == open_profile_action(id))
.count(),
1,
"replaced, not duplicated"
);
assert_eq!(bound_chord(&bindings, other).as_deref(), Some("Cmd+2"));
assert_eq!(bindings.len(), 3);
}
#[test]
fn clearing_removes_only_that_profiles_binding() {
let id = uuid::Uuid::new_v4();
let mut bindings = vec![
kb("Cmd+1", &open_profile_action(id)),
kb("Cmd+3", "new_tab"),
];
apply_binding(
&mut bindings,
&PendingBinding {
profile: id,
chord: None,
},
);
assert_eq!(bound_chord(&bindings, id), None);
assert_eq!(bindings.len(), 1);
}
#[test]
fn editor_recording_lands_as_the_profiles_open_binding() {
let profile = par_term_config::Profile::new("work");
let id = profile.id;
let mut modal = ProfileModalUI::new();
modal.add_profile_for_test(profile);
modal.start_edit_for_test(id);
let mut bindings = vec![kb("Cmd+1", &open_profile_action(id))];
modal.set_config_shortcut(bound_chord(&bindings, id));
modal.staged_shortcut = Some(Some("Cmd+Shift+9".to_string()));
modal.unchecked_chord = Some("Cmd+Shift+9".to_string());
assert_eq!(
modal.take_chord_to_check(),
Some(("Cmd+Shift+9".to_string(), open_profile_action(id)))
);
modal.save_form();
let change = modal.take_pending_binding().expect("Done stages a binding");
apply_binding(&mut bindings, &change);
assert_eq!(bindings.len(), 1);
assert_eq!(bound_chord(&bindings, id).as_deref(), Some("Cmd+Shift+9"));
assert!(modal.take_pending_binding().is_none());
}
#[test]
fn footer_save_with_the_editor_open_keeps_the_recorded_shortcut() {
let profile = par_term_config::Profile::new("work");
let id = profile.id;
let mut settings =
crate::settings_ui::SettingsUI::new_for_tests(par_term_config::Config::default());
settings.sync_profiles(vec![profile]);
settings.profile_modal_ui.start_edit_for_test(id);
settings.profile_modal_ui.staged_shortcut = Some(Some("Cmd+Shift+9".to_string()));
let saved = settings.request_save().expect("the form is valid");
assert_eq!(
bound_chord(&saved.keybindings, id).as_deref(),
Some("Cmd+Shift+9")
);
settings.take_profile_save_request();
assert!(!settings.has_unsaved_changes());
}
}