radicle_term/ansi/
paint.rs

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