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}