blob: 6444c17c1b4117d812f35b5ff4bdd2e0db1fb27f [file] [log] [blame] [edit]
use core::fmt;
use core::sync::atomic::AtomicU8;
use core::sync::atomic::{AtomicPtr, Ordering};
/// A function that decides whether styling should be applied.
///
/// A styling `Condition` can be specified globally via
/// [`yansi::whenever()`](crate::whenever()) or locally to a specific style via
/// the [`whenever()`](crate::Style::whenever()) builder method. Any time a
/// [`Painted`](crate::Painted) value is formatted, both the local and global
/// conditions are checked, and only when both evaluate to `true` is styling
/// actually applied.
///
/// A `Condition` is nothing more than a function that returns a `bool`. The
/// function is called each and every time a `Painted` is formatted, and so it
/// is expected to be fast. All of the built-in conditions (except for their
/// "live" variants) cache their first evaluation as a result: the
/// [`Condition::cached()`] constructor can do the same for your conditions.
///
/// # Built-In Conditions
///
/// `yansi` comes with built-in conditions for common scenarios that can be
/// enabled via crate features:
///
/// | feature(s) | condition | implication |
/// |------------------------------|---------------------------------|------------------------|
/// | `detect-tty` | [TTY Detectors] | `std`, [`is-terminal`] |
/// | `detect-env` | [Environment Variable Checkers] | `std` |
/// | [`detect-tty`, `detect-env`] | All Above, [Combo Detectors] | `std`, [`is-terminal`] |
///
/// [`is-terminal`]: https://docs.rs/is-terminal
///
/// For example, to enable the TTY detectors, enable the `detect-tty` feature:
///
/// ```toml
/// yansi = { version = "...", features = ["detect-tty"] }
/// ```
///
/// To enable the TTY detectors, env-var checkers, and combo detectors, enable
/// `detect-tty` _and_ `detect-env`:
///
/// ```toml
/// yansi = { version = "...", features = ["detect-tty", "detect-env"] }
/// ```
///
/// ```rust
/// # #[cfg(all(feature = "detect-tty", feature = "detect-env"))] {
/// use yansi::Condition;
///
/// yansi::whenever(Condition::TTY_AND_COLOR);
/// # }
/// ```
///
/// [TTY detectors]: Condition#impl-Condition-1
/// [Environment Variable Checkers]: Condition#impl-Condition-2
/// [Combo Detectors]: Condition#impl-Condition-3
///
/// # Custom Conditions
///
/// Custom, arbitrary conditions can be created with [`Condition::from()`] or
/// [`Condition::cached()`].
///
/// ```rust
/// # #[cfg(all(feature = "detect-tty", feature = "detect-env"))] {
/// use yansi::{Condition, Style, Color::*};
///
/// // Combine two conditions (`stderr` is a TTY, `CLICOLOR` is set) into one.
/// static STDERR_COLOR: Condition = Condition::from(||
/// Condition::stderr_is_tty() && Condition::clicolor()
/// );
///
/// static DEBUG: Style = Yellow.bold().on_primary().invert().whenever(STDERR_COLOR);
/// # }
/// ```
#[repr(transparent)]
#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct Condition(
/// The function that gets called to check the condition.
pub fn() -> bool
);
#[repr(transparent)]
pub struct AtomicCondition(AtomicPtr<()>);
#[allow(unused)]
#[repr(transparent)]
pub struct CachedBool(AtomicU8);
impl Condition {
/// A condition that evaluates to `true` if the OS supports coloring.
///
/// Uses [`Condition::os_support()`]. On Windows, this condition tries to
/// enable coloring support on the first call and caches the result for
/// subsequent calls. Outside of Windows, this always evaluates to `true`.
pub const DEFAULT: Condition = Condition(Condition::os_support);
/// A condition that always evaluates to `true`.
pub const ALWAYS: Condition = Condition(Condition::always);
/// A condition that always evaluated to `false`.
pub const NEVER: Condition = Condition(Condition::never);
/// Creates a dynamically checked condition from a function `f`.
///
/// The function `f` is called anytime the condition is checked, including
/// every time a style with the condition is used.
///
/// # Example
///
/// ```rust,no_run
/// use yansi::Condition;
///
/// fn some_function() -> bool {
/// /* checking arbitrary conditions */
/// todo!()
/// }
///
/// // Create a custom static condition from a function.
/// static MY_CONDITION: Condition = Condition::from(some_function);
///
/// // Create a condition on the stack from a function.
/// let my_condition = Condition::from(some_function);
///
/// // Create a static condition from a closure that becomes a `fn`.
/// static MY_CONDITION_2: Condition = Condition::from(|| false);
///
/// // Create a condition on the stack from a closure that becomes a `fn`.
/// let my_condition = Condition::from(|| some_function());
/// ```
pub const fn from(f: fn() -> bool) -> Self {
Condition(f)
}
/// Creates a condition that is [`ALWAYS`](Self::ALWAYS) when `value` is
/// `true` and [`NEVER`](Self::NEVER) otherwise.
///
/// # Example
///
/// ```rust,no_run
/// use yansi::Condition;
///
/// fn some_function() -> bool {
/// /* checking arbitrary conditions */
/// todo!()
/// }
///
/// // Cache the result of `some_function()` so it doesn't get called each
/// // time the condition needs to be checked.
/// let my_condition = Condition::cached(some_function());
/// ```
pub const fn cached(value: bool) -> Self {
match value {
true => Condition::ALWAYS,
false => Condition::NEVER,
}
}
/// The backing function for [`Condition::ALWAYS`]. Returns `true` always.
pub const fn always() -> bool { true }
/// The backing function for [`Condition::NEVER`]. Returns `false` always.
pub const fn never() -> bool { false }
/// The backing function for [`Condition::DEFAULT`].
///
/// Returns `true` if the current OS supports ANSI escape sequences for
/// coloring. Outside of Windows, this always returns `true`. On Windows,
/// the first call to this function attempts to enable support and returns
/// whether it was successful every time thereafter.
pub fn os_support() -> bool {
crate::windows::cache_enable()
}
}
impl Default for Condition {
fn default() -> Self {
Condition::DEFAULT
}
}
impl core::ops::Deref for Condition {
type Target = fn() -> bool;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl AtomicCondition {
pub const DEFAULT: AtomicCondition = AtomicCondition::from(Condition::DEFAULT);
pub const fn from(value: Condition) -> Self {
AtomicCondition(AtomicPtr::new(value.0 as *mut ()))
}
pub fn store(&self, cond: Condition) {
self.0.store(cond.0 as *mut (), Ordering::Release)
}
pub fn read(&self) -> bool {
let condition = unsafe {
Condition(core::mem::transmute(self.0.load(Ordering::Acquire)))
};
condition()
}
}
#[allow(unused)]
impl CachedBool {
const TRUE: u8 = 1;
const UNINIT: u8 = 2;
const INITING: u8 = 3;
pub const fn new() -> Self {
CachedBool(AtomicU8::new(Self::UNINIT))
}
pub fn get_or_init(&self, f: impl FnOnce() -> bool) -> bool {
use core::sync::atomic::Ordering::*;
match self.0.compare_exchange(Self::UNINIT, Self::INITING, AcqRel, Relaxed) {
Ok(_) => {
let new_value = f();
self.0.store(new_value as u8 /* false = 0, true = 1 */, Release);
new_value
}
Err(Self::INITING) => {
let mut value;
while { value = self.0.load(Acquire); value } == Self::INITING {
#[cfg(feature = "std")]
std::thread::yield_now();
}
value == Self::TRUE
},
Err(value) => value == Self::TRUE,
}
}
}
impl fmt::Debug for Condition {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if *self == Condition::DEFAULT {
f.write_str("Condition::DEFAULT")
} else if *self == Condition::ALWAYS {
f.write_str("Condition::ALWAYS")
} else if *self == Condition::NEVER {
f.write_str("Condition::NEVER")
} else {
f.debug_tuple("Condition").field(&self.0).finish()
}
}
}
macro_rules! conditions {
($feat:meta $($f:expr, $CACHED:ident: $cached:ident, $LIVE:ident: $live:ident),* $(,)?) => (
#[cfg($feat)]
#[cfg_attr(feature = "_nightly", doc(cfg($feat)))]
/// Feature dependent conditions.
///
/// Available when compiled with
#[doc = concat!('`', stringify!($feat), "`.")]
impl Condition {
$(
/// Evaluates to `true` if
#[doc = concat!('`', stringify!($f), "`.")]
///
/// The result of the first check is cached for subsequent
/// checks. Internally uses
#[doc = concat!("[`", stringify!($cached), "`](Condition::", stringify!($cached), ").")]
pub const $CACHED: Condition = Condition(Condition::$cached);
)*
$(
/// Evaluates to `true` if
#[doc = concat!('`', stringify!($f), "`.")]
///
/// A call is dispatched each time the condition is checked.
/// This is expensive, so prefer to use
#[doc = concat!("[`", stringify!($CACHED), "`](Condition::", stringify!($CACHED), ")")]
/// instead.
///
/// Internally uses
#[doc = concat!("[`", stringify!($live), "`](Condition::", stringify!($live), ").")]
pub const $LIVE: Condition = Condition(Condition::$live);
)*
$(
/// Returns `true` if
#[doc = concat!('`', stringify!($f), "`.")]
///
/// The result of the first check is cached for subsequent
/// checks. This is the backing function for
#[doc = concat!("[`", stringify!($CACHED), "`](Condition::", stringify!($CACHED), ").")]
pub fn $cached() -> bool {
static IS_TTY: CachedBool = CachedBool::new();
IS_TTY.get_or_init(Condition::$live)
}
)*
$(
/// Returns `true` if
#[doc = concat!('`', stringify!($f), "`.")]
///
/// This is the backing function for
#[doc = concat!("[`", stringify!($LIVE), "`](Condition::", stringify!($LIVE), ").")]
pub fn $live() -> bool {
$f
}
)*
}
)
}
#[cfg(feature = "detect-tty")]
use is_terminal::is_terminal as is_tty;
conditions! { feature = "detect-tty"
is_tty(&std::io::stdout()),
STDOUT_IS_TTY: stdout_is_tty,
STDOUT_IS_TTY_LIVE: stdout_is_tty_live,
is_tty(&std::io::stderr()),
STDERR_IS_TTY: stderr_is_tty,
STDERR_IS_TTY_LIVE: stderr_is_tty_live,
is_tty(&std::io::stdin()),
STDIN_IS_TTY: stdin_is_tty,
STDIN_IS_TTY_LIVE: stdin_is_tty_live,
is_tty(&std::io::stdout()) && is_tty(&std::io::stderr()),
STDOUTERR_ARE_TTY: stdouterr_are_tty,
STDOUTERR_ARE_TTY_LIVE: stdouterr_are_tty_live,
}
#[cfg(feature = "detect-env")]
pub fn env_set_or(name: &str, default: bool) -> bool {
std::env::var_os(name).map_or(default, |v| v != "0")
}
conditions! { feature = "detect-env"
env_set_or("CLICOLOR_FORCE", false) || env_set_or("CLICOLOR", true),
CLICOLOR: clicolor,
CLICOLOR_LIVE: clicolor_live,
!env_set_or("NO_COLOR", false),
YES_COLOR: no_color,
YES_COLOR_LIVE: no_color_live,
}
conditions! { all(feature = "detect-env", feature = "detect-tty")
Condition::stdouterr_are_tty() && Condition::clicolor() && Condition::no_color(),
TTY_AND_COLOR: tty_and_color,
TTY_AND_COLOR_LIVE: tty_and_color_live,
}