1use std::collections::HashMap;
2
3use rmux_proto::{RmuxError, ScopeSelector, SessionName};
4
5pub const ENVIRON_HIDDEN: u8 = 0x1;
7
8#[derive(Debug, Clone, PartialEq, Eq)]
10pub struct ShowEnvironmentEntry {
11 pub name: String,
13 pub value: Option<String>,
15 pub flags: u8,
17}
18
19impl ShowEnvironmentEntry {
20 #[must_use]
22 pub const fn is_hidden(&self) -> bool {
23 self.flags & ENVIRON_HIDDEN != 0
24 }
25
26 #[must_use]
28 pub const fn is_cleared(&self) -> bool {
29 self.value.is_none()
30 }
31}
32
33#[derive(Debug, Clone, PartialEq, Eq, Default)]
34struct EnvironmentEntry {
35 value: Option<String>,
36 flags: u8,
37}
38
39impl EnvironmentEntry {
40 fn new(value: String, flags: u8) -> Self {
41 Self {
42 value: Some(value),
43 flags,
44 }
45 }
46
47 fn clear(&mut self) {
48 self.value = None;
49 }
50
51 const fn flags(&self) -> u8 {
52 self.flags
53 }
54
55 fn value(&self) -> Option<&str> {
56 self.value.as_deref()
57 }
58
59 const fn is_hidden(&self) -> bool {
60 self.flags & ENVIRON_HIDDEN != 0
61 }
62
63 const fn is_cleared(&self) -> bool {
64 self.value.is_none()
65 }
66}
67
68#[derive(Debug, Clone, PartialEq, Eq, Default)]
70pub struct EnvironmentStore {
71 global: HashMap<String, EnvironmentEntry>,
72 sessions: HashMap<SessionName, HashMap<String, EnvironmentEntry>>,
73}
74
75impl EnvironmentStore {
76 #[must_use]
78 pub fn new() -> Self {
79 Self::default()
80 }
81
82 #[must_use]
84 pub fn is_empty(&self) -> bool {
85 self.global.is_empty() && self.sessions.is_empty()
86 }
87
88 pub fn set(&mut self, scope: ScopeSelector, name: String, value: String) {
90 self.set_with_flags(scope, name, value, 0);
91 }
92
93 pub fn set_with_flags(&mut self, scope: ScopeSelector, name: String, value: String, flags: u8) {
95 self.scope_entries_mut(scope)
96 .insert(name, EnvironmentEntry::new(value, flags));
97 }
98
99 pub fn clear(&mut self, scope: ScopeSelector, name: String) {
101 let entries = self.scope_entries_mut(scope);
102 if let Some(entry) = entries.get_mut(&name) {
103 entry.clear();
104 } else {
105 entries.insert(name, EnvironmentEntry::default());
106 }
107 }
108
109 pub fn unset(&mut self, scope: ScopeSelector, name: &str) -> bool {
111 self.scope_entries_mut(scope).remove(name).is_some()
112 }
113
114 #[must_use]
116 pub fn contains_entry(&self, scope: &ScopeSelector, name: &str) -> bool {
117 self.scope_entries(scope)
118 .is_some_and(|entries| entries.contains_key(name))
119 }
120
121 #[must_use]
123 pub fn global_value(&self, name: &str) -> Option<&str> {
124 self.global.get(name).and_then(EnvironmentEntry::value)
125 }
126
127 pub fn global_entries(&self) -> impl Iterator<Item = (&str, &str)> {
129 self.global
130 .iter()
131 .filter_map(|(name, entry)| entry.value().map(|value| (name.as_str(), value)))
132 }
133
134 #[must_use]
136 pub fn session_value(&self, session_name: &SessionName, name: &str) -> Option<&str> {
137 self.sessions
138 .get(session_name)
139 .and_then(|values| values.get(name))
140 .and_then(EnvironmentEntry::value)
141 }
142
143 #[must_use]
145 pub fn resolve(&self, session_name: Option<&SessionName>, name: &str) -> Option<&str> {
146 if let Some(session_name) = session_name {
147 if let Some(entry) = self
148 .sessions
149 .get(session_name)
150 .and_then(|values| values.get(name))
151 {
152 return entry.value();
153 }
154 }
155
156 self.global.get(name).and_then(EnvironmentEntry::value)
157 }
158
159 #[must_use]
161 pub fn resolved(&self, session_name: &SessionName) -> HashMap<String, String> {
162 let mut values = HashMap::new();
163 for (name, entry) in &self.global {
164 apply_entry_to_child_environment(&mut values, name, entry);
165 }
166 if let Some(session_values) = self.sessions.get(session_name) {
167 for (name, entry) in session_values {
168 apply_entry_to_child_environment(&mut values, name, entry);
169 }
170 }
171 values
172 }
173
174 pub fn apply_to_process_environment(
176 &self,
177 session_name: Option<&SessionName>,
178 values: &mut HashMap<String, String>,
179 ) {
180 for (name, entry) in &self.global {
181 apply_entry_to_child_environment(values, name, entry);
182 }
183
184 if let Some(session_name) = session_name {
185 if let Some(session_values) = self.sessions.get(session_name) {
186 for (name, entry) in session_values {
187 apply_entry_to_child_environment(values, name, entry);
188 }
189 }
190 }
191 }
192
193 pub fn update(
195 &mut self,
196 session_name: &SessionName,
197 patterns: &[String],
198 source: &HashMap<String, String>,
199 ) {
200 for pattern in patterns {
201 let mut found = false;
202 for (name, value) in source {
203 if crate::fnmatch(pattern, name) {
204 self.set(
205 ScopeSelector::Session(session_name.clone()),
206 name.clone(),
207 value.clone(),
208 );
209 found = true;
210 }
211 }
212 if !found {
213 self.clear(
214 ScopeSelector::Session(session_name.clone()),
215 pattern.clone(),
216 );
217 }
218 }
219 }
220
221 pub fn show_environment_entries(
223 &self,
224 scope: &ScopeSelector,
225 hidden_only: bool,
226 name: Option<&str>,
227 ) -> Result<Vec<ShowEnvironmentEntry>, RmuxError> {
228 let exact_entries = match scope {
229 ScopeSelector::Global => &self.global,
230 ScopeSelector::Session(session_name) => {
231 if let Some(entries) = self.sessions.get(session_name) {
232 entries
233 } else {
234 empty_environment_entries()
235 }
236 }
237 ScopeSelector::Window(_) | ScopeSelector::Pane(_) => {
238 return Err(RmuxError::Server(
239 "show-environment only supports global or session scope".to_owned(),
240 ));
241 }
242 };
243
244 if let Some(name) = name {
245 let Some(entry) = exact_entries.get(name) else {
246 return Err(RmuxError::Server(format!("unknown variable: {name}")));
247 };
248 if hidden_only && !entry.is_hidden() {
249 return Ok(Vec::new());
250 }
251 if !hidden_only && entry.is_hidden() {
252 return Ok(Vec::new());
253 }
254 return Ok(vec![ShowEnvironmentEntry {
255 name: name.to_owned(),
256 value: entry.value.clone(),
257 flags: entry.flags(),
258 }]);
259 }
260
261 let mut values = exact_entries
262 .iter()
263 .filter(|(_, entry)| hidden_only == entry.is_hidden())
264 .map(|(name, entry)| ShowEnvironmentEntry {
265 name: name.clone(),
266 value: entry.value.clone(),
267 flags: entry.flags(),
268 })
269 .collect::<Vec<_>>();
270 values.sort_by(|left, right| left.name.cmp(&right.name));
271 Ok(values)
272 }
273
274 pub fn remove_session(
276 &mut self,
277 session_name: &SessionName,
278 ) -> Option<HashMap<String, String>> {
279 self.sessions.remove(session_name).map(|entries| {
280 entries
281 .into_iter()
282 .filter_map(|(name, entry)| entry.value.map(|value| (name, value)))
283 .collect()
284 })
285 }
286
287 pub fn rename_session(
289 &mut self,
290 session_name: &SessionName,
291 new_name: SessionName,
292 ) -> Result<(), RmuxError> {
293 if self.sessions.contains_key(&new_name) {
294 return Err(RmuxError::Server(format!(
295 "environment already exists for session {new_name}"
296 )));
297 }
298
299 let mut sessions = std::mem::take(&mut self.sessions);
300 if let Some(values) = sessions.remove(session_name) {
301 let replaced = sessions.insert(new_name, values);
302 debug_assert!(replaced.is_none());
303 }
304 self.sessions = sessions;
305 Ok(())
306 }
307
308 fn scope_entries_mut(
309 &mut self,
310 scope: ScopeSelector,
311 ) -> &mut HashMap<String, EnvironmentEntry> {
312 match scope {
313 ScopeSelector::Global => &mut self.global,
314 ScopeSelector::Session(session_name) => self.sessions.entry(session_name).or_default(),
315 ScopeSelector::Window(_) | ScopeSelector::Pane(_) => {
316 unreachable!("environment mutations are validated before storage")
317 }
318 }
319 }
320
321 fn scope_entries(&self, scope: &ScopeSelector) -> Option<&HashMap<String, EnvironmentEntry>> {
322 match scope {
323 ScopeSelector::Global => Some(&self.global),
324 ScopeSelector::Session(session_name) => self.sessions.get(session_name),
325 ScopeSelector::Window(_) | ScopeSelector::Pane(_) => None,
326 }
327 }
328}
329
330fn apply_entry_to_child_environment(
331 values: &mut HashMap<String, String>,
332 name: &str,
333 entry: &EnvironmentEntry,
334) {
335 if entry.is_hidden() || entry.is_cleared() {
336 values.remove(name);
337 } else if let Some(value) = entry.value() {
338 values.insert(name.to_owned(), value.to_owned());
339 }
340}
341
342fn empty_environment_entries() -> &'static HashMap<String, EnvironmentEntry> {
343 static EMPTY: std::sync::OnceLock<HashMap<String, EnvironmentEntry>> =
344 std::sync::OnceLock::new();
345 EMPTY.get_or_init(HashMap::new)
346}
347
348#[cfg(test)]
349#[path = "environment/tests.rs"]
350mod tests;