1use std::collections::{HashMap, HashSet};
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 pub value_is_display_escape: bool,
19}
20
21impl ShowEnvironmentEntry {
22 #[must_use]
24 pub const fn is_hidden(&self) -> bool {
25 self.flags & ENVIRON_HIDDEN != 0
26 }
27
28 #[must_use]
30 pub const fn is_cleared(&self) -> bool {
31 self.value.is_none()
32 }
33}
34
35#[derive(Debug, Clone, PartialEq, Eq, Default)]
36struct EnvironmentEntry {
37 value: Option<String>,
38 display_value: Option<String>,
39 flags: u8,
40 implicit: bool,
41}
42
43impl EnvironmentEntry {
44 fn new(value: String, flags: u8) -> Self {
45 Self {
46 value: Some(value),
47 display_value: None,
48 flags,
49 implicit: false,
50 }
51 }
52
53 fn implicit(value: String) -> Self {
54 Self {
55 value: Some(value),
56 display_value: None,
57 flags: 0,
58 implicit: true,
59 }
60 }
61
62 fn implicit_display(value: String) -> Self {
63 Self {
64 value: None,
65 display_value: Some(value),
66 flags: 0,
67 implicit: true,
68 }
69 }
70
71 fn clear(&mut self) {
72 self.value = None;
73 self.display_value = None;
74 self.implicit = false;
75 }
76
77 const fn flags(&self) -> u8 {
78 self.flags
79 }
80
81 fn value(&self) -> Option<&str> {
82 self.value.as_deref()
83 }
84
85 fn show_value(&self) -> Option<&str> {
86 self.value.as_deref().or(self.display_value.as_deref())
87 }
88
89 const fn is_hidden(&self) -> bool {
90 self.flags & ENVIRON_HIDDEN != 0
91 }
92
93 const fn is_cleared(&self) -> bool {
94 self.value.is_none() && self.display_value.is_none()
95 }
96
97 const fn is_implicit(&self) -> bool {
98 self.implicit
99 }
100}
101
102#[derive(Debug, Clone, PartialEq, Eq, Default)]
104pub struct EnvironmentStore {
105 global: HashMap<String, EnvironmentEntry>,
106 sessions: HashMap<SessionName, HashMap<String, EnvironmentEntry>>,
107 global_unsets: HashSet<String>,
108 session_unsets: HashMap<SessionName, HashSet<String>>,
109}
110
111impl EnvironmentStore {
112 #[must_use]
114 pub fn new() -> Self {
115 Self::default()
116 }
117
118 #[must_use]
120 pub fn is_empty(&self) -> bool {
121 self.global.is_empty() && self.sessions.is_empty()
122 }
123
124 pub fn set(&mut self, scope: ScopeSelector, name: String, value: String) {
126 self.set_with_flags(scope, name, value, 0);
127 }
128
129 pub fn set_with_flags(&mut self, scope: ScopeSelector, name: String, value: String, flags: u8) {
131 self.forget_unset(&scope, &name);
132 insert_environment_entry(
133 self.scope_entries_mut(scope),
134 name,
135 EnvironmentEntry::new(value, flags),
136 );
137 }
138
139 pub fn set_implicit_global(&mut self, name: String, value: String) {
141 remove_name_from_set(&mut self.global_unsets, &name);
142 insert_environment_entry(&mut self.global, name, EnvironmentEntry::implicit(value));
143 }
144
145 pub fn set_implicit_global_display(&mut self, name: String, value: String) {
147 remove_name_from_set(&mut self.global_unsets, &name);
148 insert_environment_entry(
149 &mut self.global,
150 name,
151 EnvironmentEntry::implicit_display(value),
152 );
153 }
154
155 pub fn clear(&mut self, scope: ScopeSelector, name: String) {
157 self.record_unset(&scope, name.clone());
158 let entries = self.scope_entries_mut(scope);
159 if let Some(entry) = environment_entry_mut(entries, &name) {
160 entry.clear();
161 } else {
162 insert_environment_entry(entries, name, EnvironmentEntry::default());
163 }
164 }
165
166 pub fn unset(&mut self, scope: ScopeSelector, name: &str) -> bool {
168 match &scope {
169 ScopeSelector::Global => self.record_unset(&scope, name.to_owned()),
170 ScopeSelector::Session(_) => self.forget_unset(&scope, name),
171 ScopeSelector::Window(_) | ScopeSelector::Pane(_) => {}
172 }
173 remove_environment_entry(self.scope_entries_mut(scope), name).is_some()
174 }
175
176 #[must_use]
178 pub fn contains_entry(&self, scope: &ScopeSelector, name: &str) -> bool {
179 self.scope_entries(scope)
180 .is_some_and(|entries| environment_entry(entries, name).is_some())
181 }
182
183 #[must_use]
185 pub fn global_value(&self, name: &str) -> Option<&str> {
186 environment_entry(&self.global, name).and_then(EnvironmentEntry::value)
187 }
188
189 pub fn global_entries(&self) -> impl Iterator<Item = (&str, &str)> {
191 self.global
192 .iter()
193 .filter_map(|(name, entry)| entry.value().map(|value| (name.as_str(), value)))
194 }
195
196 #[must_use]
198 pub fn session_value(&self, session_name: &SessionName, name: &str) -> Option<&str> {
199 self.sessions
200 .get(session_name)
201 .and_then(|values| environment_entry(values, name))
202 .and_then(EnvironmentEntry::value)
203 }
204
205 #[must_use]
207 pub fn resolve(&self, session_name: Option<&SessionName>, name: &str) -> Option<&str> {
208 if let Some(session_name) = session_name {
209 if let Some(entry) = self
210 .sessions
211 .get(session_name)
212 .and_then(|values| environment_entry(values, name))
213 {
214 return entry.value();
215 }
216 }
217
218 environment_entry(&self.global, name).and_then(EnvironmentEntry::value)
219 }
220
221 #[must_use]
223 pub fn resolved(&self, session_name: &SessionName) -> HashMap<String, String> {
224 let mut values = HashMap::new();
225 for (name, entry) in &self.global {
226 apply_entry_to_child_environment(&mut values, name, entry);
227 }
228 if let Some(session_values) = self.sessions.get(session_name) {
229 for (name, entry) in session_values {
230 apply_entry_to_child_environment(&mut values, name, entry);
231 }
232 }
233 values
234 }
235
236 pub fn apply_to_process_environment(
238 &self,
239 session_name: Option<&SessionName>,
240 values: &mut HashMap<String, String>,
241 ) {
242 self.apply_to_process_environment_inner(session_name, values, true);
243 }
244
245 pub fn apply_to_process_environment_without_implicit_globals(
247 &self,
248 session_name: Option<&SessionName>,
249 values: &mut HashMap<String, String>,
250 ) {
251 self.apply_to_process_environment_inner(session_name, values, false);
252 }
253
254 #[must_use]
256 pub fn suppressed_process_environment_names(
257 &self,
258 session_name: Option<&SessionName>,
259 include_implicit_globals: bool,
260 ) -> HashSet<String> {
261 let mut names = self.global_unsets.clone();
262 collect_suppressed_entry_names(&self.global, include_implicit_globals, &mut names);
263
264 if let Some(session_name) = session_name {
265 if let Some(session_unsets) = self.session_unsets.get(session_name) {
266 names.extend(session_unsets.iter().cloned());
267 }
268 if let Some(entries) = self.sessions.get(session_name) {
269 collect_suppressed_entry_names(entries, true, &mut names);
270 }
271 }
272
273 names
274 }
275
276 fn apply_to_process_environment_inner(
277 &self,
278 session_name: Option<&SessionName>,
279 values: &mut HashMap<String, String>,
280 include_implicit_globals: bool,
281 ) {
282 for (name, entry) in &self.global {
283 if !include_implicit_globals && entry.is_implicit() {
284 continue;
285 }
286 apply_entry_to_child_environment(values, name, entry);
287 }
288 remove_unset_names(values, &self.global_unsets);
289
290 if let Some(session_name) = session_name {
291 if let Some(session_values) = self.sessions.get(session_name) {
292 for (name, entry) in session_values {
293 apply_entry_to_child_environment(values, name, entry);
294 }
295 }
296 if let Some(session_unsets) = self.session_unsets.get(session_name) {
297 remove_unset_names(values, session_unsets);
298 }
299 }
300 }
301
302 pub fn update(
304 &mut self,
305 session_name: &SessionName,
306 patterns: &[String],
307 source: &HashMap<String, String>,
308 ) {
309 for pattern in patterns {
310 let mut found = false;
311 for (name, value) in source {
312 if crate::fnmatch(pattern, name) {
313 self.set(
314 ScopeSelector::Session(session_name.clone()),
315 name.clone(),
316 value.clone(),
317 );
318 found = true;
319 }
320 }
321 if !found {
322 self.clear(
323 ScopeSelector::Session(session_name.clone()),
324 pattern.clone(),
325 );
326 }
327 }
328 }
329
330 pub fn show_environment_entries(
332 &self,
333 scope: &ScopeSelector,
334 hidden_only: bool,
335 name: Option<&str>,
336 ) -> Result<Vec<ShowEnvironmentEntry>, RmuxError> {
337 let exact_entries = match scope {
338 ScopeSelector::Global => &self.global,
339 ScopeSelector::Session(session_name) => {
340 if let Some(entries) = self.sessions.get(session_name) {
341 entries
342 } else {
343 empty_environment_entries()
344 }
345 }
346 ScopeSelector::Window(_) | ScopeSelector::Pane(_) => {
347 return Err(RmuxError::Server(
348 "show-environment only supports global or session scope".to_owned(),
349 ));
350 }
351 };
352
353 if let Some(name) = name {
354 let Some((stored_name, entry)) = environment_entry_with_name(exact_entries, name)
355 else {
356 return Err(RmuxError::Server(format!("unknown variable: {name}")));
357 };
358 if hidden_only && !entry.is_hidden() {
359 return Ok(Vec::new());
360 }
361 if !hidden_only && entry.is_hidden() {
362 return Ok(Vec::new());
363 }
364 return Ok(vec![ShowEnvironmentEntry {
365 name: stored_name.to_owned(),
366 value: entry.show_value().map(str::to_owned),
367 flags: entry.flags(),
368 value_is_display_escape: entry.value.is_none() && entry.display_value.is_some(),
369 }]);
370 }
371
372 let mut values = exact_entries
373 .iter()
374 .filter(|(_, entry)| hidden_only == entry.is_hidden())
375 .map(|(name, entry)| ShowEnvironmentEntry {
376 name: name.clone(),
377 value: entry.show_value().map(str::to_owned),
378 flags: entry.flags(),
379 value_is_display_escape: entry.value.is_none() && entry.display_value.is_some(),
380 })
381 .collect::<Vec<_>>();
382 values.sort_by(|left, right| left.name.cmp(&right.name));
383 Ok(values)
384 }
385
386 pub fn remove_session(
388 &mut self,
389 session_name: &SessionName,
390 ) -> Option<HashMap<String, String>> {
391 self.session_unsets.remove(session_name);
392 self.sessions.remove(session_name).map(|entries| {
393 entries
394 .into_iter()
395 .filter_map(|(name, entry)| entry.value.map(|value| (name, value)))
396 .collect()
397 })
398 }
399
400 pub fn rename_session(
402 &mut self,
403 session_name: &SessionName,
404 new_name: SessionName,
405 ) -> Result<(), RmuxError> {
406 if self.sessions.contains_key(&new_name) {
407 return Err(RmuxError::Server(format!(
408 "environment already exists for session {new_name}"
409 )));
410 }
411
412 let mut sessions = std::mem::take(&mut self.sessions);
413 if let Some(values) = sessions.remove(session_name) {
414 let replaced = sessions.insert(new_name.clone(), values);
415 debug_assert!(replaced.is_none());
416 }
417 self.sessions = sessions;
418 let unsets = self.session_unsets.remove(session_name);
419 if let Some(unsets) = unsets {
420 let replaced = self.session_unsets.insert(new_name, unsets);
421 debug_assert!(replaced.is_none());
422 }
423 Ok(())
424 }
425
426 fn scope_entries_mut(
427 &mut self,
428 scope: ScopeSelector,
429 ) -> &mut HashMap<String, EnvironmentEntry> {
430 match scope {
431 ScopeSelector::Global => &mut self.global,
432 ScopeSelector::Session(session_name) => self.sessions.entry(session_name).or_default(),
433 ScopeSelector::Window(_) | ScopeSelector::Pane(_) => {
434 unreachable!("environment mutations are validated before storage")
435 }
436 }
437 }
438
439 fn scope_entries(&self, scope: &ScopeSelector) -> Option<&HashMap<String, EnvironmentEntry>> {
440 match scope {
441 ScopeSelector::Global => Some(&self.global),
442 ScopeSelector::Session(session_name) => self.sessions.get(session_name),
443 ScopeSelector::Window(_) | ScopeSelector::Pane(_) => None,
444 }
445 }
446
447 fn record_unset(&mut self, scope: &ScopeSelector, name: String) {
448 match scope {
449 ScopeSelector::Global => {
450 insert_name_into_set(&mut self.global_unsets, name);
451 }
452 ScopeSelector::Session(session_name) => {
453 let unsets = self.session_unsets.entry(session_name.clone()).or_default();
454 insert_name_into_set(unsets, name);
455 }
456 ScopeSelector::Window(_) | ScopeSelector::Pane(_) => {}
457 }
458 }
459
460 fn forget_unset(&mut self, scope: &ScopeSelector, name: &str) {
461 match scope {
462 ScopeSelector::Global => {
463 remove_name_from_set(&mut self.global_unsets, name);
464 }
465 ScopeSelector::Session(session_name) => {
466 let remove_bucket = if let Some(unsets) = self.session_unsets.get_mut(session_name)
467 {
468 remove_name_from_set(unsets, name);
469 unsets.is_empty()
470 } else {
471 false
472 };
473 if remove_bucket {
474 self.session_unsets.remove(session_name);
475 }
476 }
477 ScopeSelector::Window(_) | ScopeSelector::Pane(_) => {}
478 }
479 }
480}
481
482fn apply_entry_to_child_environment(
483 values: &mut HashMap<String, String>,
484 name: &str,
485 entry: &EnvironmentEntry,
486) {
487 if entry.is_hidden() || entry.is_cleared() {
488 remove_environment_name(values, name);
489 } else if let Some(value) = entry.value() {
490 remove_environment_name(values, name);
491 values.insert(name.to_owned(), value.to_owned());
492 }
493}
494
495fn collect_suppressed_entry_names(
496 entries: &HashMap<String, EnvironmentEntry>,
497 include_implicit: bool,
498 names: &mut HashSet<String>,
499) {
500 for (name, entry) in entries {
501 if !include_implicit && entry.is_implicit() {
502 continue;
503 }
504 if entry.is_hidden() || entry.is_cleared() {
505 names.insert(name.clone());
506 }
507 }
508}
509
510fn remove_unset_names(values: &mut HashMap<String, String>, names: &HashSet<String>) {
511 for name in names {
512 remove_environment_name(values, name);
513 }
514}
515
516fn remove_environment_name(values: &mut HashMap<String, String>, name: &str) {
517 #[cfg(windows)]
518 if let Some(existing) = values
519 .keys()
520 .find(|key| key.eq_ignore_ascii_case(name))
521 .cloned()
522 {
523 values.remove(&existing);
524 return;
525 }
526
527 values.remove(name);
528}
529
530fn insert_environment_entry(
531 entries: &mut HashMap<String, EnvironmentEntry>,
532 name: String,
533 entry: EnvironmentEntry,
534) {
535 let _ = remove_environment_entry(entries, &name);
536 entries.insert(name, entry);
537}
538
539fn remove_environment_entry(
540 entries: &mut HashMap<String, EnvironmentEntry>,
541 name: &str,
542) -> Option<EnvironmentEntry> {
543 #[cfg(windows)]
544 if let Some(existing) = entries
545 .keys()
546 .find(|key| key.eq_ignore_ascii_case(name))
547 .cloned()
548 {
549 return entries.remove(&existing);
550 }
551
552 entries.remove(name)
553}
554
555fn environment_entry<'a>(
556 entries: &'a HashMap<String, EnvironmentEntry>,
557 name: &str,
558) -> Option<&'a EnvironmentEntry> {
559 environment_entry_with_name(entries, name).map(|(_, entry)| entry)
560}
561
562fn environment_entry_with_name<'a>(
563 entries: &'a HashMap<String, EnvironmentEntry>,
564 name: &str,
565) -> Option<(&'a str, &'a EnvironmentEntry)> {
566 #[cfg(windows)]
567 if let Some((existing, entry)) = entries
568 .iter()
569 .find(|(key, _)| key.eq_ignore_ascii_case(name))
570 {
571 return Some((existing.as_str(), entry));
572 }
573
574 entries
575 .get_key_value(name)
576 .map(|(key, entry)| (key.as_str(), entry))
577}
578
579fn environment_entry_mut<'a>(
580 entries: &'a mut HashMap<String, EnvironmentEntry>,
581 name: &str,
582) -> Option<&'a mut EnvironmentEntry> {
583 #[cfg(windows)]
584 if let Some(existing) = entries
585 .keys()
586 .find(|key| key.eq_ignore_ascii_case(name))
587 .cloned()
588 {
589 return entries.get_mut(&existing);
590 }
591
592 entries.get_mut(name)
593}
594
595fn insert_name_into_set(names: &mut HashSet<String>, name: String) {
596 remove_name_from_set(names, &name);
597 names.insert(name);
598}
599
600fn remove_name_from_set(names: &mut HashSet<String>, name: &str) -> bool {
601 #[cfg(windows)]
602 if let Some(existing) = names
603 .iter()
604 .find(|candidate| candidate.eq_ignore_ascii_case(name))
605 .cloned()
606 {
607 return names.remove(&existing);
608 }
609
610 names.remove(name)
611}
612
613fn empty_environment_entries() -> &'static HashMap<String, EnvironmentEntry> {
614 static EMPTY: std::sync::OnceLock<HashMap<String, EnvironmentEntry>> =
615 std::sync::OnceLock::new();
616 EMPTY.get_or_init(HashMap::new)
617}
618
619#[cfg(test)]
620#[path = "environment/tests.rs"]
621mod tests;