use std::collections::HashMap;
use super::process::{ScriptProcess, ScriptStatus};
use super::protocol::{ScriptCommand, ScriptEvent};
use par_term_config::ScriptConfig;
pub type ScriptId = u64;
#[cfg(windows)]
const PYTHON_CANDIDATES: &[&str] = &["python", "python3", "py"];
#[cfg(not(windows))]
const PYTHON_CANDIDATES: &[&str] = &["python3", "python"];
fn executable_on_path(name: &str) -> bool {
let Some(path) = std::env::var_os("PATH") else {
return false;
};
#[cfg(windows)]
let extensions: Vec<String> = std::env::var("PATHEXT")
.unwrap_or_else(|_| ".EXE;.BAT;.CMD".to_string())
.split(';')
.filter(|e| !e.is_empty())
.map(str::to_string)
.collect();
for dir in std::env::split_paths(&path) {
#[cfg(windows)]
{
for ext in &extensions {
if dir.join(format!("{}{}", name, ext)).is_file() {
return true;
}
}
}
#[cfg(not(windows))]
{
if dir.join(name).is_file() {
return true;
}
}
}
false
}
pub fn python_interpreter() -> Option<&'static str> {
PYTHON_CANDIDATES
.iter()
.copied()
.find(|candidate| executable_on_path(candidate))
}
pub const DEFAULT_WRITE_TEXT_RATE: u32 = 10;
pub const DEFAULT_RUN_COMMAND_RATE: u32 = 1;
pub struct ScriptManager {
next_id: ScriptId,
processes: HashMap<ScriptId, ScriptProcess>,
panels: HashMap<ScriptId, (String, String)>,
write_text_times: HashMap<ScriptId, std::time::Instant>,
run_command_times: HashMap<ScriptId, std::time::Instant>,
}
impl ScriptManager {
pub fn new() -> Self {
Self {
next_id: 1,
processes: HashMap::new(),
panels: HashMap::new(),
write_text_times: HashMap::new(),
run_command_times: HashMap::new(),
}
}
pub fn start_script(&mut self, config: &ScriptConfig) -> Result<ScriptId, String> {
self.spawn_command(&config.script_path, &config.args, &config.env_vars)
}
pub fn spawn_command(
&mut self,
script_path: &str,
args: &[String],
env_vars: &HashMap<String, String>,
) -> Result<ScriptId, String> {
let (command, args) = if script_path.ends_with(".py") {
let interpreter = python_interpreter().ok_or_else(|| {
format!(
"no Python interpreter found on PATH (tried {}) to run '{}'",
PYTHON_CANDIDATES.join(", "),
script_path
)
})?;
let mut full_args = vec![script_path.to_string()];
full_args.extend(args.iter().cloned());
(interpreter.to_string(), full_args)
} else {
(script_path.to_string(), args.to_vec())
};
let arg_strs: Vec<&str> = args.iter().map(String::as_str).collect();
let process = ScriptProcess::spawn(&command, &arg_strs, env_vars)?;
let id = self.next_id;
self.next_id += 1;
self.processes.insert(id, process);
Ok(id)
}
pub fn is_running(&mut self, id: ScriptId) -> bool {
self.processes.get_mut(&id).is_some_and(|p| p.is_running())
}
pub fn poll_status(&mut self, id: ScriptId) -> ScriptStatus {
match self.processes.get_mut(&id) {
Some(p) => p.poll_status(),
None => ScriptStatus::Exited { success: true },
}
}
pub fn send_event(&mut self, id: ScriptId, event: &ScriptEvent) -> Result<(), String> {
let process = self
.processes
.get_mut(&id)
.ok_or_else(|| format!("No script with id {}", id))?;
process.send_event(event)
}
pub fn broadcast_event(&mut self, event: &ScriptEvent) {
for process in self.processes.values_mut() {
let _ = process.send_event(event);
}
}
pub fn read_commands(&self, id: ScriptId) -> Vec<ScriptCommand> {
self.processes
.get(&id)
.map(|p| p.read_commands())
.unwrap_or_default()
}
pub fn read_errors(&self, id: ScriptId) -> Vec<String> {
self.processes
.get(&id)
.map(|p| p.read_errors())
.unwrap_or_default()
}
pub fn stop_script(&mut self, id: ScriptId) {
if let Some(mut process) = self.processes.remove(&id) {
process.stop();
}
self.panels.remove(&id);
self.write_text_times.remove(&id);
self.run_command_times.remove(&id);
}
pub fn stop_all(&mut self) {
for (_, mut process) in self.processes.drain() {
process.stop();
}
self.panels.clear();
self.write_text_times.clear();
self.run_command_times.clear();
}
pub fn check_write_text_rate(&mut self, id: ScriptId, limit_per_sec: u32) -> bool {
let rate = if limit_per_sec == 0 {
DEFAULT_WRITE_TEXT_RATE
} else {
limit_per_sec
};
let min_interval_ms = 1000u64 / rate as u64;
let now = std::time::Instant::now();
if self
.write_text_times
.get(&id)
.is_some_and(|last| (now.duration_since(*last).as_millis() as u64) < min_interval_ms)
{
return false;
}
self.write_text_times.insert(id, now);
true
}
pub fn check_run_command_rate(&mut self, id: ScriptId, limit_per_sec: u32) -> bool {
let rate = if limit_per_sec == 0 {
DEFAULT_RUN_COMMAND_RATE
} else {
limit_per_sec
};
let min_interval_ms = 1000u64 / rate as u64;
let now = std::time::Instant::now();
if self
.run_command_times
.get(&id)
.is_some_and(|last| (now.duration_since(*last).as_millis() as u64) < min_interval_ms)
{
return false;
}
self.run_command_times.insert(id, now);
true
}
pub fn get_panel(&self, id: ScriptId) -> Option<&(String, String)> {
self.panels.get(&id)
}
pub fn set_panel(&mut self, id: ScriptId, title: String, content: String) {
self.panels.insert(id, (title, content));
}
pub fn clear_panel(&mut self, id: ScriptId) {
self.panels.remove(&id);
}
pub fn script_ids(&self) -> Vec<ScriptId> {
self.processes.keys().copied().collect()
}
}
impl Default for ScriptManager {
fn default() -> Self {
Self::new()
}
}
impl Drop for ScriptManager {
fn drop(&mut self) {
self.stop_all();
}
}