Skip to main content

radicle_term/ansi/
paint.rs

1use std::io::IsTerminal as _;
2use std::sync::LazyLock;
3use std::sync::atomic::{AtomicBool, AtomicI32};
4use std::{fmt, sync};
5
6use super::color::Color;
7use super::style::{Property, Style};
8
9#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
10#[repr(i32)]
11pub enum TerminalFile {
12    Stdout = 1,
13    Stderr = 2,
14}
15
16impl TryFrom<i32> for TerminalFile {
17    type Error = ();
18
19    fn try_from(value: i32) -> Result<Self, Self::Error> {
20        match value {
21            1 => Ok(TerminalFile::Stdout),
22            2 => Ok(TerminalFile::Stderr),
23            _ => Err(()),
24        }
25    }
26}
27
28impl TerminalFile {
29    fn is_terminal(&self) -> bool {
30        match self {
31            TerminalFile::Stdout => std::io::stdout().is_terminal(),
32            TerminalFile::Stderr => std::io::stderr().is_terminal(),
33        }
34    }
35}
36
37/// What file is used for text output.
38static TERMINAL: AtomicI32 = AtomicI32::new(TerminalFile::Stdout as i32);
39/// Whether or not paint styling is enabled.
40static ENABLED: AtomicBool = AtomicBool::new(true);
41/// Whether paint styling should be forced.
42static FORCED: AtomicBool = AtomicBool::new(false);
43
44/// A structure encapsulating an item and styling.
45#[derive(Debug, Default, Eq, PartialEq, Ord, PartialOrd, Hash, Copy, Clone)]
46pub struct Paint<T> {
47    pub item: T,
48    pub style: Style,
49}
50
51impl Paint<&str> {
52    /// Return plain content.
53    pub fn content(&self) -> &str {
54        self.item
55    }
56}
57
58impl Paint<String> {
59    /// Return plain content.
60    pub fn content(&self) -> &str {
61        self.item.as_str()
62    }
63}
64
65impl<T> From<T> for Paint<T> {
66    fn from(value: T) -> Self {
67        Self::new(value)
68    }
69}
70
71impl From<&str> for Paint<String> {
72    fn from(item: &str) -> Self {
73        Self::new(item.to_string())
74    }
75}
76
77impl From<Paint<&str>> for Paint<String> {
78    fn from(paint: Paint<&str>) -> Self {
79        Self {
80            item: paint.item.to_owned(),
81            style: paint.style,
82        }
83    }
84}
85
86impl From<Paint<String>> for String {
87    fn from(value: Paint<String>) -> Self {
88        value.item
89    }
90}
91
92impl<T> Paint<T> {
93    /// Constructs a new `Paint` structure encapsulating `item` with no set
94    /// styling.
95    #[inline]
96    pub const fn new(item: T) -> Paint<T> {
97        Paint {
98            item,
99            style: Style {
100                foreground: Color::Unset,
101                background: Color::Unset,
102                properties: Property::new(),
103                wrap: false,
104            },
105        }
106    }
107
108    /// Constructs a new _wrapping_ `Paint` structure encapsulating `item` with
109    /// default styling.
110    ///
111    /// A wrapping `Paint` converts all color resets written out by the internal
112    /// value to the styling of itself. This allows for seamless color wrapping
113    /// of other colored text.
114    ///
115    /// # Performance
116    ///
117    /// In order to wrap an internal value, the internal value must first be
118    /// written out to a local buffer and examined. As a result, displaying a
119    /// wrapped value is likely to result in a heap allocation and copy.
120    #[inline]
121    pub const fn wrapping(item: T) -> Paint<T> {
122        Paint::new(item).wrap()
123    }
124
125    /// Constructs a new `Paint` structure encapsulating `item` with the
126    /// foreground color set to the RGB color `r`, `g`, `b`.
127    #[inline]
128    pub const fn rgb(r: u8, g: u8, b: u8, item: T) -> Paint<T> {
129        Paint::new(item).fg(Color::RGB(r, g, b))
130    }
131
132    /// Constructs a new `Paint` structure encapsulating `item` with the
133    /// foreground color set to the fixed 8-bit color `color`.
134    #[inline]
135    pub const fn fixed(color: u8, item: T) -> Paint<T> {
136        Paint::new(item).fg(Color::Fixed(color))
137    }
138
139    pub const fn red(item: T) -> Paint<T> {
140        Paint::new(item).fg(Color::Red)
141    }
142
143    pub const fn black(item: T) -> Paint<T> {
144        Paint::new(item).fg(Color::Black)
145    }
146
147    pub const fn yellow(item: T) -> Paint<T> {
148        Paint::new(item).fg(Color::Yellow)
149    }
150
151    pub const fn green(item: T) -> Paint<T> {
152        Paint::new(item).fg(Color::Green)
153    }
154
155    pub const fn cyan(item: T) -> Paint<T> {
156        Paint::new(item).fg(Color::Cyan)
157    }
158
159    pub const fn blue(item: T) -> Paint<T> {
160        Paint::new(item).fg(Color::Blue)
161    }
162
163    pub const fn magenta(item: T) -> Paint<T> {
164        Paint::new(item).fg(Color::Magenta)
165    }
166
167    pub const fn white(item: T) -> Paint<T> {
168        Paint::new(item).fg(Color::White)
169    }
170
171    /// Retrieves the style currently set on `self`.
172    #[inline]
173    pub const fn style(&self) -> Style {
174        self.style
175    }
176
177    /// Retrieves a borrow to the inner item.
178    #[inline]
179    pub const fn inner(&self) -> &T {
180        &self.item
181    }
182
183    /// Sets the style of `self` to `style`.
184    #[inline]
185    pub fn with_style(mut self, style: Style) -> Paint<T> {
186        self.style = style;
187        self
188    }
189
190    /// Makes `self` a _wrapping_ `Paint`.
191    ///
192    /// A wrapping `Paint` converts all color resets written out by the internal
193    /// value to the styling of itself. This allows for seamless color wrapping
194    /// of other colored text.
195    ///
196    /// # Performance
197    ///
198    /// In order to wrap an internal value, the internal value must first be
199    /// written out to a local buffer and examined. As a result, displaying a
200    /// wrapped value is likely to result in a heap allocation and copy.
201    #[inline]
202    pub const fn wrap(mut self) -> Paint<T> {
203        self.style.wrap = true;
204        self
205    }
206
207    /// Sets the foreground to `color`.
208    #[inline]
209    pub const fn fg(mut self, color: Color) -> Paint<T> {
210        self.style.foreground = color;
211        self
212    }
213
214    /// Sets the background to `color`.
215    #[inline]
216    pub const fn bg(mut self, color: Color) -> Paint<T> {
217        self.style.background = color;
218        self
219    }
220
221    pub fn bold(mut self) -> Self {
222        self.style.properties.set(Property::BOLD);
223        self
224    }
225
226    pub fn dim(mut self) -> Self {
227        self.style.properties.set(Property::DIM);
228        self
229    }
230
231    pub fn italic(mut self) -> Self {
232        self.style.properties.set(Property::ITALIC);
233        self
234    }
235
236    pub fn underline(mut self) -> Self {
237        self.style.properties.set(Property::UNDERLINE);
238        self
239    }
240
241    pub fn invert(mut self) -> Self {
242        self.style.properties.set(Property::INVERT);
243        self
244    }
245
246    pub fn strikethrough(mut self) -> Self {
247        self.style.properties.set(Property::STRIKETHROUGH);
248        self
249    }
250
251    pub fn blink(mut self) -> Self {
252        self.style.properties.set(Property::BLINK);
253        self
254    }
255
256    pub fn hidden(mut self) -> Self {
257        self.style.properties.set(Property::HIDDEN);
258        self
259    }
260}
261
262impl<T: fmt::Display> fmt::Display for Paint<T> {
263    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
264        if !Paint::is_enabled() {
265            return fmt::Display::fmt(&self.item, f);
266        }
267
268        if self.style.wrap {
269            let mut prefix = String::new();
270            prefix.push_str("\x1B[0m");
271            self.style.fmt_prefix(&mut prefix)?;
272            self.style.fmt_prefix(f)?;
273
274            let item = format!("{}", self.item).replace("\x1B[0m", &prefix);
275            fmt::Display::fmt(&item, f)?;
276            self.style.fmt_suffix(f)
277        } else {
278            self.style.fmt_prefix(f)?;
279            fmt::Display::fmt(&self.item, f)?;
280            self.style.fmt_suffix(f)
281        }
282    }
283}
284
285impl Paint<()> {
286    /// Returns `true` if ANSI styling (bold, italic, underline, reverse, etc.)
287    /// is enabled. This reflects the general terminal capability and user
288    /// preferences governed by `CLICOLOR`, `ENABLED` / `FORCED`, and terminal
289    /// detection. It does **not** take `NO_COLOR` into account.
290    pub fn is_styling_enabled() -> bool {
291        if FORCED.load(sync::atomic::Ordering::SeqCst) {
292            return true;
293        }
294        let clicolor = anstyle_query::clicolor();
295        let clicolor_enabled = clicolor.unwrap_or(false);
296        let clicolor_disabled = !clicolor.unwrap_or(true);
297        let terminal = TERMINAL.load(sync::atomic::Ordering::SeqCst);
298        let is_terminal = TerminalFile::try_from(terminal).is_ok_and(|tf| tf.is_terminal());
299        let is_enabled = ENABLED.load(sync::atomic::Ordering::SeqCst);
300
301        is_terminal
302            && is_enabled
303            && !clicolor_disabled
304            && (anstyle_query::term_supports_color() || clicolor_enabled || anstyle_query::is_ci())
305            || anstyle_query::clicolor_force()
306    }
307
308    /// Returns `true` if any painting is enabled.
309    ///
310    /// This is equivalent to `is_styling_enabled()`.
311    pub fn is_enabled() -> bool {
312        Self::is_styling_enabled()
313    }
314
315    /// Check 24-bit RGB color support.
316    pub fn truecolor() -> bool {
317        static TRUECOLOR: LazyLock<bool> = LazyLock::new(anstyle_query::term_supports_color);
318        *TRUECOLOR
319    }
320
321    /// Enable paint styling.
322    pub fn enable() {
323        ENABLED.store(true, sync::atomic::Ordering::SeqCst);
324    }
325
326    /// Set the terminal we are writing to. This influences the logic that checks whether or not to
327    /// include colors.
328    pub fn set_terminal(tf: TerminalFile) {
329        TERMINAL.store(tf as i32, sync::atomic::Ordering::SeqCst);
330    }
331
332    /// Force paint styling.
333    /// Useful when you want to output colors to a non-TTY.
334    pub fn force(force: bool) {
335        FORCED.store(force, sync::atomic::Ordering::SeqCst);
336    }
337
338    /// Disable paint styling.
339    pub fn disable() {
340        ENABLED.store(false, sync::atomic::Ordering::SeqCst);
341    }
342}
343
344/// An object filled with a background color.
345#[derive(Debug, Clone)]
346pub struct Filled<T> {
347    pub item: T,
348    pub color: Color,
349}
350
351impl<T: fmt::Display> fmt::Display for Filled<T> {
352    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
353        write!(f, "{}", Paint::wrapping(&self.item).bg(self.color))
354    }
355}
356
357/// Shorthand for [`Paint::new`].
358pub fn paint<T>(item: T) -> Paint<T> {
359    Paint::new(item)
360}