Skip to main content

par_term_scripting/
manager.rs

1//! Per-tab multi-script orchestrator.
2//!
3//! [`ScriptManager`] manages multiple [`ScriptProcess`] instances for a single tab,
4//! providing lifecycle management, event broadcasting, and panel state tracking.
5
6use std::collections::HashMap;
7
8use super::process::ScriptProcess;
9use super::protocol::{ScriptCommand, ScriptEvent};
10use par_term_config::ScriptConfig;
11
12/// Unique identifier for a managed script process.
13pub type ScriptId = u64;
14
15/// Manages multiple script subprocess instances for a single tab.
16///
17/// Each script is assigned a unique [`ScriptId`] and can be individually started,
18/// stopped, and communicated with. Supports panel state tracking per script and
19/// event broadcasting to all running scripts.
20pub struct ScriptManager {
21    /// Next ID to assign to a new script process.
22    next_id: ScriptId,
23    /// Map of active script processes keyed by their assigned ID.
24    processes: HashMap<ScriptId, ScriptProcess>,
25    /// Panel state per script: script_id -> (title, content).
26    panels: HashMap<ScriptId, (String, String)>,
27}
28
29impl ScriptManager {
30    /// Create a new empty `ScriptManager`.
31    pub fn new() -> Self {
32        Self {
33            next_id: 1,
34            processes: HashMap::new(),
35            panels: HashMap::new(),
36        }
37    }
38
39    /// Start a script subprocess from the given configuration.
40    ///
41    /// If `script_path` ends with `.py`, the command is `python3` with the script path
42    /// prepended to the args. Otherwise, `script_path` is used as the command directly.
43    ///
44    /// Returns the assigned [`ScriptId`] on success.
45    ///
46    /// # Errors
47    /// Returns an error string if the subprocess cannot be spawned.
48    pub fn start_script(&mut self, config: &ScriptConfig) -> Result<ScriptId, String> {
49        let (command, args) = if config.script_path.ends_with(".py") {
50            let mut full_args = vec![config.script_path.as_str()];
51            let arg_refs: Vec<&str> = config.args.iter().map(String::as_str).collect();
52            full_args.extend(arg_refs);
53            (
54                "python3".to_string(),
55                full_args.into_iter().map(String::from).collect::<Vec<_>>(),
56            )
57        } else {
58            let arg_refs: Vec<String> = config.args.to_vec();
59            (config.script_path.clone(), arg_refs)
60        };
61
62        let arg_strs: Vec<&str> = args.iter().map(String::as_str).collect();
63        let process = ScriptProcess::spawn(&command, &arg_strs, &config.env_vars)?;
64
65        let id = self.next_id;
66        self.next_id += 1;
67        self.processes.insert(id, process);
68
69        Ok(id)
70    }
71
72    /// Check if a script with the given ID is still running.
73    ///
74    /// Returns `false` if the script ID is unknown or the process has exited.
75    pub fn is_running(&mut self, id: ScriptId) -> bool {
76        self.processes.get_mut(&id).is_some_and(|p| p.is_running())
77    }
78
79    /// Send a [`ScriptEvent`] to a specific script by ID.
80    ///
81    /// # Errors
82    /// Returns an error if the script ID is unknown or the write fails.
83    pub fn send_event(&mut self, id: ScriptId, event: &ScriptEvent) -> Result<(), String> {
84        let process = self
85            .processes
86            .get_mut(&id)
87            .ok_or_else(|| format!("No script with id {}", id))?;
88        process.send_event(event)
89    }
90
91    /// Broadcast a [`ScriptEvent`] to all running scripts.
92    ///
93    /// Errors on individual scripts are silently ignored; the event is sent on a
94    /// best-effort basis to all processes.
95    pub fn broadcast_event(&mut self, event: &ScriptEvent) {
96        for process in self.processes.values_mut() {
97            let _ = process.send_event(event);
98        }
99    }
100
101    /// Drain pending [`ScriptCommand`]s from a specific script's stdout buffer.
102    ///
103    /// Returns an empty `Vec` if the script ID is unknown.
104    pub fn read_commands(&self, id: ScriptId) -> Vec<ScriptCommand> {
105        self.processes
106            .get(&id)
107            .map(|p| p.read_commands())
108            .unwrap_or_default()
109    }
110
111    /// Drain pending error lines from a specific script's stderr buffer.
112    ///
113    /// Returns an empty `Vec` if the script ID is unknown.
114    pub fn read_errors(&self, id: ScriptId) -> Vec<String> {
115        self.processes
116            .get(&id)
117            .map(|p| p.read_errors())
118            .unwrap_or_default()
119    }
120
121    /// Stop and remove a specific script by ID.
122    ///
123    /// Also clears the associated panel state. Does nothing if the ID is unknown.
124    pub fn stop_script(&mut self, id: ScriptId) {
125        if let Some(mut process) = self.processes.remove(&id) {
126            process.stop();
127        }
128        self.panels.remove(&id);
129    }
130
131    /// Stop and remove all managed scripts.
132    pub fn stop_all(&mut self) {
133        for (_, mut process) in self.processes.drain() {
134            process.stop();
135        }
136        self.panels.clear();
137    }
138
139    /// Get the panel state for a script.
140    ///
141    /// Returns `None` if the script ID has no panel set.
142    pub fn get_panel(&self, id: ScriptId) -> Option<&(String, String)> {
143        self.panels.get(&id)
144    }
145
146    /// Set the panel state (title, content) for a script.
147    pub fn set_panel(&mut self, id: ScriptId, title: String, content: String) {
148        self.panels.insert(id, (title, content));
149    }
150
151    /// Clear the panel state for a script.
152    pub fn clear_panel(&mut self, id: ScriptId) {
153        self.panels.remove(&id);
154    }
155
156    /// Get the IDs of all currently managed scripts.
157    pub fn script_ids(&self) -> Vec<ScriptId> {
158        self.processes.keys().copied().collect()
159    }
160}
161
162impl Default for ScriptManager {
163    fn default() -> Self {
164        Self::new()
165    }
166}
167
168impl Drop for ScriptManager {
169    fn drop(&mut self) {
170        self.stop_all();
171    }
172}