#[derive(Debug, Clone, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub struct SpawnEnv {
pub overrides: Vec<(String, String)>,
pub cwd: Option<String>,
}
impl SpawnEnv {
#[must_use]
pub fn none() -> Self {
Self::default()
}
#[must_use]
pub fn from_overrides(overrides: Vec<(String, String)>) -> Self {
Self {
overrides,
cwd: None,
}
}
#[must_use]
pub fn with_cwd(mut self, cwd: Option<String>) -> Self {
self.cwd = cwd;
self
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.overrides.is_empty() && self.cwd.is_none()
}
pub fn apply_to(&self, env: &mut Vec<(String, String)>) {
for (k, v) in &self.overrides {
if let Some(slot) = env.iter_mut().find(|(ek, _)| ek == k) {
slot.1.clone_from(v);
} else {
env.push((k.clone(), v.clone()));
}
}
match &self.cwd {
Some(dir) => {
if let Some(slot) = env.iter_mut().find(|(k, _)| k == "PWD") {
slot.1.clone_from(dir);
} else {
env.push(("PWD".to_owned(), dir.clone()));
}
}
None => env.retain(|(k, _)| k != "PWD"),
}
}
}
#[cfg(test)]
mod tests {
use super::SpawnEnv;
#[test]
fn override_replaces_an_existing_key() {
let mut env = vec![
("TERM".to_owned(), "xterm-256color".to_owned()),
("PATH".to_owned(), "/usr/bin".to_owned()),
];
SpawnEnv::from_overrides(vec![("TERM".to_owned(), "xterm-ghostty".to_owned())])
.apply_to(&mut env);
assert_eq!(
env.iter().find(|(k, _)| k == "TERM").map(|(_, v)| v.as_str()),
Some("xterm-ghostty"),
"the embedder's TERM must override the backend fallback"
);
assert_eq!(env.iter().filter(|(k, _)| k == "TERM").count(), 1);
}
#[test]
fn override_pushes_a_missing_key() {
let mut env = vec![("PATH".to_owned(), "/usr/bin".to_owned())];
SpawnEnv::from_overrides(vec![("COLORTERM".to_owned(), "truecolor".to_owned())])
.apply_to(&mut env);
assert_eq!(
env.iter()
.find(|(k, _)| k == "COLORTERM")
.map(|(_, v)| v.as_str()),
Some("truecolor")
);
}
#[test]
fn cwd_some_stamps_pwd_to_match() {
let mut env = vec![("PWD".to_owned(), "/stale/parent".to_owned())];
SpawnEnv::none()
.with_cwd(Some("/real/child".to_owned()))
.apply_to(&mut env);
assert_eq!(
env.iter().find(|(k, _)| k == "PWD").map(|(_, v)| v.as_str()),
Some("/real/child"),
"a set cwd must overwrite a stale inherited PWD"
);
}
#[test]
fn cwd_none_strips_any_inherited_pwd() {
let mut env = vec![("PWD".to_owned(), "/stale/parent".to_owned())];
SpawnEnv::none().apply_to(&mut env);
assert!(
!env.iter().any(|(k, _)| k == "PWD"),
"no cwd → no inherited PWD may leak to the child"
);
}
#[test]
fn empty_override_is_a_noop_except_pwd_strip() {
let mut env = vec![("TERM".to_owned(), "xterm-256color".to_owned())];
let before = env.clone();
SpawnEnv::none().apply_to(&mut env);
assert_eq!(env, before);
}
}