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
15#[derive(Debug, Clone, Copy, PartialEq, Eq)]
17pub enum ClientContextParent {
18 None,
20 Rmux,
22 Tmux,
24}
25
26impl ClientContext {
27 #[must_use]
29 pub const fn is_nested(self) -> bool {
30 matches!(self, Self::Nested)
31 }
32}
33
34#[derive(Debug, Clone, Copy, PartialEq, Eq)]
36pub struct NestedContextError;
37
38impl fmt::Display for NestedContextError {
39 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
40 formatter.write_str("switch-client requires a nested client context")
41 }
42}
43
44impl StdError for NestedContextError {}
45
46#[must_use]
51pub fn detect_context() -> ClientContext {
52 detect_context_from_env(
53 std::env::var_os("RMUX").as_deref(),
54 std::env::var_os("TMUX").as_deref(),
55 )
56}
57
58#[must_use]
60pub fn detect_parent() -> ClientContextParent {
61 detect_parent_from_env(
62 std::env::var_os("RMUX").as_deref(),
63 std::env::var_os("TMUX").as_deref(),
64 )
65}
66
67pub fn ensure_nested_context(context: ClientContext) -> Result<(), NestedContextError> {
69 if context.is_nested() {
70 Ok(())
71 } else {
72 Err(NestedContextError)
73 }
74}
75
76pub fn require_nested_context() -> Result<(), NestedContextError> {
78 ensure_nested_context(detect_context())
79}
80
81fn detect_context_from_env(
85 rmux_value: Option<&std::ffi::OsStr>,
86 tmux_value: Option<&std::ffi::OsStr>,
87) -> ClientContext {
88 detect_parent_from_env(rmux_value, tmux_value).context()
89}
90
91fn detect_parent_from_env(
92 rmux_value: Option<&std::ffi::OsStr>,
93 tmux_value: Option<&std::ffi::OsStr>,
94) -> ClientContextParent {
95 if rmux_value.is_some_and(|value| !value.is_empty()) {
96 return ClientContextParent::Rmux;
97 }
98 if tmux_value.is_some_and(|value| !value.is_empty()) {
99 return ClientContextParent::Tmux;
100 }
101 ClientContextParent::None
102}
103
104impl ClientContextParent {
105 fn context(self) -> ClientContext {
106 match self {
107 Self::None => ClientContext::Outside,
108 Self::Rmux | Self::Tmux => ClientContext::Nested,
109 }
110 }
111}
112
113#[cfg(test)]
114mod tests {
115 use super::{
116 detect_context_from_env, detect_parent_from_env, ensure_nested_context,
117 require_nested_context, ClientContext, ClientContextParent, NestedContextError,
118 };
119 use std::ffi::OsStr;
120 use std::sync::Mutex;
121
122 static RMUX_ENV_LOCK: Mutex<()> = Mutex::new(());
123
124 #[test]
125 fn absent_rmux_is_outside() {
126 assert_eq!(detect_context_from_env(None, None), ClientContext::Outside);
127 assert_eq!(
128 detect_parent_from_env(None, None),
129 ClientContextParent::None
130 );
131 }
132
133 #[test]
134 fn empty_rmux_is_outside() {
135 assert_eq!(
136 detect_context_from_env(Some(OsStr::new("")), Some(OsStr::new(""))),
137 ClientContext::Outside
138 );
139 assert_eq!(
140 detect_parent_from_env(Some(OsStr::new("")), Some(OsStr::new(""))),
141 ClientContextParent::None
142 );
143 }
144
145 #[test]
146 fn nonempty_rmux_is_nested() {
147 assert_eq!(
148 detect_context_from_env(Some(OsStr::new("/tmp/rmux-1000/default,12345,0")), None),
149 ClientContext::Nested
150 );
151 assert_eq!(
152 detect_parent_from_env(Some(OsStr::new("/tmp/rmux-1000/default,12345,0")), None),
153 ClientContextParent::Rmux
154 );
155 }
156
157 #[test]
158 fn nonempty_tmux_is_nested() {
159 assert_eq!(
160 detect_context_from_env(None, Some(OsStr::new("/tmp/rmux-1000/default,12345,0"))),
161 ClientContext::Nested
162 );
163 assert_eq!(
164 detect_parent_from_env(None, Some(OsStr::new("/tmp/rmux-1000/default,12345,0"))),
165 ClientContextParent::Tmux
166 );
167 }
168
169 #[test]
170 fn any_nonempty_value_is_nested() {
171 assert_eq!(
172 detect_context_from_env(Some(OsStr::new("x")), None),
173 ClientContext::Nested
174 );
175 }
176
177 #[test]
178 fn rmux_parent_takes_precedence_over_tmux_parent() {
179 assert_eq!(
180 detect_parent_from_env(Some(OsStr::new("rmux")), Some(OsStr::new("tmux"))),
181 ClientContextParent::Rmux
182 );
183 }
184
185 #[test]
186 fn is_nested_accessor() {
187 assert!(ClientContext::Nested.is_nested());
188 assert!(!ClientContext::Outside.is_nested());
189 }
190
191 #[test]
192 fn ensure_nested_context_rejects_outside_contexts() {
193 assert_eq!(
194 ensure_nested_context(ClientContext::Outside),
195 Err(NestedContextError)
196 );
197 assert_eq!(ensure_nested_context(ClientContext::Nested), Ok(()));
198 }
199
200 #[test]
201 fn require_nested_context_reads_env() {
202 let _guard = RMUX_ENV_LOCK.lock().expect("rmux env lock");
203 let original = std::env::var_os("RMUX");
204 let original_tmux = std::env::var_os("TMUX");
205
206 std::env::remove_var("RMUX");
207 std::env::remove_var("TMUX");
208 assert_eq!(super::detect_context(), ClientContext::Outside);
209 assert_eq!(require_nested_context(), Err(NestedContextError));
210
211 std::env::set_var("RMUX", "/tmp/rmux-1000/default,1,0");
212 assert_eq!(super::detect_context(), ClientContext::Nested);
213 assert_eq!(require_nested_context(), Ok(()));
214
215 std::env::remove_var("RMUX");
216 std::env::set_var("TMUX", "/tmp/rmux-1000/default,1,0");
217 assert_eq!(super::detect_context(), ClientContext::Nested);
218 assert_eq!(require_nested_context(), Ok(()));
219
220 match original {
221 Some(value) => std::env::set_var("RMUX", value),
222 None => std::env::remove_var("RMUX"),
223 }
224 match original_tmux {
225 Some(value) => std::env::set_var("TMUX", value),
226 None => std::env::remove_var("TMUX"),
227 }
228 }
229}