1use std::collections::{HashMap, HashSet, VecDeque};
8use std::sync::Mutex;
9
10use par_term_emu_core_rust::observer::TerminalObserver;
11use par_term_emu_core_rust::terminal::TerminalEvent;
12
13use super::protocol::{ScriptEvent, ScriptEventData};
14
15const MAX_BUFFERED_EVENTS: usize = 1024;
24
25struct EventBuffer {
27 events: VecDeque<ScriptEvent>,
28 overflow_reported: bool,
33}
34
35pub struct ScriptEventForwarder {
41 subscription_filter: Option<HashSet<String>>,
44 event_buffer: Mutex<EventBuffer>,
47}
48
49impl ScriptEventForwarder {
50 pub fn new(subscriptions: Option<HashSet<String>>) -> Self {
56 Self {
57 subscription_filter: subscriptions,
58 event_buffer: Mutex::new(EventBuffer {
59 events: VecDeque::new(),
60 overflow_reported: false,
61 }),
62 }
63 }
64
65 pub fn drain_events(&self) -> Vec<ScriptEvent> {
67 let mut buf = self.event_buffer.lock().expect("event_buffer poisoned");
68 buf.events.drain(..).collect()
69 }
70
71 fn event_kind_name(event: &TerminalEvent) -> String {
73 match event {
74 TerminalEvent::BellRang(_) => "bell_rang".to_string(),
75 TerminalEvent::TitleChanged(_) => "title_changed".to_string(),
76 TerminalEvent::SizeChanged(_, _) => "size_changed".to_string(),
77 TerminalEvent::ModeChanged(_, _) => "mode_changed".to_string(),
78 TerminalEvent::GraphicsAdded(_) => "graphics_added".to_string(),
79 TerminalEvent::HyperlinkAdded { .. } => "hyperlink_added".to_string(),
80 TerminalEvent::DirtyRegion(_, _) => "dirty_region".to_string(),
81 TerminalEvent::CwdChanged(_) => "cwd_changed".to_string(),
82 TerminalEvent::TriggerMatched(_) => "trigger_matched".to_string(),
83 TerminalEvent::UserVarChanged { .. } => "user_var_changed".to_string(),
84 TerminalEvent::ProgressBarChanged { .. } => "progress_bar_changed".to_string(),
85 TerminalEvent::BadgeChanged(_) => "badge_changed".to_string(),
86 TerminalEvent::ShellIntegrationEvent { .. } => "command_complete".to_string(),
87 TerminalEvent::ZoneOpened { .. } => "zone_opened".to_string(),
88 TerminalEvent::ZoneClosed { .. } => "zone_closed".to_string(),
89 TerminalEvent::ZoneScrolledOut { .. } => "zone_scrolled_out".to_string(),
90 TerminalEvent::EnvironmentChanged { .. } => "environment_changed".to_string(),
91 TerminalEvent::RemoteHostTransition { .. } => "remote_host_transition".to_string(),
92 TerminalEvent::SubShellDetected { .. } => "sub_shell_detected".to_string(),
93 TerminalEvent::FileTransferStarted { .. } => "file_transfer_started".to_string(),
94 TerminalEvent::FileTransferProgress { .. } => "file_transfer_progress".to_string(),
95 TerminalEvent::FileTransferCompleted { .. } => "file_transfer_completed".to_string(),
96 TerminalEvent::FileTransferFailed { .. } => "file_transfer_failed".to_string(),
97 TerminalEvent::UploadRequested { .. } => "upload_requested".to_string(),
98 TerminalEvent::ScreenCleared { .. } => "screen_cleared".to_string(),
99 }
100 }
101
102 fn convert_event(event: &TerminalEvent) -> ScriptEvent {
104 let kind = Self::event_kind_name(event);
105
106 let data = match event {
107 TerminalEvent::BellRang(_) => ScriptEventData::Empty {},
108
109 TerminalEvent::TitleChanged(title) => ScriptEventData::TitleChanged {
110 title: title.clone(),
111 },
112
113 TerminalEvent::SizeChanged(cols, rows) => ScriptEventData::SizeChanged {
114 cols: *cols,
115 rows: *rows,
116 },
117
118 TerminalEvent::CwdChanged(cwd_change) => ScriptEventData::CwdChanged {
119 cwd: cwd_change.new_cwd.clone(),
120 },
121
122 TerminalEvent::UserVarChanged {
123 name,
124 value,
125 old_value,
126 } => ScriptEventData::VariableChanged {
127 name: name.clone(),
128 value: value.clone(),
129 old_value: old_value.clone(),
130 },
131
132 TerminalEvent::EnvironmentChanged {
133 key,
134 value,
135 old_value,
136 } => ScriptEventData::EnvironmentChanged {
137 key: key.clone(),
138 value: value.clone(),
139 old_value: old_value.clone(),
140 },
141
142 TerminalEvent::BadgeChanged(text) => {
143 ScriptEventData::BadgeChanged { text: text.clone() }
144 }
145
146 TerminalEvent::ShellIntegrationEvent {
147 command, exit_code, ..
148 } => ScriptEventData::CommandComplete {
149 command: command.clone().unwrap_or_default(),
150 exit_code: *exit_code,
151 },
152
153 TerminalEvent::TriggerMatched(trigger_match) => ScriptEventData::TriggerMatched {
154 pattern: format!("trigger:{}", trigger_match.trigger_id),
155 matched_text: trigger_match.text.clone(),
156 line: trigger_match.row,
157 },
158
159 TerminalEvent::ZoneOpened {
160 zone_id, zone_type, ..
161 } => ScriptEventData::ZoneEvent {
162 zone_id: *zone_id as u64,
163 zone_type: zone_type.to_string(),
164 event: "opened".to_string(),
165 },
166
167 TerminalEvent::ZoneClosed {
168 zone_id, zone_type, ..
169 } => ScriptEventData::ZoneEvent {
170 zone_id: *zone_id as u64,
171 zone_type: zone_type.to_string(),
172 event: "closed".to_string(),
173 },
174
175 TerminalEvent::ZoneScrolledOut {
176 zone_id, zone_type, ..
177 } => ScriptEventData::ZoneEvent {
178 zone_id: *zone_id as u64,
179 zone_type: zone_type.to_string(),
180 event: "scrolled_out".to_string(),
181 },
182
183 other => {
185 let mut fields = HashMap::new();
186 fields.insert(
187 "debug".to_string(),
188 serde_json::Value::String(format!("{:?}", other)),
189 );
190 ScriptEventData::Generic { fields }
191 }
192 };
193
194 ScriptEvent { kind, data }
195 }
196}
197
198impl TerminalObserver for ScriptEventForwarder {
209 fn on_event(&self, event: &TerminalEvent) {
210 if let Some(ref filter) = self.subscription_filter {
212 let kind = Self::event_kind_name(event);
213 if !filter.contains(&kind) {
214 return;
215 }
216 }
217
218 let script_event = Self::convert_event(event);
219 let mut buf = self.event_buffer.lock().expect("event_buffer poisoned");
220
221 if buf.events.len() >= MAX_BUFFERED_EVENTS {
222 buf.events.pop_front();
226 if !buf.overflow_reported {
227 buf.overflow_reported = true;
228 log::warn!(
229 "Script event buffer reached its {} event cap; discarding oldest \
230 events from here on. Nothing is draining this forwarder — the \
231 script most likely exited while its observer stayed registered.",
232 MAX_BUFFERED_EVENTS
233 );
234 }
235 }
236
237 buf.events.push_back(script_event);
238 }
239
240 }
244
245#[cfg(test)]
246mod tests {
247 use super::*;
248
249 #[test]
250 fn test_event_kind_name_bell() {
251 let event =
252 TerminalEvent::BellRang(par_term_emu_core_rust::terminal::BellEvent::VisualBell);
253 assert_eq!(ScriptEventForwarder::event_kind_name(&event), "bell_rang");
254 }
255
256 #[test]
257 fn test_event_kind_name_title() {
258 let event = TerminalEvent::TitleChanged("hello".to_string());
259 assert_eq!(
260 ScriptEventForwarder::event_kind_name(&event),
261 "title_changed"
262 );
263 }
264
265 #[test]
266 fn test_convert_bell_event() {
267 let event =
268 TerminalEvent::BellRang(par_term_emu_core_rust::terminal::BellEvent::VisualBell);
269 let script_event = ScriptEventForwarder::convert_event(&event);
270 assert_eq!(script_event.kind, "bell_rang");
271 assert_eq!(script_event.data, ScriptEventData::Empty {});
272 }
273
274 #[test]
275 fn test_convert_title_event() {
276 let event = TerminalEvent::TitleChanged("My Title".to_string());
277 let script_event = ScriptEventForwarder::convert_event(&event);
278 assert_eq!(script_event.kind, "title_changed");
279 assert_eq!(
280 script_event.data,
281 ScriptEventData::TitleChanged {
282 title: "My Title".to_string(),
283 }
284 );
285 }
286
287 #[test]
288 fn test_convert_size_event() {
289 let event = TerminalEvent::SizeChanged(120, 40);
290 let script_event = ScriptEventForwarder::convert_event(&event);
291 assert_eq!(script_event.kind, "size_changed");
292 assert_eq!(
293 script_event.data,
294 ScriptEventData::SizeChanged {
295 cols: 120,
296 rows: 40,
297 }
298 );
299 }
300
301 #[test]
302 fn test_forwarder_no_filter_captures_all() {
303 let fwd = ScriptEventForwarder::new(None);
304 let bell = TerminalEvent::BellRang(par_term_emu_core_rust::terminal::BellEvent::VisualBell);
305 let title = TerminalEvent::TitleChanged("t".to_string());
306
307 fwd.on_event(&bell);
308 fwd.on_event(&title);
309
310 let events = fwd.drain_events();
311 assert_eq!(events.len(), 2);
312 assert_eq!(events[0].kind, "bell_rang");
313 assert_eq!(events[1].kind, "title_changed");
314 }
315
316 #[test]
317 fn test_forwarder_filters_by_subscription() {
318 let filter = HashSet::from(["bell_rang".to_string()]);
319 let fwd = ScriptEventForwarder::new(Some(filter));
320
321 let bell = TerminalEvent::BellRang(par_term_emu_core_rust::terminal::BellEvent::VisualBell);
322 let title = TerminalEvent::TitleChanged("t".to_string());
323
324 fwd.on_event(&bell);
325 fwd.on_event(&title);
326
327 let events = fwd.drain_events();
328 assert_eq!(events.len(), 1);
329 assert_eq!(events[0].kind, "bell_rang");
330 }
331
332 fn title_of(event: &ScriptEvent) -> &str {
335 match &event.data {
336 ScriptEventData::TitleChanged { title } => title,
337 other => panic!("expected a title_changed event, got {other:?}"),
338 }
339 }
340
341 #[test]
342 fn buffer_stops_growing_at_the_cap() {
343 let fwd = ScriptEventForwarder::new(None);
344 for i in 0..(MAX_BUFFERED_EVENTS * 2) {
345 fwd.on_event(&TerminalEvent::TitleChanged(i.to_string()));
346 }
347 assert_eq!(fwd.drain_events().len(), MAX_BUFFERED_EVENTS);
348 }
349
350 #[test]
351 fn overflow_drops_the_oldest_events_not_the_newest() {
352 let fwd = ScriptEventForwarder::new(None);
353 let overflow = 10;
354 for i in 0..(MAX_BUFFERED_EVENTS + overflow) {
355 fwd.on_event(&TerminalEvent::TitleChanged(i.to_string()));
356 }
357
358 let events = fwd.drain_events();
359 assert_eq!(events.len(), MAX_BUFFERED_EVENTS);
360 assert_eq!(title_of(&events[0]), overflow.to_string());
362 assert_eq!(
363 title_of(&events[MAX_BUFFERED_EVENTS - 1]),
364 (MAX_BUFFERED_EVENTS + overflow - 1).to_string()
365 );
366 }
367
368 #[test]
369 fn draining_frees_the_capacity_again() {
370 let fwd = ScriptEventForwarder::new(None);
371 for i in 0..MAX_BUFFERED_EVENTS {
372 fwd.on_event(&TerminalEvent::TitleChanged(i.to_string()));
373 }
374 assert_eq!(fwd.drain_events().len(), MAX_BUFFERED_EVENTS);
375
376 fwd.on_event(&TerminalEvent::TitleChanged("after-drain".to_string()));
378 let events = fwd.drain_events();
379 assert_eq!(events.len(), 1);
380 assert_eq!(title_of(&events[0]), "after-drain");
381 }
382
383 #[test]
384 fn filtered_out_events_do_not_consume_buffer_capacity() {
385 let filter = HashSet::from(["bell_rang".to_string()]);
386 let fwd = ScriptEventForwarder::new(Some(filter));
387 for i in 0..(MAX_BUFFERED_EVENTS * 2) {
388 fwd.on_event(&TerminalEvent::TitleChanged(i.to_string()));
389 }
390 assert!(fwd.drain_events().is_empty());
391 }
392
393 #[test]
394 fn test_drain_clears_buffer() {
395 let fwd = ScriptEventForwarder::new(None);
396 let bell = TerminalEvent::BellRang(par_term_emu_core_rust::terminal::BellEvent::VisualBell);
397
398 fwd.on_event(&bell);
399 let events = fwd.drain_events();
400 assert_eq!(events.len(), 1);
401
402 let events2 = fwd.drain_events();
404 assert!(events2.is_empty());
405 }
406}