yansi/condition.rs
1use core::fmt;
2use core::sync::atomic::AtomicU8;
3use core::sync::atomic::{AtomicPtr, Ordering};
4
5/// A function that decides whether styling should be applied.
6///
7/// A styling `Condition` can be specified globally via
8/// [`yansi::whenever()`](crate::whenever()) or locally to a specific style via
9/// the [`whenever()`](crate::Style::whenever()) builder method. Any time a
10/// [`Painted`](crate::Painted) value is formatted, both the local and global
11/// conditions are checked, and only when both evaluate to `true` is styling
12/// actually applied.
13///
14/// A `Condition` is nothing more than a function that returns a `bool`. The
15/// function is called each and every time a `Painted` is formatted, and so it
16/// is expected to be fast. All of the built-in conditions (except for their
17/// "live" variants) cache their first evaluation as a result: the
18/// [`Condition::cached()`] constructor can do the same for your conditions.
19///
20/// # Built-In Conditions
21///
22/// `yansi` comes with built-in conditions for common scenarios that can be
23/// enabled via crate features:
24///
25/// | feature(s) | condition | implication |
26/// |------------------------------|---------------------------------|------------------------|
27/// | `detect-tty` | [TTY Detectors] | `std`, [`is-terminal`] |
28/// | `detect-env` | [Environment Variable Checkers] | `std` |
29/// | [`detect-tty`, `detect-env`] | All Above, [Combo Detectors] | `std`, [`is-terminal`] |
30///
31/// [`is-terminal`]: https://docs.rs/is-terminal
32///
33/// For example, to enable the TTY detectors, enable the `detect-tty` feature:
34///
35/// ```toml
36/// yansi = { version = "...", features = ["detect-tty"] }
37/// ```
38///
39/// To enable the TTY detectors, env-var checkers, and combo detectors, enable
40/// `detect-tty` _and_ `detect-env`:
41///
42/// ```toml
43/// yansi = { version = "...", features = ["detect-tty", "detect-env"] }
44/// ```
45///
46/// ```rust
47/// # #[cfg(all(feature = "detect-tty", feature = "detect-env"))] {
48/// use yansi::Condition;
49///
50/// yansi::whenever(Condition::TTY_AND_COLOR);
51/// # }
52/// ```
53///
54/// [TTY detectors]: Condition#impl-Condition-1
55/// [Environment Variable Checkers]: Condition#impl-Condition-2
56/// [Combo Detectors]: Condition#impl-Condition-3
57///
58/// # Custom Conditions
59///
60/// Custom, arbitrary conditions can be created with [`Condition::from()`] or
61/// [`Condition::cached()`].
62///
63/// ```rust
64/// # #[cfg(all(feature = "detect-tty", feature = "detect-env"))] {
65/// use yansi::{Condition, Style, Color::*};
66///
67/// // Combine two conditions (`stderr` is a TTY, `CLICOLOR` is set) into one.
68/// static STDERR_COLOR: Condition = Condition::from(||
69/// Condition::stderr_is_tty() && Condition::clicolor()
70/// );
71///
72/// static DEBUG: Style = Yellow.bold().on_primary().invert().whenever(STDERR_COLOR);
73/// # }
74/// ```
75#[repr(transparent)]
76#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
77pub struct Condition(
78 /// The function that gets called to check the condition.
79 pub fn() -> bool
80);
81
82#[repr(transparent)]
83pub struct AtomicCondition(AtomicPtr<()>);
84
85#[allow(unused)]
86#[repr(transparent)]
87pub struct CachedBool(AtomicU8);
88
89impl Condition {
90 /// A condition that evaluates to `true` if the OS supports coloring.
91 ///
92 /// Uses [`Condition::os_support()`]. On Windows, this condition tries to
93 /// enable coloring support on the first call and caches the result for
94 /// subsequent calls. Outside of Windows, this always evaluates to `true`.
95 pub const DEFAULT: Condition = Condition(Condition::os_support);
96
97 /// A condition that always evaluates to `true`.
98 pub const ALWAYS: Condition = Condition(Condition::always);
99
100 /// A condition that always evaluated to `false`.
101 pub const NEVER: Condition = Condition(Condition::never);
102
103 /// Creates a dynamically checked condition from a function `f`.
104 ///
105 /// The function `f` is called anytime the condition is checked, including
106 /// every time a style with the condition is used.
107 ///
108 /// # Example
109 ///
110 /// ```rust,no_run
111 /// use yansi::Condition;
112 ///
113 /// fn some_function() -> bool {
114 /// /* checking arbitrary conditions */
115 /// todo!()
116 /// }
117 ///
118 /// // Create a custom static condition from a function.
119 /// static MY_CONDITION: Condition = Condition::from(some_function);
120 ///
121 /// // Create a condition on the stack from a function.
122 /// let my_condition = Condition::from(some_function);
123 ///
124 /// // Create a static condition from a closure that becomes a `fn`.
125 /// static MY_CONDITION_2: Condition = Condition::from(|| false);
126 ///
127 /// // Create a condition on the stack from a closure that becomes a `fn`.
128 /// let my_condition = Condition::from(|| some_function());
129 /// ```
130 pub const fn from(f: fn() -> bool) -> Self {
131 Condition(f)
132 }
133
134 /// Creates a condition that is [`ALWAYS`](Self::ALWAYS) when `value` is
135 /// `true` and [`NEVER`](Self::NEVER) otherwise.
136 ///
137 /// # Example
138 ///
139 /// ```rust,no_run
140 /// use yansi::Condition;
141 ///
142 /// fn some_function() -> bool {
143 /// /* checking arbitrary conditions */
144 /// todo!()
145 /// }
146 ///
147 /// // Cache the result of `some_function()` so it doesn't get called each
148 /// // time the condition needs to be checked.
149 /// let my_condition = Condition::cached(some_function());
150 /// ```
151 pub const fn cached(value: bool) -> Self {
152 match value {
153 true => Condition::ALWAYS,
154 false => Condition::NEVER,
155 }
156 }
157
158 /// The backing function for [`Condition::ALWAYS`]. Returns `true` always.
159 pub const fn always() -> bool { true }
160
161 /// The backing function for [`Condition::NEVER`]. Returns `false` always.
162 pub const fn never() -> bool { false }
163
164 /// The backing function for [`Condition::DEFAULT`].
165 ///
166 /// Returns `true` if the current OS supports ANSI escape sequences for
167 /// coloring. Outside of Windows, this always returns `true`. On Windows,
168 /// the first call to this function attempts to enable support and returns
169 /// whether it was successful every time thereafter.
170 pub fn os_support() -> bool {
171 crate::windows::cache_enable()
172 }
173}
174
175impl Default for Condition {
176 fn default() -> Self {
177 Condition::DEFAULT
178 }
179}
180
181impl core::ops::Deref for Condition {
182 type Target = fn() -> bool;
183
184 fn deref(&self) -> &Self::Target {
185 &self.0
186 }
187}
188
189impl AtomicCondition {
190 pub const DEFAULT: AtomicCondition = AtomicCondition::from(Condition::DEFAULT);
191
192 pub const fn from(value: Condition) -> Self {
193 AtomicCondition(AtomicPtr::new(value.0 as *mut ()))
194 }
195
196 pub fn store(&self, cond: Condition) {
197 self.0.store(cond.0 as *mut (), Ordering::Release)
198 }
199
200 pub fn read(&self) -> bool {
201 let condition = unsafe {
202 Condition(core::mem::transmute(self.0.load(Ordering::Acquire)))
203 };
204
205 condition()
206 }
207}
208
209#[allow(unused)]
210impl CachedBool {
211 const TRUE: u8 = 1;
212 const UNINIT: u8 = 2;
213 const INITING: u8 = 3;
214
215 pub const fn new() -> Self {
216 CachedBool(AtomicU8::new(Self::UNINIT))
217 }
218
219 pub fn get_or_init(&self, f: impl FnOnce() -> bool) -> bool {
220 use core::sync::atomic::Ordering::*;
221
222 match self.0.compare_exchange(Self::UNINIT, Self::INITING, AcqRel, Relaxed) {
223 Ok(_) => {
224 let new_value = f();
225 self.0.store(new_value as u8 /* false = 0, true = 1 */, Release);
226 new_value
227 }
228 Err(Self::INITING) => {
229 let mut value;
230 while { value = self.0.load(Acquire); value } == Self::INITING {
231 #[cfg(feature = "std")]
232 std::thread::yield_now();
233 }
234
235 value == Self::TRUE
236 },
237 Err(value) => value == Self::TRUE,
238 }
239 }
240}
241
242impl fmt::Debug for Condition {
243 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
244 if *self == Condition::DEFAULT {
245 f.write_str("Condition::DEFAULT")
246 } else if *self == Condition::ALWAYS {
247 f.write_str("Condition::ALWAYS")
248 } else if *self == Condition::NEVER {
249 f.write_str("Condition::NEVER")
250 } else {
251 f.debug_tuple("Condition").field(&self.0).finish()
252 }
253 }
254}
255
256macro_rules! conditions {
257 ($feat:meta $($f:expr, $CACHED:ident: $cached:ident, $LIVE:ident: $live:ident),* $(,)?) => (
258 #[cfg($feat)]
259 #[cfg_attr(feature = "_nightly", doc(cfg($feat)))]
260 /// Feature dependent conditions.
261 ///
262 /// Available when compiled with
263 #[doc = concat!('`', stringify!($feat), "`.")]
264 impl Condition {
265 $(
266 /// Evaluates to `true` if
267 #[doc = concat!('`', stringify!($f), "`.")]
268 ///
269 /// The result of the first check is cached for subsequent
270 /// checks. Internally uses
271 #[doc = concat!("[`", stringify!($cached), "`](Condition::", stringify!($cached), ").")]
272 pub const $CACHED: Condition = Condition(Condition::$cached);
273 )*
274
275 $(
276 /// Evaluates to `true` if
277 #[doc = concat!('`', stringify!($f), "`.")]
278 ///
279 /// A call is dispatched each time the condition is checked.
280 /// This is expensive, so prefer to use
281 #[doc = concat!("[`", stringify!($CACHED), "`](Condition::", stringify!($CACHED), ")")]
282 /// instead.
283 ///
284 /// Internally uses
285 #[doc = concat!("[`", stringify!($live), "`](Condition::", stringify!($live), ").")]
286 pub const $LIVE: Condition = Condition(Condition::$live);
287 )*
288
289 $(
290 /// Returns `true` if
291 #[doc = concat!('`', stringify!($f), "`.")]
292 ///
293 /// The result of the first check is cached for subsequent
294 /// checks. This is the backing function for
295 #[doc = concat!("[`", stringify!($CACHED), "`](Condition::", stringify!($CACHED), ").")]
296 pub fn $cached() -> bool {
297 static IS_TTY: CachedBool = CachedBool::new();
298 IS_TTY.get_or_init(Condition::$live)
299 }
300 )*
301
302 $(
303 /// Returns `true` if
304 #[doc = concat!('`', stringify!($f), "`.")]
305 ///
306 /// This is the backing function for
307 #[doc = concat!("[`", stringify!($LIVE), "`](Condition::", stringify!($LIVE), ").")]
308 pub fn $live() -> bool {
309 $f
310 }
311 )*
312 }
313 )
314}
315
316#[cfg(feature = "detect-tty")]
317use is_terminal::is_terminal as is_tty;
318
319conditions! { feature = "detect-tty"
320 is_tty(&std::io::stdout()),
321 STDOUT_IS_TTY: stdout_is_tty,
322 STDOUT_IS_TTY_LIVE: stdout_is_tty_live,
323
324 is_tty(&std::io::stderr()),
325 STDERR_IS_TTY: stderr_is_tty,
326 STDERR_IS_TTY_LIVE: stderr_is_tty_live,
327
328 is_tty(&std::io::stdin()),
329 STDIN_IS_TTY: stdin_is_tty,
330 STDIN_IS_TTY_LIVE: stdin_is_tty_live,
331
332 is_tty(&std::io::stdout()) && is_tty(&std::io::stderr()),
333 STDOUTERR_ARE_TTY: stdouterr_are_tty,
334 STDOUTERR_ARE_TTY_LIVE: stdouterr_are_tty_live,
335}
336
337#[cfg(feature = "detect-env")]
338pub fn env_set_or(name: &str, default: bool) -> bool {
339 std::env::var_os(name).map_or(default, |v| v != "0")
340}
341
342conditions! { feature = "detect-env"
343 env_set_or("CLICOLOR_FORCE", false) || env_set_or("CLICOLOR", true),
344 CLICOLOR: clicolor,
345 CLICOLOR_LIVE: clicolor_live,
346
347 !env_set_or("NO_COLOR", false),
348 YES_COLOR: no_color,
349 YES_COLOR_LIVE: no_color_live,
350}
351
352conditions! { all(feature = "detect-env", feature = "detect-tty")
353 Condition::stdouterr_are_tty() && Condition::clicolor() && Condition::no_color(),
354 TTY_AND_COLOR: tty_and_color,
355 TTY_AND_COLOR_LIVE: tty_and_color_live,
356}