Skip to main content

rmux_client/
nested.rs

1//! Nested-session detection via `$RMUX`.
2
3use std::error::Error as StdError;
4use std::fmt;
5
6/// The client context inferred from the `$RMUX` environment variable.
7#[derive(Debug, Clone, Copy, PartialEq, Eq)]
8pub enum ClientContext {
9    /// No `$RMUX` variable is set - the client is outside any multiplexer.
10    Outside,
11    /// `$RMUX` is set - the client is inside an existing multiplexer session.
12    Nested,
13}
14
15impl ClientContext {
16    /// Returns `true` when the client is inside a nested multiplexer session.
17    #[must_use]
18    pub const fn is_nested(self) -> bool {
19        matches!(self, Self::Nested)
20    }
21}
22
23/// Error returned when a command requires a nested client context.
24#[derive(Debug, Clone, Copy, PartialEq, Eq)]
25pub struct NestedContextError;
26
27impl fmt::Display for NestedContextError {
28    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
29        formatter.write_str("switch-client requires a nested client context")
30    }
31}
32
33impl StdError for NestedContextError {}
34
35/// Detects the client context by inspecting the `$RMUX` environment variable.
36///
37/// A non-empty `$RMUX` value indicates a nested context. An absent or empty
38/// value indicates the client is outside any multiplexer session.
39#[must_use]
40pub fn detect_context() -> ClientContext {
41    detect_context_from_env(std::env::var_os("RMUX").as_deref())
42}
43
44/// Returns an error when the supplied context is not nested.
45pub fn ensure_nested_context(context: ClientContext) -> Result<(), NestedContextError> {
46    if context.is_nested() {
47        Ok(())
48    } else {
49        Err(NestedContextError)
50    }
51}
52
53/// Detects the current client context and validates that it is nested.
54pub fn require_nested_context() -> Result<(), NestedContextError> {
55    ensure_nested_context(detect_context())
56}
57
58/// Pure detection logic that does not access the environment directly.
59///
60/// Exposed for deterministic unit testing.
61fn detect_context_from_env(tmux_value: Option<&std::ffi::OsStr>) -> ClientContext {
62    match tmux_value {
63        Some(value) if !value.is_empty() => ClientContext::Nested,
64        _ => ClientContext::Outside,
65    }
66}
67
68#[cfg(test)]
69mod tests {
70    use super::{
71        detect_context_from_env, ensure_nested_context, require_nested_context, ClientContext,
72        NestedContextError,
73    };
74    use std::ffi::OsStr;
75    use std::sync::Mutex;
76
77    static RMUX_ENV_LOCK: Mutex<()> = Mutex::new(());
78
79    #[test]
80    fn absent_rmux_is_outside() {
81        assert_eq!(detect_context_from_env(None), ClientContext::Outside);
82    }
83
84    #[test]
85    fn empty_rmux_is_outside() {
86        assert_eq!(
87            detect_context_from_env(Some(OsStr::new(""))),
88            ClientContext::Outside
89        );
90    }
91
92    #[test]
93    fn nonempty_rmux_is_nested() {
94        assert_eq!(
95            detect_context_from_env(Some(OsStr::new("/tmp/rmux-1000/default,12345,0"))),
96            ClientContext::Nested
97        );
98    }
99
100    #[test]
101    fn any_nonempty_value_is_nested() {
102        assert_eq!(
103            detect_context_from_env(Some(OsStr::new("x"))),
104            ClientContext::Nested
105        );
106    }
107
108    #[test]
109    fn is_nested_accessor() {
110        assert!(ClientContext::Nested.is_nested());
111        assert!(!ClientContext::Outside.is_nested());
112    }
113
114    #[test]
115    fn ensure_nested_context_rejects_outside_contexts() {
116        assert_eq!(
117            ensure_nested_context(ClientContext::Outside),
118            Err(NestedContextError)
119        );
120        assert_eq!(ensure_nested_context(ClientContext::Nested), Ok(()));
121    }
122
123    #[test]
124    fn require_nested_context_reads_env() {
125        let _guard = RMUX_ENV_LOCK.lock().expect("rmux env lock");
126        let original = std::env::var_os("RMUX");
127
128        std::env::remove_var("RMUX");
129        assert_eq!(super::detect_context(), ClientContext::Outside);
130        assert_eq!(require_nested_context(), Err(NestedContextError));
131
132        std::env::set_var("RMUX", "/tmp/rmux-1000/default,1,0");
133        assert_eq!(super::detect_context(), ClientContext::Nested);
134        assert_eq!(require_nested_context(), Ok(()));
135
136        match original {
137            Some(value) => std::env::set_var("RMUX", value),
138            None => std::env::remove_var("RMUX"),
139        }
140    }
141}