Skip to main content

netspeed_cli/
terminal.rs

1//! Terminal environment detection and abstraction.
2//!
3//! This module provides terminal display environment detection functions
4//! and a trait abstraction for terminal capabilities.
5//!
6//! Functions:
7//! - [`no_color()`] — Detect if colored output should be disabled (`NO_COLOR` env)
8//! - [`no_emoji()`] — Detect if emojis should be disabled (`NO_EMOJI` env)
9//! - [`no_animation()`] — Detect if animations should be skipped (`PREFER_REDUCED_MOTION`)
10//!
11//! Trait:
12//! - [`Capabilities`] — Abstraction for terminal display capabilities
13
14/// Detect if [`NO_COLOR`](https://no-color.org/) environment variable is set.
15///
16/// When set, all colorization should be disabled regardless of theme settings.
17/// This follows the standard: <https://no-color.org/>
18///
19/// # Example
20///
21/// ```
22/// use netspeed_cli::terminal::no_color;
23///
24/// if no_color() {
25///     println!("Plain output");
26/// } else {
27///     println!("Colorized output");
28/// }
29/// ```
30#[must_use]
31pub fn no_color() -> bool {
32    std::env::var("NO_COLOR").is_ok()
33}
34
35/// Detect if emojis should be disabled.
36///
37/// Checked via `NO_EMOJI` environment variable, or set programmatically
38/// via `--no-emoji` flag in the CLI.
39///
40/// # Example
41///
42/// ```
43/// use netspeed_cli::terminal::no_emoji;
44///
45/// let rating = if no_emoji() { "Good" } else { "✅ Good" };
46/// ```
47#[must_use]
48pub fn no_emoji() -> bool {
49    std::env::var("NO_EMOJI").is_ok()
50}
51
52/// Detect if animations should be skipped.
53///
54/// Skips intentional-friction delays and spinner animations for users with
55/// vestibular disorders. Follows the `PREFER_REDUCED_MOTION` accessibility
56/// media query convention.
57///
58/// # Example
59///
60/// ```
61/// use netspeed_cli::terminal::no_animation;
62///
63/// if no_animation() {
64///     print!("Result: A");
65/// } else {
66///     // Skip animation for accessibility
67///     print!("Result: A (animation skipped)");
68/// }
69/// ```
70#[must_use]
71pub fn no_animation() -> bool {
72    std::env::var("PREFER_REDUCED_MOTION").is_ok()
73}
74
75/// Trait for terminal display capabilities.
76///
77/// Implement this trait to provide custom terminal display behavior,
78/// useful for testing or alternative terminal implementations.
79pub trait Capabilities {
80    /// Returns true if colored output should be disabled.
81    fn is_color_disabled(&self) -> bool;
82
83    /// Returns true if emojis should be disabled.
84    fn is_emoji_disabled(&self) -> bool;
85
86    /// Returns true if animations should be skipped.
87    fn prefers_reduced_motion(&self) -> bool;
88}
89
90/// Default terminal display based on environment variables.
91pub struct Env;
92
93impl Capabilities for Env {
94    fn is_color_disabled(&self) -> bool {
95        no_color()
96    }
97
98    fn is_emoji_disabled(&self) -> bool {
99        no_emoji()
100    }
101
102    fn prefers_reduced_motion(&self) -> bool {
103        no_animation()
104    }
105}
106
107/// Terminal settings resolved at startup.
108///
109/// Captures terminal capabilities once to avoid repeated env var lookups.
110#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
111pub struct Settings {
112    /// Whether colors should be disabled.
113    pub no_color: bool,
114    /// Whether emojis should be disabled.
115    pub no_emoji: bool,
116    /// Whether animations should be skipped.
117    pub no_animation: bool,
118}
119
120impl Settings {
121    /// Create terminal settings from current environment.
122    ///
123    /// This captures the environment state at initialization time.
124    /// For testing, construct manually or use `Settings::default()`.
125    #[must_use]
126    pub fn from_environment() -> Self {
127        Self {
128            no_color: no_color(),
129            no_emoji: no_emoji(),
130            no_animation: no_animation(),
131        }
132    }
133}
134
135impl Capabilities for Settings {
136    fn is_color_disabled(&self) -> bool {
137        self.no_color
138    }
139
140    fn is_emoji_disabled(&self) -> bool {
141        self.no_emoji
142    }
143
144    fn prefers_reduced_motion(&self) -> bool {
145        self.no_animation
146    }
147}
148
149#[cfg(test)]
150mod tests {
151    use super::*;
152
153    #[test]
154    fn test_no_color_default() {
155        // Just verify it doesn't panic - actual value depends on env
156        let _ = no_color();
157    }
158
159    #[test]
160    fn test_no_emoji_default() {
161        let _ = no_emoji();
162    }
163
164    #[test]
165    fn test_no_animation_default() {
166        let _ = no_animation();
167    }
168
169    #[test]
170    fn test_terminal_settings_default() {
171        let settings = Settings::default();
172        // Default is all false (unless env vars set)
173        assert!(!settings.is_color_disabled() || no_color());
174    }
175
176    #[test]
177    fn test_terminal_settings_from_environment() {
178        let settings = Settings::from_environment();
179        assert_eq!(settings.is_color_disabled(), no_color());
180        assert_eq!(settings.is_emoji_disabled(), no_emoji());
181        assert_eq!(settings.prefers_reduced_motion(), no_animation());
182    }
183
184    #[test]
185    fn test_default_terminal_trait() {
186        let terminal = Env;
187        // Just verify trait methods don't panic
188        let _ = terminal.is_color_disabled();
189        let _ = terminal.is_emoji_disabled();
190        let _ = terminal.prefers_reduced_motion();
191    }
192}