Skip to main content

nils_common/
env.rs

1pub fn is_truthy(input: &str) -> bool {
2    matches!(input.to_lowercase().as_str(), "1" | "true" | "yes" | "on")
3}
4
5pub fn is_truthy_or(input: Option<&str>, default: bool) -> bool {
6    input.map(is_truthy).unwrap_or(default)
7}
8
9fn truthy_from_env(name: &str) -> Option<bool> {
10    std::env::var_os(name).map(|value| {
11        let value = value.to_string_lossy();
12        is_truthy(value.trim())
13    })
14}
15
16pub fn env_present(name: &str) -> bool {
17    std::env::var_os(name).is_some()
18}
19
20pub fn env_truthy_if_present(name: &str) -> Option<bool> {
21    std::env::var(name)
22        .ok()
23        .map(|value| is_truthy(value.trim()))
24}
25
26pub fn env_truthy(name: &str) -> bool {
27    truthy_from_env(name).unwrap_or(false)
28}
29
30pub fn env_truthy_or(name: &str, default: bool) -> bool {
31    truthy_from_env(name).unwrap_or(default)
32}
33
34pub fn env_or_default(name: &str, default: &str) -> String {
35    std::env::var(name).unwrap_or_else(|_| default.to_string())
36}
37
38pub fn env_non_empty(name: &str) -> Option<String> {
39    std::env::var(name)
40        .ok()
41        .map(|value| value.trim().to_string())
42        .filter(|value| !value.is_empty())
43}
44
45pub fn parse_duration_seconds(raw: &str) -> Option<u64> {
46    let raw = raw.trim();
47    if raw.is_empty() {
48        return None;
49    }
50
51    let raw = raw.to_ascii_lowercase();
52    let (num_part, multiplier): (&str, u64) = match raw.chars().last()? {
53        's' => (&raw[..raw.len().saturating_sub(1)], 1),
54        'm' => (&raw[..raw.len().saturating_sub(1)], 60),
55        'h' => (&raw[..raw.len().saturating_sub(1)], 60 * 60),
56        'd' => (&raw[..raw.len().saturating_sub(1)], 60 * 60 * 24),
57        'w' => (&raw[..raw.len().saturating_sub(1)], 60 * 60 * 24 * 7),
58        ch if ch.is_ascii_digit() => (raw.as_str(), 1),
59        _ => return None,
60    };
61
62    let num_part = num_part.trim();
63    if num_part.is_empty() {
64        return None;
65    }
66
67    let value = num_part.parse::<u64>().ok()?;
68    if value == 0 {
69        return None;
70    }
71
72    value.checked_mul(multiplier)
73}
74
75pub fn no_color_enabled() -> bool {
76    env_present("NO_COLOR")
77}
78
79pub fn no_color_non_empty_enabled() -> bool {
80    std::env::var("NO_COLOR")
81        .ok()
82        .is_some_and(|value| !value.trim().is_empty())
83}
84
85pub fn no_color_requested(explicit_no_color: bool) -> bool {
86    explicit_no_color || no_color_enabled()
87}
88
89pub fn prompt_segment_color_enabled(explicit_toggle_env: &str) -> bool {
90    use std::io::IsTerminal;
91
92    if no_color_enabled() {
93        return false;
94    }
95
96    if env_present(explicit_toggle_env) {
97        return env_truthy(explicit_toggle_env);
98    }
99
100    if env_present("STARSHIP_SESSION_KEY") || env_present("STARSHIP_SHELL") {
101        return true;
102    }
103
104    std::io::stdout().is_terminal()
105}
106
107#[cfg(test)]
108mod tests {
109    use super::*;
110    use nils_test_support::{EnvGuard, GlobalStateLock};
111
112    #[test]
113    fn is_truthy_matches_expected_values() {
114        for value in ["1", "true", "TRUE", "yes", "On"] {
115            assert!(is_truthy(value), "expected truthy value: {value}");
116        }
117    }
118
119    #[test]
120    fn is_truthy_rejects_falsey_or_unknown_values() {
121        for value in ["", "0", "false", "no", "off", " yes ", "enabled"] {
122            assert!(!is_truthy(value), "expected falsey value: {value}");
123        }
124    }
125
126    #[test]
127    fn is_truthy_or_uses_default_when_missing() {
128        assert!(is_truthy_or(None, true));
129        assert!(!is_truthy_or(None, false));
130        assert!(is_truthy_or(Some("1"), false));
131    }
132
133    #[test]
134    fn env_truthy_reads_process_environment() {
135        let lock = GlobalStateLock::new();
136        let _guard = EnvGuard::set(&lock, "NILS_COMMON_ENV_TRUTHY_TEST", "yes");
137        assert!(env_truthy("NILS_COMMON_ENV_TRUTHY_TEST"));
138    }
139
140    #[test]
141    fn env_present_checks_var_presence() {
142        let lock = GlobalStateLock::new();
143        let _guard = EnvGuard::set(&lock, "NILS_COMMON_ENV_PRESENT_TEST", "");
144        assert!(env_present("NILS_COMMON_ENV_PRESENT_TEST"));
145    }
146
147    #[test]
148    fn env_truthy_if_present_returns_none_when_missing() {
149        let lock = GlobalStateLock::new();
150        let _guard = EnvGuard::remove(&lock, "NILS_COMMON_ENV_TRUTHY_IF_PRESENT_MISSING_TEST");
151        assert_eq!(
152            env_truthy_if_present("NILS_COMMON_ENV_TRUTHY_IF_PRESENT_MISSING_TEST"),
153            None
154        );
155    }
156
157    #[test]
158    fn env_truthy_if_present_parses_trimmed_value() {
159        let lock = GlobalStateLock::new();
160        let _guard = EnvGuard::set(
161            &lock,
162            "NILS_COMMON_ENV_TRUTHY_IF_PRESENT_VALUE_TEST",
163            " yes ",
164        );
165        assert_eq!(
166            env_truthy_if_present("NILS_COMMON_ENV_TRUTHY_IF_PRESENT_VALUE_TEST"),
167            Some(true)
168        );
169    }
170
171    #[test]
172    fn env_truthy_trims_whitespace() {
173        let lock = GlobalStateLock::new();
174        let _guard = EnvGuard::set(&lock, "NILS_COMMON_ENV_TRUTHY_TRIM_TEST", " yes ");
175        assert!(env_truthy("NILS_COMMON_ENV_TRUTHY_TRIM_TEST"));
176    }
177
178    #[test]
179    fn env_truthy_or_falls_back_to_default() {
180        let lock = GlobalStateLock::new();
181        let _guard = EnvGuard::remove(&lock, "NILS_COMMON_ENV_TRUTHY_OR_TEST");
182        assert!(env_truthy_or("NILS_COMMON_ENV_TRUTHY_OR_TEST", true));
183        assert!(!env_truthy_or("NILS_COMMON_ENV_TRUTHY_OR_TEST", false));
184    }
185
186    #[test]
187    fn env_truthy_or_prefers_present_trimmed_values() {
188        let lock = GlobalStateLock::new();
189        let _guard = EnvGuard::set(&lock, "NILS_COMMON_ENV_TRUTHY_OR_VALUE_TEST", " off ");
190        assert!(!env_truthy_or("NILS_COMMON_ENV_TRUTHY_OR_VALUE_TEST", true));
191    }
192
193    #[test]
194    fn env_or_default_prefers_present_value() {
195        let lock = GlobalStateLock::new();
196        let _guard = EnvGuard::set(&lock, "NILS_COMMON_ENV_OR_DEFAULT_PRESENT_TEST", "custom");
197        assert_eq!(
198            env_or_default("NILS_COMMON_ENV_OR_DEFAULT_PRESENT_TEST", "fallback"),
199            "custom"
200        );
201    }
202
203    #[test]
204    fn env_or_default_uses_default_when_missing() {
205        let lock = GlobalStateLock::new();
206        let _guard = EnvGuard::remove(&lock, "NILS_COMMON_ENV_OR_DEFAULT_MISSING_TEST");
207        assert_eq!(
208            env_or_default("NILS_COMMON_ENV_OR_DEFAULT_MISSING_TEST", "fallback"),
209            "fallback"
210        );
211    }
212
213    #[test]
214    fn env_non_empty_returns_none_for_missing_or_blank_values() {
215        let lock = GlobalStateLock::new();
216        let _missing = EnvGuard::remove(&lock, "NILS_COMMON_ENV_NON_EMPTY_MISSING_TEST");
217        assert_eq!(
218            env_non_empty("NILS_COMMON_ENV_NON_EMPTY_MISSING_TEST"),
219            None
220        );
221
222        let _blank = EnvGuard::set(&lock, "NILS_COMMON_ENV_NON_EMPTY_MISSING_TEST", "   ");
223        assert_eq!(
224            env_non_empty("NILS_COMMON_ENV_NON_EMPTY_MISSING_TEST"),
225            None
226        );
227    }
228
229    #[test]
230    fn env_non_empty_returns_trimmed_value_when_present() {
231        let lock = GlobalStateLock::new();
232        let _guard = EnvGuard::set(&lock, "NILS_COMMON_ENV_NON_EMPTY_VALUE_TEST", "  value  ");
233        assert_eq!(
234            env_non_empty("NILS_COMMON_ENV_NON_EMPTY_VALUE_TEST"),
235            Some("value".to_string())
236        );
237    }
238
239    #[test]
240    fn parse_duration_seconds_accepts_plain_and_suffixed_values() {
241        assert_eq!(parse_duration_seconds("45"), Some(45));
242        assert_eq!(parse_duration_seconds("45s"), Some(45));
243        assert_eq!(parse_duration_seconds("2m"), Some(120));
244        assert_eq!(parse_duration_seconds("3h"), Some(10_800));
245        assert_eq!(parse_duration_seconds("4d"), Some(345_600));
246        assert_eq!(parse_duration_seconds("2w"), Some(1_209_600));
247        assert_eq!(parse_duration_seconds(" 7H "), Some(25_200));
248    }
249
250    #[test]
251    fn parse_duration_seconds_rejects_invalid_inputs() {
252        for value in ["", " ", "0", "0s", "s", "-1", "1x", "ms"] {
253            assert_eq!(parse_duration_seconds(value), None, "value={value}");
254        }
255    }
256
257    #[test]
258    fn parse_duration_seconds_rejects_overflow() {
259        assert_eq!(parse_duration_seconds("18446744073709551615w"), None);
260    }
261
262    #[test]
263    fn no_color_enabled_checks_var_presence() {
264        let lock = GlobalStateLock::new();
265        let _guard = EnvGuard::set(&lock, "NO_COLOR", "");
266        assert!(no_color_enabled());
267    }
268
269    #[test]
270    fn no_color_non_empty_enabled_distinguishes_empty_and_non_empty() {
271        let lock = GlobalStateLock::new();
272
273        {
274            let _guard = EnvGuard::set(&lock, "NO_COLOR", "1");
275            assert!(no_color_non_empty_enabled());
276        }
277
278        {
279            let _guard = EnvGuard::set(&lock, "NO_COLOR", "");
280            assert!(!no_color_non_empty_enabled());
281        }
282    }
283
284    #[test]
285    fn no_color_requested_respects_explicit_flag() {
286        let lock = GlobalStateLock::new();
287        let _guard = EnvGuard::remove(&lock, "NO_COLOR");
288        assert!(no_color_requested(true));
289        assert!(!no_color_requested(false));
290    }
291
292    #[test]
293    fn no_color_requested_respects_env_presence() {
294        let lock = GlobalStateLock::new();
295        let _guard = EnvGuard::set(&lock, "NO_COLOR", "1");
296        assert!(no_color_requested(false));
297    }
298
299    #[test]
300    fn prompt_segment_color_enabled_no_color_has_highest_priority() {
301        let lock = GlobalStateLock::new();
302        let _no_color = EnvGuard::set(&lock, "NO_COLOR", "1");
303        let _explicit = EnvGuard::set(&lock, "NILS_COMMON_PROMPT_SEGMENT_COLOR_ENABLED", "1");
304        let _session = EnvGuard::set(&lock, "STARSHIP_SESSION_KEY", "session");
305        assert!(!prompt_segment_color_enabled(
306            "NILS_COMMON_PROMPT_SEGMENT_COLOR_ENABLED"
307        ));
308    }
309
310    #[test]
311    fn prompt_segment_color_enabled_honors_explicit_truthy_and_falsey_values() {
312        let lock = GlobalStateLock::new();
313        let _no_color = EnvGuard::remove(&lock, "NO_COLOR");
314        let _session = EnvGuard::remove(&lock, "STARSHIP_SESSION_KEY");
315        let _shell = EnvGuard::remove(&lock, "STARSHIP_SHELL");
316
317        for value in ["1", " true ", "YES", "on"] {
318            let _explicit = EnvGuard::set(&lock, "NILS_COMMON_PROMPT_SEGMENT_COLOR_ENABLED", value);
319            assert!(
320                prompt_segment_color_enabled("NILS_COMMON_PROMPT_SEGMENT_COLOR_ENABLED"),
321                "expected truthy value: {value}"
322            );
323        }
324
325        for value in ["", " ", "0", "false", "no", "off", "y", "enabled"] {
326            let _explicit = EnvGuard::set(&lock, "NILS_COMMON_PROMPT_SEGMENT_COLOR_ENABLED", value);
327            assert!(
328                !prompt_segment_color_enabled("NILS_COMMON_PROMPT_SEGMENT_COLOR_ENABLED"),
329                "expected falsey value: {value}"
330            );
331        }
332    }
333
334    #[test]
335    fn prompt_segment_color_enabled_uses_prompt_markers_when_not_overridden() {
336        let lock = GlobalStateLock::new();
337        let _no_color = EnvGuard::remove(&lock, "NO_COLOR");
338        let _explicit = EnvGuard::remove(&lock, "NILS_COMMON_PROMPT_SEGMENT_COLOR_ENABLED");
339        let _session = EnvGuard::set(&lock, "STARSHIP_SESSION_KEY", "session");
340        assert!(prompt_segment_color_enabled(
341            "NILS_COMMON_PROMPT_SEGMENT_COLOR_ENABLED"
342        ));
343    }
344}