1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
//! Scripting, coprocess, and trigger state for a terminal tab.
//!
//! Groups all fields related to script execution, coprocess management,
//! and trigger handling.
use crate::scripting::manager::ScriptId;
use crate::terminal::TerminalManager;
use par_term_config::ScriptConfig;
/// Scripting, coprocess, and trigger state for a terminal tab.
pub(crate) struct TabScriptingState {
/// Script manager for this tab
pub(crate) script_manager: crate::scripting::manager::ScriptManager,
/// Maps config index to ScriptId for running scripts
pub(crate) script_ids: Vec<Option<crate::scripting::manager::ScriptId>>,
/// Observer IDs registered with the terminal for script event forwarding
pub(crate) script_observer_ids: Vec<Option<par_term_emu_core_rust::observer::ObserverId>>,
/// Event forwarders (shared with observer registration)
pub(crate) script_forwarders:
Vec<Option<std::sync::Arc<crate::scripting::observer::ScriptEventForwarder>>>,
/// Mapping from config index to coprocess ID (for UI tracking)
pub(crate) coprocess_ids: Vec<Option<par_term_emu_core_rust::coprocess::CoprocessId>>,
/// Trigger-generated scrollbar marks (from MarkLine actions)
pub(crate) trigger_marks: Vec<crate::scrollback_metadata::ScrollbackMark>,
/// Security metadata: maps trigger_id -> prompt_before_run flag.
/// When true, dangerous actions show a confirmation dialog instead of executing automatically.
pub(crate) trigger_prompt_before_run: std::collections::HashMap<u64, bool>,
/// Rate limiter for output-triggered dangerous actions.
pub(crate) trigger_rate_limiter: par_term_config::TriggerRateLimiter,
}
impl TabScriptingState {
/// Spawn the script at `config_index` and register its event forwarder as a
/// terminal observer, recording all per-index tracking state on success.
///
/// Shared by the tab-creation auto-start loop and the Settings UI start
/// button so both paths populate `script_ids` / `script_observer_ids` /
/// `script_forwarders` identically. Callers are responsible for checking
/// [`ScriptConfig::enabled`] — this method starts whatever it is given.
///
/// On spawn failure the observer is unregistered again so a failed start
/// leaves no dangling observer on the terminal.
///
/// Any previous script at `config_index` is torn down first. A script that
/// exits on its own leaves its observer registered (only an explicit stop
/// removes it), so restarting after a crash would otherwise orphan the old
/// forwarder — still attached to the terminal, still being pushed events
/// into an unbounded buffer that nobody drains.
///
/// # Errors
/// Returns the underlying spawn error string if the process cannot start.
pub(crate) fn start_script_at(
&mut self,
terminal: &TerminalManager,
config_index: usize,
script_config: &ScriptConfig,
) -> Result<ScriptId, String> {
self.clear_script_at(terminal, config_index);
let subscription_filter = if script_config.subscriptions.is_empty() {
None
} else {
Some(
script_config
.subscriptions
.iter()
.cloned()
.collect::<std::collections::HashSet<String>>(),
)
};
let forwarder = std::sync::Arc::new(crate::scripting::observer::ScriptEventForwarder::new(
subscription_filter,
));
let observer_id = terminal.add_observer(forwarder.clone());
match self.script_manager.start_script(script_config) {
Ok(script_id) => {
let slots_needed = config_index + 1;
if self.script_ids.len() < slots_needed {
self.script_ids.resize(slots_needed, None);
}
if self.script_observer_ids.len() < slots_needed {
self.script_observer_ids.resize(slots_needed, None);
}
if self.script_forwarders.len() < slots_needed {
self.script_forwarders.resize(slots_needed, None);
}
self.script_ids[config_index] = Some(script_id);
self.script_observer_ids[config_index] = Some(observer_id);
self.script_forwarders[config_index] = Some(forwarder);
Ok(script_id)
}
Err(e) => {
terminal.remove_observer(observer_id);
Err(e)
}
}
}
/// Stop the script at `config_index` (if any), unregister its observer, and
/// clear its tracking slots.
///
/// Safe to call for an index that was never started or has already exited.
pub(crate) fn clear_script_at(&mut self, terminal: &TerminalManager, config_index: usize) {
if let Some(slot) = self.script_ids.get_mut(config_index)
&& let Some(script_id) = slot.take()
{
self.script_manager.stop_script(script_id);
log::info!(
"Stopped script at index {} (id={})",
config_index,
script_id
);
}
if let Some(slot) = self.script_observer_ids.get_mut(config_index)
&& let Some(observer_id) = slot.take()
{
terminal.remove_observer(observer_id);
}
if let Some(slot) = self.script_forwarders.get_mut(config_index) {
*slot = None;
}
}
}
impl Default for TabScriptingState {
fn default() -> Self {
Self {
script_manager: crate::scripting::manager::ScriptManager::new(),
script_ids: Vec::new(),
script_observer_ids: Vec::new(),
script_forwarders: Vec::new(),
coprocess_ids: Vec::new(),
trigger_marks: Vec::new(),
trigger_prompt_before_run: std::collections::HashMap::new(),
trigger_rate_limiter: par_term_config::TriggerRateLimiter::default(),
}
}
}