use crate::SettingsUI;
use crate::section::{collapsing_section, section_matches};
use par_term_config::automation::{TriggerActionConfig, TriggerSplitDirection, TriggerSplitTarget};
use std::collections::HashSet;
mod action_fields;
mod editor;
const ACTION_TYPE_NAMES: &[&str] = &[
"Highlight",
"Notify",
"Mark Line",
"Set Variable",
"Run Command",
"Play Sound",
"Send Text",
"Prettify",
"Split Pane",
];
fn default_action_for_type(type_index: usize) -> TriggerActionConfig {
match type_index {
0 => TriggerActionConfig::Highlight {
fg: None,
bg: Some([255, 255, 0]),
duration_ms: 5000,
},
1 => TriggerActionConfig::Notify {
title: "Trigger".to_string(),
message: "Pattern matched".to_string(),
},
2 => TriggerActionConfig::MarkLine {
label: None,
color: Some([0, 180, 255]),
},
3 => TriggerActionConfig::SetVariable {
name: String::new(),
value: String::new(),
},
4 => TriggerActionConfig::RunCommand {
command: String::new(),
args: Vec::new(),
},
5 => TriggerActionConfig::PlaySound {
sound_id: String::new(),
volume: 50,
},
6 => TriggerActionConfig::SendText {
text: String::new(),
delay_ms: 0,
},
7 => TriggerActionConfig::Prettify {
format: "json".to_string(),
scope: crate::config::automation::PrettifyScope::default(),
block_end: None,
sub_format: None,
command_filter: None,
},
8 => TriggerActionConfig::SplitPane {
direction: TriggerSplitDirection::Horizontal,
command: None,
focus_new_pane: true,
target: TriggerSplitTarget::default(),
split_percent: 66,
},
_ => TriggerActionConfig::Highlight {
fg: None,
bg: Some([255, 255, 0]),
duration_ms: 5000,
},
}
}
pub(super) fn show_triggers_section(
ui: &mut egui::Ui,
settings: &mut SettingsUI,
changes_this_frame: &mut bool,
collapsed: &mut HashSet<String>,
) {
if section_matches(
&settings.search_query.trim().to_lowercase(),
"Triggers",
&[
"trigger",
"regex",
"pattern",
"match",
"action",
"highlight",
"notify",
"badge",
"set variable",
"automatic",
],
) {
show_triggers_collapsing(ui, settings, changes_this_frame, collapsed);
}
}
fn show_triggers_collapsing(
ui: &mut egui::Ui,
settings: &mut SettingsUI,
changes_this_frame: &mut bool,
collapsed: &mut HashSet<String>,
) {
collapsing_section(
ui,
"Triggers",
"automation_triggers",
true,
collapsed,
|ui| {
ui.label("Define regex patterns to match terminal output and trigger actions.");
ui.add_space(4.0);
let has_unsafe_trigger = settings
.config
.triggers
.iter()
.any(|t| !t.prompt_before_run && t.actions.iter().any(|a| a.is_dangerous()));
if has_unsafe_trigger {
egui::Frame::new()
.fill(egui::Color32::from_rgb(80, 50, 10))
.inner_margin(egui::Margin::symmetric(8_i8, 6_i8))
.corner_radius(egui::CornerRadius::same(4))
.show(ui, |ui| {
ui.horizontal_wrapped(|ui| {
ui.label(
egui::RichText::new("Security Warning:")
.strong()
.color(egui::Color32::from_rgb(255, 190, 60)),
);
ui.label(
egui::RichText::new(
"One or more triggers have `prompt_before_run: false` \
with dangerous actions (RunCommand / SendText). \
These can be fired directly by terminal output — \
malicious content could exploit pattern matching to \
execute commands. The command denylist provides only \
limited protection. Review [unsafe] triggers below.",
)
.color(egui::Color32::from_rgb(220, 180, 100)),
);
});
});
ui.add_space(6.0);
}
let mut delete_index: Option<usize> = None;
let mut toggle_index: Option<usize> = None;
let mut start_edit_index: Option<usize> = None;
let trigger_count = settings.config.triggers.len();
for i in 0..trigger_count {
let trigger = &settings.config.triggers[i];
let is_editing =
settings.editing_trigger_index == Some(i) && !settings.adding_new_trigger;
if is_editing {
editor::show_trigger_edit_form(ui, settings, changes_this_frame, Some(i));
} else {
show_trigger_row(
ui,
trigger,
i,
&mut toggle_index,
&mut start_edit_index,
&mut delete_index,
);
}
}
if let Some(i) = toggle_index {
settings.config.triggers[i].enabled = !settings.config.triggers[i].enabled;
settings.has_changes = true;
*changes_this_frame = true;
}
if let Some(i) = delete_index {
settings.config.triggers.remove(i);
settings.has_changes = true;
*changes_this_frame = true;
if settings.editing_trigger_index == Some(i) {
settings.editing_trigger_index = None;
}
}
if let Some(i) = start_edit_index {
let trigger = &settings.config.triggers[i];
settings.editing_trigger_index = Some(i);
settings.adding_new_trigger = false;
settings.temp_trigger_name = trigger.name.clone();
settings.temp_trigger_pattern = trigger.pattern.clone();
settings.temp_trigger_actions = trigger.actions.clone();
settings.temp_trigger_prompt_before_run = trigger.prompt_before_run;
settings.trigger_pattern_error = None;
}
ui.add_space(4.0);
if settings.adding_new_trigger {
ui.separator();
ui.label(egui::RichText::new("New Trigger").strong());
editor::show_trigger_edit_form(ui, settings, changes_this_frame, None);
} else if settings.editing_trigger_index.is_none()
&& ui
.button("+ Add Trigger")
.on_hover_text("Add a new trigger definition")
.clicked()
{
settings.adding_new_trigger = true;
settings.editing_trigger_index = None;
settings.temp_trigger_name = String::new();
settings.temp_trigger_pattern = String::new();
settings.temp_trigger_actions = Vec::new();
settings.temp_trigger_prompt_before_run = true;
settings.trigger_pattern_error = None;
}
},
);
}
fn show_trigger_row(
ui: &mut egui::Ui,
trigger: &par_term_config::automation::TriggerConfig,
i: usize,
toggle_index: &mut Option<usize>,
start_edit_index: &mut Option<usize>,
delete_index: &mut Option<usize>,
) {
ui.horizontal(|ui| {
let mut enabled = trigger.enabled;
if ui.checkbox(&mut enabled, "").changed() {
*toggle_index = Some(i);
}
ui.label(egui::RichText::new(&trigger.name).strong());
ui.label(
egui::RichText::new(format!("/{}/", &trigger.pattern))
.monospace()
.color(egui::Color32::from_rgb(150, 150, 200)),
);
let action_count = trigger.actions.len();
ui.label(
egui::RichText::new(format!(
"{} action{}",
action_count,
if action_count == 1 { "" } else { "s" }
))
.color(egui::Color32::GRAY),
);
if !trigger.prompt_before_run && trigger.actions.iter().any(|a| a.is_dangerous()) {
ui.label(
egui::RichText::new("[unsafe]")
.small()
.color(egui::Color32::from_rgb(220, 160, 50)),
)
.on_hover_text(
"This trigger can execute dangerous actions \
(RunCommand/SendText) from passive terminal output",
);
}
if ui.small_button("Edit").clicked() {
*start_edit_index = Some(i);
}
if ui
.small_button(egui::RichText::new("Delete").color(egui::Color32::from_rgb(200, 80, 80)))
.clicked()
{
*delete_index = Some(i);
}
});
}