use crate::SettingsUI;
use crate::input_tab::capture_key_combo;
use par_term_config::snippets::CustomActionConfig;
use super::action_forms::*;
pub fn show_action_edit_form(
ui: &mut egui::Ui,
settings: &mut SettingsUI,
changes_this_frame: &mut bool,
edit_index: Option<usize>,
) {
ui.separator();
ui.horizontal(|ui| {
if ui.button("Save").clicked() {
let action = build_action_from_form(settings, edit_index);
if let Some(i) = edit_index {
settings.config.actions[i] = action;
} else {
settings.config.actions.push(action);
}
settings.has_changes = true;
*changes_this_frame = true;
settings.actions_tab.editing_action_index = None;
settings.actions_tab.adding_new_action = false;
}
if ui.button("Cancel").clicked() {
settings.actions_tab.editing_action_index = None;
settings.actions_tab.adding_new_action = false;
settings.actions_tab.recording_action_keybinding = false;
settings.actions_tab.action_recorded_combo = None;
}
});
ui.separator();
ui.scope(|ui| {
ui.label("Title:");
if ui
.text_edit_singleline(&mut settings.actions_tab.temp_action_title)
.changed()
{
*changes_this_frame = true;
}
ui.label("ID:");
ui.label(
egui::RichText::new(&settings.actions_tab.temp_action_id)
.monospace()
.small(),
);
ui.label("Type:");
let types = [
"Shell Command",
"New Tab",
"Insert Text",
"Key Sequence",
"Split Pane",
"Sequence",
"Condition",
"Repeat",
];
egui::ComboBox::from_id_salt("action_type")
.selected_text(types[settings.actions_tab.temp_action_type])
.width(150.0)
.show_ui(ui, |ui| {
for (i, &type_name) in types.iter().enumerate() {
if ui
.selectable_label(settings.actions_tab.temp_action_type == i, type_name)
.clicked()
{
settings.actions_tab.temp_action_type = i;
*changes_this_frame = true;
}
}
});
ui.label("Keybinding:");
ui.horizontal(|ui| {
if settings.actions_tab.recording_action_keybinding {
ui.label(egui::RichText::new("🔴 Recording...").color(egui::Color32::RED));
if let Some(combo) = capture_key_combo(ui) {
settings.actions_tab.action_recorded_combo = Some(combo.clone());
settings.actions_tab.temp_action_keybinding = combo;
settings.actions_tab.recording_action_keybinding = false;
*changes_this_frame = true;
}
} else {
if ui
.text_edit_singleline(&mut settings.actions_tab.temp_action_keybinding)
.changed()
{
*changes_this_frame = true;
}
if ui
.small_button("🎤")
.on_hover_text("Record keybinding")
.clicked()
{
settings.actions_tab.recording_action_keybinding = true;
settings.actions_tab.action_recorded_combo = None;
}
}
});
if !settings.actions_tab.recording_action_keybinding
&& !settings.actions_tab.temp_action_keybinding.is_empty()
{
let exclude_id = if let Some(i) = edit_index {
settings.config.actions.get(i).map(|a| a.id())
} else {
None
};
if let Some(conflict) = settings
.check_keybinding_conflict(&settings.actions_tab.temp_action_keybinding, exclude_id)
{
ui.label(
egui::RichText::new(format!("⚠️ {}", conflict))
.color(egui::Color32::from_rgb(255, 180, 0))
.small(),
);
}
}
ui.label("Prefix char:");
if ui
.text_edit_singleline(&mut settings.actions_tab.temp_action_prefix_char)
.changed()
{
settings.actions_tab.temp_action_prefix_char = settings
.actions_tab
.temp_action_prefix_char
.chars()
.find(|ch| !ch.is_whitespace())
.map(|ch| ch.to_string())
.unwrap_or_default();
*changes_this_frame = true;
}
if let Some(prefix_char) = settings.actions_tab.temp_action_prefix_char.chars().next() {
let exclude_id = if let Some(i) = edit_index {
settings.config.actions.get(i).map(|a| a.id())
} else {
None
};
if let Some(conflict) =
settings.check_action_prefix_char_conflict(prefix_char, exclude_id)
{
ui.label(
egui::RichText::new(format!("⚠️ {}", conflict))
.color(egui::Color32::from_rgb(255, 180, 0))
.small(),
);
}
}
match settings.actions_tab.temp_action_type {
0 => show_shell_command_form(ui, settings, changes_this_frame),
1 => show_new_tab_form(ui, settings, changes_this_frame),
2 => show_insert_text_form(ui, settings, changes_this_frame),
3 => show_key_sequence_form(ui, settings, changes_this_frame),
4 => show_split_pane_form(ui, settings, changes_this_frame),
5 => show_sequence_form(ui, settings, changes_this_frame),
6 => show_condition_form(ui, settings, changes_this_frame),
7 => show_repeat_form(ui, settings, changes_this_frame),
_ => {}
}
});
ui.separator();
}
pub(super) fn build_action_from_form(
settings: &SettingsUI,
edit_index: Option<usize>,
) -> CustomActionConfig {
let keybinding = if settings.actions_tab.temp_action_keybinding.is_empty() {
None
} else {
Some(settings.actions_tab.temp_action_keybinding.clone())
};
let prefix_char = settings.actions_tab.temp_action_prefix_char.chars().next();
let action = match settings.actions_tab.temp_action_type {
0 => CustomActionConfig::ShellCommand {
id: settings.actions_tab.temp_action_id.clone(),
title: settings.actions_tab.temp_action_title.clone(),
command: settings.actions_tab.temp_action_command.clone(),
args: if settings.actions_tab.temp_action_args.is_empty() {
Vec::new()
} else {
settings
.actions_tab
.temp_action_args
.split_whitespace()
.map(|s| s.to_string())
.collect()
},
notify_on_success: false,
timeout_secs: 30, capture_output: settings.actions_tab.temp_action_capture_output,
keybinding,
prefix_char,
keybinding_enabled: settings.actions_tab.temp_action_keybinding_enabled,
description: None,
},
1 => CustomActionConfig::NewTab {
id: settings.actions_tab.temp_action_id.clone(),
title: settings.actions_tab.temp_action_title.clone(),
command: if settings.actions_tab.temp_action_new_tab_command.is_empty() {
None
} else {
Some(settings.actions_tab.temp_action_new_tab_command.clone())
},
keybinding,
prefix_char,
keybinding_enabled: true,
description: None,
},
2 => CustomActionConfig::InsertText {
id: settings.actions_tab.temp_action_id.clone(),
title: settings.actions_tab.temp_action_title.clone(),
text: settings.actions_tab.temp_action_text.clone(),
variables: std::collections::HashMap::new(),
keybinding,
prefix_char,
keybinding_enabled: true,
description: None,
},
3 => CustomActionConfig::KeySequence {
id: settings.actions_tab.temp_action_id.clone(),
title: settings.actions_tab.temp_action_title.clone(),
keys: settings.actions_tab.temp_action_keys.clone(),
keybinding,
prefix_char,
keybinding_enabled: true,
description: None,
},
4 => CustomActionConfig::SplitPane {
id: settings.actions_tab.temp_action_id.clone(),
title: settings.actions_tab.temp_action_title.clone(),
direction: if settings.actions_tab.temp_action_split_direction == 0 {
par_term_config::snippets::ActionSplitDirection::Horizontal
} else {
par_term_config::snippets::ActionSplitDirection::Vertical
},
command: if settings.actions_tab.temp_action_split_command.is_empty() {
None
} else {
Some(settings.actions_tab.temp_action_split_command.clone())
},
command_is_direct: settings.actions_tab.temp_action_split_command_is_direct,
focus_new_pane: settings.actions_tab.temp_action_split_focus_new,
delay_ms: settings.actions_tab.temp_action_split_delay_ms,
split_percent: settings.actions_tab.temp_action_split_percent,
keybinding,
prefix_char,
keybinding_enabled: true,
description: None,
},
5 => {
let steps = settings
.actions_tab
.temp_action_steps
.iter()
.map(
|(id, delay, behavior)| par_term_config::snippets::SequenceStep {
action_id: id.clone(),
delay_ms: *delay,
on_failure: *behavior,
},
)
.collect();
CustomActionConfig::Sequence {
id: settings.actions_tab.temp_action_id.clone(),
title: settings.actions_tab.temp_action_title.clone(),
keybinding,
prefix_char,
keybinding_enabled: settings.actions_tab.temp_action_keybinding_enabled,
description: None,
steps,
}
}
6 => {
use par_term_config::snippets::ConditionCheck;
let check = match settings.actions_tab.temp_action_check_type {
0 => ConditionCheck::ExitCode {
value: settings
.actions_tab
.temp_action_check_value
.parse()
.unwrap_or(0),
},
1 => ConditionCheck::OutputContains {
pattern: settings.actions_tab.temp_action_check_value.clone(),
case_sensitive: settings.actions_tab.temp_action_case_sensitive,
},
2 => ConditionCheck::EnvVar {
name: settings.actions_tab.temp_action_env_name.clone(),
value: if settings.actions_tab.temp_action_env_check_existence {
None
} else {
Some(settings.actions_tab.temp_action_env_value.clone())
},
},
3 => ConditionCheck::DirMatches {
pattern: settings.actions_tab.temp_action_check_value.clone(),
},
4 => ConditionCheck::GitBranch {
pattern: settings.actions_tab.temp_action_check_value.clone(),
},
_ => ConditionCheck::ExitCode { value: 0 },
};
CustomActionConfig::Condition {
id: settings.actions_tab.temp_action_id.clone(),
title: settings.actions_tab.temp_action_title.clone(),
keybinding,
prefix_char,
keybinding_enabled: settings.actions_tab.temp_action_keybinding_enabled,
description: None,
check,
on_true_id: if settings.actions_tab.temp_action_on_true_id.is_empty() {
None
} else {
Some(settings.actions_tab.temp_action_on_true_id.clone())
},
on_false_id: if settings.actions_tab.temp_action_on_false_id.is_empty() {
None
} else {
Some(settings.actions_tab.temp_action_on_false_id.clone())
},
}
}
7 => CustomActionConfig::Repeat {
id: settings.actions_tab.temp_action_id.clone(),
title: settings.actions_tab.temp_action_title.clone(),
keybinding,
prefix_char,
keybinding_enabled: settings.actions_tab.temp_action_keybinding_enabled,
description: None,
action_id: settings.actions_tab.temp_action_repeat_action_id.clone(),
count: settings.actions_tab.temp_action_repeat_count.clamp(1, 100),
delay_ms: settings.actions_tab.temp_action_repeat_delay_ms,
stop_on_success: settings.actions_tab.temp_action_stop_on_success,
stop_on_failure: settings.actions_tab.temp_action_stop_on_failure,
},
_ => unreachable!(),
};
let Some(existing) = edit_index.and_then(|i| settings.config.actions.get(i)) else {
return action;
};
preserve_hidden_fields(action, existing)
}
fn preserve_hidden_fields(
mut action: CustomActionConfig,
existing: &CustomActionConfig,
) -> CustomActionConfig {
let old_base = existing.base();
let mut base = action.base();
base.description = old_base.description;
base.keybinding_enabled = old_base.keybinding_enabled;
action.apply_base(base);
match (&mut action, existing) {
(
CustomActionConfig::ShellCommand {
timeout_secs,
notify_on_success,
..
},
CustomActionConfig::ShellCommand {
timeout_secs: old_timeout,
notify_on_success: old_notify,
..
},
) => {
*timeout_secs = *old_timeout;
*notify_on_success = *old_notify;
}
(
CustomActionConfig::InsertText { variables, .. },
CustomActionConfig::InsertText {
variables: old_variables,
..
},
) => {
*variables = old_variables.clone();
}
_ => {}
}
action
}
#[cfg(test)]
mod tests {
use super::*;
use par_term_config::Config;
fn shell_action(timeout_secs: u64, description: Option<&str>) -> CustomActionConfig {
CustomActionConfig::ShellCommand {
id: "act".to_string(),
title: "Build".to_string(),
command: "make".to_string(),
args: vec!["build".to_string()],
notify_on_success: true,
timeout_secs,
capture_output: false,
keybinding: None,
prefix_char: None,
keybinding_enabled: false,
description: description.map(str::to_string),
}
}
fn settings_editing(action: CustomActionConfig) -> SettingsUI {
let config = Config {
actions: vec![action],
..Config::default()
};
let mut settings = SettingsUI::new_for_tests(config);
super::super::action_list::populate_edit_fields(&mut settings, 0);
settings
}
#[test]
fn saving_a_shell_command_preserves_fields_the_form_hides() {
let mut settings = settings_editing(shell_action(90, Some("runs the build")));
settings.actions_tab.temp_action_title = "Build all".to_string();
let CustomActionConfig::ShellCommand {
title,
timeout_secs,
notify_on_success,
keybinding_enabled,
description,
..
} = build_action_from_form(&settings, Some(0))
else {
panic!("type must stay ShellCommand");
};
assert_eq!(title, "Build all", "visible edits still apply");
assert_eq!(timeout_secs, 90);
assert!(notify_on_success);
assert!(!keybinding_enabled);
assert_eq!(description.as_deref(), Some("runs the build"));
}
#[test]
fn saving_insert_text_preserves_variables_and_keybinding_enabled() {
let mut variables = std::collections::HashMap::new();
variables.insert("name".to_string(), "value".to_string());
let action = CustomActionConfig::InsertText {
id: "ins".to_string(),
title: "Insert".to_string(),
text: "hello".to_string(),
variables: variables.clone(),
keybinding: None,
prefix_char: None,
keybinding_enabled: false,
description: Some("greets".to_string()),
};
let settings = settings_editing(action);
let CustomActionConfig::InsertText {
variables: saved_vars,
keybinding_enabled,
description,
..
} = build_action_from_form(&settings, Some(0))
else {
panic!("type must stay InsertText");
};
assert_eq!(saved_vars, variables);
assert!(!keybinding_enabled);
assert_eq!(description.as_deref(), Some("greets"));
}
#[test]
fn a_new_action_gets_the_documented_defaults() {
let mut settings = SettingsUI::new_for_tests(Config::default());
settings.actions_tab.temp_action_type = 0;
settings.actions_tab.temp_action_command = "ls".to_string();
let CustomActionConfig::ShellCommand {
timeout_secs,
description,
..
} = build_action_from_form(&settings, None)
else {
panic!("expected ShellCommand");
};
assert_eq!(timeout_secs, 30);
assert_eq!(description, None);
}
}