1use std::error::Error as StdError;
4use std::fmt;
5
6#[derive(Debug, Clone, Copy, PartialEq, Eq)]
8pub enum ClientContext {
9 Outside,
11 Nested,
13}
14
15impl ClientContext {
16 #[must_use]
18 pub const fn is_nested(self) -> bool {
19 matches!(self, Self::Nested)
20 }
21}
22
23#[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#[must_use]
40pub fn detect_context() -> ClientContext {
41 detect_context_from_env(std::env::var_os("RMUX").as_deref())
42}
43
44pub fn ensure_nested_context(context: ClientContext) -> Result<(), NestedContextError> {
46 if context.is_nested() {
47 Ok(())
48 } else {
49 Err(NestedContextError)
50 }
51}
52
53pub fn require_nested_context() -> Result<(), NestedContextError> {
55 ensure_nested_context(detect_context())
56}
57
58fn 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}