Skip to main content

Mountain/Environment/
TerminalEnvCollection.rs

1//! Global registry of per-extension `EnvironmentVariableCollection`
2//! mutations.
3//!
4//! Each extension that calls `context.environmentVariableCollection.replace(
5//! variable, value)` lands a `Mutator` in this registry keyed by
6//! `(ExtensionId, VariableName)`. Every PTY spawn consults the registry
7//! and applies the accumulated mutations to the child env BEFORE the
8//! shell process is launched, so terminals created after an extension's
9//! activation observe the env it requested.
10//!
11//! Persistence: `persistent=true` mutations survive a window reload via
12//! Mountain's storage provider (key
13//! `__envCollection:<extensionId>`). Non-persistent mutations live only
14//! for the current session.
15//!
16//! Wire format matches VS Code's `EnvironmentVariableMutator`:
17//!   • Type::Replace (1) - set to `value`, ignore inherited
18//!   • Type::Append  (2) - `inherited + value`
19//!   • Type::Prepend (3) - `value + inherited`
20//!
21//! `applyAtProcessCreation` (default true) is the only application
22//! point in the Tauri+PTY world; the upstream `applyAtShellIntegration`
23//! second-chance hook is irrelevant when we already control the spawn.
24
25use std::{
26	collections::HashMap,
27	sync::{Mutex, OnceLock},
28};
29
30use serde_json::Value;
31
32#[derive(Clone, Copy, PartialEq, Eq, Debug)]
33pub enum MutatorType {
34	Replace = 1,
35
36	Append = 2,
37
38	Prepend = 3,
39}
40
41#[derive(Clone, Debug)]
42pub struct Mutator {
43	pub Variable:String,
44
45	pub Value:String,
46
47	pub Kind:MutatorType,
48}
49
50#[derive(Clone, Default)]
51pub struct ExtensionCollection {
52	pub Persistent:bool,
53
54	pub Description:Option<String>,
55
56	pub Mutators:HashMap<String, Mutator>,
57}
58
59static REGISTRY:OnceLock<Mutex<HashMap<String, ExtensionCollection>>> = OnceLock::new();
60
61fn Get() -> &'static Mutex<HashMap<String, ExtensionCollection>> { REGISTRY.get_or_init(|| Mutex::new(HashMap::new())) }
62
63pub fn Replace(ExtensionId:&str, Variable:String, Value:String) {
64	if let Ok(mut Guard) = Get().lock() {
65		let Entry = Guard.entry(ExtensionId.to_string()).or_default();
66
67		Entry
68			.Mutators
69			.insert(Variable.clone(), Mutator { Variable, Value, Kind:MutatorType::Replace });
70	}
71}
72
73pub fn Append(ExtensionId:&str, Variable:String, Value:String) {
74	if let Ok(mut Guard) = Get().lock() {
75		let Entry = Guard.entry(ExtensionId.to_string()).or_default();
76
77		Entry
78			.Mutators
79			.insert(Variable.clone(), Mutator { Variable, Value, Kind:MutatorType::Append });
80	}
81}
82
83pub fn Prepend(ExtensionId:&str, Variable:String, Value:String) {
84	if let Ok(mut Guard) = Get().lock() {
85		let Entry = Guard.entry(ExtensionId.to_string()).or_default();
86
87		Entry
88			.Mutators
89			.insert(Variable.clone(), Mutator { Variable, Value, Kind:MutatorType::Prepend });
90	}
91}
92
93pub fn Delete(ExtensionId:&str, Variable:&str) {
94	if let Ok(mut Guard) = Get().lock() {
95		if let Some(Entry) = Guard.get_mut(ExtensionId) {
96			Entry.Mutators.remove(Variable);
97		}
98	}
99}
100
101pub fn Clear(ExtensionId:&str) {
102	if let Ok(mut Guard) = Get().lock() {
103		if let Some(Entry) = Guard.get_mut(ExtensionId) {
104			Entry.Mutators.clear();
105		}
106	}
107}
108
109pub fn SetPersistent(ExtensionId:&str, Persistent:bool) {
110	if let Ok(mut Guard) = Get().lock() {
111		let Entry = Guard.entry(ExtensionId.to_string()).or_default();
112
113		Entry.Persistent = Persistent;
114	}
115}
116
117pub fn SetDescription(ExtensionId:&str, Description:Option<String>) {
118	if let Ok(mut Guard) = Get().lock() {
119		let Entry = Guard.entry(ExtensionId.to_string()).or_default();
120
121		Entry.Description = Description;
122	}
123}
124
125/// Apply every registered mutation across every extension to the supplied
126/// env map. Mutations are deterministic per (extensionId, variable);
127/// across extensions the order is iteration-stable but unordered, so
128/// avoid relying on cross-extension ordering for the same variable
129/// (matches VS Code's documented behavior).
130pub fn ApplyToEnv(Env:&mut HashMap<String, String>) {
131	let Snapshot = match Get().lock() {
132		Ok(Guard) => Guard.clone(),
133
134		Err(_) => return,
135	};
136
137	for (_ExtId, Collection) in Snapshot {
138		for Mut in Collection.Mutators.values() {
139			let Inherited = Env.get(&Mut.Variable).cloned().unwrap_or_default();
140
141			let Next = match Mut.Kind {
142				MutatorType::Replace => Mut.Value.clone(),
143
144				MutatorType::Append => format!("{}{}", Inherited, Mut.Value),
145
146				MutatorType::Prepend => format!("{}{}", Mut.Value, Inherited),
147			};
148
149			Env.insert(Mut.Variable.clone(), Next);
150		}
151	}
152}
153
154/// Parse the wire payload `{ extensionId, variable, value, persistent,
155/// description }` into a uniform tuple. Missing fields surface as empty
156/// strings / None; the dispatcher discards calls whose ExtensionId is
157/// empty.
158pub fn ParsePayload(Payload:&Value) -> (String, String, String) {
159	let ExtensionId = Payload.get("extensionId").and_then(|V| V.as_str()).unwrap_or("").to_string();
160
161	let Variable = Payload.get("variable").and_then(|V| V.as_str()).unwrap_or("").to_string();
162
163	let Value_ = Payload.get("value").and_then(|V| V.as_str()).unwrap_or("").to_string();
164
165	(ExtensionId, Variable, Value_)
166}