use par_term_emu_core_rust::terminal::ActionResult;
use winit::window::WindowId;
use crate::tab::TabId;
use super::WindowState;
const MAX_TARGET_TITLE_CHARS: usize = 48;
#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
pub(crate) struct AutomationTarget {
pub(crate) window_id: WindowId,
pub(crate) tab_id: TabId,
}
pub(crate) fn describe_write_target(index: usize, title: &str, is_active: bool) -> String {
let title = title.trim();
let named = if title.is_empty() {
format!("tab {}", index + 1)
} else {
let mut shown: String = title.chars().take(MAX_TARGET_TITLE_CHARS).collect();
if shown.chars().count() < title.chars().count() {
shown.push('…');
}
format!("tab {} \"{}\"", index + 1, shown)
};
if is_active {
format!("Writes into {} — the tab you are looking at.", named)
} else {
format!("Writes into {} — NOT the tab you are looking at.", named)
}
}
impl WindowState {
fn automation_window_id(&self) -> Option<WindowId> {
self.window.as_ref().map(|w| w.id())
}
pub(crate) fn prune_orphaned_pending_actions(&mut self) {
if self.trigger_state.pending_trigger_actions.is_empty() {
return;
}
let Some(window_id) = self.automation_window_id() else {
return;
};
let orphaned: Vec<usize> = self
.trigger_state
.pending_trigger_actions
.iter()
.enumerate()
.filter(|(_, pending)| match pending.target {
Some(target) => {
target.window_id != window_id
|| self.tab_manager.get_tab(target.tab_id).is_none()
}
None => false,
})
.map(|(index, _)| index)
.collect();
if orphaned.is_empty() {
return;
}
let head_removed = orphaned.contains(&0);
for index in orphaned.into_iter().rev() {
let pending = self.trigger_state.pending_trigger_actions.remove(index);
self.trigger_state
.automation_action_notes
.remove(&pending.trigger_id);
log::warn!(
"Automation confirmation '{}' withdrawn: the tab it was aimed at is gone",
pending.trigger_name
);
}
if head_removed {
self.trigger_state.trigger_prompt_dialog_open = false;
self.trigger_state.trigger_prompt_activated_frame = None;
}
self.request_redraw();
}
pub(crate) fn pending_action_target_note(&self) -> Option<String> {
let target = self.trigger_state.pending_trigger_actions.first()?.target?;
if self.automation_window_id() != Some(target.window_id) {
return None;
}
let index = self
.tab_manager
.tabs()
.iter()
.position(|tab| tab.id == target.tab_id)?;
let title = self.tab_manager.tabs()[index].title.as_str();
let is_active = self.tab_manager.active_tab_id() == Some(target.tab_id);
Some(describe_write_target(index, title, is_active))
}
pub(crate) fn execute_approved_targeted_actions(&mut self) {
if self.trigger_state.approved_targeted_actions.is_empty() {
return;
}
let Some(window_id) = self.automation_window_id() else {
return;
};
let approved = std::mem::take(&mut self.trigger_state.approved_targeted_actions);
for (target, action) in approved {
let ActionResult::SendText { text, delay_ms, .. } = action else {
log::error!(
"Targeted automation action for tab {} is not SendText; discarding it",
target.tab_id
);
continue;
};
if delay_ms != 0 {
log::error!(
"Targeted automation write for tab {} requested a {}ms delay, \
which this path does not support; discarding it",
target.tab_id,
delay_ms
);
continue;
}
if target.window_id != window_id {
log::warn!(
"Approved automation write discarded: it targets window {:?}, \
not the window holding it",
target.window_id
);
continue;
}
let Some(tab) = self.tab_manager.get_tab(target.tab_id) else {
log::warn!(
"Approved automation write discarded: tab {} closed before it ran",
target.tab_id
);
continue;
};
let term = tab.terminal.blocking_read();
match term.write_str(&text) {
Ok(()) => crate::debug_info!(
"SCRIPT",
"AUDIT approved automation write tab={} bytes={}",
target.tab_id,
text.len()
),
Err(e) => log::error!(
"Approved automation write to tab {} failed: {}",
target.tab_id,
e
),
}
}
}
}
#[cfg(test)]
mod tests {
use super::{MAX_TARGET_TITLE_CHARS, describe_write_target};
#[test]
fn target_line_numbers_tabs_from_one() {
assert_eq!(
describe_write_target(0, "zsh", false),
"Writes into tab 1 \"zsh\" — NOT the tab you are looking at."
);
assert_eq!(
describe_write_target(4, "zsh", false),
"Writes into tab 5 \"zsh\" — NOT the tab you are looking at."
);
}
#[test]
fn target_line_distinguishes_the_active_tab() {
let background = describe_write_target(2, "build", false);
let foreground = describe_write_target(2, "build", true);
assert_ne!(background, foreground);
assert!(background.contains("NOT the tab you are looking at"));
assert!(!foreground.contains("NOT"));
}
#[test]
fn target_line_survives_an_empty_title() {
let described = describe_write_target(1, " ", false);
assert!(described.contains("tab 2"));
assert!(!described.contains('"'));
}
#[test]
fn a_long_title_is_truncated() {
let title = "x".repeat(MAX_TARGET_TITLE_CHARS + 20);
let described = describe_write_target(0, &title, true);
assert!(described.contains('…'));
assert!(described.chars().count() < title.chars().count() + 60);
}
}